<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Dropbox V2 API.
*
* @since Moodle 3.2
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace repository_dropbox;
< defined('MOODLE_INTERNAL') || die();
<
< require_once($CFG->libdir . '/oauthlib.php');
> use core\oauth2\client;
> use core\oauth2\issuer;
/**
* Dropbox V2 API.
*
* @package repository_dropbox
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
< class dropbox extends \oauth2_client {
> class dropbox extends client {
/**
* @var array Custom continue endpoints that differ from the standard.
*/
private $mappedcontinueoverides = [
'files/search_v2' => 'files/search/continue_v2'
];
/**
* Create the DropBox API Client.
*
< * @param string $key The API key
< * @param string $secret The API secret
> * @param issuer $issuer The dropbox issuer
* @param string $callback The callback URL
*/
< public function __construct($key, $secret, $callback) {
< parent::__construct($key, $secret, $callback, '');
> public function __construct(issuer $issuer, $callback) {
> parent::__construct($issuer, $callback, '', false, true);
> }
>
> /**
> * Override - Return an empty string to override parent function.
> *
> * Dropbox does not require scopes to be provided and can function without them.
> * Additional information MDL-70268
> *
> * @return string
> */
> protected function get_login_scopes() {
> return '';
}
/**
* Returns the auth url for OAuth 2.0 request.
*
* @return string the auth url
*/
protected function auth_url() {
return 'https://www.dropbox.com/oauth2/authorize';
}
/**
* Returns the token url for OAuth 2.0 request.
*
* @return string the auth url
*/
protected function token_url() {
return 'https://api.dropboxapi.com/oauth2/token';
}
/**
* Return the constructed API endpoint URL.
*
* @param string $endpoint The endpoint to be contacted
* @return moodle_url The constructed API URL
*/
protected function get_api_endpoint($endpoint) {
return new \moodle_url('https://api.dropboxapi.com/2/' . $endpoint);
}
/**
* Return the constructed content endpoint URL.
*
* @param string $endpoint The endpoint to be contacted
* @return moodle_url The constructed content URL
*/
protected function get_content_endpoint($endpoint) {
return new \moodle_url('https://api-content.dropbox.com/2/' . $endpoint);
}
/**
* Get the continue endpoint for the provided endpoint.
*
* @param string $endpoint The original endpoint
* @return string $endpoint The generated/mapped continue link
*/
protected function get_endpoint_for_continue(string $endpoint) {
// Any API endpoint returning 'has_more' will provide a cursor, and also have a matching endpoint suffixed
// with /continue which takes that cursor.
if (preg_match('_/continue$_', $endpoint) === 0) {
// First check if the API call uses a custom mapped continue endpoint.
if (isset($this->mappedcontinueoverides[$endpoint])) {
$endpoint = $this->mappedcontinueoverides[$endpoint];
} else {
// Only add /continue if it is not already present.
$endpoint .= '/continue';
}
}
return $endpoint;
}
/**
* Make an API call against the specified endpoint with supplied data.
*
* @param string $endpoint The endpoint to be contacted
* @param array $data Any data to pass to the endpoint
* @param string $resultnode The name of the node that contains the data
* @return object Content decoded from the endpoint
*/
protected function fetch_dropbox_data($endpoint, $data = [], string $resultnode = 'entries') {
$url = $this->get_api_endpoint($endpoint);
$this->cleanopt();
$this->resetHeader();
if ($data === null) {
// Some API endpoints explicitly expect a data submission of 'null'.
$options['CURLOPT_POSTFIELDS'] = 'null';
} else {
$options['CURLOPT_POSTFIELDS'] = json_encode($data);
}
$options['CURLOPT_POST'] = 1;
$this->setHeader('Content-Type: application/json');
$response = $this->request($url, $options);
$result = json_decode($response);
$this->check_and_handle_api_errors($result);
if ($this->has_additional_results($result)) {
$endpoint = $this->get_endpoint_for_continue($endpoint);
// Fetch the next page of results.
$additionaldata = $this->fetch_dropbox_data($endpoint, [
'cursor' => $result->cursor,
], $resultnode);
// Merge the list of entries.
$result->$resultnode = array_merge($result->$resultnode, $additionaldata->$resultnode);
}
if (isset($result->has_more)) {
// Unset the cursor and has_more flags.
unset($result->cursor);
unset($result->has_more);
}
return $result;
}
/**
* Whether the supplied result is paginated and not the final page.
*
* @param object $result The result of an operation
* @return boolean
*/
public function has_additional_results($result) {
return !empty($result->has_more) && !empty($result->cursor);
}
/**
* Fetch content from the specified endpoint with the supplied data.
*
* @param string $endpoint The endpoint to be contacted
* @param array $data Any data to pass to the endpoint
* @return string The returned data
*/
protected function fetch_dropbox_content($endpoint, $data = []) {
$url = $this->get_content_endpoint($endpoint);
$this->cleanopt();
$this->resetHeader();
$options['CURLOPT_POST'] = 1;
$this->setHeader('Content-Type: ');
$this->setHeader('Dropbox-API-Arg: ' . json_encode($data));
$response = $this->request($url, $options);
$this->check_and_handle_api_errors($response);
return $response;
}
/**
* Check for an attempt to handle API errors.
*
* This function attempts to deal with errors as per
* https://www.dropbox.com/developers/documentation/http/documentation#error-handling.
*
* @param mixed $data The returned content.
* @throws moodle_exception
*/
protected function check_and_handle_api_errors($data) {
if (!is_array($this->info) or $this->info['http_code'] == 200) {
// Dropbox only returns errors on non-200 response codes.
return;
}
switch($this->info['http_code']) {
case 400:
// Bad input parameter. Error message should indicate which one and why.
throw new \coding_exception('Invalid input parameter passed to DropBox API.');
break;
case 401:
// Bad or expired token. This can happen if the access token is expired or if the access token has been
// revoked by Dropbox or the user. To fix this, you should re-authenticate the user.
throw new authentication_exception('Authentication token expired');
break;
case 409:
// Endpoint-specific error. Look to the JSON response body for the specifics of the error.
throw new \coding_exception('Endpoint specific error: ' . $data->error_summary);
break;
case 429:
// Your app is making too many requests for the given user or team and is being rate limited. Your app
// should wait for the number of seconds specified in the "Retry-After" response header before trying
// again.
throw new rate_limit_exception();
break;
default:
break;
}
if ($this->info['http_code'] >= 500 && $this->info['http_code'] < 600) {
throw new \invalid_response_exception($this->info['http_code'] . ": " . $data);
}
}
/**
* Get file listing from dropbox.
*
* @param string $path The path to query
* @return object The returned directory listing, or null on failure
*/
public function get_listing($path = '') {
if ($path === '/') {
$path = '';
}
$data = $this->fetch_dropbox_data('files/list_folder', [
'path' => $path,
]);
return $data;
}
/**
* Get file search results from dropbox.
*
* @param string $query The search query
* @return object The returned directory listing, or null on failure
*/
public function search($query = '') {
// There is nothing to be searched. Return an empty array to mimic the response from Dropbox.
if (!$query) {
return [];
}
$data = $this->fetch_dropbox_data('files/search_v2', [
'options' => [
'path' => '',
'filename_only' => true,
],
'query' => $query,
], 'matches');
return $data;
}
/**
* Whether the entry is expected to have a thumbnail.
* See docs at https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail.
*
* @param object $entry The file entry received from the DropBox API
* @return boolean Whether dropbox has a thumbnail available
*/
public function supports_thumbnail($entry) {
if ($entry->{".tag"} !== "file") {
// Not a file. No thumbnail available.
return false;
}
// Thumbnails are available for files under 20MB with file extensions jpg, jpeg, png, tiff, tif, gif, and bmp.
if ($entry->size > 20 * 1024 * 1024) {
return false;
}
$supportedtypes = [
'jpg' => true,
'jpeg' => true,
'png' => true,
'tiff' => true,
'tif' => true,
'gif' => true,
'bmp' => true,
];
$extension = substr($entry->path_lower, strrpos($entry->path_lower, '.') + 1);
return isset($supportedtypes[$extension]) && $supportedtypes[$extension];
}
/**
* Retrieves the thumbnail for the content, as supplied by dropbox.
*
* @param string $path The path to fetch a thumbnail for
* @return string Thumbnail image content
*/
public function get_thumbnail($path) {
$content = $this->fetch_dropbox_content('files/get_thumbnail', [
'path' => $path,
]);
return $content;
}
/**
* Fetch a valid public share link for the specified file.
*
* @param string $id The file path or file id of the file to fetch information for.
* @return object An object containing the id, path, size, and URL of the entry
*/
public function get_file_share_info($id) {
// Attempt to fetch any existing shared link first.
$data = $this->fetch_dropbox_data('sharing/list_shared_links', [
'path' => $id,
]);
if (isset($data->links)) {
$link = reset($data->links);
if (isset($link->{".tag"}) && $link->{".tag"} === "file") {
return $this->normalize_file_share_info($link);
}
}
// No existing link available.
// Create a new one.
$link = $this->fetch_dropbox_data('sharing/create_shared_link_with_settings', [
'path' => $id,
'settings' => [
'requested_visibility' => 'public',
],
]);
if (isset($link->{".tag"}) && $link->{".tag"} === "file") {
return $this->normalize_file_share_info($link);
}
// Some kind of error we don't know how to handle at this stage.
return null;
}
/**
* Normalize the file share info.
*
* @param object $entry Information retrieved from share endpoints
* @return object Normalized entry information to store as repository information
*/
protected function normalize_file_share_info($entry) {
return (object) [
'id' => $entry->id,
'path' => $entry->path_lower,
'url' => $entry->url,
];
}
/**
* Process the callback.
*/
public function callback() {
$this->log_out();
$this->is_logged_in();
}
/**
* Revoke the current access token.
*
* @return string
*/
public function logout() {
try {
$this->fetch_dropbox_data('auth/token/revoke', null);
} catch(authentication_exception $e) {
// An authentication_exception may be expected if the token has
// already expired.
}
}
}