PATH:
home
/
thebhoeo
/
.trash
/
backwpup
/
inc
<?php use Inpsyde\BackWPup\Infrastructure\Http\Authentication\BasicAuthCredentials; use Inpsyde\BackWPup\Infrastructure\Http\Client\WpHttpClient; use Inpsyde\BackWPup\Infrastructure\Http\Message\Decorator\AuthorizationRequest; use Inpsyde\BackWPup\Infrastructure\Http\Message\Decorator\FormRequest; use Inpsyde\BackWPup\Infrastructure\Http\Message\Decorator\JsonRequest; use Inpsyde\BackWPup\Infrastructure\Http\Message\RequestFactory; use Inpsyde\BackWPup\Infrastructure\Http\Message\ResponseFactory; use Inpsyde\BackWPup\Infrastructure\Http\Message\StreamFactory; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Webmozart\Assert\Assert; use WPMedia\BackWPup\Backup\FailureContext\Dropbox\DropboxFailureContextMapper; use WPMedia\BackWPup\Backup\ReasonCode; /** * Class for communicating with Dropbox API V2. */ class BackWPup_Destination_Dropbox_API { /** * URL to Dropbox API endpoint. */ public const API_URL = 'https://api.dropboxapi.com/'; /** * URL to Dropbox content endpoint. */ public const API_CONTENT_URL = 'https://content.dropboxapi.com/'; /** * URL to Dropbox for authentication. */ public const API_WWW_URL = 'https://www.dropbox.com/'; /** * API version. */ public const API_VERSION_URL = '2/'; /** * OAuth app key. * * @var string */ private $oauth_app_key = ''; /** * OAuth app secret. * * @var string */ private $oauth_app_secret = ''; /** * OAuth token data. * * @var array */ private $oauth_token = []; /** * Job object for logging. * * @var BackWPup_Job */ private $job_object; /** * Callback to call when token is refreshed. * * @var callable */ private $listener; /** * The user agent to use in Dropbox requests. * * @var string */ private $user_agent; /** * SSL ca-bundle path. * * @var string */ private $ca_bundle; /** * Failure context mapper. * * @var DropboxFailureContextMapper */ private $failure_mapper; /** * BackWPup_Destination_Dropbox_API constructor. * * @param string $boxtype Dropbox type. * @param BackWPup_Job|null $job_object Job object for logging. * * @throws BackWPup_Destination_Dropbox_API_Exception When credentials are missing. */ public function __construct( $boxtype = 'dropbox', ?BackWPup_Job $job_object = null ) { if ( 'dropbox' === $boxtype ) { $this->oauth_app_key = get_site_option( 'backwpup_cfg_dropboxappkey', '5wmuytrnjg0yhxp' ); $this->oauth_app_secret = BackWPup_Encryption::decrypt( get_site_option( 'backwpup_cfg_dropboxappsecret', 'qv3fjv7b1pmkmlr' ) ); } else { $this->oauth_app_key = get_site_option( 'backwpup_cfg_dropboxsandboxappkey', 'ktjy2puqpeeruov' ); $this->oauth_app_secret = BackWPup_Encryption::decrypt( get_site_option( 'backwpup_cfg_dropboxsandboxappsecret', 'irux1wbof0s9xjz' ) ); } if ( empty( $this->oauth_app_key ) || empty( $this->oauth_app_secret ) ) { throw new BackWPup_Destination_Dropbox_API_Exception( esc_html__( 'No App key or App Secret specified.', 'backwpup' ) ); } $this->job_object = $job_object; $this->failure_mapper = new DropboxFailureContextMapper(); } /** * List a folder. * * This is a functions method to use files_list_folder and * files_list_folder_continue to construct an array of files within a given * folder path. * * @param string $path Folder path. * * @return array */ public function list_folder( $path ) { $files = []; $result = $this->files_list_folder( [ 'path' => $path ] ); if ( ! $result ) { return []; } $files = array_merge( $files, $result['entries'] ); $args = [ 'cursor' => $result['cursor'] ]; while ( true === $result['has_more'] ) { $result = $this->files_list_folder_continue( $args ); $files = array_merge( $files, $result['entries'] ); $args['cursor'] = $result['cursor']; } return $files; } /** * Uploads a file to Dropbox. * * @param string $file File path. * @param string $path Destination path. * @param bool $overwrite Whether to overwrite existing file. * * @return array * @throws BackWPup_Destination_Dropbox_API_Exception When upload fails. */ public function upload( string $file, string $path = '', bool $overwrite = true ) { $file = str_replace( '\\', '/', $file ); if ( ! is_readable( $file ) ) { throw new BackWPup_Destination_Dropbox_API_Exception( sprintf( // translators: %s = File name. esc_html__( "Error: File \"%s\" is not readable or doesn't exist.", 'backwpup' ), esc_html( $file ) ) ); } if ( filesize( $file ) < 5242880 ) { // Chunk transfer on bigger uploads. return $this->files_upload( [ 'contents' => $this->get_file_contents( $file ), 'path' => $path, 'mode' => ( $overwrite ) ? 'overwrite' : 'add', ] ); } return $this->multipart_upload( $file, $path, $overwrite ); } /** * Uploads a file in multiple parts. * * @param string $file File path. * @param string $path Destination path. * @param bool $overwrite Whether to overwrite existing file. * * @return array|mixed|string * @throws BackWPup_Destination_Dropbox_API_Exception When upload fails. */ public function multipart_upload( $file, $path = '', $overwrite = true ) { $file = str_replace( '\\', '/', (string) $file ); if ( ! is_readable( $file ) ) { throw new BackWPup_Destination_Dropbox_API_Exception( sprintf( // translators: %s = File name. esc_html__( "Error: File \"%s\" is not readable or doesn't exist.", 'backwpup' ), esc_html( $file ) ) ); } $chunk_size = 4194304; // 4194304 = 4MB. $file_handle = fopen( $file, 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen if ( ! $file_handle ) { throw new BackWPup_Destination_Dropbox_API_Exception( esc_html__( 'Can not open source file for transfer.', 'backwpup' ) ); } if ( ! isset( $this->job_object->steps_data[ $this->job_object->step_working ]['uploadid'] ) ) { $this->job_object->log( __( 'Beginning new file upload session', 'backwpup' ) ); $session = $this->files_upload_session_start( ); $this->job_object->steps_data[ $this->job_object->step_working ]['uploadid'] = $session['session_id']; } if ( ! isset( $this->job_object->steps_data[ $this->job_object->step_working ]['offset'] ) ) { $this->job_object->steps_data[ $this->job_object->step_working ]['offset'] = 0; } if ( ! isset( $this->job_object->steps_data[ $this->job_object->step_working ]['totalread'] ) ) { $this->job_object->steps_data[ $this->job_object->step_working ]['totalread'] = 0; } // seek to current position. if ( $this->job_object->steps_data[ $this->job_object->step_working ]['offset'] > 0 ) { fseek( $file_handle, $this->job_object->steps_data[ $this->job_object->step_working ]['offset'] ); } while ($data = fread($file_handle, $chunk_size)) { // phpcs:ignore $chunk_upload_start = microtime( true ); if ( $this->job_object->is_debug() ) { $this->job_object->log( // translators: %s is the size of the data. sprintf( __( 'Uploading %s of data', 'backwpup' ), size_format( strlen( $data ) ) ) ); } $this->files_upload_session_append_v2( [ 'contents' => $data, 'cursor' => [ 'session_id' => $this->job_object->steps_data[ $this->job_object->step_working ]['uploadid'], 'offset' => $this->job_object->steps_data[ $this->job_object->step_working ]['offset'], ], ] ); $chunk_upload_time = microtime( true ) - $chunk_upload_start; $chunk_bytes = strlen( $data ); $this->job_object->steps_data[ $this->job_object->step_working ]['totalread'] += $chunk_bytes; // args for next chunk. $this->job_object->steps_data[ $this->job_object->step_working ]['offset'] += $chunk_bytes; if ( 'archive' === $this->job_object->job['backuptype'] ) { $this->job_object->substeps_done = $this->job_object->steps_data[ $this->job_object->step_working ]['offset']; if ( strlen( $data ) === $chunk_size ) { $time_remaining = $this->job_object->do_restart_time(); // calc next chunk. if ( $time_remaining < $chunk_upload_time ) { $chunk_size = floor( $chunk_size / $chunk_upload_time * ( $time_remaining - 3 ) ); if ( $chunk_size < 0 ) { $chunk_size = 1024; } if ( $chunk_size > 4194304 ) { $chunk_size = 4194304; } } } } $this->job_object->update_working_data(); // correct position. fseek( $file_handle, $this->job_object->steps_data[ $this->job_object->step_working ]['offset'] ); } fclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose $this->job_object->log( sprintf( // translators: %s is the size of the data. __( 'Finishing upload session with a total of %s uploaded', 'backwpup' ), size_format( $this->job_object->steps_data[ $this->job_object->step_working ]['totalread'] ) ) ); $response = $this->files_upload_session_finish( [ 'cursor' => [ 'session_id' => $this->job_object->steps_data[ $this->job_object->step_working ]['uploadid'], 'offset' => $this->job_object->steps_data[ $this->job_object->step_working ]['totalread'], ], 'commit' => [ 'path' => $path, 'mode' => ( $overwrite ) ? 'overwrite' : 'add', ], ] ); unset( $this->job_object->steps_data[ $this->job_object->step_working ]['uploadid'], $this->job_object->steps_data[ $this->job_object->step_working ]['offset'] ); return $response; } /** * Set the OAuth tokens for this request. * * @param array $token The array with access and refresh tokens. * @param callable $listener The callback to be called when a new token is fetched. * * @throws BackWPup_Destination_Dropbox_API_Exception When tokens are missing. */ public function set_o_auth_tokens( array $token, $listener = null ) { if ( empty( $token['access_token'] ) ) { throw new BackWPup_Destination_Dropbox_API_Exception( esc_html__( 'No access token provided', 'backwpup' ) ); } if ( empty( $token['refresh_token'] ) ) { throw new BackWPup_Destination_Dropbox_API_Exception( esc_html__( 'No refresh token provided. You may need to reauthenticate with Dropbox', 'backwpup' ) ); } $this->oauth_token = $token; if ( isset( $listener ) && is_callable( $listener ) ) { $this->listener = $listener; } if ( isset( $token['expires'] ) && time() > $token['expires'] ) { $token = $this->refresh( $token['refresh_token'] ); $this->notify_refresh( $token ); } } /** * Get the current token array. * * Also modifies expires_in to match how much time is left until the access token expires. * * @throws \BadMethodCallException If tokens have not been set. * * @return array The token array. */ public function get_tokens() { $now = time(); $tokens = $this->oauth_token; if ( empty( $tokens ) ) { throw new \BadMethodCallException( esc_html__( 'OAuth tokens have not been set.', 'backwpup' ) ); } if ( $tokens['expires'] > $now ) { $tokens['expires_in'] = $tokens['expires'] - $now; } else { $tokens = $this->refresh( $tokens['refresh_token'] ); $this->notify_refresh( $tokens ); } return $tokens; } /** * Returns the URL to authorize the user. * * @return string The authorization URL. */ public function o_auth_authorize() { return self::API_WWW_URL . 'oauth2/authorize?response_type=code&client_id=' . $this->oauth_app_key . '&token_access_type=offline'; } /** * Takes the oauth code and returns the access token. * * @param string $code The OAuth code. * * @return array An array including the access token, account ID, expiration, and other information. */ public function o_auth_token( $code ) { $token = $this->request( 'oauth2/token', [ 'code' => trim( $code ), 'grant_type' => 'authorization_code', ], 'oauth' ); $token['expires'] = time() + $token['expires_in']; return $token; } /** * Returns a new access token given the refresh token. * * @param string $refresh_token The refresh token. * * @return array An array including the access token, account ID, expiration, and other information. */ public function refresh( $refresh_token ) { $token = $this->request( 'oauth2/token', [ 'refresh_token' => trim( $refresh_token ), 'grant_type' => 'refresh_token', ], 'oauth' ); $token['expires'] = time() + $token['expires_in']; $this->oauth_token = array_merge( $this->oauth_token, $token ); return $this->oauth_token; } /** * Notifies the listener that the access token was refreshed. * * @param array $token The new token. * * @return void */ private function notify_refresh( array $token ) { if ( isset( $this->listener ) ) { call_user_func( $this->listener, $token ); } } /** * Revokes the auth token. * * @return array */ public function auth_token_revoke() { return $this->request( 'auth/token/revoke' ); } /** * Download. * * @param array $args Argument for the API request. * @param int|null $start_byte Start byte offset. * @param int|null $end_byte End byte offset. * * @throws BackWPup_Destination_Dropbox_API_Exception Because of a rate limit. * * @return mixed Whatever the API request returns. */ public function download( $args, $start_byte = null, $end_byte = null ) { $args['path'] = $this->format_path( $args['path'] ); if ( null !== $start_byte && null !== $end_byte ) { return $this->request( 'files/download', $args, 'download', false, "{$start_byte}-{$end_byte}" ); } return $this->request( 'files/download', $args, 'download' ); } /** * Deletes a file. * * @param array $args An array of arguments. * * @return array|null Information on the deleted file. */ public function files_delete( $args ): ?array { $args['path'] = $this->format_path( $args['path'] ); try { return $this->request( 'files/delete', $args ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $this->handle_files_delete_error( $e->getError() ); return null; } } /** * Gets the metadata of a file. * * @param array $args An array of arguments. * * @return array|null The file's metadata. */ public function files_get_metadata( $args ): ?array { $args['path'] = $this->format_path( $args['path'] ); try { return $this->request( 'files/get_metadata', $args ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $this->handle_files_get_metadata_error( $e->getError() ); return null; } } /** * Gets a temporary link from Dropbox to access the file. * * @param array $args An array of arguments. * * @return array|null Information on the file and link. */ public function files_get_temporary_link( $args ): ?array { $args['path'] = $this->format_path( $args['path'] ); try { return $this->request( 'files/get_temporary_link', $args ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $this->handle_files_get_temporary_link_error( $e->getError() ); return null; } } /** * Lists all the files within a folder. * * @param array $args An array of arguments. * * @return array|null A list of files. */ public function files_list_folder( $args ): ?array { $args['path'] = $this->format_path( $args['path'] ); try { return $this->request( 'files/list_folder', $args ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $this->handle_files_list_folder_error( $e->getError() ); return null; } } /** * Continue to list more files. * * When a folder has a lot of files, the API won't return all at once. * So this method is to fetch more of them. * * @param array $args An array of arguments. * * @return array|null An array of files. */ public function files_list_folder_continue( $args ): ?array { try { return $this->request( 'files/list_folder/continue', $args ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $this->handle_files_list_folder_continue_error( $e->getError() ); return null; } } /** * Uploads a file to Dropbox. * * The file must be no greater than 150 MB. * * @param array $args An array of arguments. * * @return array|null The uploaded file's information. */ public function files_upload( $args ) { $args['path'] = $this->format_path( $args['path'] ); if ( isset( $args['client_modified'] ) && $args['client_modified'] instanceof DateTime ) { $args['client_modified'] = $args['client_modified']->format( 'Y-m-d\TH:m:s\Z' ); } try { return $this->request( 'files/upload', $args, 'upload' ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $this->handle_files_upload_error( $e->getError(), $e->getContext() ); return null; } } /** * Append more data to an uploading file. * * @param array $args An array of arguments. * * @return array|null */ public function files_upload_session_append_v2( $args ) { try { return $this->request( 'files/upload_session/append_v2', $args, 'upload' ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $error = $e->getError(); // See if we can fix the error first. if ( 'incorrect_offset' === $error['.tag'] ) { $args['cursor']['offset'] = $error['correct_offset']; return $this->request( 'files/upload_session/append_v2', $args, 'upload' ); } // Otherwise, cannot fix. $this->handle_files_upload_session_lookup_error( $error, $e->getContext(), E_USER_ERROR ); return null; } } /** * Finish an upload session. * * @param array $args An array of arguments. * * @return array|null Information on the uploaded file. */ public function files_upload_session_finish( $args ): ?array { $args['commit']['path'] = $this->format_path( $args['commit']['path'] ); try { return $this->request( 'files/upload_session/finish', $args, 'upload' ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $error = $e->getError(); if ( 'lookup_failed' === $error['.tag'] ) { if ( 'incorrect_offset' === $error['lookup_failed']['.tag'] ) { $args['cursor']['offset'] = $error['lookup_failed']['correct_offset']; return $this->request( 'files/upload_session/finish', $args, 'upload' ); } } $this->handle_files_upload_session_finish_error( $e->getError(), $e->getContext() ); return null; } } /** * Starts an upload session. * * When a file larger than 150 MB needs to be uploaded, then this API * endpoint is used to start a session to allow the file to be uploaded in * chunks. * * @param array $args An array of arguments. * * @return array An array containing the session's ID. */ public function files_upload_session_start( $args = [] ) { return $this->request( 'files/upload_session/start', $args, 'upload' ); } /** * Get user's current account info. * * @return array */ public function users_get_current_account() { return $this->request( 'users/get_current_account' ); } /** * Get quota info for this user. * * @return array */ public function users_get_space_usage() { return $this->request( 'users/get_space_usage' ); } /** * Get the user agent. * * If no user agent has been provided, defaults to `BackWPup::get_plugin_data('User-Agent')`. * * @return string The user agent */ public function get_user_agent() { return $this->user_agent ?: \BackWPup::get_plugin_data( 'User-Agent' ); } /** * Set the user agent. * * @param string $user_agent User agent string. */ public function set_user_agent( $user_agent ) { $this->user_agent = $user_agent; } /** * Get the SSL ca-bundle path. * * If no ca-bundle has been provided, defaults to `BackWPup::get_plugin_data('cacert')`. * * @return string The SSL ca-bundle */ public function get_ca_bundle() { return $this->ca_bundle ?: \BackWPup::get_plugin_data( 'cacert' ); } /** * Set the path to the SSL ca-bundle. * * @param string $ca_bundle The path to the ca-bundle file. */ public function set_ca_bundle( $ca_bundle ) { $this->ca_bundle = $ca_bundle; } /** * Set the job object. * * @param BackWPup_Job $job_object The job object to set. * * @return void */ public function set_job_object( BackWPup_Job $job_object ) { $this->job_object = $job_object; } /** * Logs a message to the current job. * * @param string $message The message to log. * @param int $level The log level. * @param array $context Optional context payload. * * @return bool|null True on success, null if no job object set. */ protected function log( $message, $level = E_USER_NOTICE, array $context = [] ) { if ( ! isset( $this->job_object ) ) { return null; } return $this->job_object->log( $message, $level, '', 0, $context ); } /** * Logs debug info about the current request. * * @param string $endpoint The current request endpoint. * @param array $args The request args. * * @return bool|null True on success, null if no job object set or debug is not enabled. */ protected function log_request( $endpoint, array $args ) { if ( ! isset( $this->job_object ) || ! $this->job_object->is_debug() ) { return null; } $message = "Call to {$endpoint}"; if ( isset( $args['contents'] ) ) { $message .= ' with ' . size_format( strlen( (string) $args['contents'] ) ) . ' of data,'; unset( $args['contents'] ); } if ( ! empty( $args ) ) { $message .= ' with parameters ' . wp_json_encode( $args ); } return $this->log( $message ); } /** * Request. * * @param string $endpoint API endpoint. * @param array $args Request arguments. * @param string $endpoint_format Endpoint format. * @param bool $should_echo Whether to echo response body. * @param string|null $bytes Byte range header value. * * @throws BackWPup_Destination_Dropbox_API_Exception When the request fails. * * @return array|mixed|string */ public function request( $endpoint, $args = [], $endpoint_format = 'rpc', $should_echo = false, $bytes = null ) { // Log request. $this->log_request( $endpoint, $args ); if ( null !== $bytes ) { $args['bytes'] = $bytes; } $request = $this->build_request( $endpoint, $args, $endpoint_format ); $client = $this->create_client(); try { $response = $client->sendRequest( $request ); } catch ( \Psr\Http\Client\ClientExceptionInterface $e ) { throw new BackWPup_Destination_Dropbox_API_Exception( esc_html( $e->getMessage() ), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped $e->getCode(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped $e, // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped $this->failure_mapper->map_reason( ReasonCode::REASON_TIMEOUT_OR_NETWORK ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } if ( $response->getStatusCode() >= 500 ) { $this->handle_server_exception( $response ); } elseif ( $response->getStatusCode() >= 400 ) { $this->handle_request_exception( $response ); // If we're still here, then recurse. return $this->request( $endpoint, $args, $endpoint_format, $should_echo, $bytes ); } if ( true === $should_echo ) { echo $response->getBody(); // phpcs:ignore } if ( 'application/json' === $response->getHeaderLine( 'Content-Type' ) ) { return json_decode( $response->getBody(), true ); } return $response->getBody()->getContents(); } /** * Gets the full URL to the Dropbox endpoint. * * @param string $endpoint The API endpoint. * @param string $format The endpoint format. * * @return string The full URL. */ private function get_url( $endpoint, $format ) { Assert::oneOf( $format, [ 'oauth', 'rpc', 'upload', 'download' ] ); switch ( $format ) { case 'oauth': return self::API_URL . $endpoint; case 'rpc': return self::API_URL . self::API_VERSION_URL . $endpoint; default: return self::API_CONTENT_URL . self::API_VERSION_URL . $endpoint; } } /** * Builds the options for the request. * * @param string $endpoint The endpoint to call. * @param array $args The arguments for the request. * @param string $format The endpoint format. * * @return RequestInterface The HTTP request. */ private function build_request( $endpoint, array &$args, $format ) { $url = $this->get_url( $endpoint, $format ); $request = $this->create_request_factory() ->createRequest( 'POST', $url ); if ( 'oauth' !== $format ) { $request = new AuthorizationRequest( $request ); $request = $request->withOAuthToken( $this->get_tokens()['access_token'] ); } $stream_factory = new StreamFactory(); switch ( $format ) { case 'oauth': $request = new AuthorizationRequest( new FormRequest( $request ) ); $request = $request ->withBasicAuth( BasicAuthCredentials::fromUsernameAndPassword( $this->oauth_app_key, $this->oauth_app_secret ) ) ->withFormParams( $args, $stream_factory ) ->withHeader( 'Accept', 'application/json' ); break; case 'rpc': $request = new JsonRequest( $request ); $request = $request ->withJsonData( $args ?: null, $stream_factory ) ->withHeader( 'Accept', 'application/json' ); break; case 'upload': if ( isset( $args['contents'] ) ) { $stream = $stream_factory->createStream( $args['contents'] ); $request = $request->withBody( $stream ); unset( $args['contents'] ); } $request = $request ->withHeader( 'Content-Type', 'application/octet-stream' ) ->withHeader( 'Dropbox-API-Arg', wp_json_encode( $args, JSON_FORCE_OBJECT ) ); break; case 'download': if ( isset( $args['bytes'] ) ) { $request = $request ->withHeader( 'Range', 'bytes=' . $args['bytes'] ); unset( $args['bytes'] ); } $request = $request ->withHeader( 'Content-Type', 'text/plain' ) ->withHeader( 'Accept', 'application/octet-stream' ) ->withHeader( 'Dropbox-API-Arg', wp_json_encode( $args, JSON_FORCE_OBJECT ) ); break; } return $request; } /** * Handle request exception. * * Called for 4xx responses. * * @param ResponseInterface $response The returned response. * * @throws BackWPup_Destination_Dropbox_API_Exception If the error cannot be handled. */ private function handle_request_exception( ResponseInterface $response ) { switch ( $response->getStatusCode() ) { case 400: case 401: case 403: case 409: case 429: $callback = [ $this, 'handle_' . $response->getStatusCode() . '_error' ]; call_user_func( $callback, $response ); break; default: throw new BackWPup_Destination_Dropbox_API_Exception( sprintf( // translators: %1$s is the error code, %2$s is the response body. esc_html__( '(%1$s) An unknown error has occurred. Response from server: %2$s', 'backwpup' ), esc_html( (string) $response->getStatusCode() ), esc_html( $response->getBody()->getContents() ) ) ); } } /** * Handle server exception. * * Called for 5xx responses. * * @param ResponseInterface $response The returned response. * * @throws BackWPup_Destination_Dropbox_API_Exception When the server returns an error. */ protected function handle_server_exception( ResponseInterface $response ) { throw new BackWPup_Destination_Dropbox_API_Exception( sprintf( // translators: %1$d is the error code, %2$s is the response body. esc_html__( '(%1$d) An unexpected server error was encountered. Response from server: %2$s', 'backwpup' ), esc_html( (string) $response->getStatusCode() ), esc_html( $response->getBody()->getContents() ) ), $response->getStatusCode(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped null, $this->failure_mapper->map_api_error( [], $response->getStatusCode() ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } /** * Handle 400 response error. * * @param \Psr\Http\Message\ResponseInterface $response The returned response. * * @throws BackWPup_Destination_Dropbox_API_Exception When the request is invalid. */ protected function handle_400_error( ResponseInterface $response ) { throw new BackWPup_Destination_Dropbox_API_Exception( sprintf( // translators: %s is the response body. esc_html__( '(400) Bad input parameter. Response from server: %s', 'backwpup' ), esc_html( $response->getBody()->getContents() ) ), 400, null, $this->failure_mapper->map_api_error( [], 400 ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } /** * Handle 401 response error. * * @param \Psr\Http\Message\ResponseInterface $response The returned response. * * @throws BackWPup_Destination_Dropbox_API_Exception If token is invalid. */ protected function handle_401_error( ResponseInterface $response ) { $error = json_decode( $response->getBody()->getContents(), true ); if ( 'expired_access_token' === $error['error']['.tag'] ) { $this->refresh( $this->oauth_token['refresh_token'] ); } else { throw new BackWPup_Destination_Dropbox_API_Exception( sprintf( // translators: %s is the response error. esc_html__( '(401) Bad or expired token. Response from server: %s', 'backwpup' ), esc_html( $error['error']['.tag'] ) ), 401, null, $this->failure_mapper->map_api_error( $error['error'], 401 ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } } /** * Handle 403 response error. * * @param \Psr\Http\Message\ResponseInterface $response The returned response. * * @throws BackWPup_Destination_Dropbox_API_Exception When access is denied. */ protected function handle_403_error( ResponseInterface $response ) { $error = json_decode( $response->getBody(), true ); $context = $this->failure_mapper->map_api_error( $error['error'] ?? [], 403 ); if ( 'invalid_account_type' === $error['error']['.tag'] ) { // InvalidAccountTypeError. if ( 'endpoint' === $error['error']['invalid_account_type']['.tag'] ) { throw new BackWPup_Destination_Dropbox_API_Exception( esc_html__( '(403) You do not have permission to access this endpoint.', 'backwpup' ), 403, null, $context // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } if ( 'feature' === $error['error']['invalid_account_type']['.tag'] ) { throw new BackWPup_Destination_Dropbox_API_Exception( esc_html__( '(403) You do not have permission to access this feature.', 'backwpup' ), 403, null, $context // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } } // Catch all. throw new BackWPup_Destination_Dropbox_API_Exception( sprintf( // translators: %s is the response error. esc_html__( '(403) You do not have permission to access this resource. Response from server: %s', 'backwpup' ), esc_html( $error['error_summary'] ) ), 403, null, $context // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } /** * Handle 409 response error. * * @param \Psr\Http\Message\ResponseInterface $response The returned response. * * @throws BackWPup_Destination_Dropbox_API_Request_Exception For endpoint-specific errors. */ protected function handle_409_error( ResponseInterface $response ) { $body = (string) $response->getBody(); $error = json_decode( $body, true ); $error = is_array( $error ) ? $error : []; $context = $this->failure_mapper->map_api_error( $error['error'] ?? [], 409 ); $this->log_debug_409_payload( $error, $context ); $summary = ''; if ( ! empty( $error['error_summary'] ) && is_scalar( $error['error_summary'] ) ) { $summary = (string) $error['error_summary']; } elseif ( '' !== $body ) { $summary = $body; } throw new BackWPup_Destination_Dropbox_API_Request_Exception( sprintf( // translators: %s is the response error. esc_html__( '(409) Endpoint-specific error. Response from server: %s', 'backwpup' ), esc_html( $summary ) ), (int) $response->getStatusCode(), null, is_array( $error['error'] ?? null ) ? $error['error'] : [], // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped $context // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } /** * Handle 429 response error. * * This error is encountered when requests are being rate limited. * * @param \Psr\Http\Message\ResponseInterface $response The returned response. * * @throws BackWPup_Destination_Dropbox_API_Exception If unable to detect time to wait. */ protected function handle_429_error( ResponseInterface $response ) { if ( ! $response->hasHeader( 'Retry-After' ) ) { throw new BackWPup_Destination_Dropbox_API_Exception( esc_html__( '(429) Requests are being rate limited. Please try again later.', 'backwpup' ), 429, null, $this->failure_mapper->map_api_error( [], 429 ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } sleep( (int) $response->getHeaderLine( 'Retry-After' ) ); } /** * Log the normalized Dropbox 409 payload when debug logging is enabled. * * @param array $error Decoded Dropbox error payload. * @param array $context Normalized failure context. * @return void */ private function log_debug_409_payload( array $error, array $context ): void { if ( ! isset( $this->job_object ) || ! $this->job_object->is_debug() ) { return; } $payload = wp_json_encode( [ 'error_summary' => is_scalar( $error['error_summary'] ?? null ) ? (string) $error['error_summary'] : '', 'error' => is_array( $error['error'] ?? null ) ? $error['error'] : [], 'context' => $context, ] ); if ( ! is_string( $payload ) || '' === $payload ) { $this->log( __( 'Dropbox API 409 payload could not be encoded for debug logging.', 'backwpup' ), E_USER_WARNING ); return; } if ( strlen( $payload ) > 2000 ) { $payload = substr( $payload, 0, 2000 ) . '...'; } $this->log( sprintf( /* translators: %s: encoded Dropbox 409 error payload. */ __( 'Dropbox API 409 payload: %s', 'backwpup' ), $payload ) ); } /** * Creates a new HTTP client. * * @return ClientInterface */ protected function create_client() { $options = [ 'timeout' => 60, ]; if ( empty( $this->get_ca_bundle() ) ) { $options['sslverify'] = false; } else { $options += [ 'sslverify' => true, 'sslcertificates' => $this->get_ca_bundle(), ]; } if ( ! empty( $this->get_user_agent() ) ) { $options['user-agent'] = $this->get_user_agent(); } return new WpHttpClient( new ResponseFactory(), new StreamFactory(), $options ); } /** * Creates a request factory for creating requests. * * @return RequestFactory */ protected function create_request_factory() { return new RequestFactory(); } /** * Formats a path to be valid for Dropbox. * * @param string $path Path to format. * * @return string The formatted path. */ private function format_path( $path ) { if ( ! empty( $path ) && '/' !== substr( $path, 0, 1 ) ) { $path = '/' . rtrim( $path, '/' ); } elseif ( '/' === $path ) { $path = ''; } return $path; } /** * Reads a local file using the WordPress filesystem API. * * @param string $file File path. * * @return string File contents. * @throws BackWPup_Destination_Dropbox_API_Exception When the file cannot be read. */ private function get_file_contents( string $file ): string { global $wp_filesystem; if ( ! $wp_filesystem ) { if ( ! function_exists( 'WP_Filesystem' ) ) { $filesystem_path = ''; if ( defined( 'ABSPATH' ) && is_string( ABSPATH ) ) { $filesystem_path = rtrim( ABSPATH, '/\\' ) . '/wp-admin/includes/file.php'; } if ( '' !== $filesystem_path && file_exists( $filesystem_path ) ) { require_once $filesystem_path; } } if ( function_exists( 'WP_Filesystem' ) ) { WP_Filesystem(); } } if ( ! $wp_filesystem instanceof \WP_Filesystem_Base ) { throw new BackWPup_Destination_Dropbox_API_Exception( esc_html__( 'File system could not be initialized.', 'backwpup' ) ); } $contents = $wp_filesystem->get_contents( $file ); if ( false === $contents ) { throw new BackWPup_Destination_Dropbox_API_Exception( esc_html__( 'Cannot read file contents.', 'backwpup' ) ); } return $contents; } // Error handlers. /** * Logs a Dropbox API warning message. * * @param string $message Warning message. * * @return void */ private function log_warning( string $message ): void { $this->log_message( $message, E_USER_WARNING ); } /** * Logs a Dropbox API message with optional context. * * @param string $message Message to log. * @param int $level Log level. * @param array $context Optional normalized context. * * @return void */ private function log_message( string $message, int $level = E_USER_NOTICE, array $context = [] ): void { if ( $this->job_object instanceof BackWPup_Job ) { $this->job_object->log( $message, $level, '', 0, $context ); return; } /** * Fires when the Dropbox API emits a warning outside of a job context. * * @param string $message Warning message. * @param BackWPup_Destination_Dropbox_API $client API client instance. */ do_action( 'backwpup_dropbox_api_warning', $message, $this ); } /** * Handle file delete errors. * * @param array $error Error payload. * * @return void */ private function handle_files_delete_error( $error ) { switch ( $error['.tag'] ) { case 'path_lookup': $this->handle_files_lookup_error( $error['path_lookup'] ); break; case 'path_write': $this->handle_files_write_error( $error['path_write'] ); break; case 'other': $this->log_warning( 'Could not delete file.' ); break; } } /** * Handle file metadata errors. * * @param array $error Error payload. * * @return void */ private function handle_files_get_metadata_error( $error ) { switch ( $error['.tag'] ) { case 'path': $this->handle_files_lookup_error( $error['path'] ); break; case 'other': $this->log_warning( 'Cannot look up file metadata.' ); break; } } /** * Handle temporary link errors. * * @param array $error Error payload. * * @return void */ private function handle_files_get_temporary_link_error( $error ) { switch ( $error['.tag'] ) { case 'path': $this->handle_files_lookup_error( $error['path'] ); break; case 'other': $this->log_warning( 'Cannot get temporary link.' ); break; } } /** * Handle list folder errors. * * @param array $error Error payload. * * @return void */ private function handle_files_list_folder_error( $error ) { switch ( $error['.tag'] ) { case 'path': $this->handle_files_lookup_error( $error['path'] ); break; case 'other': $this->log_warning( 'Cannot list files in folder.' ); break; } } /** * Handle list folder continue errors. * * @param array $error Error payload. * * @return void */ private function handle_files_list_folder_continue_error( $error ) { switch ( $error['.tag'] ) { case 'path': $this->handle_files_lookup_error( $error['path'] ); break; case 'reset': $this->log_warning( 'This cursor has been invalidated.' ); break; case 'other': $this->log_warning( 'Cannot list files in folder.' ); break; } } /** * Handle lookup errors. * * @param array $error Error payload. * * @return void */ private function handle_files_lookup_error( $error ) { switch ( $error['.tag'] ) { case 'malformed_path': $this->log_warning( 'The path was malformed.' ); break; case 'not_found': $this->log_warning( 'File could not be found.' ); break; case 'not_file': $this->log_warning( 'That is not a file.' ); break; case 'not_folder': $this->log_warning( 'That is not a folder.' ); break; case 'restricted_content': $this->log_warning( 'This content is restricted.' ); break; case 'invalid_path_root': $this->log_warning( 'Path root is invalid.' ); break; case 'other': $this->log_warning( 'File could not be found.' ); break; } } /** * Handle upload session finish errors. * * @param array $error Error payload. * @param array $context Normalized failure context. * * @return void */ private function handle_files_upload_session_finish_error( $error, array $context = [] ) { switch ( $error['.tag'] ) { case 'lookup_failed': $this->handle_files_upload_session_lookup_error( $error['lookup_failed'], $context, E_USER_ERROR ); break; case 'path': $this->handle_files_write_error( $error['path'], $context, E_USER_ERROR ); break; case 'too_many_shared_folder_targets': $this->log_message( 'Too many shared folder targets.', E_USER_ERROR, $context ); break; case 'other': $this->log_message( 'The file could not be uploaded.', E_USER_ERROR, $context ); break; default: $this->log_message( 'The file could not be uploaded.', E_USER_ERROR, $context ); break; } } /** * Handle upload session lookup errors. * * @param array $error Error payload. * @param array $context Normalized failure context. * @param int $level Log level. * * @return void */ private function handle_files_upload_session_lookup_error( $error, array $context = [], int $level = E_USER_WARNING ) { switch ( $error['.tag'] ) { case 'not_found': $this->log_message( 'Session not found.', $level, $context ); break; case 'incorrect_offset': $this->log_message( 'Incorrect offset given. Correct offset is ' . intval( $error['correct_offset'] ) . '.', $level, $context ); break; case 'closed': $this->log_message( 'This session has been closed already.', $level, $context ); break; case 'not_closed': $this->log_message( 'This session is not closed.', $level, $context ); break; case 'other': $this->log_message( 'Could not look up the file session.', $level, $context ); break; default: $this->log_message( 'Could not look up the file session.', $level, $context ); break; } } /** * Handle upload errors. * * @param array $error Error payload. * @param array $context Normalized failure context. * * @return void */ private function handle_files_upload_error( $error, array $context = [] ) { if ( ! is_array( $error ) || empty( $error['.tag'] ) ) { $this->log_message( 'There was an unknown error when uploading the file.', E_USER_ERROR, $context ); return; } switch ( $error['.tag'] ) { case 'path': if ( ! empty( $error['reason'] ) && is_array( $error['reason'] ) ) { $this->handle_files_write_error( $error['reason'], $context, E_USER_ERROR ); break; } if ( empty( $error['path'] ) || ! is_array( $error['path'] ) ) { $this->log_message( 'There was an unknown error when uploading the file.', E_USER_ERROR, $context ); break; } if ( ! empty( $error['path']['reason'] ) && is_array( $error['path']['reason'] ) ) { $this->handle_files_upload_write_failed( $error['path'], $context ); break; } $this->handle_files_write_error( $error['path'], $context, E_USER_ERROR ); break; case 'other': $this->log_message( 'There was an unknown error when uploading the file.', E_USER_ERROR, $context ); break; default: $this->log_message( 'There was an unknown error when uploading the file.', E_USER_ERROR, $context ); break; } } /** * Handle upload write failed errors. * * @param array $error Error payload. * @param array $context Normalized failure context. * * @return void */ private function handle_files_upload_write_failed( $error, array $context = [] ) { if ( ! is_array( $error ) || empty( $error['reason'] ) || ! is_array( $error['reason'] ) ) { $this->log_message( 'There was an unknown error when uploading the file.', E_USER_ERROR, $context ); return; } $this->handle_files_write_error( $error['reason'], $context, E_USER_ERROR ); } /** * Handle file write errors. * * @param array $error Error payload. * @param array $context Normalized failure context. * @param int $level Log level. * * @return void */ private function handle_files_write_error( $error, array $context = [], int $level = E_USER_WARNING ) { if ( ! is_array( $error ) || empty( $error['.tag'] ) ) { $this->log_warning( 'There was an unknown error when uploading the file.' ); return; } $message = ''; // Type of error. switch ( $error['.tag'] ) { case 'malformed_path': $message = 'The path was malformed.'; break; case 'conflict': $message = 'Cannot write to the target path due to conflict.'; break; case 'no_write_permission': $message = 'You do not have permission to save to this location.'; break; case 'insufficient_space': $message = 'You do not have enough space in your Dropbox.'; break; case 'disallowed_name': $message = 'The given name is disallowed by Dropbox.'; break; case 'team_folder': $message = 'Unable to modify team folders.'; break; case 'other': $message = 'There was an unknown error when uploading the file.'; break; default: $message = 'There was an unknown error when uploading the file.'; break; } if ( empty( $context ) ) { $context = $this->failure_mapper->map_api_error( $error, 0 ); } $this->log_message( $message, $level, $context ); } }
[-] class-system-tests-runner.php
[edit]
[-] class-jobtype-dbdump.php
[edit]
[-] class-destination-rsc.php
[edit]
[-] class-encryption.php
[edit]
[-] class-destination-downloader-factory.php
[edit]
[-] class-destination-downloader-interface.php
[edit]
[-] class-destination-ftp-type-ftp.php
[edit]
[-] class-path-fixer.php
[edit]
[-] class-message-box.php
[edit]
[-] class-destination-dropbox-api.php
[edit]
[-] class-option.php
[edit]
[-] class-destination-dropbox-api-request-exception.php
[edit]
[-] class-page-about.php
[edit]
[-] class-migrate.php
[edit]
[-] class-system-requirements.php
[edit]
[-] class-adminbar.php
[edit]
[-] class-msazure-destination-configuration.php
[edit]
[-] class-job.php
[edit]
[-] class-destination-ftp-type-exception.php
[edit]
[-] class-destination-sugarsync-api.php
[edit]
[-] class-download-file-interface.php
[edit]
[-] class-encryption-fallback.php
[edit]
[-] BackWPup.php
[edit]
[-] class-page-backwpup.php
[edit]
[-] class-destination-downloader-data.php
[edit]
[-] class-mysqldump-exception.php
[edit]
[-] class-s3-destination.php
[edit]
[-] class-destination-ftp.php
[edit]
[-] class-cron.php
[edit]
[+]
Notice
[-] class-destination-downloader.php
[edit]
[-] class-destinations.php
[edit]
[-] class-destination-ftp-downloader.php
[edit]
[-] class-destination-dropbox-downloader.php
[edit]
[-] class-download-handler.php
[edit]
[-] class-destination-dropbox.php
[edit]
[-] class-system-tests.php
[edit]
[-] class-destination-folder-downloader.php
[edit]
[-] class-destination-ftp-type.php
[edit]
[-] class-thirdparties.php
[edit]
[-] class-jobtype-wpplugin.php
[edit]
[-] class-directory.php
[edit]
[-] class-recursive-directory.php
[edit]
[-] class-destination-msazure-downloader.php
[edit]
[-] class-destination-connect-interface.php
[edit]
[-] class-install.php
[edit]
[-] class-admin.php
[edit]
[-] class-jobtype-file.php
[edit]
[-] functions.php
[edit]
[+]
Utils
[+]
ThirdParty
[-] class-destination-s3-downloader.php
[edit]
[-] class-destination-rsc-downloader.php
[edit]
[-] class-destination-sugarsync-downloader.php
[edit]
[-] class-destination-email.php
[edit]
[+]
..
[-] class-mysqldump.php
[edit]
[-] class-page-firstbackup.php
[edit]
[-] class-jobtype-wpexp.php
[edit]
[-] class-jobtype-dbcheck.php
[edit]
[-] class-file.php
[edit]
[-] class-sanitize-path.php
[edit]
[+]
Settings
[-] class-destination-s3.php
[edit]
[-] class-page-logs.php
[edit]
[-] class-factory-exception.php
[edit]
[-] class-page-restore.php
[edit]
[+]
dependencies
[-] class-create-archive-exception.php
[edit]
[-] class-page-editjob.php
[edit]
[-] class-page-backups.php
[edit]
[-] class-destination-dropbox-api-exception.php
[edit]
[-] class-create-archive.php
[edit]
[-] class-encryption-openssl.php
[edit]
[-] class-destination-onedrive-config-trait.php
[edit]
[-] class-destination-download-exception.php
[edit]
[-] class-download-file.php
[edit]
[-] class-page-settings.php
[edit]
[-] class-destination-msazure.php
[edit]
[-] class-page-onboarding.php
[edit]
[-] class-destination-folder.php
[edit]
[-] class-jobtypes.php
[edit]
[-] class-destination-connect-exception.php
[edit]
[-] class-destination-sugarsync.php
[edit]
[-] class-page-jobs.php
[edit]
[-] class-destination-sugarsync-api-exception.php
[edit]
[-] class-encryption-mcrypt.php
[edit]