Current Path : /var/www/html/wp-content/plugins/duplicator-pro/src/Models/Storages/ |
Current File : /var/www/html/wp-content/plugins/duplicator-pro/src/Models/Storages/DropboxStorage.php |
<?php /** * * @package Duplicator * @copyright (c) 2022, Snap Creek LLC */ namespace Duplicator\Models\Storages; use DUP_PRO_Dropbox_Transfer_Mode; use DUP_PRO_DropboxClient; use DUP_PRO_DropboxV2Client; use DUP_PRO_Global_Entity; use DUP_PRO_Log; use DUP_PRO_Package; use DUP_PRO_Package_File_Type; use DUP_PRO_Package_Upload_Info; use DUP_PRO_STR; use DUP_PRO_U; use Duplicator\Core\Views\TplMng; use Duplicator\Libs\Snap\SnapUtil; use Duplicator\Utils\Crypt\CryptBlowfish; use Exception; use stdClass; class DropboxStorage extends AbstractStorageEntity implements StorageAuthInterface { /** @var ?DUP_PRO_DropboxV2Client */ protected $client = null; /** * Get default config * * @return array<string,scalar> */ protected static function getDefaultCoinfig() { $config = parent::getDefaultCoinfig(); $config = array_merge( $config, [ 'access_token' => '', 'access_token_secret' => '', 'v2_access_token' => '', 'authorized' => false, ] ); return $config; } /** * Serialize * * Wakeup method. * * @return void */ public function __wakeup() { parent::__wakeup(); if ($this->legacyEntity) { // Old storage entity $this->legacyEntity = false; // Make sure the storage type is right from the old entity $this->storage_type = $this->getSType(); $this->config = [ 'access_token' => $this->dropbox_access_token, 'access_token_secret' => $this->dropbox_access_token_secret, 'v2_access_token' => $this->dropbox_v2_access_token, 'storage_folder' => ltrim($this->dropbox_storage_folder, '/\\'), 'max_packages' => $this->dropbox_max_files, 'authorized' => ($this->dropbox_authorization_state == 4), ]; // reset old values $this->dropbox_access_token = ''; $this->dropbox_access_token_secret = ''; $this->dropbox_v2_access_token = ''; $this->dropbox_storage_folder = ''; $this->dropbox_max_files = 10; $this->dropbox_authorization_state = 0; } } /** * Will be called, automatically, when Serialize * * @return array<string, mixed> */ public function __serialize() // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__serializeFound { $data = parent::__serialize(); unset($data['client']); return $data; } /** * Return the storage type * * @return int */ public static function getSType() { return 1; } /** * Returns the storage type icon. * * @return string Returns the storage icon */ public static function getStypeIcon() { return '<img src="' . esc_url(DUPLICATOR_PRO_IMG_URL . '/dropbox.svg') . '" class="dup-storage-icon" alt="' . esc_attr(static::getStypeName()) . '" />'; } /** * Returns the storage type name. * * @return string */ public static function getStypeName() { return __('Dropbox', 'duplicator-pro'); } /** * Get storage location string * * @return string */ public function getLocationString() { $dropBoxInfo = $this->getAccountInfo(); if (!isset($dropBoxInfo->locale) || $dropBoxInfo->locale == 'en') { return "https://dropbox.com/home/Apps/Duplicator%20Pro" . $this->getStorageFolder(); } else { return "https://dropbox.com/home"; } } /** * Check if storage is valid * * @return bool Return true if storage is valid and ready to use, false otherwise */ public function isValid() { return $this->isAuthorized(); } /** * Is autorized * * @return bool */ public function isAuthorized() { return $this->config['authorized']; } /** * Authorized from HTTP request * * @param string $message Message * * @return bool True if authorized, false if failed */ public function authorizeFromRequest(&$message = '') { try { if (($authCode = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'auth_code')) === '') { throw new Exception(__('Authorization code is empty', 'duplicator-pro')); } $this->name = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'name', ''); $this->notes = SnapUtil::sanitizeDefaultInput(SnapUtil::INPUT_REQUEST, 'notes', ''); $this->config['max_packages'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'max_packages', 10); $this->config['storage_folder'] = self::getSanitizedInputFolder('storage_folder', 'remove'); $this->revokeAuthorization(); $client = $this->getDropboxClient(); if (($token = $client->authenticate($authCode)) === false) { throw new Exception(__("Couldn't connect. Dropbox access token not found.", 'duplicator-pro')); } $this->config['v2_access_token'] = $token; $this->config['authorized'] = true; } catch (Exception $e) { DUP_PRO_Log::trace("Problem authorizing Dropbox access token msg: " . $e->getMessage()); $message = $e->getMessage(); return false; } $message = __('Dropbox is connected successfully and Storage Provider Updated.', 'duplicator-pro'); return true; } /** * Revokes authorization * * @param string $message Message * * @return bool True if authorized, false if failed */ public function revokeAuthorization(&$message = '') { if (!$this->isAuthorized()) { $message = __('Dropbox isn\'t authorized.', 'duplicator-pro'); return true; } try { $client = $this->getDropboxClient(); if ($client->revokeToken() === false) { throw new Exception(__('DropBox can\'t be unauthorized.', 'duplicator-pro')); } $this->config['v2_access_token'] = ''; $this->config['authorized'] = false; } catch (Exception $e) { DUP_PRO_Log::trace("Problem revoking Dropbox access token msg: " . $e->getMessage()); $message = $e->getMessage(); return false; } $message = __('Dropbox is disconnected successfully.', 'duplicator-pro'); return true; } /** * Get authorization URL * * @return string */ public function getAuthorizationUrl() { $dropbox_client = $this->getDropboxClient(false); return $dropbox_client->createAuthUrl(); } /** * Get action key text * * @param string $key Key name (action, pending, failed, cancelled, success) * * @return string */ protected function getActionKeyText($key) { switch ($key) { case 'action': return sprintf( __('Transferring to Dropbox folder:<br/> <i>%1$s</i>', "duplicator-pro"), $this->getStorageFolder() ); case 'pending': return sprintf( __('Transfer to Dropbox folder %1$s is pending', "duplicator-pro"), $this->getStorageFolder() ); case 'failed': return sprintf( __('Failed to transfer to Dropbox folder %1$s', "duplicator-pro"), $this->getStorageFolder() ); case 'cancelled': return sprintf( __('Cancelled before could transfer to Dropbox folder %1$s', "duplicator-pro"), $this->getStorageFolder() ); case 'success': return sprintf( __('Transferred package to Dropbox folder %1$s', "duplicator-pro"), $this->getStorageFolder() ); default: throw new Exception('Invalid key'); } } /** * Render form config fields * * @param bool $echo Echo or return * * @return string */ public function renderConfigFields($echo = true) { return TplMng::getInstance()->render( 'admin_pages/storages/configs/dropbox', [ 'storage' => $this, 'accountInfo' => $this->getAccountInfo(), 'quotaInfo' => $this->getQuota(), 'storageFolder' => $this->config['storage_folder'], 'maxPackages' => $this->config['max_packages'], 'accessToken' => $this->config['access_token'], 'accessTokenSecret' => $this->config['access_token_secret'], 'v2AccessToken' => $this->config['v2_access_token'], ], $echo ); } /** * Update data from http request, this method don't save data, just update object properties * * @param string $message Message * * @return bool True if success and all data is valid, false otherwise */ public function updateFromHttpRequest(&$message = '') { if ((parent::updateFromHttpRequest($message) === false)) { return false; } $this->config['max_packages'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'dropbox_max_files', 10); $this->config['storage_folder'] = self::getSanitizedInputFolder('_dropbox_storage_folder', 'remove'); $message = sprintf( __('Dropbox Storage Updated. Folder: %1$s', 'duplicator-pro'), $this->getStorageFolder() ); return true; } /** * Storages test * * @param string $message Test message * * @return bool return true if success, false otherwise */ public function test(&$message = '') { if (parent::test($message) == false) { return false; } $result = false; $source_handle = null; $source_filepath = null; try { $source_filepath = tempnam(sys_get_temp_dir(), 'DUP'); if ($source_filepath === false) { throw new Exception(__("Couldn't create the temp file for the Dropbox send test", 'duplicator-pro')); } DUP_PRO_Log::trace("Created temp file $source_filepath"); $source_handle = fopen($source_filepath, 'w'); $rnd = rand(); fwrite($source_handle, "$rnd"); DUP_PRO_Log::trace("Wrote $rnd to $source_filepath"); fclose($source_handle); $source_handle = null; // -- Send the file -- $basename = basename($source_filepath); $filepath = $this->getStorageFolder() . "/$basename"; $client = $this->getClientWithAccess(); if ($client == null) { throw new Exception(__("Couldn't get the DropBox client when performing the DropBox file test", 'duplicator-pro')); } DUP_PRO_Log::trace("About to send $source_filepath to $filepath in dropbox"); $upload_result = $client->UploadFile($source_filepath, $filepath); $client->Delete($filepath); /* The values better match or there was a problem */ if ($client->checkFileHash($upload_result, $source_filepath)) { DUP_PRO_Log::trace("Files match!"); $result = true; $message = __('Successfully stored and retrieved file', 'duplicator-pro'); } else { DUP_PRO_Log::traceError("mismatch in files"); $message = __('There was a problem storing or retrieving the temporary file on this account.', 'duplicator-pro'); } } catch (Exception $ex) { DUP_PRO_Log::trace($ex->getMessage()); $message = $ex->getMessage(); } if (file_exists($source_filepath)) { DUP_PRO_Log::trace("Removing temp file $source_filepath"); unlink($source_filepath); } if ($result) { $this->testLog->addMessage(__('Successfully stored and deleted file', 'duplicator-pro')); $message = __('Successfully stored and deleted file', 'duplicator-pro'); return true; } else { return false; } } /** * Copies the package files from the default local storage to another local storage location * * @param DUP_PRO_Package $package the package * @param DUP_PRO_Package_Upload_Info $upload_info the upload info * * @return void */ public function copyFromDefault(DUP_PRO_Package $package, DUP_PRO_Package_Upload_Info $upload_info) { DUP_PRO_Log::infoTrace("Copyng to Storage " . $this->name . '[ID: ' . $this->id . '] type:' . $this->getStypeName()); $source_archive_filepath = $package->getLocalPackageFilePath(DUP_PRO_Package_File_Type::Archive); $source_installer_filepath = $package->getLocalPackageFilePath(DUP_PRO_Package_File_Type::Installer); if ($source_archive_filepath === false) { DUP_PRO_Log::traceError("Archive doesn't exist for $package->Name!? - $source_archive_filepath"); $upload_info->failed = true; } if ($source_installer_filepath === false) { DUP_PRO_Log::traceError("Installer doesn't exist for $package->Name!? - $source_installer_filepath"); $upload_info->failed = true; } if ($upload_info->failed == true) { DUP_PRO_Log::infoTrace('Dropbox storage failed flag ($upload_info->failed) has been already set.'); $package->update(); return; } try { $client = $this->getClientWithAccess(); $dropbox_archive_path = basename($source_archive_filepath); $dropbox_archive_path = $this->getStorageFolder() . "/$dropbox_archive_path"; $dest_installer_filename = $package->Installer->getInstallerName(); $dropbox_installer_path = $this->getStorageFolder() . "/$dest_installer_filename"; if (!$upload_info->copied_installer) { DUP_PRO_Log::trace("ATTEMPT: Dropbox upload installer file $source_installer_filepath to $dropbox_installer_path"); $installer_meta = $client->UploadFile($source_installer_filepath, $dropbox_installer_path, $dest_installer_filename); if (!$client->checkFileHash($installer_meta, $source_installer_filepath)) { throw new Exception( "**ERROR: installer upload to DropBox" . $dropbox_installer_path . ". Uploaded installer file may be corrupted. Hashes don't match." ); } DUP_PRO_Log::infoTrace("SUCCESS: installer upload to DropBox " . $dropbox_installer_path); $upload_info->copied_installer = true; $upload_info->progress = 5; } else { DUP_PRO_Log::trace("Already uploaded installer on previous execution of Dropbox $this->name so skipping"); } if (!$upload_info->copied_archive) { /* Delete the archive if we are just starting it (in the event they are pushing another copy */ if ($upload_info->archive_offset == 0) { DUP_PRO_Log::trace("Archive offset is 0 so deleting $dropbox_archive_path"); try { $client->Delete($dropbox_archive_path); } catch (Exception $ex) { // Burying exceptions } } $global = DUP_PRO_Global_Entity::getInstance(); $dropbox_upload_info = $client->upload_file_chunk( $source_archive_filepath, $dropbox_archive_path, $global->dropbox_upload_chunksize_in_kb * 1024, $global->php_max_worker_time_in_sec, $upload_info->archive_offset, $upload_info->upload_id, $global->getMicrosecLoadReduction() ); $upload_info->archive_offset = isset($dropbox_upload_info->next_offset) ? $dropbox_upload_info->next_offset : 0; $upload_info->upload_id = $dropbox_upload_info->upload_id; if ($dropbox_upload_info->error_details !== null) { throw new Exception("FAIL: archive upload to dropbox. Error received from Dropbox API: $dropbox_upload_info->error_details"); } // Clear the failure count - we are just looking for consecutive errors $file_size = filesize($source_archive_filepath); $upload_info->progress = max(5, DUP_PRO_U::percentage($upload_info->archive_offset, $file_size, 0)); $upload_info->failure_count = 0; DUP_PRO_Log::infoTrace( "Archive upload offset: $upload_info->archive_offset [File size: $file_size] [Upload progress: $upload_info->progress%]" ); if ( $dropbox_upload_info->file_meta != null && property_exists($dropbox_upload_info->file_meta, "size") && $dropbox_upload_info->file_meta->size === $file_size ) { DUP_PRO_Log::infoTrace("UPLOAD FINISHED. FILE META IS " . print_r($dropbox_upload_info->file_meta, true)); $upload_info->copied_archive = true; $this->purgeOldPackages(); } } else { DUP_PRO_Log::trace("Already copied archive on previous execution of Dropbox $this->name so skipping"); } } catch (Exception $e) { $upload_info->increase_failure_count(); DUP_PRO_Log::trace("Exception caught copying package $package->Name to " . $this->getStorageFolder() . " " . $e->getMessage()); } if ($upload_info->failed) { DUP_PRO_Log::infoTrace('Dropbox storage failed flag ($upload_info->failed) has been already set.'); } // The package update will automatically capture the upload_info since its part of the package $package->update(); } /** * Purge old packages * * @return bool true if success, false otherwise */ public function purgeOldPackages() { if ($this->config['max_packages'] <= 0) { return true; } DUP_PRO_Log::infoTrace("Attempting to purge old packages at " . $this->name . '[ID: ' . $this->id . '] type:' . $this->getSTypeName()); try { $client = $this->getClientWithAccess(); $global = DUP_PRO_Global_Entity::getInstance(); $file_list = $client->GetFiles($this->getStorageFolder()); usort($file_list, array(__CLASS__, 'compareFileDates')); $php_filenames = array(); $archive_filenames = array(); foreach ($file_list as $file_metadata) { if (DUP_PRO_STR::endsWith($file_metadata->file_path, "_{$global->installer_base_name}")) { array_push($php_filenames, $file_metadata); } elseif ( DUP_PRO_STR::endsWith($file_metadata->file_path, '_archive.zip') || DUP_PRO_STR::endsWith($file_metadata->file_path, '_archive.daf') ) { array_push($archive_filenames, $file_metadata); } } DUP_PRO_Log::infoTrace("Dropbox archive file names: " . print_r($archive_filenames, true)); if ($this->config['max_packages'] > 0) { $num_php_files = count($php_filenames); $num_php_to_delete = $num_php_files - $this->config['max_packages']; $index = 0; DUP_PRO_Log::trace("Num php files to delete=$num_php_to_delete"); while ($index < $num_php_to_delete) { $client->Delete($php_filenames[$index]->file_path); $index++; } $index = 0; $num_archives = count($archive_filenames); $num_archives_to_delete = $num_archives - $this->config['max_packages']; DUP_PRO_Log::trace("Num archives to delete=$num_archives_to_delete"); while ($index < $num_archives_to_delete) { $client->Delete($archive_filenames[$index]->file_path); $index++; } } } catch (Exception $e) { DUP_PRO_Log::infoTraceException($e, "FAIL: purge package for storage " . $this->name . '[ID: ' . $this->id . '] type:' . $this->getStypeName()); return false; } DUP_PRO_Log::infoTrace("Purge of old packages at " . $this->getStypeName() . " storage completed."); return true; } /** * Get dropbox client * * @param bool $full_access if true, will return a client with full access * * @return DUP_PRO_DropboxV2Client */ protected function getDropboxClient($full_access = false) { if (is_null($this->client)) { $global = DUP_PRO_Global_Entity::getInstance(); $use_curl = ($global->dropbox_transfer_mode == DUP_PRO_Dropbox_Transfer_Mode::cURL); $configuration = self::getApiKeySecret(); if ($full_access) { $configuration['app_full_access'] = true; } // Note it's possible dropbox is in disabled mode but we are still constructing it. Should have better error handling $this->client = new DUP_PRO_DropboxV2Client($configuration, 'en', $use_curl); } return $this->client; } /** * Set access token * * @return DUP_PRO_DropboxV2Client */ protected function getClientWithAccess() { $client = $this->getDropboxClient(); $this->setV2AccessTokenFromV1Client(); $client->SetAccessToken([ 'v2_access_token' => $this->config['v2_access_token'], ]); return $client; } /** * Get dropbox api key and secret * * @return array{app_key:string,app_secret:string} */ protected static function getApiKeySecret() { $dk = self::getDk1(); $dk = self::getDk2() . $dk; $akey = CryptBlowfish::decrypt('EQNJ53++6/40fuF5ke+IaQ==', $dk); $asec = CryptBlowfish::decrypt('ui25chqoBexPt6QDi9qmGg==', $dk); $akey = trim($akey); $asec = trim($asec); if (($akey != $asec) || ($akey != "fdda100")) { $akey = self::getAk1() . self::getAk2(); $asec = self::getAs1() . self::getAs2(); } $configuration = array( 'app_key' => $asec, 'app_secret' => $akey, ); return $configuration; } /** * Get dk1 * * @return string */ private static function getDk1() { return 'y8!!'; } /** * Get dk2 * * @return string */ private static function getDk2() { return '32897'; } /** * Get ak1 * * @return string */ private static function getAk1() { return strrev('i6gh72iv'); } /** * Get ak2 * * @return string */ private static function getAk2() { return strrev('1xgkhw2'); } /** * Get as1 * * @return string */ private static function getAs1() { return strrev('z7fl2twoo'); } /** * Get as2 * * @return string */ private static function getAs2() { return strrev('2z2bfm'); } /** * Set v2 access token from v1 client * * @return string V2 access token */ protected function setV2AccessTokenFromV1Client() { if (strlen($this->config['v2_access_token']) > 0) { return $this->config['v2_access_token']; } if (strlen($this->config['access_token']) == 0 || strlen($this->config['access_token_secret']) == 0) { return ''; } $useCurl = (DUP_PRO_Global_Entity::getInstance()->dropbox_transfer_mode === DUP_PRO_Dropbox_Transfer_Mode::cURL); $configuration = self::getApiKeySecret(); $dropbox_v1 = new DUP_PRO_DropboxClient($configuration, 'en', $useCurl); $dropbox_v1->SetAccessToken([ 't' => $this->config['access_token'], 's' => $this->config['access_token_secret'], ]); $response = $dropbox_v1->token_from_oauth1(); if (isset($response->access_token)) { $this->config['access_token'] = ''; $this->config['access_token_secret'] = ''; $this->config['v2_access_token'] = $response->access_token; $this->save(); } return $this->config['v2_access_token']; } /** * Get account info * * @return false|object */ protected function getAccountInfo() { if (!$this->config['authorized']) { return false; } return $this->getClientWithAccess()->GetAccountInfo(); } /** * Get dropbox quota * * @return false|array{used:int,total:int,perc:float,available:string} */ protected function getQuota() { if (!$this->config['authorized']) { return false; } $client = $this->getClientWithAccess(); $quota = $client->getQuota(); if ( !isset($quota->used) || !isset($quota->allocation->allocated) || $quota->allocation->allocated <= 0 ) { return false; } $quota_used = $quota->used; $quota_total = $quota->allocation->allocated; $used_perc = round($quota_used * 100 / $quota_total, 1); $available_quota = $quota_total - $quota_used; return array( 'used' => $quota_used, 'total' => $quota_total, 'perc' => $used_perc, 'available' => round($available_quota / 1048576, 1) . ' MB', ); } /** * Dropbox compare file dates * * @param stdClass $a File info * @param stdClass $b File info * * @return int */ protected static function compareFileDates($a, $b) { $a_ts = strtotime($a->modified); $b_ts = strtotime($b->modified); if ($a_ts == $b_ts) { return 0; } return ($a_ts < $b_ts) ? -1 : 1; } }