See Release Notes
Long Term Support Release
<?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. } } }