You are here

class BackupMigrateDropboxAPI in Backup and Migrate Dropbox 7.3

Same name and namespace in other branches
  1. 7 backup_migrate_dropbox.dropbox_api.inc \BackupMigrateDropboxAPI
  2. 7.2 backup_migrate_dropbox.dropbox_api.inc \BackupMigrateDropboxAPI

BackupMigrateDropboxAPI contains all the details about the Dropbox api, authorization calls, endpoints, uris, parameters, error handling, and split requests for large uploads/downloads

The public methods are the interface to the B&M destination.

Hierarchy

Expanded class hierarchy of BackupMigrateDropboxAPI

File

./backup_migrate_dropbox.dropbox_api.inc, line 10

View source
class BackupMigrateDropboxAPI {

  /**
   * @var resource|false
   *   A Curl handle. To reuse the curl handle we keep it open across calls
   *   to the Dropbox API.
   */
  private $ch;

  /**
   * @var backup_migrate_destination_dropbox
   *   Contains the B&M destination that uses this Dropbox Api object to
   *   communicate with Dropbox as this Api needs access to its name, machine
   *   name, and settings.
   */
  private $destination;

  /** @var array */
  private $upload_session = [];

  /**
   * @var bool
   *   Indicates if we are resending a request that failed earlier because the
   *   bearer token had expired. This prevents resending over and over again in
   *   case something else is going wrong.
   */
  private $is_resending_after_refresh = FALSE;

  /**
   * BackupMigrateDropboxAPI constructor.
   *
   * @param backup_migrate_destination_dropbox $destination
   */
  public function __construct(backup_migrate_destination_dropbox $destination) {
    $this->ch = FALSE;
    $this->destination = $destination;
  }
  public function __destruct() {
    if (!empty($this->ch)) {
      curl_close($this->ch);
    }
  }

  /**
   * Returns the (parameterized) authorize URL.
   *
   * @link https://www.dropbox.com/developers/documentation/http/documentation#oauth2-authorize
   * @link https://dropbox.tech/developers/pkce--what-and-why-
   *
   * @param string $code_verifier
   *   The code verifier to use to construct the authorize url.
   *
   * @return string
   *   The URL to direct the user to, to allow him to give this app permission
   *   to access (a app specific folder on) his Dropbox account.
   *
   */
  public function get_authorize_url($code_verifier) {
    global $language;
    return url('https://www.dropbox.com/oauth2/authorize', [
      'external' => TRUE,
      'query' => [
        'client_id' => $this
          ->get_app_id(),
        'response_type' => 'code',
        'token_access_type' => 'offline',
        'code_challenge_method' => 'S256',
        'code_challenge' => $this
          ->get_code_challenge($code_verifier),
        'locale' => $language->language,
      ],
    ]);
  }

  /**
   * Returns the Dropbox app id of this app.
   *
   * @return string
   *
   * @throws RuntimeException
   */
  private function get_app_id() {
    $client_id = variable_get('backup_migrate_dropbox_app_key');
    if ($client_id === NULL) {
      throw new RuntimeException('Module "Backup and Migrate Dropbox" not installed or updated correctly. Please run update.php on your site.');
    }
    return $client_id;
  }

  /**
   * Makes a Dropbox code verifier for this installation.
   *
   * @link https://dropbox.tech/developers/pkce--what-and-why-
   *
   * @return string
   *   A Dropbox code verifier.
   *
   * @throws Exception
   */
  public function create_code_verifier() {
    if (version_compare(phpversion(), '7.0', '>=')) {

      /** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */
      $random = random_bytes(32);
    }
    else {
      $random = '';
      while (strlen($random) < 32) {
        $random .= chr(mt_rand(0, 255));
      }
    }
    return $this
      ->base64_url_encode($random);
  }

  /**
   * Returns the code challenge for the given code verifier.
   *
   * @link https://dropbox.tech/developers/pkce--what-and-why-
   *
   * @param string $code_verifier
   *
   * @return string
   *  The code challenge: the sha256 hashed code verifier.
   */
  private function get_code_challenge($code_verifier) {
    if ($code_verifier === NULL) {
      throw new RuntimeException("Cannot create a code challenge when no code verifier is available.");
    }
    return $this
      ->base64_url_encode(hash('sha256', $code_verifier, TRUE));
  }

