| Server IP : 216.238.66.20 / Your IP : 216.73.216.249 Web Server : nginx/1.30.4 System : Linux woropds 5.15.0-187-generic #197-Ubuntu SMP Fri Jul 17 19:17:01 UTC 2026 x86_64 User : root ( 0) PHP Version : 8.2.33 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /var/www/sweetheart.mx/htdocs/wp-content/plugins/backwpup/inc/ |
Upload File : |
<?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 );
}
}