first commit
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Duplicator\Addons\DropboxAddon;
|
||||
|
||||
use Duplicator\Addons\DropboxAddon\Models\DropboxStorage;
|
||||
use Duplicator\Addons\DropboxAddon\Utils\Autoloader;
|
||||
use Duplicator\Core\Addons\AbstractAddonCore;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
|
||||
class DropboxAddon extends AbstractAddonCore
|
||||
{
|
||||
const ADDON_PATH = __DIR__;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
Autoloader::register();
|
||||
|
||||
add_action('duplicator_pro_register_storage_types', [$this, 'registerStorages']);
|
||||
add_filter('duplicator_template_file', array(__CLASS__, 'getTemplateFile'), 10, 2);
|
||||
add_filter('duplicator_usage_stats_storages_infos', array(__CLASS__, 'getStorageUsageStats'), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return template file path
|
||||
*
|
||||
* @param string $path path to the template file
|
||||
* @param string $slugTpl slug of the template
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTemplateFile($path, $slugTpl)
|
||||
{
|
||||
if (strpos($slugTpl, 'dropboxaddon/') === 0) {
|
||||
return self::getAddonPath() . '/template/' . $slugTpl . '.php';
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage usage stats
|
||||
*
|
||||
* @param array<string,int> $storageNums Storages num
|
||||
*
|
||||
* @return array<string,int>
|
||||
*/
|
||||
public static function getStorageUsageStats($storageNums)
|
||||
{
|
||||
if (($storages = AbstractStorageEntity::getAll()) === false) {
|
||||
$storages = [];
|
||||
}
|
||||
|
||||
$storageNums['storages_dropbox_count'] = 0;
|
||||
|
||||
foreach ($storages as $storage) {
|
||||
if ($storage->getSType() === DropboxStorage::getSType()) {
|
||||
$storageNums['storages_dropbox_count']++;
|
||||
}
|
||||
}
|
||||
|
||||
return $storageNums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register storages
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerStorages()
|
||||
{
|
||||
DropboxStorage::registerType();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonPath()
|
||||
{
|
||||
return self::ADDON_PATH;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonFile()
|
||||
{
|
||||
return __FILE__;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
<?php
|
||||
|
||||
namespace Duplicator\Addons\DropboxAddon\Models;
|
||||
|
||||
use Duplicator\Addons\DropboxAddon\Utils\DropboxClient;
|
||||
use Duplicator\Models\Storages\AbstractStorageAdapter;
|
||||
use Duplicator\Models\Storages\StoragePathInfo;
|
||||
use VendorDuplicator\Dropbox\Spatie\Dropbox\UploadSessionCursor;
|
||||
|
||||
class DropboxAdapter extends AbstractStorageAdapter
|
||||
{
|
||||
/** @var string */
|
||||
protected $accessToken = '';
|
||||
/** @var DropboxClient */
|
||||
protected $client = null;
|
||||
/** @var string */
|
||||
protected $storageFolder = '';
|
||||
/** @var bool */
|
||||
protected $sslVerify = true;
|
||||
/** @var string If empty use server cert else use custom cert path */
|
||||
protected $sslCert = '';
|
||||
/** @var bool */
|
||||
protected $ipv4Only = false;
|
||||
|
||||
/**
|
||||
* @param string $accessToken Dropbox access token.
|
||||
* @param string $storageFolder Dropbox storage folder.
|
||||
* @param bool $sslVerify If true, use SSL
|
||||
* @param string $sslCert If empty use server cert
|
||||
* @param bool $ipv4Only If true, use IPv4 only
|
||||
*/
|
||||
public function __construct(
|
||||
$accessToken,
|
||||
$storageFolder = '',
|
||||
$sslVerify = true,
|
||||
$sslCert = '',
|
||||
$ipv4Only = false
|
||||
) {
|
||||
$this->accessToken = $accessToken;
|
||||
$this->storageFolder = '/' . trim($storageFolder, '/') . '/';
|
||||
$this->sslVerify = $sslVerify;
|
||||
$this->sslCert = $sslCert;
|
||||
$this->ipv4Only = $ipv4Only;
|
||||
$this->client = new DropboxClient($accessToken, null, DropboxClient::MAX_CHUNK_SIZE, 0, $sslVerify, $sslCert, $ipv4Only);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Dropbox client.
|
||||
*
|
||||
* @return DropboxClient
|
||||
*/
|
||||
public function getClient()
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the storage on creation.
|
||||
*
|
||||
* @param string $errorMsg The error message if storage is invalid.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
public function initialize(&$errorMsg = '')
|
||||
{
|
||||
if (! $this->exists('/')) {
|
||||
try {
|
||||
$this->createDir('/');
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::trace($e->getMessage());
|
||||
$errorMsg = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the storage on deletion.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
public function destroy()
|
||||
{
|
||||
$this->delete('/', true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is valid and ready to use.
|
||||
*
|
||||
* @param string $errorMsg The error message if storage is invalid.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(&$errorMsg = '')
|
||||
{
|
||||
try {
|
||||
$this->client->getMetadata($this->storageFolder);
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::trace("Dropbox storage is invalid: " . $e->getMessage());
|
||||
$errorMsg = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the directory specified by pathname, recursively if necessary.
|
||||
*
|
||||
* @param string $path The directory path.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
protected function realCreateDir($path)
|
||||
{
|
||||
$path = $this->formatPath($path);
|
||||
|
||||
try {
|
||||
$this->client->createFolder($path);
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::trace($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file with content.
|
||||
*
|
||||
* @param string $path The path to file.
|
||||
* @param string $content The content of file.
|
||||
*
|
||||
* @return false|int The number of bytes that were written to the file, or false on failure.
|
||||
*/
|
||||
protected function realCreateFile($path, $content)
|
||||
{
|
||||
$path = $this->formatPath($path);
|
||||
|
||||
try {
|
||||
$response = $this->client->upload($path, $content, 'overwrite');
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::trace($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response['size'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete relative path from storage root.
|
||||
*
|
||||
* @param string $path The path to delete. (Accepts directories and files)
|
||||
* @param bool $recursive Allows the deletion of nested directories specified in the pathname. Default to false.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
protected function realDelete($path, $recursive = false)
|
||||
{
|
||||
$path = $this->formatPath($path);
|
||||
if (! $recursive) {
|
||||
try {
|
||||
$response = $this->client->listFolder($path);
|
||||
if (count($response['entries']) > 0) {
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Path is not a directory, so we can delete it.
|
||||
}
|
||||
}
|
||||
try {
|
||||
$this->client->delete($path);
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::trace($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file content.
|
||||
*
|
||||
* @param string $path The path to file.
|
||||
*
|
||||
* @return string|false The content of file or false on failure.
|
||||
*/
|
||||
public function getFileContent($path)
|
||||
{
|
||||
$content = '';
|
||||
|
||||
try {
|
||||
$stream = $this->client->download($this->formatPath($path));
|
||||
while ($chunk = fgets($stream)) {
|
||||
$content .= $chunk;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::trace($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move and/or rename a file or directory.
|
||||
*
|
||||
* @param string $oldPath Relative storage path
|
||||
* @param string $newPath Relative storage path
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
protected function realMove($oldPath, $newPath)
|
||||
{
|
||||
$oldPath = $this->formatPath($oldPath);
|
||||
$newPath = $this->formatPath($newPath);
|
||||
|
||||
try {
|
||||
$this->client->move($oldPath, $newPath);
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::trace($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path info.
|
||||
*
|
||||
* @param string $path Relative storage path, if empty, return root path info.
|
||||
*
|
||||
* @return StoragePathInfo The path info or false if path is invalid.
|
||||
*/
|
||||
protected function getRealPathInfo($path)
|
||||
{
|
||||
try {
|
||||
$response = $this->client->getMetadata($this->formatPath($path));
|
||||
} catch (\Exception $e) {
|
||||
$response = [];
|
||||
}
|
||||
|
||||
return $this->buildPathInfo($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of files and directories inside the specified path.
|
||||
*
|
||||
* @param string $path Relative storage path, if empty, scan root path.
|
||||
* @param bool $files If true, add files to the list. Default to true.
|
||||
* @param bool $folders If true, add folders to the list. Default to true.
|
||||
*
|
||||
* @return string[] The list of files and directories, empty array if path is invalid.
|
||||
*/
|
||||
public function scanDir($path, $files = true, $folders = true)
|
||||
{
|
||||
$path = rtrim($this->formatPath($path), '/') . '/';
|
||||
|
||||
$filterFunc = function ($entry) use ($files, $folders) {
|
||||
if ($entry['.tag'] === 'file' && $files) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($entry['.tag'] === 'folder' && $folders) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
try {
|
||||
$response = $this->client->listFolder($path);
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::trace('[DropboxAddon] ' . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
|
||||
// We filter out the entries as needed, then only keep the path.
|
||||
// We do this early to keep the memory usage as low as possible.
|
||||
$entries = array_map(function ($entry) use ($path) {
|
||||
return substr($entry['path_display'], strlen($path));
|
||||
}, array_filter($response['entries'], $filterFunc));
|
||||
|
||||
while ($response['has_more']) {
|
||||
$response = $this->client->listFolderContinue($response['cursor']);
|
||||
$entries = array_merge($entries, array_map(function ($entry) use ($path) {
|
||||
return substr($entry['path_display'], strlen($path));
|
||||
}, array_filter($response['entries'], $filterFunc)));
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if directory is empty.
|
||||
*
|
||||
* @param string $path The folder path
|
||||
* @param string[] $filters Filters to exclude files and folders from the check, if start and end with /, use regex.
|
||||
*
|
||||
* @return bool True is ok, false otherwise
|
||||
*/
|
||||
public function isDirEmpty($path, $filters = [])
|
||||
{
|
||||
$path = $this->formatPath($path);
|
||||
try {
|
||||
$response = $this->client->listFolder($path);
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::trace($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
if (count($response['entries']) === 0) {
|
||||
return true;
|
||||
} elseif (empty($filters)) {
|
||||
// we have no filters, and the folder is not empty, so it must contain something
|
||||
return false;
|
||||
}
|
||||
$regexFilters = $normalFilters = [];
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if ($filter[0] === '/' && substr($filter, -1) === '/') {
|
||||
$regexFilters[] = $filter; // It's a regex filter as it starts and ends with a slash
|
||||
} else {
|
||||
$normalFilters[] = $filter;
|
||||
}
|
||||
}
|
||||
|
||||
$contents = $this->scanDir($path);
|
||||
foreach ($contents as $item) {
|
||||
if (in_array($item, $normalFilters)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($regexFilters as $regexFilter) {
|
||||
if (preg_match($regexFilter, $item) === 1) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy local file to storage, partial copy is supported.
|
||||
* If destination file exists, it will be overwritten.
|
||||
* If offset is less than the destination file size, the file will be truncated.
|
||||
*
|
||||
* @param string $sourceFile The source file full path
|
||||
* @param string $storageFile Storage destination path
|
||||
* @param int<0,max> $offset The offset where the data starts.
|
||||
* @param int $length The maximum number of bytes read. Default to -1 (read all the remaining buffer).
|
||||
* @param int $timeout The timeout for the copy operation in microseconds. Default to 0 (no timeout).
|
||||
* @param array<string,mixed> $extraData Extra data to pass to copy function and updated during copy.
|
||||
* Used for storages that need to maintain persistent data during copy intra-session.
|
||||
*
|
||||
* @return false|int The number of bytes that were written to the file, or false on failure.
|
||||
*/
|
||||
protected function realCopyToStorage($sourceFile, $storageFile, $offset = 0, $length = -1, $timeout = 0, &$extraData = [])
|
||||
{
|
||||
$storageFile = $this->formatPath($storageFile);
|
||||
|
||||
if (!$handle = fopen($sourceFile, 'rb')) {
|
||||
\DUP_PRO_Log::trace("[DropboxAddon] Could not open source file: {$sourceFile}");
|
||||
return false;
|
||||
}
|
||||
fseek($handle, $offset);
|
||||
$chunkSize = $length > 0 ? $length : MB_IN_BYTES;
|
||||
|
||||
if (!empty($extraData['sessionId'])) {
|
||||
$sessionId = $extraData['sessionId'];
|
||||
$cursor = new UploadSessionCursor($sessionId, $offset);
|
||||
} else {
|
||||
// We need to start a new session.
|
||||
try {
|
||||
$contents = fread($handle, $chunkSize);
|
||||
$cursor = $this->client->uploadSessionStart($contents);
|
||||
$extraData['sessionId'] = $cursor->session_id;
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::trace($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
\DUP_PRO_Log::info("[Dropbox] Setting timeout for upload request for: {$storageFile} to " . ($timeout / SECONDS_IN_MICROSECONDS) . " seconds");
|
||||
$this->client->setTimeout($timeout / SECONDS_IN_MICROSECONDS);
|
||||
while (($contents = fread($handle, $chunkSize)) && ($length < 0 || $offset > 0)) {
|
||||
try {
|
||||
$this->client->uploadSessionAppend($contents, $cursor);
|
||||
} catch (\Exception $e) {
|
||||
unset($extraData['sessionId']);
|
||||
$this->client->setTimeout(0);
|
||||
\DUP_PRO_Log::trace('[DropboxAddon] ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
if ($length > 0) {
|
||||
// A specific length was requested, so we can break out of the loop.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$fileSize = filesize($sourceFile);
|
||||
$this->client->setTimeout(0);
|
||||
\DUP_PRO_Log::info("[Dropbox] uploaded {$storageFile} from {$offset} to {$cursor->offset}, progress: " . ceil($cursor->offset / $fileSize * 100) . "%");
|
||||
// If we have finished uploading, we need to finish the session & clear the cache
|
||||
if ($cursor->offset >= $fileSize) {
|
||||
try {
|
||||
\DUP_PRO_Log::info("[DropboxAddon] Finishing upload request for: {$storageFile}");
|
||||
$this->client->uploadSessionFinish('', $cursor, $storageFile, 'overwrite'); // this will return the file metadata
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::trace('[DropboxAddon] ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
unset($extraData['sessionId']);
|
||||
}
|
||||
|
||||
return $length > 0 ? $length : $fileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize path, add storage root path if needed.
|
||||
*
|
||||
* @param string $path Relative storage path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function formatPath($path)
|
||||
{
|
||||
return $this->storageFolder . ltrim($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build StoragePathInfo object from Dropbox API response.
|
||||
*
|
||||
* @param array<string,mixed> $response Dropbox API response.
|
||||
*
|
||||
* @return StoragePathInfo
|
||||
*/
|
||||
protected function buildPathInfo($response)
|
||||
{
|
||||
$info = new StoragePathInfo();
|
||||
$info->exists = isset($response['.tag']);
|
||||
|
||||
if (!$info->exists) {
|
||||
return $info;
|
||||
}
|
||||
|
||||
$info->path = $this->getRelativeStoragePath($response['path_display']);
|
||||
$info->isDir = $response['.tag'] === 'folder';
|
||||
$info->size = isset($response['size']) ? $response['size'] : 0;
|
||||
$info->created = isset($response['client_modified']) ? strtotime($response['client_modified']) : time();
|
||||
$info->modified = isset($response['server_modified']) ? strtotime($response['server_modified']) : time();
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative storage path from Dropbox path display.
|
||||
*
|
||||
* @param string $path_display Dropbox path display.
|
||||
* @param string $subPath Sub path to remove from the path display.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getRelativeStoragePath($path_display, $subPath = '')
|
||||
{
|
||||
$rootPath = $this->storageFolder;
|
||||
if (!empty($subPath)) {
|
||||
$rootPath .= trim($subPath) . '/';
|
||||
}
|
||||
return substr($path_display, strlen($rootPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy storage file to local file, partial copy is supported.
|
||||
* If destination file exists, it will be overwritten.
|
||||
* If offset is less than the destination file size, the file will be truncated.
|
||||
*
|
||||
* @param string $storageFile The storage file path
|
||||
* @param string $destFile The destination local file full path
|
||||
* @param int<0,max> $offset The offset where the data starts.
|
||||
* @param int $length The maximum number of bytes read. Default to -1 (read all the remaining buffer).
|
||||
* @param int $timeout The timeout for the copy operation in microseconds. Default to 0 (no timeout).
|
||||
* @param array<string,mixed> $extraData Extra data to pass to copy function and updated during copy.
|
||||
* Used for storages that need to maintain persistent data during copy intra-session.
|
||||
*
|
||||
* @return false|int The number of bytes that were written to the file, or false on failure.
|
||||
*/
|
||||
public function copyFromStorage($storageFile, $destFile, $offset = 0, $length = -1, $timeout = 0, &$extraData = [])
|
||||
{
|
||||
if (! $this->exists($storageFile)) {
|
||||
\DUP_PRO_Log::trace("[DropboxAddon] Storage file {$storageFile} does not exist");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! isset($extraData['resuming']) && file_put_contents($destFile, '') === false) {
|
||||
\DUP_PRO_Log::trace("[DropboxAddon] Could not open destination file for writing. File: {$destFile}");
|
||||
return false;
|
||||
}
|
||||
$extraData['resuming'] = true;
|
||||
|
||||
$this->client->setTimeout($timeout / SECONDS_IN_MICROSECONDS);
|
||||
$resource = $this->client->download($this->formatPath($storageFile));
|
||||
if (! $resource) {
|
||||
$this->client->setTimeout(0);
|
||||
\DUP_PRO_Log::trace("[DropboxAddon] Could not open storage file for downloading. File: {$storageFile}");
|
||||
return false;
|
||||
}
|
||||
fseek($resource, $offset);
|
||||
|
||||
$content = '';
|
||||
while (!feof($resource)) {
|
||||
$content .= fread($resource, $length > 0 ? $length : MB_IN_BYTES);
|
||||
if ($length > 0 && strlen($content) > $length) {
|
||||
$content = substr($content, 0, $length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->client->setTimeout(0);
|
||||
if (file_put_contents($destFile, $content, FILE_APPEND) !== false) {
|
||||
return $length < 0 ? strlen($content) : $length;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\DropboxAddon\Models;
|
||||
|
||||
use DUP_PRO_Dropbox_Transfer_Mode;
|
||||
use DUP_PRO_Global_Entity;
|
||||
use DUP_PRO_Log;
|
||||
use Duplicator\Addons\DropboxAddon\Utils\DropboxClient;
|
||||
use Duplicator\Core\Views\TplMng;
|
||||
use Duplicator\Libs\Snap\SnapUtil;
|
||||
use Duplicator\Models\DynamicGlobalEntity;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
use Duplicator\Models\Storages\StorageAuthInterface;
|
||||
use Duplicator\Utils\Crypt\CryptBlowfish;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @property DropboxAdapter $adapter
|
||||
*/
|
||||
class DropboxStorage extends AbstractStorageEntity implements StorageAuthInterface
|
||||
{
|
||||
/**
|
||||
* Get default config
|
||||
*
|
||||
* @return array<string,scalar>
|
||||
*/
|
||||
protected static function getDefaultConfig()
|
||||
{
|
||||
$config = parent::getDefaultConfig();
|
||||
return array_merge(
|
||||
$config,
|
||||
[
|
||||
'access_token' => '',
|
||||
'access_token_secret' => '',
|
||||
'v2_access_token' => '',
|
||||
'authorized' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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/" . ltrim($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->getAdapter()->getClient();
|
||||
if (($token = $client->authenticate($authCode, self::getApiKeySecret())) === 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->getAdapter()->getClient();
|
||||
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
|
||||
*
|
||||
* @todo: This should be refactored to use the new TokenService class.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorizationUrl()
|
||||
{
|
||||
$config = self::getApiKeySecret();
|
||||
|
||||
return DropboxClient::OAUTH2_URL . 'authorize?client_id=' . $config['app_key'] . '&response_type=code';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
'dropboxaddon/configs/dropbox',
|
||||
[
|
||||
'storage' => $this,
|
||||
'accountInfo' => $this->getAccountInfo(),
|
||||
'quotaInfo' => $this->getQuota(),
|
||||
'storageFolder' => $this->config['storage_folder'],
|
||||
'maxPackages' => $this->config['max_packages'],
|
||||
],
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage adapter
|
||||
*
|
||||
* @return DropboxAdapter
|
||||
*/
|
||||
public function getAdapter()
|
||||
{
|
||||
if (! $this->adapter) {
|
||||
$global = DUP_PRO_Global_Entity::getInstance();
|
||||
|
||||
$this->adapter = new DropboxAdapter(
|
||||
$this->setV2AccessTokenFromV1Client(),
|
||||
$this->getStorageFolder(),
|
||||
!$global->ssl_disableverify,
|
||||
($global->ssl_useservercerts ? '' : DUPLICATOR_PRO_CERT_PATH),
|
||||
$global->ipv4_only
|
||||
);
|
||||
}
|
||||
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
return [
|
||||
'app_key' => $asec,
|
||||
'app_secret' => $akey,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 '';
|
||||
}
|
||||
|
||||
$oldToken = [
|
||||
't' => $this->config['access_token'],
|
||||
's' => $this->config['access_token_secret'],
|
||||
];
|
||||
|
||||
$accessToken = DropboxClient::accessTokenFromOauth1($oldToken, self::getApiKeySecret());
|
||||
|
||||
if ($accessToken) {
|
||||
$this->config['access_token'] = '';
|
||||
$this->config['access_token_secret'] = '';
|
||||
$this->config['v2_access_token'] = $accessToken;
|
||||
$this->save();
|
||||
} else {
|
||||
DUP_PRO_Log::trace("Problem converting Dropbox access token");
|
||||
$this->config['v2_access_token'] = '';
|
||||
}
|
||||
|
||||
return $this->config['v2_access_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account info
|
||||
*
|
||||
* @return false|array<string,mixed>
|
||||
*/
|
||||
protected function getAccountInfo()
|
||||
{
|
||||
if (!$this->isAuthorized()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return $this->getAdapter()->getClient()->getAccountInfo();
|
||||
} catch (Exception $e) {
|
||||
DUP_PRO_Log::trace("Problem getting Dropbox account info. " . $e->getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dropbox quota
|
||||
*
|
||||
* @return false|array{used:int,total:int,perc:float,available:string}
|
||||
*/
|
||||
protected function getQuota()
|
||||
{
|
||||
if (!$this->isAuthorized()) {
|
||||
return false;
|
||||
}
|
||||
$quota = $this->getAdapter()->getClient()->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 [
|
||||
'used' => $quota_used,
|
||||
'total' => $quota_total,
|
||||
'perc' => $used_perc,
|
||||
'available' => size_format($available_quota),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk size in bytes
|
||||
*
|
||||
* @return int bytes
|
||||
*/
|
||||
public function getUploadChunkSize()
|
||||
{
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
$size = (int) $dGlobal->getVal('dropbox_upload_chunksize_in_kb', 2000);
|
||||
return $size * KB_IN_BYTES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk timeout in seconds
|
||||
*
|
||||
* @return int timeout in microseconds, 0 unlimited
|
||||
*/
|
||||
public function getUploadChunkTimeout()
|
||||
{
|
||||
$global = DUP_PRO_Global_Entity::getInstance();
|
||||
return (int) ($global->php_max_worker_time_in_sec <= 0 ? 0 : $global->php_max_worker_time_in_sec * SECONDS_IN_MICROSECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function registerType()
|
||||
{
|
||||
parent::registerType();
|
||||
|
||||
add_action('duplicator_update_global_storage_settings', function () {
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
|
||||
foreach (static::getDefaultSettings() as $key => $default) {
|
||||
$value = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, $key, $default);
|
||||
$dGlobal->setVal($key, $value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default settings
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*/
|
||||
protected static function getDefaultSettings()
|
||||
{
|
||||
return [
|
||||
'dropbox_upload_chunksize_in_kb' => 2000,
|
||||
'dropbox_transfer_mode' => DUP_PRO_Dropbox_Transfer_Mode::Unconfigured,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function renderGlobalOptions()
|
||||
{
|
||||
$values = static::getDefaultSettings();
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
foreach ($values as $key => $default) {
|
||||
$values[$key] = $dGlobal->getVal($key, $default);
|
||||
}
|
||||
?>
|
||||
<h3 class="title"><?php echo esc_html(static::getStypeName()) ?> </h3>
|
||||
<hr size="1" />
|
||||
<table class="form-table">
|
||||
<tr valign="top">
|
||||
<th scope="row"><label><?php esc_html_e("Upload Chunk Size", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
class="dup-narrow-input text-right"
|
||||
name="dropbox_upload_chunksize_in_kb"
|
||||
id="dropbox_upload_chunksize_in_kb"
|
||||
type="number"
|
||||
min="100"
|
||||
data-parsley-required
|
||||
data-parsley-type="number"
|
||||
data-parsley-errors-container="#dropbox_upload_chunksize_in_kb_error_container"
|
||||
value="<?php echo (int) $values['dropbox_upload_chunksize_in_kb']; ?>"
|
||||
> <b>KB</b>
|
||||
<div id="dropbox_upload_chunksize_in_kb_error_container" class="duplicator-error-container"></div>
|
||||
<p class="description">
|
||||
<?php esc_html_e('How much should be uploaded to Dropbox per attempt. Higher=faster but less reliable.', 'duplicator-pro'); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Duplicator\Addons\DropboxAddon\Utils;
|
||||
|
||||
use Duplicator\Addons\DropboxAddon\DropboxAddon;
|
||||
use Duplicator\Utils\AbstractAutoloader;
|
||||
|
||||
class Autoloader extends AbstractAutoloader
|
||||
{
|
||||
const ROOT_VENDOR = 'VendorDuplicator\\Dropbox\\';
|
||||
|
||||
const VENDOR_PATH = DropboxAddon::ADDON_PATH . '/vendor-prefixed/';
|
||||
/**
|
||||
* Register autoloader function
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register()
|
||||
{
|
||||
spl_autoload_register([__CLASS__, 'load']);
|
||||
|
||||
require_once DropboxAddon::ADDON_PATH . '/vendor-prefixed/guzzlehttp/guzzle/src/functions_include.php';
|
||||
require_once DropboxAddon::ADDON_PATH . '/vendor-prefixed/guzzlehttp/promises/src/functions_include.php';
|
||||
require_once DropboxAddon::ADDON_PATH . '/vendor-prefixed/guzzlehttp/psr7/src/functions_include.php';
|
||||
require_once DropboxAddon::ADDON_PATH . '/vendor-prefixed/ralouphie/getallheaders/src/getallheaders.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load class
|
||||
*
|
||||
* @param string $className class name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function load($className)
|
||||
{
|
||||
if (strpos($className, self::ROOT_VENDOR) === 0) {
|
||||
foreach (self::getNamespacesVendorMapping() as $namespace => $mappedPath) {
|
||||
if (strpos($className, $namespace) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filepath = self::getFilenameFromClass($className, $namespace, $mappedPath);
|
||||
if (file_exists($filepath)) {
|
||||
include $filepath;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return namespace mapping
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected static function getNamespacesVendorMapping()
|
||||
{
|
||||
return [
|
||||
self::ROOT_VENDOR . 'Google\\Service' => self::VENDOR_PATH . 'google/apiclient-services/src',
|
||||
self::ROOT_VENDOR . 'Google\\Auth' => self::VENDOR_PATH . 'google/auth/src',
|
||||
self::ROOT_VENDOR . 'Google' => self::VENDOR_PATH . 'google/apiclient/src',
|
||||
self::ROOT_VENDOR . 'GuzzleHttp\\Promise' => self::VENDOR_PATH . 'guzzlehttp/promises/src',
|
||||
self::ROOT_VENDOR . 'GuzzleHttp\\Psr7' => self::VENDOR_PATH . 'guzzlehttp/psr7/src',
|
||||
self::ROOT_VENDOR . 'GuzzleHttp' => self::VENDOR_PATH . 'guzzlehttp/guzzle/src',
|
||||
self::ROOT_VENDOR . 'Psr\\Http\\Message' => self::VENDOR_PATH . 'psr/http-message/src',
|
||||
self::ROOT_VENDOR . 'Psr\\Log' => self::VENDOR_PATH . 'psr/log/Psr/Log',
|
||||
self::ROOT_VENDOR . 'Psr\\Cache' => self::VENDOR_PATH . 'psr/cache/src',
|
||||
self::ROOT_VENDOR . 'Spatie\\Dropbox' => self::VENDOR_PATH . 'spatie/dropbox-api/src',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace Duplicator\Addons\DropboxAddon\Utils;
|
||||
|
||||
use DUP_PRO_Global_Entity;
|
||||
use DUP_PRO_Log;
|
||||
use VendorDuplicator\Dropbox\GuzzleHttp\Client as GuzzleClient;
|
||||
use VendorDuplicator\Dropbox\Spatie\Dropbox\Client;
|
||||
|
||||
class DropboxClient extends Client
|
||||
{
|
||||
const OAUTH2_URL = 'https://www.dropbox.com/oauth2/';
|
||||
|
||||
/**
|
||||
* Class contructor
|
||||
*
|
||||
* @param string $accessToken The access token
|
||||
* @param GuzzleClient|null $client The HTTP client
|
||||
* @param int $maxChunkSize The maximum size of a chunk
|
||||
* @param int $maxUploadChunkRetries The maximum number of times to retry a chunk upload
|
||||
* @param bool $sslVerify If true, use SSL
|
||||
* @param string $sslCert If empty use server cert
|
||||
* @param bool $ipv4Only If true, use IPv4 only
|
||||
*/
|
||||
public function __construct(
|
||||
$accessToken,
|
||||
GuzzleClient $client = null,
|
||||
$maxChunkSize = self::MAX_CHUNK_SIZE,
|
||||
$maxUploadChunkRetries = 0,
|
||||
$sslVerify = true,
|
||||
$sslCert = '',
|
||||
$ipv4Only = false
|
||||
) {
|
||||
if (!$client) {
|
||||
$options = [];
|
||||
if ($sslVerify === false) {
|
||||
$verify = false;
|
||||
} elseif (strlen($sslCert) === 0) {
|
||||
$verify = true;
|
||||
} else {
|
||||
$verify = $sslCert;
|
||||
}
|
||||
$options['verify'] = $verify;
|
||||
if ($ipv4Only) {
|
||||
$options['force_ip_resolve'] = 'v4';
|
||||
}
|
||||
$options['handler'] = self::createHandler();
|
||||
$client = new GuzzleClient($options);
|
||||
}
|
||||
|
||||
parent::__construct($accessToken, $client, $maxChunkSize, $maxUploadChunkRetries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange the oauth1 token for an oauth2 token
|
||||
*
|
||||
* @see https://www.dropbox.com/developers/documentation/http/documentation#auth-token-from_oauth1
|
||||
*
|
||||
* @param array{t: string, s: string} $token The oauth1 token
|
||||
* @param array{app_key: string, app_secret: string} $app_config The app config
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public static function accessTokenFromOauth1($token, $app_config)
|
||||
{
|
||||
$url = "https://api.dropboxapi.com/2/auth/token/from_oauth1";
|
||||
|
||||
$response = wp_remote_post($url, [
|
||||
'timeout' => 30,
|
||||
'headers' => [
|
||||
'Authorization' => 'Basic ' . base64_encode($app_config['app_key'] . ':' . $app_config['app_secret']),
|
||||
],
|
||||
'body' => [
|
||||
'oauth1_token' => $token['t'],
|
||||
'oauth1_token_secret' => $token['s'],
|
||||
],
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
DUP_PRO_Log::traceObject("[DropboxClient] Something wrong with while trying to get v2_access_token with oauth1 token.", $response);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$ret_obj = json_decode($response['body']);
|
||||
|
||||
if (isset($ret_obj->oauth2_token)) {
|
||||
return $ret_obj->oauth2_token;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the app config to authenticate and get the access token
|
||||
*
|
||||
* @param string $auth_code The authorization code
|
||||
* @param array{app_key: string, app_secret: string} $app_config The app config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticate($auth_code, $app_config)
|
||||
{
|
||||
$url = self::OAUTH2_URL . 'token';
|
||||
$args = $this->injectExtraReqArgs([
|
||||
'timeout' => 30,
|
||||
'body' => [
|
||||
'client_id' => $app_config['app_key'],
|
||||
'client_secret' => $app_config['app_secret'],
|
||||
'code' => $auth_code,
|
||||
'grant_type' => 'authorization_code',
|
||||
],
|
||||
]);
|
||||
|
||||
$response = wp_remote_post($url, $args);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
DUP_PRO_Log::traceObject("Something wrong with while trying to get v2_access_token with code", $response);
|
||||
return false;
|
||||
}
|
||||
|
||||
DUP_PRO_Log::traceObject("Got v2 access_token", $response);
|
||||
$ret_obj = json_decode($response['body']);
|
||||
|
||||
if (isset($ret_obj->access_token)) {
|
||||
return $ret_obj->access_token;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the account's usage quota information.
|
||||
*
|
||||
* @return array{used: int, allocation: array{allocated: int}}|false
|
||||
*/
|
||||
public function getQuota()
|
||||
{
|
||||
try {
|
||||
return $this->rpcEndpointRequest('users/get_space_usage');
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::trace('[DropboxClient] ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the timeout for the client
|
||||
*
|
||||
* @param int $timeout The timeout in seconds
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setTimeout($timeout)
|
||||
{
|
||||
if ($timeout > 0) {
|
||||
$this->client = new GuzzleClient(['handler' => self::createHandler(), 'timeout' => $timeout]);
|
||||
} else {
|
||||
$this->client = new GuzzleClient(['handler' => self::createHandler()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject extra request arguments
|
||||
*
|
||||
* @param array<string, mixed> $opts The request options
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function injectExtraReqArgs(array $opts)
|
||||
{
|
||||
$global = DUP_PRO_Global_Entity::getInstance();
|
||||
$opts['sslverify'] = !$global->ssl_disableverify;
|
||||
if (!$global->ssl_useservercerts) {
|
||||
$opts['sslcertificates'] = DUPLICATOR_PRO_CERT_PATH;
|
||||
}
|
||||
return $opts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Duplicator messages sections
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\DropboxAddon\Models\DropboxStorage;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var \Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
* @var DropboxStorage $storage
|
||||
*/
|
||||
$storage = $tplData["storage"];
|
||||
/** @var false|object */
|
||||
$accountInfo = $tplData["accountInfo"];
|
||||
/** @var false|array{used:int,total:int,perc:float,available:string} */
|
||||
$quotaInfo = $tplData["quotaInfo"];
|
||||
/** @var string */
|
||||
$storageFolder = $tplData["storageFolder"];
|
||||
/** @var int */
|
||||
$maxPackages = $tplData["maxPackages"];
|
||||
|
||||
|
||||
$tplMng->render('admin_pages/storages/parts/provider_head');
|
||||
?>
|
||||
<tr>
|
||||
<th scope="row"><label><?php esc_html_e("Authorization", 'duplicator-pro'); ?></label></th>
|
||||
<td class="dropbox-authorize">
|
||||
<div class="authorization-state" id="state-unauthorized">
|
||||
<!-- CONNECT -->
|
||||
<button id="dpro-dropbox-connect-btn" type="button" class="button button-large" onclick="DupPro.Storage.Dropbox.DropboxGetAuthUrl();">
|
||||
<i class="fa fa-plug"></i> <?php esc_html_e('Connect to Dropbox', 'duplicator-pro'); ?>
|
||||
<img
|
||||
src="<?php echo esc_url(DUPLICATOR_PRO_IMG_URL . '/dropbox.svg'); ?>"
|
||||
style='vertical-align: middle; margin:-2px 0 0 3px; height:18px; width:18px'
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="authorization-state" id="state-waiting-for-request-token">
|
||||
<div style="padding:10px">
|
||||
<i class="fas fa-circle-notch fa-spin"></i> <?php esc_html_e('Getting Dropbox request token...', 'duplicator-pro'); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="authorization-state" id="state-waiting-for-auth-button-click">
|
||||
<!-- STEP 2 -->
|
||||
<b><?php esc_html_e("Step 1:", 'duplicator-pro'); ?></b>
|
||||
<?php esc_html_e(' Duplicator needs to authorize at the Dropbox.com website.', 'duplicator-pro'); ?>
|
||||
<div class="auth-code-popup-note">
|
||||
<?php
|
||||
echo esc_html__(
|
||||
'Note: Clicking the button below will open a new tab/window. Please be sure your browser does not block popups.',
|
||||
'duplicator-pro'
|
||||
) . ' ' .
|
||||
esc_html__('If a new tab/window does not open check your browsers address bar to allow popups from this URL.', 'duplicator-pro');
|
||||
?>
|
||||
</div>
|
||||
<button id="auth-redirect" type="button" class="button button-large" onclick="DupPro.Storage.Dropbox.OpenAuthPage(); return false;">
|
||||
<i class="fa fa-user"></i> <?php esc_html_e('Authorize Dropbox', 'duplicator-pro'); ?>
|
||||
</button>
|
||||
<br/><br/>
|
||||
|
||||
<div id="dropbox-auth-code-area">
|
||||
<b><?php esc_html_e('Step 2:', 'duplicator-pro'); ?></b>
|
||||
<?php esc_html_e("Paste code from Dropbox authorization page.", 'duplicator-pro'); ?> <br/>
|
||||
<input style="width:400px" id="dropbox-auth-code" name="dropbox-auth-code" />
|
||||
</div>
|
||||
|
||||
<!-- STEP 3 -->
|
||||
<b><?php esc_html_e("Step 3:", 'duplicator-pro'); ?></b>
|
||||
<?php esc_html_e('Finalize Dropbox validation by clicking the "Finalize Setup" button.', 'duplicator-pro'); ?>
|
||||
<br/>
|
||||
<button id="dropbox-finalize-setup" type="button" class="button" >
|
||||
<i class="fa fa-check-square"></i> <?php esc_html_e('Finalize Setup', 'duplicator-pro'); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="authorization-state" id="state-waiting-for-access-token">
|
||||
<div>
|
||||
<i class="fas fa-circle-notch fa-spin"></i>
|
||||
<?php esc_html_e('Performing final authorization...Please wait', 'duplicator-pro'); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="authorization-state" id="state-authorized" style="margin-top:-5px">
|
||||
<?php if ($storage->isAuthorized() && is_array($accountInfo)) : ?>
|
||||
<h3>
|
||||
<?php esc_html_e('Dropbox Account', 'duplicator-pro'); ?><br/>
|
||||
<i class="dpro-edit-info">
|
||||
<?php esc_html_e('Duplicator has been authorized to access this user\'s Dropbox account', 'duplicator-pro'); ?>
|
||||
</i>
|
||||
</h3>
|
||||
<div id="dropbox-account-info">
|
||||
<label><?php esc_html_e('Name', 'duplicator-pro'); ?>:</label>
|
||||
<?php echo esc_html($accountInfo['name']['display_name']); ?><br/>
|
||||
|
||||
<label><?php esc_html_e('Email', 'duplicator-pro'); ?>:</label>
|
||||
<?php echo esc_html($accountInfo['email']); ?>
|
||||
<?php if ($quotaInfo) { ?>
|
||||
<br/>
|
||||
<label><?php esc_html_e('Quota Usage', 'duplicator-pro'); ?>:</label>
|
||||
<?php
|
||||
printf(esc_html__('%1$s%% used, %2$s available', 'duplicator-pro'), (int) $quotaInfo['perc'], $quotaInfo['available']);
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<br/>
|
||||
<button type="button" class="button" onclick='DupPro.Storage.Dropbox.CancelAuthorization();'>
|
||||
<?php esc_html_e('Cancel Authorization', 'duplicator-pro'); ?>
|
||||
</button><br/>
|
||||
<i class="dpro-edit-info">
|
||||
<?php esc_html_e('Disassociates storage provider with the Dropbox account. Will require re-authorization.', 'duplicator-pro'); ?>
|
||||
</i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="_dropbox_storage_folder"><?php esc_html_e("Storage Folder", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<b>//Dropbox/Apps/Duplicator Pro/</b>
|
||||
<input id="_dropbox_storage_folder" name="_dropbox_storage_folder"
|
||||
type="text" value="<?php echo esc_attr($storageFolder); ?>" class="dpro-storeage-folder-path" />
|
||||
<p>
|
||||
<i>
|
||||
<?php
|
||||
esc_html_e(
|
||||
"Folder where packages will be stored. This should be unique for each web-site using Duplicator.",
|
||||
'duplicator-pro'
|
||||
); ?>
|
||||
</i>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for=""><?php esc_html_e("Max Packages", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<label for="dropbox_max_files">
|
||||
<input
|
||||
id="dropbox_max_files"
|
||||
name="dropbox_max_files"
|
||||
type="number"
|
||||
value="<?php echo (int) $maxPackages; ?>"
|
||||
min="0"
|
||||
maxlength="4"
|
||||
data-parsley-errors-container="#dropbox_max_files_error_container"
|
||||
data-parsley-required="true"
|
||||
data-parsley-type="number"
|
||||
data-parsley-min="0"
|
||||
>
|
||||
<?php esc_html_e("Number of packages to keep in folder.", 'duplicator-pro'); ?> <br/>
|
||||
<i><?php esc_html_e("When this limit is exceeded, the oldest package will be deleted. Set to 0 for no limit.", 'duplicator-pro'); ?></i>
|
||||
</label>
|
||||
<div id="dropbox_max_files_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php $tplMng->render('admin_pages/storages/parts/provider_foot'); ?>
|
||||
<?php
|
||||
$alertConnStatus = new DUP_PRO_UI_Dialog();
|
||||
$alertConnStatus->title = __('Dropbox Connection Status', 'duplicator-pro');
|
||||
$alertConnStatus->message = ''; // javascript inserted message
|
||||
$alertConnStatus->initAlert();
|
||||
?>
|
||||
<script>
|
||||
jQuery(document).ready(function ($) {
|
||||
// DROPBOX RELATED METHODS
|
||||
DupPro.Storage.Dropbox.AuthorizationStates = {
|
||||
UNAUTHORIZED: 0,
|
||||
WAITING_FOR_REQUEST_TOKEN: 1,
|
||||
WAITING_FOR_AUTH_BUTTON_CLICK: 2,
|
||||
WAITING_FOR_ACCESS_TOKEN: 3,
|
||||
AUTHORIZED: 4
|
||||
}
|
||||
|
||||
DupPro.Storage.Dropbox.authorizationState = <?php echo ($storage->isAuthorized() ? 4 : 0); ?>;
|
||||
|
||||
DupPro.Storage.Dropbox.CancelAuthorization = function () {
|
||||
DupPro.Storage.RevokeAuth(<?php echo (int) $storage->getId(); ?>);
|
||||
}
|
||||
|
||||
DupPro.Storage.Dropbox.DropboxGetAuthUrl = function ()
|
||||
{
|
||||
DupPro.Storage.Dropbox.AuthUrl = <?php echo json_encode($storage->getAuthorizationUrl()); ?>;
|
||||
jQuery("#state-waiting-for-auth-button-click").show();
|
||||
};
|
||||
|
||||
DupPro.Storage.Dropbox.TransitionAuthorizationState = function (newState)
|
||||
{
|
||||
jQuery('.authorization-state').hide();
|
||||
jQuery('.dropbox_access_type').prop('disabled', true);
|
||||
jQuery('.button_dropbox_test').prop('disabled', true);
|
||||
|
||||
switch (newState) {
|
||||
case DupPro.Storage.Dropbox.AuthorizationStates.UNAUTHORIZED:
|
||||
jQuery('.dropbox_access_type').prop('disabled', false);
|
||||
$("#dropbox_authorization_state").val(DupPro.Storage.Dropbox.AuthorizationStates.UNAUTHORIZED);
|
||||
DupPro.Storage.Dropbox.requestToken = null;
|
||||
jQuery("#state-unauthorized").show();
|
||||
break;
|
||||
|
||||
case DupPro.Storage.Dropbox.AuthorizationStates.WAITING_FOR_REQUEST_TOKEN:
|
||||
DupPro.Storage.Dropbox.GetRequestToken();
|
||||
jQuery("#state-waiting-for-request-token").show();
|
||||
break;
|
||||
|
||||
case DupPro.Storage.Dropbox.AuthorizationStates.WAITING_FOR_AUTH_BUTTON_CLICK:
|
||||
// Nothing to do here other than show the button and wait
|
||||
jQuery("#state-waiting-for-auth-button-click").show();
|
||||
break;
|
||||
|
||||
case DupPro.Storage.Dropbox.AuthorizationStates.WAITING_FOR_ACCESS_TOKEN:
|
||||
jQuery("#state-waiting-for-access-token").show();
|
||||
if (DupPro.Storage.Dropbox.requestToken != null) {
|
||||
DupPro.Storage.Dropbox.GetAccessToken();
|
||||
} else {
|
||||
<?php $alertConnStatus->showAlert(); ?>
|
||||
let alertMsg = "<i class='fas fa-exclamation-triangle'></i> " +
|
||||
"<?php esc_html_e('Tried transitioning to auth button click but don\'t have the request token!', 'duplicator-pro'); ?>";
|
||||
<?php $alertConnStatus->updateMessage("alertMsg"); ?>
|
||||
DupPro.Storage.Dropbox.TransitionAuthorizationState(DupPro.Storage.Dropbox.AuthorizationStates.UNAUTHORIZED);
|
||||
}
|
||||
break;
|
||||
|
||||
case DupPro.Storage.Dropbox.AuthorizationStates.AUTHORIZED:
|
||||
var token = $("#dropbox_access_token").val();
|
||||
var token_secret = $("#dropbox_access_token_secret").val();
|
||||
DupPro.Storage.Dropbox.accessToken = {};
|
||||
DupPro.Storage.Dropbox.accessToken.t = token;
|
||||
DupPro.Storage.Dropbox.accessToken.s = token_secret;
|
||||
jQuery("#state-authorized").show();
|
||||
jQuery('.button_dropbox_test').prop('disabled', false);
|
||||
break;
|
||||
}
|
||||
|
||||
DupPro.Storage.Dropbox.authorizationState = newState;
|
||||
}
|
||||
|
||||
DupPro.Storage.Dropbox.OpenAuthPage = function ()
|
||||
{
|
||||
window.open(DupPro.Storage.Dropbox.AuthUrl, '_blank');
|
||||
}
|
||||
|
||||
$('#dropbox-finalize-setup').click(function (event) {
|
||||
event.stopPropagation();
|
||||
|
||||
if ($('#dropbox-auth-code').val().length > 5) {
|
||||
DupPro.Storage.PrepareForSubmit();
|
||||
|
||||
//$("#dup-storage-form").submit();
|
||||
|
||||
DupPro.Storage.Authorize(
|
||||
<?php echo (int) $storage->getId(); ?>,
|
||||
<?php echo (int) $storage->getSType(); ?>,
|
||||
{
|
||||
'name': $('#name').val(),
|
||||
'notes': $('#notes').val(),
|
||||
'storage_folder': $('#_dropbox_storage_folder').val(),
|
||||
'max_packages': $('#dropbox_max_files').val(),
|
||||
'auth_code' : $('#dropbox-auth-code').val()
|
||||
}
|
||||
);
|
||||
} else {
|
||||
<?php $alertConnStatus->showAlert(); ?>
|
||||
let alertMsg = "<i class='fas fa-exclamation-triangle'></i> " +
|
||||
"<?php esc_html_e('Please enter your Dropbox authorization code!', 'duplicator-pro'); ?>";
|
||||
<?php $alertConnStatus->updateMessage("alertMsg"); ?>
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
DupPro.Storage.Dropbox.TransitionAuthorizationState(DupPro.Storage.Dropbox.authorizationState);
|
||||
$('button#auth-validate').prop('disabled', true);
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user