  /**
   * Obtains a first bearer and a refresh token.
   *
   * The first time we get a bearer token we do so with the just obtained, short
   * lived, access code. This will return a short lived bearer token and a long
   * lived refresh token which we have to store for future use. This because the
   * subsequent bearer tokens must be obtained with the refresh token, not the
   * access code (that will have expired by then).
   *
   * @param string $access_code
   *   The short lived access code that the user obtained from the Dropbox
   *   authorization page.
   * @param string $code_verifier
   *   The code verifier whose code challenge was used to obtain the access
   *   code.
   *
   * @return string|null
   *   The refresh token, or null if it could not be obtained. As a side effect,
   *   the short lived bearer token that is also returned will be stored for use
   *   during the next few hours.
   */
  public function obtain_refresh_token($access_code, $code_verifier) {
    $parameters = [
      'code' => $access_code,
      'grant_type' => 'authorization_code',
      'code_verifier' => $code_verifier,
      'client_id' => $this
        ->get_app_id(),
    ];
    try {
      $response = $this
        ->send_message('api', 'oauth2/token', $parameters);
      if (!isset($response->refresh_token)) {
        throw new RuntimeException('Could not obtain a refresh token');
      }
      BearerTokenInfos::set($this->destination
        ->get_id(), $response);
      return $response->refresh_token;
    } catch (RuntimeException $e) {
      watchdog('backup_migrate', 'Backup Migrate Dropbox Error: ' . $e
        ->getMessage(), [], WATCHDOG_ERROR);
      drupal_set_message('Backup Migrate Dropbox Error: ' . $e
        ->getMessage(), 'error');
      return NULL;
    }
  }

  /**
   * Returns the refresh token for this Dropbox destination.
   *
   * @return string
   *   The refresh token for this Dropbox destination.
   *
   * @throws RuntimeException
   */
  private function get_refresh_token() {
    $refresh_token = $this->destination
      ->settings('refresh_token');
    if ($refresh_token === NULL) {
      $name = $this->destination
        ->get_name();
      $id = $this->destination
        ->get_id();
      $edit_destination_page = l('Edit destination page for this destination', "admin/config/system/backup_migrate/settings/destination/edit/{$id}");
      throw new RuntimeException("Dropbox Destination '{$name}' not authorized correctly: refresh token missing. Please visit the {$edit_destination_page} and configure it anew.");
    }
    return $refresh_token;
  }

  /**
   * Returns a short lived but not yet expired bearer token.
   *
   * If a bearer token is stored and not yet expired that is returned, otherwise
   * that bearer token is replaced (and returned) by a new one.
   *
   * @return string
   *   A short lived but not yet expired bearer token.
   *
   * @throws RuntimeException
   */
  private function get_bearer_token() {
    $bearer_token = BearerTokenInfos::get($this->destination
      ->get_id());
    return $bearer_token !== NULL ? $bearer_token : $this
      ->refresh_bearer_token();
  }

  /**
   * Refreshes and returns an expired bearer token.
   *
   * If a bearer token expires, it needs to be refreshed, i.e. replaced by a new
   * short lived (typically 4 hours) bearer token. We store these in a variable
   * but also return it as it is needed directly.
   *
   * @return string
   *   The refreshed bearer token for this Dropbox destination.
   *
   * @throws RuntimeException
   */
  private function refresh_bearer_token() {
    $parameters = [
      'refresh_token' => $this
        ->get_refresh_token(),
      'grant_type' => 'refresh_token',
      'client_id' => $this
        ->get_app_id(),
    ];
    $response = $this
      ->send_message('api', 'oauth2/token', $parameters);
    return BearerTokenInfos::set($this->destination
      ->get_id(), $response);
  }

  /**
   * Returns the contents of a Dropbox folder.
   *
   * @link https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder Dropbox API /list_folder
   *
   * @param string $folder
   *
   * @return object[]
   *   A list of file metadata of the files in the folder.
   *
   * @throws RuntimeException
   */
  public function list_folder($folder) {

    // Note, I once got this message: Dropbox error: Error in call to API
    // function "files/list_folder": request body: path: Specify the root folder
    // as an empty string rather than as "/". So we handle that case here.
    if ($folder === '/') {
      $folder = '';
    }

    // Simple listing: using Dropbox defaults:
    // - Not recursive.
    // - No deleted files.
    // - Include mounted files (the app folder is a mounted folder).
    $parameters = [
      'path' => $folder,
      'include_non_downloadable_files' => TRUE,
    ];
    $response = $this
      ->send_message('api', 'files/list_folder', $parameters);
    $files = $response->entries;
    while ($response->has_more) {
      $parameters = [
        'cursor' => $response->cursor,
      ];
      $response = $this
        ->send_message('api', 'files/list_folder/continue', $parameters);
      $files = array_merge($files, $response->entries);
    }
    return $files;
  }

