Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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  }