See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Dropbox V2 API. 19 * 20 * @since Moodle 3.2 21 * @package repository_dropbox 22 * @copyright Andrew Nicols <andrew@nicols.co.uk> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace repository_dropbox; 27 28 use core\oauth2\client; 29 use core\oauth2\issuer; 30 31 /** 32 * Dropbox V2 API. 33 * 34 * @package repository_dropbox 35 * @copyright Andrew Nicols <andrew@nicols.co.uk> 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class dropbox extends client { 39 40 /** 41 * @var array Custom continue endpoints that differ from the standard. 42 */ 43 private $mappedcontinueoverides = [ 44 'files/search_v2' => 'files/search/continue_v2' 45 ]; 46 47 /** 48 * Create the DropBox API Client. 49 * 50 * @param issuer $issuer The dropbox issuer 51 * @param string $callback The callback URL 52 */ 53 public function __construct(issuer $issuer, $callback) { 54 parent::__construct($issuer, $callback, '', false, true); 55 } 56 57 /** 58 * Override - Return an empty string to override parent function. 59 * 60 * Dropbox does not require scopes to be provided and can function without them. 61 * Additional information MDL-70268 62 * 63 * @return string 64 */ 65 protected function get_login_scopes() { 66 return ''; 67 } 68 69 /** 70 * Returns the auth url for OAuth 2.0 request. 71 * 72 * @return string the auth url 73 */ 74 protected function auth_url() { 75 return 'https://www.dropbox.com/oauth2/authorize'; 76 } 77 78 /** 79 * Returns the token url for OAuth 2.0 request. 80 * 81 * @return string the auth url 82 */ 83 protected function token_url() { 84 return 'https://api.dropboxapi.com/oauth2/token'; 85 } 86 87 /** 88 * Return the constructed API endpoint URL. 89 * 90 * @param string $endpoint The endpoint to be contacted 91 * @return moodle_url The constructed API URL 92 */ 93 protected function get_api_endpoint($endpoint) { 94 return new \moodle_url('https://api.dropboxapi.com/2/' . $endpoint); 95 } 96 97 /** 98 * Return the constructed content endpoint URL. 99 * 100 * @param string $endpoint The endpoint to be contacted 101 * @return moodle_url The constructed content URL 102 */ 103 protected function get_content_endpoint($endpoint) { 104 return new \moodle_url('https://api-content.dropbox.com/2/' . $endpoint); 105 } 106 107 /** 108 * Get the continue endpoint for the provided endpoint. 109 * 110 * @param string $endpoint The original endpoint 111 * @return string $endpoint The generated/mapped continue link 112 */ 113 protected function get_endpoint_for_continue(string $endpoint) { 114 // Any API endpoint returning 'has_more' will provide a cursor, and also have a matching endpoint suffixed 115 // with /continue which takes that cursor. 116 if (preg_match('_/continue$_', $endpoint) === 0) { 117 // First check if the API call uses a custom mapped continue endpoint. 118 if (isset($this->mappedcontinueoverides[$endpoint])) { 119 $endpoint = $this->mappedcontinueoverides[$endpoint]; 120 } else { 121 // Only add /continue if it is not already present. 122 $endpoint .= '/continue'; 123 } 124 } 125 126 return $endpoint; 127 } 128 129 /** 130 * Make an API call against the specified endpoint with supplied data. 131 * 132 * @param string $endpoint The endpoint to be contacted 133 * @param array $data Any data to pass to the endpoint 134 * @param string $resultnode The name of the node that contains the data 135 * @return object Content decoded from the endpoint 136 */ 137 protected function fetch_dropbox_data($endpoint, $data = [], string $resultnode = 'entries') { 138 $url = $this->get_api_endpoint($endpoint); 139 $this->cleanopt(); 140 $this->resetHeader(); 141 142 if ($data === null) { 143 // Some API endpoints explicitly expect a data submission of 'null'. 144 $options['CURLOPT_POSTFIELDS'] = 'null'; 145 } else { 146 $options['CURLOPT_POSTFIELDS'] = json_encode($data); 147 } 148 $options['CURLOPT_POST'] = 1; 149 $this->setHeader('Content-Type: application/json'); 150 151 $response = $this->request($url, $options); 152 $result = json_decode($response); 153 154 $this->check_and_handle_api_errors($result); 155 156 if ($this->has_additional_results($result)) { 157 $endpoint = $this->get_endpoint_for_continue($endpoint); 158 159 // Fetch the next page of results. 160 $additionaldata = $this->fetch_dropbox_data($endpoint, [ 161 'cursor' => $result->cursor, 162 ], $resultnode); 163 164 // Merge the list of entries. 165 $result->$resultnode = array_merge($result->$resultnode, $additionaldata->$resultnode); 166 } 167 168 if (isset($result->has_more)) { 169 // Unset the cursor and has_more flags. 170 unset($result->cursor); 171 unset($result->has_more); 172 } 173 174 return $result; 175 } 176 177 /** 178 * Whether the supplied result is paginated and not the final page. 179 * 180 * @param object $result The result of an operation 181 * @return boolean 182 */ 183 public function has_additional_results($result) { 184 return !empty($result->has_more) && !empty($result->cursor); 185 } 186 187 /** 188 * Fetch content from the specified endpoint with the supplied data. 189 * 190 * @param string $endpoint The endpoint to be contacted 191 * @param array $data Any data to pass to the endpoint 192 * @return string The returned data 193 */ 194 protected function fetch_dropbox_content($endpoint, $data = []) { 195 $url = $this->get_content_endpoint($endpoint); 196 $this->cleanopt(); 197 $this->resetHeader(); 198 199 $options['CURLOPT_POST'] = 1; 200 $this->setHeader('Content-Type: '); 201 $this->setHeader('Dropbox-API-Arg: ' . json_encode($data)); 202 203 $response = $this->request($url, $options); 204 205 $this->check_and_handle_api_errors($response); 206 return $response; 207 } 208 209 /** 210 * Check for an attempt to handle API errors. 211 * 212 * This function attempts to deal with errors as per 213 * https://www.dropbox.com/developers/documentation/http/documentation#error-handling. 214 * 215 * @param mixed $data The returned content. 216 * @throws moodle_exception 217 */ 218 protected function check_and_handle_api_errors($data) { 219 if (!is_array($this->info) or $this->info['http_code'] == 200) { 220 // Dropbox only returns errors on non-200 response codes. 221 return; 222 } 223 224 switch($this->info['http_code']) { 225 case 400: 226 // Bad input parameter. Error message should indicate which one and why. 227 throw new \coding_exception('Invalid input parameter passed to DropBox API.'); 228 break; 229 case 401: 230 // Bad or expired token. This can happen if the access token is expired or if the access token has been 231 // revoked by Dropbox or the user. To fix this, you should re-authenticate the user. 232 throw new authentication_exception('Authentication token expired'); 233 break; 234 case 409: 235 // Endpoint-specific error. Look to the JSON response body for the specifics of the error. 236 throw new \coding_exception('Endpoint specific error: ' . $data->error_summary); 237 break; 238 case 429: 239 // Your app is making too many requests for the given user or team and is being rate limited. Your app 240 // should wait for the number of seconds specified in the "Retry-After" response header before trying 241 // again. 242 throw new rate_limit_exception(); 243 break; 244 default: 245 break; 246 } 247 248 if ($this->info['http_code'] >= 500 && $this->info['http_code'] < 600) { 249 throw new \invalid_response_exception($this->info['http_code'] . ": " . $data); 250 } 251 } 252 253 /** 254 * Get file listing from dropbox. 255 * 256 * @param string $path The path to query 257 * @return object The returned directory listing, or null on failure 258 */ 259 public function get_listing($path = '') { 260 if ($path === '/') { 261 $path = ''; 262 } 263 264 $data = $this->fetch_dropbox_data('files/list_folder', [ 265 'path' => $path, 266 ]); 267 268 return $data; 269 } 270 271 /** 272 * Get file search results from dropbox. 273 * 274 * @param string $query The search query 275 * @return object The returned directory listing, or null on failure 276 */ 277 public function search($query = '') { 278 // There is nothing to be searched. Return an empty array to mimic the response from Dropbox. 279 if (!$query) { 280 return []; 281 } 282 283 $data = $this->fetch_dropbox_data('files/search_v2', [ 284 'options' => [ 285 'path' => '', 286 'filename_only' => true, 287 ], 288 'query' => $query, 289 ], 'matches'); 290 291 return $data; 292 } 293 294 /** 295 * Whether the entry is expected to have a thumbnail. 296 * See docs at https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail. 297 * 298 * @param object $entry The file entry received from the DropBox API 299 * @return boolean Whether dropbox has a thumbnail available 300 */ 301 public function supports_thumbnail($entry) { 302 if ($entry->{".tag"} !== "file") { 303 // Not a file. No thumbnail available. 304 return false; 305 } 306 307 // Thumbnails are available for files under 20MB with file extensions jpg, jpeg, png, tiff, tif, gif, and bmp. 308 if ($entry->size > 20 * 1024 * 1024) { 309 return false; 310 } 311 312 $supportedtypes = [ 313 'jpg' => true, 314 'jpeg' => true, 315 'png' => true, 316 'tiff' => true, 317 'tif' => true, 318 'gif' => true, 319 'bmp' => true, 320 ]; 321 322 $extension = substr($entry->path_lower, strrpos($entry->path_lower, '.') + 1); 323 return isset($supportedtypes[$extension]) && $supportedtypes[$extension]; 324 } 325 326 /** 327 * Retrieves the thumbnail for the content, as supplied by dropbox. 328 * 329 * @param string $path The path to fetch a thumbnail for 330 * @return string Thumbnail image content 331 */ 332 public function get_thumbnail($path) { 333 $content = $this->fetch_dropbox_content('files/get_thumbnail', [ 334 'path' => $path, 335 ]); 336 337 return $content; 338 } 339 340 /** 341 * Fetch a valid public share link for the specified file. 342 * 343 * @param string $id The file path or file id of the file to fetch information for. 344 * @return object An object containing the id, path, size, and URL of the entry 345 */ 346 public function get_file_share_info($id) { 347 // Attempt to fetch any existing shared link first. 348 $data = $this->fetch_dropbox_data('sharing/list_shared_links', [ 349 'path' => $id, 350 ]); 351 352 if (isset($data->links)) { 353 $link = reset($data->links); 354 if (isset($link->{".tag"}) && $link->{".tag"} === "file") { 355 return $this->normalize_file_share_info($link); 356 } 357 } 358 359 // No existing link available. 360 // Create a new one. 361 $link = $this->fetch_dropbox_data('sharing/create_shared_link_with_settings', [ 362 'path' => $id, 363 'settings' => [ 364 'requested_visibility' => 'public', 365 ], 366 ]); 367 368 if (isset($link->{".tag"}) && $link->{".tag"} === "file") { 369 return $this->normalize_file_share_info($link); 370 } 371 372 // Some kind of error we don't know how to handle at this stage. 373 return null; 374 } 375 376 /** 377 * Normalize the file share info. 378 * 379 * @param object $entry Information retrieved from share endpoints 380 * @return object Normalized entry information to store as repository information 381 */ 382 protected function normalize_file_share_info($entry) { 383 return (object) [ 384 'id' => $entry->id, 385 'path' => $entry->path_lower, 386 'url' => $entry->url, 387 ]; 388 } 389 390 /** 391 * Process the callback. 392 */ 393 public function callback() { 394 $this->log_out(); 395 $this->is_logged_in(); 396 } 397 398 /** 399 * Revoke the current access token. 400 * 401 * @return string 402 */ 403 public function logout() { 404 try { 405 $this->fetch_dropbox_data('auth/token/revoke', null); 406 } catch(authentication_exception $e) { 407 // An authentication_exception may be expected if the token has 408 // already expired. 409 } 410 } 411 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body