  /**
   * Creates a folder on Dropbox.
   *
   * @link https://www.dropbox.com/developers/documentation/http/documentation#files-create_folder
   *
   * @param string $folder
   *   The folder to create.
   *
   * @return object
   *   A list of folder metadata for the created folder.
   *
   * @throws RuntimeException
   *   The folder could not be created. If that is because it already exists,
   *   the exception message will contain something like
   *   '... path/conflict/folder/.. ...'.
   */
  public function create_folder($folder) {
    if ($folder[0] !== '/') {
      $folder = '/' . $folder;
    }
    $parameters = [
      'path' => $folder,
      'autorename' => FALSE,
    ];
    return $this
      ->send_message('api', 'files/create_folder', $parameters);
  }

  /**
   * Downloads the file from the given Dropbox $path.
   *
   * @link https://www.dropbox.com/developers/documentation/http/documentation#files-download Dropbox API /download
   *
   * @param string $path
   *   Path of file to download.
   * @return string
   *   The contents of the requested file.
   *
   * @throws RuntimeException
   */
  public function file_download($path) {
    $parameters = [
      'path' => $path,
    ];
    return $this
      ->send_message('content', 'files/download', $parameters);
  }

  /**
   * Uploads a file to the given path.
   *
   * If the upload is larger then:
   * - what Dropbox can handle per request.
   * - or the internal memory available to PHP
   * The upload is split into multiple smaller chunks, otherwise it is uploaded
   * in 1 part.
   *
   * @param string $file
   *   Name of local file to upload its contents from.
   * @param string $path
   *   Path on Dropbox (including the file name) to upload the file contents to.
   *
   * @return object
   *   The json decoded response.
   *
   * @throws RuntimeException
   */
  public function file_upload($file, $path) {

    // Cut PHP memory limit by 10% to allow for other in memory data.
    $php_memory_limit = intval($this
      ->byte_size(ini_get('memory_limit')) * 0.9);

    // Dropbox currently has a 150M upload limit per transaction.
    $dropbox_upload_limit = $this
      ->byte_size('150M');

    // For testing or in case the 10% leeway isn't enough allow a smaller upload
    // limit as an advanced setting. This variable has no ui but can be set with
    // drush or through the variable module.
    $manual_upload_limit = $this
      ->byte_size(variable_get('backup_migrate_dropbox_upload_limit', '150M'));

    // Use the smallest value for the max file size.
    $max_file_size = min($php_memory_limit, $dropbox_upload_limit, $manual_upload_limit);

    // File.
    $file_size = filesize($file);

    // If the file size is greater than
    if ($file_size > $max_file_size) {

      // Open file.
      $file_handle = fopen($file, 'rb');
      if (!$file_handle) {
        throw new RuntimeException('Cannot open backup file (1).');
      }

      // Start.
      $content = fread($file_handle, $max_file_size);
      if (!$content) {
        throw new RuntimeException('Cannot read backup file (2).');
      }
      $this
        ->_file_upload_session_start($content);

      // Append.
      while (!feof($file_handle)) {

        // Get content.
        $content = fread($file_handle, $max_file_size);
        if (!$content) {
          throw new RuntimeException('Cannot read backup file (3).');
        }
        $this
          ->_file_upload_session_append($content);
      }

      // Finish.
      $result = $this
        ->_file_upload_session_finish($path);
    }
    else {
      $content = file_get_contents($file);
      if (!$content) {
        throw new RuntimeException('Cannot open backup file (4).');
      }
      $result = $this
        ->_file_upload_upload($path, $content);
    }
    return $result;
  }

  /**
   * Starts a multi-request upload.
   *
   * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-start Dropbox API /upload_session/start
   *
   * @param string $content
   *
   * @return object
   *   The json decoded response.
   *
   * @throws RuntimeException
   * @noinspection PhpReturnValueOfMethodIsNeverUsedInspection
   */
  private function _file_upload_session_start($content) {
    $result = $this
      ->send_message('content', 'files/upload_session/start', [], $content);
    if (!isset($result->session_id)) {
      throw new RuntimeException('No session id returned.');
    }
    $this->upload_session['session_id'] = $result->session_id;
    $this->upload_session['offset'] = strlen($content);
    return $result;
  }

