oauth_app_key = get_site_option( 'backwpup_cfg_dropboxappkey', base64_decode( 'NXdtdXl0cm5qZzB5aHhw' ) ); $this->oauth_app_secret = BackWPup_Encryption::decrypt( get_site_option( 'backwpup_cfg_dropboxappsecret', base64_decode( 'cXYzZmp2N2IxcG1rbWxy' ) ) ); } else { $this->oauth_app_key = get_site_option( 'backwpup_cfg_dropboxsandboxappkey', base64_decode( 'a3RqeTJwdXFwZWVydW92' ) ); $this->oauth_app_secret = BackWPup_Encryption::decrypt( get_site_option( 'backwpup_cfg_dropboxsandboxappsecret', base64_decode( 'aXJ1eDF3Ym9mMHM5eGp6' ) ) ); } if ( empty( $this->oauth_app_key ) || empty( $this->oauth_app_secret ) ) { throw new BackWPup_Destination_Dropbox_API_Exception( "No App key or App Secret specified." ); } $this->job_object = $job_object; } /** * List a folder * * This is a functions method to use filesListFolder and * filesListFolderContinue to construct an array of files within a given * folder path. * * @param string $path * * @return array */ public function listFolder( $path ) { $files = array(); $result = $this->filesListFolder( array( 'path' => $path ) ); if ( ! $result ) { return array(); } $files = array_merge( $files, $result['entries'] ); $args = array( 'cursor' => $result['cursor'] ); while ( $result['has_more'] == true ) { $result = $this->filesListFolderContinue( $args ); $files = array_merge( $files, $result['entries'] ); } return $files; } /** * Uploads a file to Dropbox. * * @param $file * @param string $path * @param bool $overwrite * * @return array * @throws BackWPup_Destination_Dropbox_API_Exception */ public function upload( $file, $path = '', $overwrite = true ) { $file = str_replace( "\\", "/", $file ); if ( ! is_readable( $file ) ) { throw new BackWPup_Destination_Dropbox_API_Exception( "Error: File \"$file\" is not readable or doesn't exist." ); } if ( filesize( $file ) < 5242880 ) { //chunk transfer on bigger uploads $output = $this->filesUpload( array( 'contents' => file_get_contents( $file ), 'path' => $path, 'mode' => ( $overwrite ) ? 'overwrite' : 'add', ) ); } else { $output = $this->multipartUpload( $file, $path, $overwrite ); } return $output; } /** * @param $file * @param string $path * @param bool $overwrite * * @return array|mixed|string * @throws BackWPup_Destination_Dropbox_API_Exception */ public function multipartUpload( $file, $path = '', $overwrite = true ) { $file = str_replace( "\\", "/", $file ); if ( ! is_readable( $file ) ) { throw new BackWPup_Destination_Dropbox_API_Exception( "Error: File \"$file\" is not readable or doesn't exist." ); } $chunk_size = 4194304; //4194304 = 4MB $file_handel = fopen( $file, 'rb' ); if ( ! $file_handel ) { throw new BackWPup_Destination_Dropbox_API_Exception( "Can not open source file for transfer." ); } 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->filesUploadSessionStart( ); $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_handel, $this->job_object->steps_data[ $this->job_object->step_working ]['offset'] ); } while ( $data = fread( $file_handel, $chunk_size ) ) { $chunk_upload_start = microtime( true ); if ( $this->job_object->is_debug() ) { $this->job_object->log( sprintf( __( 'Uploading %s of data', 'backwpup' ), size_format( strlen( $data ) ) ) ); } $this->filesUploadSessionAppendV2( array( 'contents' => $data, 'cursor' => array( '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; $this->job_object->steps_data[ $this->job_object->step_working ]['totalread'] += strlen( $data ); //args for next chunk $this->job_object->steps_data[ $this->job_object->step_working ]['offset'] += $chunk_size; if ( $this->job_object->job['backuptype'] === 'archive' ) { $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_handel, $this->job_object->steps_data[ $this->job_object->step_working ]['offset'] ); } fclose( $file_handel ); $this->job_object->log( sprintf( __( '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->filesUploadSessionFinish( array( 'cursor' => array( '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' => array( 'path' => $path, 'mode' => ( $overwrite ) ? 'overwrite' : 'add', ), ) ); unset( $this->job_object->steps_data[ $this->job_object->step_working ]['uploadid'] ); unset( $this->job_object->steps_data[ $this->job_object->step_working ]['offset'] ); return $response; } /** * Set the oauth tokens for this request. * * @param $token * * @throws BackWPup_Destination_Dropbox_API_Exception */ public function setOAuthTokens( $token ) { if ( empty( $token['access_token'] ) ) { throw new BackWPup_Destination_Dropbox_API_Exception( "No oAuth token specified." ); } $this->oauth_token = $token; } /** * Returns the URL to authorize the user. * * @return string The authorization URL */ public function oAuthAuthorize() { return self::API_WWW_URL . 'oauth2/authorize?response_type=code&client_id=' . $this->oauth_app_key; } /** * Tkes the oauth code and returns the access token. * * @param string $code The oauth code * * @return array An array including the access token, account ID, and * other information. */ public function oAuthToken( $code ) { return $this->request( 'oauth2/token', array( 'code' => trim( $code ), 'grant_type' => 'authorization_code', 'client_id' => $this->oauth_app_key, 'client_secret' => $this->oauth_app_secret, ), 'oauth' ); } /** * Revokes the auth token. * * @return array */ public function authTokenRevoke() { return $this->request( 'auth/token/revoke' ); } /** * Download * * @throws BackWPup_Destination_Dropbox_API_Exception Because of a rate limit. * * @param array $args Argument for the api request. * * @return mixed Whatever the api request returns. */ public function download( $args, $start_byte = null, $end_byte = null ) { $args['path'] = $this->formatPath( $args['path'] ); if ( $start_byte !== null && $end_byte !== null ) { 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 Information on the deleted file */ public function filesDelete( $args ) { $args['path'] = $this->formatPath( $args['path'] ); try { return $this->request( 'files/delete', $args ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $this->handleFilesDeleteError( $e->getError() ); } } /** * Gets the metadata of a file. * * @param array $args An array of arguments * * @return array The file's metadata */ public function filesGetMetadata( $args ) { $args['path'] = $this->formatPath( $args['path'] ); try { return $this->request( 'files/get_metadata', $args ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $this->handleFilesGetMetadataError( $e->getError() ); } } /** * Gets a temporary link from Dropbox to access the file. * * @param array $args An array of arguments * * @return array Information on the file and link */ public function filesGetTemporaryLink( $args ) { $args['path'] = $this->formatPath( $args['path'] ); try { return $this->request( 'files/get_temporary_link', $args ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $this->handleFilesGetTemporaryLinkError( $e->getError() ); } } /** * Lists all the files within a folder. * * @param array $args An array of arguments * * @return array A list of files */ public function filesListFolder( $args ) { $args['path'] = $this->formatPath( $args['path'] ); try { Return $this->request( 'files/list_folder', $args ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $this->handleFilesListFolderError( $e->getError() ); } } /** * 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 An array of files */ public function filesListFolderContinue( $args ) { try { Return $this->request( 'files/list_folder/continue', $args ); } catch ( BackWPup_Destination_Dropbox_API_Request_Exception $e ) { $this->handleFilesListFolderContinueError( $e->getError() ); } } /** * Uploads a file to Dropbox. * * The file must be no greater than 150 MB. * * @param array $args An array of arguments * * @return array The uploaded file's information. */ public function filesUpload( $args ) { $args['path'] = $this->formatPath( $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->handleFilesUploadError( $e->getError() ); } } /** * Append more data to an uploading file * * @param array $args An array of arguments */ public function filesUploadSessionAppendV2( $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 ( $error['.tag'] == 'incorrect_offset' ) { $args['cursor']['offset'] = $error['correct_offset']; return $this->request( 'files/upload_session/append_v2', $args, 'upload' ); } // Otherwise, can't fix $this->handleFilesUploadSessionLookupError( $error ); } } /** * Finish an upload session. * * @param array $args * * @return array Information on the uploaded file */ public function filesUploadSessionFinish( $args ) { $args['commit']['path'] = $this->formatPath( $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 ( $error['.tag'] == 'lookup_failed' ) { if ( $error['lookup_failed']['.tag'] == 'incorrect_offset' ) { $args['cursor']['offset'] = $error['lookup_failed']['correct_offset']; return $this->request( 'files/upload_session/finish', $args, 'upload' ); } } $this->handleFilesUploadSessionFinishError( $e->getError() ); } } /** * 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 * * @return array An array containing the session's ID. */ public function filesUploadSessionStart( $args = array() ) { return $this->request( 'files/upload_session/start', $args, 'upload' ); } /** * Get user's current account info. * * @return array */ public function usersGetCurrentAccount() { return $this->request( 'users/get_current_account' ); } /** * Get quota info for this user. * * @return array */ public function usersGetSpaceUsage() { return $this->request( 'users/get_space_usage' ); } /** * Request * * @param $url * @param array $args * @param string $endpointFormat * @param string $data * @param bool $echo * @param string $bytes * * @throws BackWPup_Destination_Dropbox_API_Exception * * @return array|mixed|string */ private function request( $endpoint, $args = array(), $endpointFormat = 'rpc', $echo = false, $bytes = null ) { // Get complete URL switch ( $endpointFormat ) { case 'oauth': $url = self::API_URL . $endpoint; break; case 'rpc': $url = self::API_URL . self::API_VERSION_URL . $endpoint; break; case 'upload': case 'download': $url = self::API_CONTENT_URL . self::API_VERSION_URL . $endpoint; break; } if ( $this->job_object && $this->job_object->is_debug() && $endpointFormat != 'oauth' ) { $message = 'Call to ' . $endpoint; $parameters = $args; if ( isset( $parameters['contents'] ) ) { $message .= ', with ' . size_format( strlen( $parameters['contents'] ) ) . ' of data'; unset( $parameters['contents'] ); } if ( ! empty( $parameters ) ) { $message .= ', with parameters: ' . json_encode( $parameters ); } $this->job_object->log( $message ); } // Build cURL Request $ch = curl_init(); curl_setopt( $ch, CURLOPT_URL, $url ); curl_setopt( $ch, CURLOPT_POST, true ); $headers[] = 'Expect:'; if ( $endpointFormat !== 'oauth' ) { $headers[] = 'Authorization: Bearer ' . $this->oauth_token['access_token']; } switch ( $endpointFormat ) { case 'oauth': curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query( $args, null, '&' ) ); $headers[] = 'Content-Type: application/x-www-form-urlencoded'; break; case 'rpc': $args = empty( $args ) ? null : $args; $headers[] = 'Content-Type: application/json'; curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( $args ) ); break; case 'upload': $args['contents'] = isset( $args['contents'] ) ? $args['contents'] : ''; curl_setopt( $ch, CURLOPT_POSTFIELDS, $args['contents'] ); unset( $args['contents'] ); $headers[] = 'Content-Type: application/octet-stream'; $headers[] = empty( $args ) ? 'Dropbox-API-Arg: {}' : 'Dropbox-API-Arg: ' . json_encode( $args ); break; case 'download': curl_setopt( $ch, CURLOPT_BINARYTRANSFER, true ); $headers[] = 'Content-Type: text/plain'; $headers[] = 'Dropbox-API-Arg: ' . json_encode( $args ); if ( $bytes !== null ) { $headers[] = "Range: bytes=$bytes"; } break; default: curl_setopt( $ch, CURLOPT_BINARYTRANSFER, true ); $headers[] = 'Dropbox-API-Arg: ' . json_encode( $args ); break; } curl_setopt( $ch, CURLOPT_USERAGENT, BackWPup::get_plugin_data( 'User-Agent' ) ); curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); if ( BackWPup::get_plugin_data( 'cacert' ) ) { curl_setopt( $ch, CURLOPT_SSLVERSION, 1 ); curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); $curl_version = curl_version(); if ( strstr( $curl_version['ssl_version'], 'NSS/' ) === false ) { curl_setopt( $ch, CURLOPT_SSL_CIPHER_LIST, 'ECDHE-RSA-AES256-GCM-SHA384:' . 'ECDHE-RSA-AES128-GCM-SHA256:' . 'ECDHE-RSA-AES256-SHA384:' . 'ECDHE-RSA-AES128-SHA256:' . 'ECDHE-RSA-AES256-SHA:' . 'ECDHE-RSA-AES128-SHA:' . 'ECDHE-RSA-RC4-SHA:' . 'DHE-RSA-AES256-GCM-SHA384:' . 'DHE-RSA-AES128-GCM-SHA256:' . 'DHE-RSA-AES256-SHA256:' . 'DHE-RSA-AES128-SHA256:' . 'DHE-RSA-AES256-SHA:' . 'DHE-RSA-AES128-SHA:' . 'AES256-GCM-SHA384:' . 'AES128-GCM-SHA256:' . 'AES256-SHA256:' . 'AES128-SHA256:' . 'AES256-SHA:' . 'AES128-SHA' ); } if ( defined( 'CURLOPT_PROTOCOLS' ) ) { curl_setopt( $ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS ); } if ( defined( 'CURLOPT_REDIR_PROTOCOLS' ) ) { curl_setopt( $ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS ); } curl_setopt( $ch, CURLOPT_CAINFO, BackWPup::get_plugin_data( 'cacert' ) ); curl_setopt( $ch, CURLOPT_CAPATH, dirname( BackWPup::get_plugin_data( 'cacert' ) ) ); } else { curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); } curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers ); $output = ''; if ( $echo ) { echo curl_exec( $ch ); } else { curl_setopt( $ch, CURLOPT_HEADER, true ); $response = curl_exec( $ch ); if ( stripos( $response, "HTTP/1.0 200 Connection established\r\n\r\n" ) !== false ) { $response = str_ireplace( "HTTP/1.0 200 Connection established\r\n\r\n", '', $response ); } $response = explode( "\r\n\r\n", $response, 2 ); if ( ! empty( $response[1] ) ) { $output = json_decode( $response[1], true ); } } $status = curl_getinfo( $ch ); // Handle error codes // If 409 (endpoint-specific error), let the calling method handle it // Code 429 = rate limited if ( $status['http_code'] == 429 ) { $wait = 0; if ( preg_match( "/retry-after:\s*(.*?)\r/i", $response[0], $matches ) ) { $wait = trim( $matches[1] ); } //only wait if we get a retry-after header. if ( ! empty( $wait ) ) { trigger_error( sprintf( '(429) Your app is making too many requests and is being rate limited. Error 429 can be triggered on a per-app or per-user basis. Wait for %d seconds.', $wait ), E_USER_WARNING ); sleep( $wait ); } else { throw new BackWPup_Destination_Dropbox_API_Exception( '(429) This indicates a transient server error.' ); } //redo request return $this->request( $url, $args, $endpointFormat, $endpointFormat, $echo ); } // We can't really handle anything else, so throw it back to the caller elseif ( isset( $output['error'] ) || $status['http_code'] >= 400 || curl_errno( $ch ) > 0 ) { $code = $status['http_code']; if ( curl_errno( $ch ) != 0 ) { $message = '(' . curl_errno( $ch ) . ') ' . curl_error( $ch ); $code = 0; } elseif ( $status['http_code'] == 400 ) { $message = '(400) Bad input parameter: ' . strip_tags( $response[1] ); } elseif ( $status['http_code'] == 401 ) { $message = '(401) Bad or expired token. This can happen if the user or Dropbox revoked or expired an access token. To fix, you should re-authenticate the user.'; } elseif ( $status['http_code'] == 409 ) { $message = $output['error_summary']; } elseif ( $status['http_code'] >= 500 ) { $message = '(' . $status['http_code'] . ') There is an error on the Dropbox server.'; } else { $message = '(' . $status['http_code'] . ') Invalid response.'; } if ( $this->job_object && $this->job_object->is_debug() ) { $this->job_object->log( 'Response with header: ' . $response[0] ); } throw new BackWPup_Destination_Dropbox_API_Request_Exception( $message, $code, null, isset($output['error']) ? $output['error'] : ['.tag' => 'other'] ); } else { curl_close( $ch ); if ( ! is_array( $output ) ) { return $response[1]; } else { return $output; } } } /** * Formats a path to be valid for Dropbox. * * @param string $path * * @return string The formatted path */ private function formatPath( $path ) { if ( ! empty( $path ) && substr( $path, 0, 1 ) != '/' ) { $path = '/' . rtrim( $path, '/' ); } elseif ( $path == '/' ) { $path = ''; } return $path; } // Error Handlers private function handleFilesDeleteError( $error ) { switch ( $error['.tag'] ) { case 'path_lookup': $this->handleFilesLookupError( $error['path_lookup'] ); break; case 'path_write': $this->handleFilesWriteError( $error['path_write'] ); break; case 'other': trigger_error( 'Could not delete file.', E_USER_WARNING ); break; } } private function handleFilesGetMetadataError( $error ) { switch ( $error['.tag'] ) { case 'path': $this->handleFilesLookupError( $error['path'] ); break; case 'other': trigger_error( 'Cannot look up file metadata.', E_USER_WARNING ); break; } } private function handleFilesGetTemporaryLinkError( $error ) { switch ( $error['.tag'] ) { case 'path': $this->handleFilesLookupError( $error['path'] ); break; case 'other': trigger_error( 'Cannot get temporary link.', E_USER_WARNING ); break; } } private function handleFilesListFolderError( $error ) { switch ( $error['.tag'] ) { case 'path': $this->handleFilesLookupError( $error['path'] ); break; case 'other': trigger_error( 'Cannot list files in folder.', E_USER_WARNING ); break; } } private function handleFilesListFolderContinueError( $error ) { switch ( $error['.tag'] ) { case 'path': $this->handleFilesLookupError( $error['path'] ); break; case 'reset': trigger_error( 'This cursor has been invalidated.', E_USER_WARNING ); break; case 'other': trigger_error( 'Cannot list files in folder.', E_USER_WARNING ); break; } } private function handleFilesLookupError( $error ) { switch ( $error['.tag'] ) { case 'malformed_path': trigger_error( 'The path was malformed.', E_USER_WARNING ); break; case 'not_found': trigger_error( 'File could not be found.', E_USER_WARNING ); break; case 'not_file': trigger_error( 'That is not a file.', E_USER_WARNING ); break; case 'not_folder': trigger_error( 'That is not a folder.', E_USER_WARNING ); break; case 'restricted_content': trigger_error( 'This content is restricted.', E_USER_WARNING ); break; case 'invalid_path_root': trigger_error( 'Path root is invalid.', E_USER_WARNING ); break; case 'other': trigger_error( 'File could not be found.', E_USER_WARNING ); break; } } private function handleFilesUploadSessionFinishError( $error ) { switch ( $error['.tag'] ) { case 'lookup_failed': $this->handleFilesUploadSessionLookupError( $error['lookup_failed'] ); break; case 'path': $this->handleFilesWriteError( $error['path'] ); break; case 'too_many_shared_folder_targets': trigger_error( 'Too many shared folder targets.', E_USER_WARNING ); break; case 'other': trigger_error( 'The file could not be uploaded.', E_USER_WARNING ); break; } } private function handleFilesUploadSessionLookupError( $error ) { switch ( $error['.tag'] ) { case 'not_found': trigger_error( 'Session not found.', E_USER_WARNING ); break; case 'incorrect_offset': trigger_error( 'Incorrect offset given. Correct offset is ' . $error['correct_offset'] . '.', E_USER_WARNING ); break; case 'closed': trigger_error( 'This session has been closed already.', E_USER_WARNING ); break; case 'not_closed': trigger_error( 'This session is not closed.', E_USER_WARNING ); break; case 'other': trigger_error( 'Could not look up the file session.', E_USER_WARNING ); break; } } private function handleFilesUploadError( $error ) { switch ( $error['.tag'] ) { case 'path': $this->handleFilesUploadWriteFailed( $error['path'] ); break; case 'other': trigger_error( 'There was an unknown error when uploading the file.', E_USER_WARNING ); break; } } private function handleFilesUploadWriteFailed( $error ) { $this->handleFilesWriteError( $error['reason'] ); } private function handleFilesWriteError( $error ) { $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; } trigger_error( $message, E_USER_WARNING ); } }