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