  /**
   * Uploads 1 part of a multi-request upload.
   *
   * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-append Dropbox API /upload_session/append
   *
   * @param string $content
   *
   * @return object
   *   The json decoded response.
   *
   * @throws RuntimeException
   * @noinspection PhpReturnValueOfMethodIsNeverUsedInspection
   */
  private function _file_upload_session_append($content) {
    $parameters = [
      'cursor' => $this->upload_session,
    ];
    $result = $this
      ->send_message('content', 'files/upload_session/append_v2', $parameters, $content);
    $this->upload_session['offset'] += strlen($content);
    return $result;
  }

  /**
   * Ends a multi-request upload.
   *
   * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload_session-finish Dropbox API /upload_session/finish
   *
   * @param string $path
   *
   * @return object
   *   The json decoded response.
   *
   * @throws RuntimeException
   */
  private function _file_upload_session_finish($path) {
    $parameters = [
      'cursor' => $this->upload_session,
      'commit' => [
        'path' => $path,
        'mode' => 'add',
        'autorename' => TRUE,
        'mute' => TRUE,
      ],
    ];
    return $this
      ->send_message('content', 'files/upload_session/finish', $parameters);
  }

  /**
   * Uploads the $contents of a file (with 1 request) to the indicated $path.
   *
   * @link https://www.dropbox.com/developers/documentation/http/documentation#files-upload Dropbox API /upload
   *
   * @param string $path
   * @param string $content
   *
   * @return object
   *   The json decoded response.
   *
   * @throws RuntimeException
   */
  private function _file_upload_upload($path, $content) {

    // Simple upload.
    $parameters = [
      'path' => $path,
      'mode' => 'add',
      'autorename' => TRUE,
      'mute' => FALSE,
    ];
    return $this
      ->send_message('content', 'files/upload', $parameters, $content);
  }

  /**
   * Deletes the file at the given $path.
   *
   * @link https://www.dropbox.com/developers/documentation/http/documentation#files-delete Dropbox API /delete
   *
   * @param string $path
   *
   * @return object
   *   The json decoded response.
   *
   * @throws RuntimeException
   */
  public function file_delete($path) {
    $parameters = [
      'path' => $path,
    ];
    return $this
      ->send_message('api', 'files/delete_v2', $parameters);
  }

  /**
   * Sends a request to Dropbox and returns the response.
   *
   * @param string $endpointType
   *   This type determines the url to use and how to process parameters. It can
   *   be one of:
   *   - 'api'
   *   - 'content'
   *   More info about end points can be found at:
   *   {@link https://www.dropbox.com/developers/documentation/http/documentation#formats}
   * @param string $command
   *   The Dropbox API function to call. Will be used as the path part of the
   *   url.
   * @param array|null $parameters
   *   The parameters for this command. Will be send in the body or as a header.
   * @param string|null $content
   *   File contents for the request. Will be send in the body.
   *
   * @return object|string
   *   The response form the Dropbox Api. If the response was a json encoded
   *   object, that object will be returned, otherwise the unaltered response
   *   will be returned, e.g. the contents of a file.
   *
   * @throws RuntimeException
   *   Excerpt from the Dropbox documentation on error handling:
   *   - 400 Bad input parameter. The response body is a plaintext message with
   *     more information.
   *   - 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. The Content-Type of
   *     the response is JSON of typeAuthError.
   *   - 403 The user or team account doesn't have access to the endpoint or
   *     feature. The Content-Type of the response is JSON of typeAccessError.
   *   - 409 Endpoint-specific error. Look to the JSON response body for the
   *     specifics of the error.
   *   - 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. The
   *     Content-Type of the response can be JSON or plaintext. If it is JSON,
   *     it will be typeRateLimitError. You can find more information in the
   *     data ingress guide.
   *   - 5xx An error occurred on the Dropbox servers. Check status.dropbox.com
   *     for announcements about Dropbox service issues.
   *   {@see https://www.dropbox.com/developers/documentation/http/documentation#error_handling}.
   */
  private function send_message($endpointType, $command, $parameters = null, $content = null) {

    // Prepare the request: url, headers and the body.
    $headers = [];
    $headers[] = 'Accept: application/json, application/octet-stream';
    if ($command === 'oauth2/token') {

      // The oauth2/token process actually belongs to a 3rd endpoint type,
      // having a different URL and content-type (and therefore body encoding).
      $headers[] = 'Content-type: application/x-www-form-urlencoded';
      $url = "https://{$endpointType}.dropbox.com/{$command}";
      $body = http_build_query($parameters);
    }
    else {
      $url = "https://{$endpointType}.dropboxapi.com/2/{$command}";
      $headers[] = 'Authorization: Bearer ' . $this
        ->get_bearer_token();
      if ($endpointType === 'content') {

        // Content end points (may) have real content in the body and therefore
        // expect the parameters in the 'Dropbox-API-Arg' header.
        $headers[] = 'Content-type: application/octet-stream';
        if (!empty($parameters)) {
          $headers[] = 'Dropbox-API-Arg: ' . json_encode($parameters);
        }
        $body = $content;
      }
      else {
        $headers[] = 'Content-type: application/json; charset=utf-8';
        $body = json_encode($parameters);
      }
    }
    $http_result = $this
      ->send_http_request($url, $headers, $body);

    //$this->log($http_result); // enable during development, or in case of problems.
    if ($http_result->code === 200) {
      if ($this
        ->is_json_response($endpointType, $command)) {
        $result = json_decode($http_result->body);

        // Not sure if errors can be returned with a 200 or if that is only done
        // with a 4xx, to be sure I just check for the error_summary field.
        if (isset($result->error_summary)) {
          $message = $this
            ->get_dropbox_error_message($http_result->body);
          throw new RuntimeException("Dropbox error: {$message}");
        }
      }
      else {

        // Plain result, e.g. file contents: no decoding needed.
        $result = $http_result->body;
      }
    }
    elseif ($http_result->code === 401) {
      $error_message = json_decode($http_result->body)->error_summary;
      if (strpos($error_message, 'expired_access_token') !== FALSE && !$this->is_resending_after_refresh) {
        try {
          $this->is_resending_after_refresh = TRUE;
          $this
            ->refresh_bearer_token();
          $result = $this
            ->send_message($endpointType, $command, $parameters, $content);
        } finally {
          $this->is_resending_after_refresh = FALSE;
        }
      }
      else {
        throw new RuntimeException("Dropbox authentication error: {$error_message}", $http_result->code);
      }
    }
    elseif ($http_result->code < 500) {
      $message = $this
        ->get_dropbox_error_message($http_result->body);
      throw new RuntimeException("Dropbox error: {$message}", $http_result->code);
    }
    else {
      $message = 'An error occurred on the Dropbox servers. Check https://status.dropbox.com/ for announcements about Dropbox service issues.';
      $body_message = $this
        ->get_dropbox_error_message($http_result->body);
      if (!empty($body_message)) {
        $message .= " Details: {$body_message}";
      }
      throw new RuntimeException($message, $http_result->code);
    }
    return $result;
  }

  /**
   * Executes a curl request.
   *
   * @param string $url
   * @param array $headers
   * @param string $body
   *
   * @return object
   *   An object with the following properties:
   *   - body: the returned response
   *   - http_code: the HTTP result code
   *   - meta: an associative array with meta info about the executed curl
   *     request: the result of {@see curl_getinfo()} called without an option
   *     passed in) plus an entry 'method_time', containing the time spent in
   *     the method (which during local development was at most 0.0002s higher
   *     then the key 'total_time' as returned by Curl). Can be used for logging
   *     or timing.
   *
   * @throws RuntimeException
   *   On any error at the curl level, which will be rare, an exception will be
   *   thrown. Thus http responses (and codes) that indicate an error are
   *   returned as an object.
   */
  private function send_http_request($url, $headers, $body) {
    $start = microtime(TRUE);
    $request = $this
      ->get_curl_handle();
    $options = [
      CURLOPT_URL => $url,
      CURLOPT_HTTPHEADER => $headers,
      CURLOPT_POST => TRUE,
      CURLOPT_RETURNTRANSFER => TRUE,
    ];
    if (!empty($body)) {
      $options[CURLOPT_POSTFIELDS] = $body;
    }
    curl_setopt_array($request, $options);
    $result = curl_exec($request);
    if ($result === FALSE || !empty(curl_error($request))) {
      watchdog('backup_migrate', "Backup Migrate Dropbox Error: send_http_request({$url}): Curl error: " . curl_error($request), [], WATCHDOG_ERROR);
      throw new RuntimeException('Curl error: ' . curl_error($request));
    }
    return (object) [
      'body' => $result,
      'code' => (int) curl_getinfo($request, CURLINFO_RESPONSE_CODE),
      'meta' => curl_getinfo($request) + [
        'method_time' => microtime(TRUE) - $start,
      ],
    ];
  }

  /**
   * Returns the Curl handle.
   *
   * If no Curl handle has been created yet, one will be created.
   *
   * Reuse of the curl handle has been seen to be given some performance
   * improvement over creating new handles for each http request. Especially
   * when getting the list of backups, a large number of requests may be sent to
   * Dropbox. But still each request easily takes more than 0.5s, so on large
   * sets of retained back-up files, building this list still takes a
   * considerable amount of time.
   *
   * Note: it would be better to keep a handle per host as the gain is in
   * setting up the connection (name lookup and connecting with ssl). (@todo?)
   *
   * A better performance improvement would be downloading multiple (info) files
   * at once, but the Dropbox API does not offer that. The only feature that
   * Dropbox offers is downloading a whole folder at once as a zip, but then the
   * info files would have to be placed in their own folder, not alongside the
   * back-up files themselves. (@todo?)
   *
   * @return resource
   *
   * @throws RuntimeException
   */
  private function get_curl_handle() {
    if (empty($this->ch)) {
      $this->ch = curl_init();
      if (empty($this->ch)) {
        throw new RuntimeException('Could not open a Curl session');
      }
    }
    else {

      // Reuse the handle but reset the options.
      curl_reset($this->ch);
    }
    return $this->ch;
  }

  /**
   * Extracts a preferably human readable error message from a Dropbox response.
   *
   * See 'Endpoint-specific errors' at
   * @link https://www.dropbox.com/developers/documentation/http/documentation#error_handling.
   *
   * @param string $response
   *
   * @return string
   *   The Dropbox error message.
   */
  private function get_dropbox_error_message($response) {
    $message_object = json_decode($response);
    if ($message_object === NULL) {

      // Plaintext message.
      $message = $response;
    }
    elseif (isset($message_object->user_message)) {
      $message = $message_object->user_message;
    }
    elseif (isset($message_object->error_summary)) {
      $message = $message_object->error_summary;
    }
    else {

      // Message with unknown/unexpected properties: return json string.
      $message = $response;
    }
    return $message;
  }

  /**
   * Returns whether the response should be a json string or file contents.
   *
   * - 'api' endpoints always return json.
   * - 'content' endpoints may return:
   *   - File contents (files/download).
   *   - A json encoded object.
   *   - Nothing (files/upload_session/append_v2).
   *
   * @param string $endpointType
   * @param string $command
   *
   * @return bool
   *   True if the response is expected to be a json string, false otherwise.
   */
  private function is_json_response($endpointType, $command) {
    return $endpointType === 'api' || !in_array($command, [
      'files/download',
      'files/upload_session/append_v2',
    ]);
  }

  /**
   * Encodes a code verifier or code challenge in Base64URL.
   *
   * @link https://dropbox.tech/developers/pkce--what-and-why-
   * @link https://tools.ietf.org/html/rfc4648#section-5
   *
   * @param string $code
   *  The code to encode in base64URL.
   *
   * @return string
   *   The code encoded in base64URL.
   */
  private function base64_url_encode($code) {
    return rtrim(strtr(base64_encode($code), '+/', '-_'), '=');
  }

  /**
   * Logs some debug/tracing/timing information.
   *
   * Will not be used in official releases, should only be used during
   * development or when problems are researched.
   *
   * @param object $http_result
   *   See the return value of {@see send_http_request}
   */
  protected function log($http_result) {
    $meta = $http_result->meta;
    $url = $meta['url'];
    $method_time = $meta['method_time'];
    $total_time = $meta['total_time'];
    $namelookup_time = $meta['namelookup_time'];
    $connect_time = $meta['connect_time'];
    $transfer_time = $total_time - $meta['starttransfer_time'];
    file_put_contents(DRUPAL_ROOT . '/curl.log', sprintf("%s: %-50s: %.4f %.4f %.4f %.4f %.4f\n", date('c'), $url, $method_time, $total_time, $namelookup_time, $connect_time, $transfer_time), FILE_APPEND);
  }

  /**
   * Converts a size pattern to a numeric size.
   *
   * Code pulled from Stack Overflow: http://stackoverflow.com/q/1336581/819883.
   *
   * @param string $byteString
   *
   * @return int
   *
   */
  private function byte_size($byteString) {
    preg_match('/^\\s*([0-9.]+)\\s*([KMGT])B?\\s*$/i', $byteString, $matches);
    if (!(count($matches) >= 3)) {
      return 0;
    }
    $num = (double) $matches[1];
    switch (strtoupper($matches[2])) {

      /** @noinspection PhpMissingBreakStatementInspection */
      case 'T':
        $num *= DRUPAL_KILOBYTE;

      /** @noinspection PhpMissingBreakStatementInspection */
      case 'G':
        $num *= DRUPAL_KILOBYTE;

      /** @noinspection PhpMissingBreakStatementInspection */
      case 'M':
        $num *= DRUPAL_KILOBYTE;
      case 'K':
        $num *= DRUPAL_KILOBYTE;
    }
    return intval($num);
  }

}

Members

Namesort descending Modifiers Type Description Overrides
BackupMigrateDropboxAPI::$ch private property A Curl handle. To reuse the curl handle we keep it open across calls to the Dropbox API.
BackupMigrateDropboxAPI::$destination private property Contains the B&M destination that uses this Dropbox Api object to communicate with Dropbox as this Api needs access to its name, machine name, and settings.
BackupMigrateDropboxAPI::$is_resending_after_refresh private property Indicates if we are resending a request that failed earlier because the bearer token had expired. This prevents resending over and over again in case something else is going wrong.
BackupMigrateDropboxAPI::$upload_session private property @var array
BackupMigrateDropboxAPI::base64_url_encode private function Encodes a code verifier or code challenge in Base64URL.
BackupMigrateDropboxAPI::byte_size private function Converts a size pattern to a numeric size.
BackupMigrateDropboxAPI::create_code_verifier public function Makes a Dropbox code verifier for this installation.
BackupMigrateDropboxAPI::create_folder public function Creates a folder on Dropbox.
BackupMigrateDropboxAPI::file_delete public function Deletes the file at the given $path.
BackupMigrateDropboxAPI::file_download public function Downloads the file from the given Dropbox $path.
BackupMigrateDropboxAPI::file_upload public function Uploads a file to the given path.
BackupMigrateDropboxAPI::get_app_id private function Returns the Dropbox app id of this app.
BackupMigrateDropboxAPI::get_authorize_url public function Returns the (parameterized) authorize URL.
BackupMigrateDropboxAPI::get_bearer_token private function Returns a short lived but not yet expired bearer token.
BackupMigrateDropboxAPI::get_code_challenge private function Returns the code challenge for the given code verifier.
BackupMigrateDropboxAPI::get_curl_handle private function Returns the Curl handle.
BackupMigrateDropboxAPI::get_dropbox_error_message private function Extracts a preferably human readable error message from a Dropbox response.
BackupMigrateDropboxAPI::get_refresh_token private function Returns the refresh token for this Dropbox destination.
BackupMigrateDropboxAPI::is_json_response private function Returns whether the response should be a json string or file contents.
BackupMigrateDropboxAPI::list_folder public function Returns the contents of a Dropbox folder.
BackupMigrateDropboxAPI::log protected function Logs some debug/tracing/timing information.
BackupMigrateDropboxAPI::obtain_refresh_token public function Obtains a first bearer and a refresh token.
BackupMigrateDropboxAPI::refresh_bearer_token private function Refreshes and returns an expired bearer token.
BackupMigrateDropboxAPI::send_http_request private function Executes a curl request.
BackupMigrateDropboxAPI::send_message private function Sends a request to Dropbox and returns the response.
BackupMigrateDropboxAPI::_file_upload_session_append private function Uploads 1 part of a multi-request upload.
BackupMigrateDropboxAPI::_file_upload_session_finish private function Ends a multi-request upload.
BackupMigrateDropboxAPI::_file_upload_session_start private function Starts a multi-request upload.
BackupMigrateDropboxAPI::_file_upload_upload private function Uploads the $contents of a file (with 1 request) to the indicated $path.
BackupMigrateDropboxAPI::__construct public function BackupMigrateDropboxAPI constructor.
BackupMigrateDropboxAPI::__destruct public function