first commit

This commit is contained in:
Roman Pyrih
2026-04-21 15:48:41 +02:00
commit 7483681901
10216 changed files with 3236626 additions and 0 deletions

View File

@@ -0,0 +1,412 @@
<?php
/**
* DUPLICATOR CLOUD ADDON
*
* Name: Duplicator PRO DupCloud Storage
* Version: 1
* Author: Duplicator
* Author URI: https://duplicator.com/
*
* PHP version 5.6
*
* @category Duplicator
* @package Plugin
* @author Duplicator
* @copyright 2011-2021 Snapcreek LLC
* @license https://www.gnu.org/licenses/gpl-3.0.html GPLv3
* @version GIT: $Id$
* @link https://duplicator.com/
*/
namespace Duplicator\Addons\DupCloudAddon;
use Duplicator\Addons\DupCloudAddon\Models\DupCloudStorage;
use Duplicator\Addons\DupCloudAddon\Models\QuickConnect;
use Duplicator\Addons\DupCloudAddon\Ajax\ServicesDupCloud;
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudRateLimitHandler;
use Duplicator\Addons\ProBase\LicensingController;
use Duplicator\Controllers\StoragePageController;
use Duplicator\Core\Addons\AbstractAddonCore;
use Duplicator\Core\Bootstrap;
use Duplicator\Core\CapMng;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\Views\TplMng;
use Duplicator\Models\Storages\AbstractStorageEntity;
use Duplicator\Package\AbstractPackage;
use Duplicator\Package\Storage\UploadInfo;
use Duplicator\Utils\Logging\DupLog;
use Duplicator\Views\AdminNotices;
use Exception;
use WP_Error;
/**
* Duplicator Cloud Storage addon class
*/
class DupCloudAddon extends AbstractAddonCore
{
const OPTION_KEY_DUPCLOUD_NO_SPACE_DISMISSED = 'dupli_opt_dup_cloud_out_of_space_notice';
const ADDON_PATH = __DIR__;
/**
* @return void
*/
public function init(): void
{
if (!defined('DUPLICATOR_CLOUD_HOST')) {
define('DUPLICATOR_CLOUD_HOST', 'https://cloud.duplicator.com');
}
add_action('duplicator_register_storage_types', [$this, 'registerStorages']);
add_filter('duplicator_template_file', [self::class, 'getTemplateFile'], 10, 2);
add_filter('duplicator_admin_notices', [self::class, 'adminNotices']);
add_action('admin_init', [self::class, 'registerJsCss']);
add_action('duplicator_settings_general_before', [self::class, 'renderLicenseContent'], 20);
add_action('duplicator_transfer_cancelled', [self::class, 'cancelUpload'], 10, 1);
add_action('duplicator_transfer_failed', [self::class, 'markUploadAsFailed'], 10, 1);
add_filter('duplicator_validate_upload_info_data', [self::class, 'validateUploadInfoData'], 10, 4);
add_action('duplicator_backups_page_header_after', [self::class, 'backupsPageHeaderAfter'], 10, 0);
DupCloudRateLimitHandler::init();
(new ServicesDupCloud())->init();
QuickConnect::init();
}
/**
* Before add upload info hook
*
* @param WP_Error $errors errors
* @param AbstractPackage $package package
* @param int $storageId storage id
* @param bool $isDownload is download
*
* @return WP_Error
*/
public static function validateUploadInfoData(WP_Error $errors, AbstractPackage $package, int $storageId, bool $isDownload): WP_Error
{
if (($storage = AbstractStorageEntity::getById($storageId)) === false) {
$errors->add('storage_id', sprintf(__('Could not find storage ID %d!', 'duplicator-pro'), $storageId));
return $errors;
}
if (
$storage->getSType() === DupCloudStorage::getSType() &&
in_array($storageId, $package->getValidStorages(true, 'id')) &&
$isDownload === false
) {
$errors->add('storage_id', __('The backup you are trying to transfer already exists in the cloud storage.', 'duplicator-pro'));
return $errors;
}
return $errors;
}
/**
* Mark upload as failed
*
* @param UploadInfo $uploadInfo Upload info
*
* @return void
*/
public static function markUploadAsFailed(UploadInfo $uploadInfo): void
{
try {
/** @var DupCloudStorage $storage */
$storage = $uploadInfo->getStorage();
if (!($storage instanceof DupCloudStorage)) {
DupLog::infoTrace("Can't fail upload, storage not found");
return;
}
if ($uploadInfo->isDownloadFromRemote()) {
return;
}
if (!$storage->failUpload($uploadInfo)) {
throw new Exception("Failed to fail upload");
}
} catch (Exception $e) {
DupLog::infoTraceException($e, "Can't fail upload");
}
}
/**
* Display cloud manage button on backups page
*
* @return void
*/
public static function backupsPageHeaderAfter(): void
{
if (!CapMng::getInstance()->can(CapMng::CAP_STORAGE, false)) {
return;
}
TplMng::getInstance()->render('dupcloudaddon/parts/backups_header_button');
}
/**
* Cancel upload
*
* @param UploadInfo $uploadInfo Upload info
*
* @return void
*/
public static function cancelUpload(UploadInfo $uploadInfo): void
{
try {
/** @var DupCloudStorage $storage */
$storage = $uploadInfo->getStorage();
if (!($storage instanceof DupCloudStorage)) {
DupLog::infoTrace("Can't cancel upload, storage not found");
return;
}
if ($uploadInfo->isDownloadFromRemote()) {
return;
}
if (!$storage->cancelUpload($uploadInfo)) {
throw new Exception("Failed to cancel upload");
}
} catch (Exception $e) {
DupLog::infoTraceException($e, "Can't cancel upload");
}
}
/**
* Render page content
*
* @return void
*/
public static function renderLicenseContent(): void
{
if (!CapMng::getInstance()->can(CapMng::CAP_LICENSE, false)) {
return;
}
if (!CapMng::getInstance()->can(CapMng::CAP_STORAGE, false)) {
return;
}
TplMng::getInstance()->render(
'dupcloudaddon/configs/general_settings',
[
'storage' => DupCloudStorage::getUniqueStorage(),
'auto_activate_storage' => LicensingController::isActivationLicenseRender(),
]
);
}
/**
* Add notice to admin notices
*
* @param callable[] $notices Admin notices
*
* @return callable[] Admin notices
*/
public static function adminNotices(array $notices): array
{
$notices[] = [
self::class,
'dupCloudOutOfSpaceNotice',
];
$notices[] = [
self::class,
'dupCloudRateLimitNotice',
];
return $notices;
}
/**
* Shows notice in case we were enable to fetch contents of S3 bucket
*
* @return void
*/
public static function dupCloudRateLimitNotice(): void
{
if (
!ControllersManager::getInstance()->isDuplicatorPage() ||
!CapMng::can(CapMng::CAP_STORAGE, false) ||
!DupCloudRateLimitHandler::hasRateLimitError()
) {
return;
}
$message = sprintf(
_x(
'You made too many requests to Duplicator Cloud. Please try again in %1$s seconds.',
'1: Time in seconds',
'duplicator-pro'
),
DupCloudRateLimitHandler::retryAfter()
);
AdminNotices::displayGeneralAdminNotice(
$message,
AdminNotices::GEN_ERROR_NOTICE,
false
);
}
/**
* Shows notice in case we were enable to fetch contents of S3 bucket
*
* @return void
*/
public static function dupCloudOutOfSpaceNotice(): void
{
if (get_option(self::OPTION_KEY_DUPCLOUD_NO_SPACE_DISMISSED, false) == true) {
return;
}
if (!ControllersManager::getInstance()->isDuplicatorPage()) {
return;
}
if (!CapMng::can(CapMng::CAP_STORAGE, false)) {
return;
}
/** @var DupCloudStorage[] $dupCloudStorages */
$dupCloudStorages = AbstractStorageEntity::getAllBySType(DupCloudStorage::getSType());
if (count($dupCloudStorages) <= 0) {
return;
}
$dupCloudStorage = $dupCloudStorages[0];
if (!$dupCloudStorage->isAuthorized()) {
//Only show message if storage is authorized and out of space
return;
}
if ($dupCloudStorage->getFreeSpace() > 0) {
return;
}
$storageEditUrl = StoragePageController::getEditUrl($dupCloudStorage);
$message = wp_kses(
sprintf(
_x(
'The Duplicator Cloud storage is out of space. Please make sure you have enough
space available in the %1$s%3$s%2$s storage location.',
'1: open link tag, 2: close link tag, 3: storage name',
'duplicator-pro'
),
'<a href="' . esc_url($storageEditUrl) . '" target="_blank">',
'</a>',
esc_html($dupCloudStorage->getName())
),
[
'a' => [
'href' => [],
'target' => [],
],
'br' => [],
]
);
AdminNotices::displayGeneralAdminNotice(
$message,
AdminNotices::GEN_ERROR_NOTICE,
true,
['dupli-quick-fix-notice'],
[
'data-to-dismiss' => self::OPTION_KEY_DUPCLOUD_NO_SPACE_DISMISSED,
]
);
}
/**
* Register storages
*
* @return void
*/
public function registerStorages(): void
{
DupCloudStorage::registerType();
}
/**
* 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, 'dupcloudaddon/') === 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_dup_cloud_count'] = 0;
foreach ($storages as $storage) {
if ($storage->getStype() === DupCloudStorage::getSType()) {
$storageNums['storages_dup_cloud_count']++;
}
}
return $storageNums;
}
/**
* Register styles and scripts
*
* @return void
*/
public static function registerJsCss(): void
{
if (wp_doing_ajax()) {
return;
}
$min = Bootstrap::getMinPrefix();
wp_register_style(
'dup-addon-dupcloud-addon',
self::getAddonUrl() . "/assets/css/dupcloudaddon{$min}.css",
['dup-plugin-global-style'],
DUPLICATOR_VERSION
);
wp_enqueue_style('dup-addon-dupcloud-addon');
}
/**
*
* @return string
*/
public static function getAddonPath(): string
{
return __DIR__;
}
/**
*
* @return string
*/
public static function getAddonFile(): string
{
return __FILE__;
}
}

View File

@@ -0,0 +1,33 @@
/*!***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
!*** css ./node_modules/.pnpm/css-loader@7.1.2_webpack@5.100.2/node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[1].use[1]!./node_modules/.pnpm/postcss-loader@8.2.0_postcss@8.5.6_webpack@5.100.2/node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[1].use[2]!./node_modules/.pnpm/sass-loader@16.0.5_sass@1.89.2_webpack@5.100.2/node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[1].use[3]!./addons/dupcloudaddon/assets/sass/main.scss ***!
\***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
/*!
DUPLICATOR DUP CLOUD ADDON STYLE
*/
/**
* Foundation for Sites
* Version 6.9.0
* https://get.foundation
* Licensed under MIT Open Source
*/
.dup-styles .dup-box.dup-cloud-connect {
width: 600px;
background: #fefefe;
border: 1px solid #1D2327;
}
.dup-styles .dup-box.dup-cloud-connect .dup-box-title {
background: rgb(240, 240, 241);
}
.dup-styles .dup-box.dup-cloud-connect .dupli-admin-notice {
width: 100%;
background: rgb(240, 240, 241);
}
.dup-styles .dup-box.dup-cloud-connect .dup-storage-info {
background-color: rgb(240, 240, 241);
border: 1px solid #c0c0c0;
margin-bottom: 1.25rem;
padding: 10px;
}
.dup-styles .dup-box.dup-cloud-connect input {
width: 100%;
}

View File

@@ -0,0 +1 @@
.dup-styles .dup-box.dup-cloud-connect{background:#fefefe;border:1px solid #1d2327;width:600px}.dup-styles .dup-box.dup-cloud-connect .dup-box-title{background:#f0f0f1}.dup-styles .dup-box.dup-cloud-connect .dupli-admin-notice{background:#f0f0f1;width:100%}.dup-styles .dup-box.dup-cloud-connect .dup-storage-info{background-color:#f0f0f1;border:1px solid silver;margin-bottom:1.25rem;padding:10px}.dup-styles .dup-box.dup-cloud-connect input{width:100%}

View File

@@ -0,0 +1,25 @@
.dup-box.dup-cloud-connect {
width: 600px;
background: $white;
border: 1px solid $black;
.dup-box-title {
background: $body-background;
}
.dupli-admin-notice {
width: 100%;
background: $body-background;
}
.dup-storage-info {
background-color: $body-background;
border: 1px solid $medium-gray;
margin-bottom: $global-margin;
padding: 10px;
}
input {
width: 100%;
}
}

View File

@@ -0,0 +1,10 @@
/*!
DUPLICATOR DUP CLOUD ADDON STYLE
*/
@import "../../../../node_modules/foundation-sites/scss/foundation";
@import '../../../../assets/sass/settings';
.dup-styles {
@import "components/cloud_connect";
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* DupCloud ajax services
*
* @package Duplicator\Addons\DupCloudAddon
* @copyright (c) 2024, Snap Creek LLC
*/
declare(strict_types=1);
namespace Duplicator\Addons\DupCloudAddon\Ajax;
use Duplicator\Addons\DupCloudAddon\Models\QuickConnect;
use Duplicator\Ajax\AbstractAjaxService;
use Duplicator\Ajax\AjaxWrapper;
use Duplicator\Core\CapMng;
use Duplicator\Core\Views\TplMng;
use Duplicator\Libs\Snap\SnapUtil;
/**
* Handles connection requests for DupCloud
*/
class ServicesDupCloud extends AbstractAjaxService
{
const AJAX_ACTION_QUICK_CONNECT = 'duplicator_dupcloud_quick_connect';
/**
* Init ajax calls
*
* @return void
*/
public function init(): void
{
$this->addAjaxCall('wp_ajax_' . self::AJAX_ACTION_QUICK_CONNECT, 'getStorageTokens');
}
/**
* Get storage tokens via license
*
* @return void
*/
public function getStorageTokens(): void
{
AjaxWrapper::json(
[
$this,
'getStorageTokensCallback',
],
self::AJAX_ACTION_QUICK_CONNECT,
SnapUtil::sanitizeStrictInput(INPUT_POST, 'nonce'),
CapMng::CAP_STORAGE
);
}
/**
* Get storage tokens callback
*
* @return array{tokens:array<string,mixed>,html:string}
*/
public function getStorageTokensCallback(): array
{
$tokens = QuickConnect::getStorageTokens();
return [
'tokens' => $tokens,
'html' => TplMng::getInstance()->render(
'dupcloudaddon/connect/quick_connect',
['tokens' => $tokens],
false
),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
declare(strict_types=1);
namespace Duplicator\Addons\DupCloudAddon\Exceptions;
use Exception;
/**
* Exception thrown when a presigned URL has expired
*/
class PresignedUrlExpiredException extends Exception
{
}

View File

@@ -0,0 +1,897 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Addons\DupCloudAddon\Models;
use Duplicator\Models\DynamicGlobalEntity;
use Duplicator\Models\GlobalEntity;
use Duplicator\Utils\Logging\DupLog;
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudClient;
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudStorageAdapter;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\UniqueId;
use Duplicator\Core\Views\TplMng;
use Duplicator\Installer\Package\ArchiveDescriptor;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Models\Storages\AbstractStorageEntity;
use Duplicator\Models\Storages\StorageAuthInterface;
use Duplicator\Package\AbstractPackage;
use Duplicator\Package\Create\BuildComponents;
use Duplicator\Package\PackageUtils;
use Duplicator\Package\Recovery\BackupPackage;
use Duplicator\Package\Recovery\RecoveryStatus;
use Duplicator\Package\Storage\UploadInfo;
use Exception;
/**
* @property ?DupCloudStorageAdapter $adapter
*
* @phpstan-import-type IneligibilityReasons from RecoveryStatus
*/
class DupCloudStorage extends AbstractStorageEntity implements StorageAuthInterface
{
/** @var int */
const DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB = 10 * 1024;
/** @var int */
const UPLOAD_CHUNK_MIN_SIZE_IN_KB = 5 * 1024;
/** @var int */
const UPLOAD_CHUNK_MAX_SIZE_IN_KB = 5 * 1024 * 1024;
/** @var int */
const DEFAULT_DOWNLOAD_CHUNK_SIZE_IN_KB = 10 * 1024;
/** @var int */
const DOWNLOAD_CHUNK_MAX_SIZE_IN_KB = 5 * 1024 * 1024;
/** @var int */
const DOWNLOAD_CHUNK_MIN_SIZE_IN_KB = 5 * 1024;
/** @var array<string, array{result:bool,reasons:IneligibilityReasons}> */
private static $eligibilityCache = [];
/**
* Class constructor
*/
public function __construct()
{
parent::__construct();
$this->name = __('Duplicator Cloud', "duplicator-pro");
}
/**
* Get priority, used to sort storages.
* 100 is neutral value, 0 is the highest priority
*
* @return int
*/
public static function getPriority(): int
{
return 20;
}
/**
* Mark an upload as failed using the upload info
*
* @param UploadInfo $uploadInfo Upload info
*
* @return bool True if success, false otherwise
*/
public function failUpload(UploadInfo $uploadInfo)
{
if (isset($uploadInfo->copyExtraData['UploadUuid'])) {
return $this->getAdapter()->failUpload($uploadInfo->copyExtraData['UploadUuid']);
}
$archiveName = $uploadInfo->generalExtraData['backup_details']['file_info']['backup_filename'] ?? '';
if (strlen($archiveName) > 0) {
return $this->getAdapter()->failUploadByName($archiveName);
}
return false;
}
/**
* Cancel an upload using the upload info
*
* @param UploadInfo $uploadInfo Upload info
*
* @return bool True if success, false otherwise
*/
public function cancelUpload(UploadInfo $uploadInfo)
{
if (isset($uploadInfo->copyExtraData['UploadUuid'])) {
return $this->getAdapter()->cancelUpload($uploadInfo->copyExtraData['UploadUuid']);
}
$archiveName = $uploadInfo->generalExtraData['backup_details']['file_info']['backup_filename'] ?? '';
if (strlen($archiveName) > 0) {
return $this->getAdapter()->cancelUploadByName($archiveName);
}
return false;
}
/**
* Get new storage object by type
*
* @return self
*/
protected static function getNewStorageInstance(): self
{
return new self();
}
/**
* Get default config
*
* @return array<string,scalar>
*/
protected static function getDefaultConfig(): array
{
$config = parent::getDefaultConfig();
return array_merge(
$config,
[
'accessToken' => '',
'userName' => '',
'userEmail' => '',
'totalSpace' => 0,
'freeSpace' => 0,
'authorized' => false,
'websiteUuid' => '',
]
);
}
/**
* Storages test
*
* @param string $message Test message
*
* @return bool return true if success, false otherwise
*/
public function test(string &$message = ''): bool
{
try {
$this->testLog->reset();
$message = sprintf(__('Testing %s storage...', 'duplicator-pro'), static::getStypeName());
$this->testLog->addMessage($message);
if (static::isSupported() == false) {
$message = sprintf(__('Storage %s isn\'t supported on current server', 'duplicator-pro'), static::getStypeName());
$this->testLog->addMessage($message);
return false;
}
$this->testLog->addMessage(__('Check if storage is ready to use.', 'duplicator-pro'));
$validMessage = '';
$adapter = $this->getAdapter();
if ($adapter->isValid($validMessage) == false) {
$message = sprintf(
__('Storage %1$s is not valid message %2$s', 'duplicator-pro'),
static::getStypeName(),
$validMessage
);
$this->testLog->addMessage($message);
return false;
}
$this->testLog->addMessage(__('Successfully storage test.', 'duplicator-pro'));
$message = __('Successfully storage test.', 'duplicator-pro');
return true;
} catch (Exception $e) {
$message = $e->getMessage();
$this->testLog->addMessage(sprintf(__('Error during storage test: %s', 'duplicator-pro'), $message));
return false;
}
}
/**
* Get the authorzation url
*
* @return string
*/
public static function getAuthUrl(): string
{
return DupCloudClient::getAuthUrl(home_url());
}
/**
* Return the storage type
*
* @return int
*/
public static function getSType(): int
{
return 17;
}
/**
* Returns the storage type icon URL
*
* @return string Returns the storage icon URL
*/
public static function getStypeIconURL(): string
{
return DUPLICATOR_IMG_URL . '/duplicator-logo-icon.svg';
}
/**
* Returns the storage type name.
*
* @return string
*/
public static function getStypeName(): string
{
return __('Duplicator Cloud', 'duplicator-pro');
}
/**
* Get storage location string
*
* @return string
*/
public function getLocationString(): string
{
return 'Manage Backups';
}
/**
* Returns URL to backup list for website
*
* @return string
*/
public function getBackupsUrl(): string
{
return SnapIO::trailingslashit(DupCloudClient::manageWebsitesUrl()) . $this->config['websiteUuid'] . '/backups';
}
/**
* Returns an html anchor tag of location
*
* @return string Returns an html anchor tag with the storage location as a hyperlink.
*/
public function getHtmlLocationLink(): string
{
if ($this->isAuthorized()) {
$websitesUrl = $this->getBackupsUrl();
return '<a href="' . esc_url($websitesUrl) . '" target="_blank" >' .
esc_html__('Manage Backups', 'duplicator-pro') .
'</a>';
} else {
return '#';
}
}
/**
* Is unique, if true only one storage of this type can exist
*
* @return bool
*/
public static function isUnique(): bool
{
return true;
}
/**
* Check if a grid break should be inserted after this storage type in the selector UI
*
* @return bool
*/
public static function isGridBreakAfter(): bool
{
return true;
}
/**
* Check if storage is valid
*
* @param ?string $errorMsg Reference to store error message
* @param bool $force Force the storage to be revalidated
*
* @return bool Return true if storage is valid and ready to use, false otherwise
*/
public function isValid(?string &$errorMsg = '', bool $force = false): bool
{
$adapter = $this->getAdapter();
$isValid = $adapter->isValid($errorMsg, $force);
// 'isValid()' can be false when there is no free space
// 'authorized' determines if the info should be considered for saving
if ($force === true && $adapter->isAuthorized()) {
$data = [
'authorized' => $adapter->isAuthorized(),
'userName' => $adapter->getUserName(),
'userEmail' => $adapter->getUserEmail(),
'totalSpace' => $adapter->getTotalSpace(),
'freeSpace' => $adapter->getFreeSpace(),
'websiteUuid' => $adapter->getWebsiteUuid(),
];
if ($this->hasConfigChanged($data)) {
$this->config = array_merge($this->config, $data);
$this->save();
}
}
return $isValid;
}
/**
* Is autorized
*
* @return bool
*/
public function isAuthorized(): bool
{
return $this->config['authorized'];
}
/**
* Authorize
*
* @param string $token Token
*
* @return bool True if authorized, false if failed
*/
public function authorize(string $token): bool
{
$adapter = new DupCloudStorageAdapter($token);
$userInfo = $adapter->getUserInfo();
$this->config['accessToken'] = $token;
$this->config['userName'] = $userInfo['name'];
$this->config['userEmail'] = $userInfo['email'];
$this->config['authorized'] = true;
$this->config['websiteUuid'] = '';
return true;
}
/**
* Authorized from HTTP request
*
* @param string $message Message
*
* @return bool True if authorized, false if failed
*/
public function authorizeFromRequest(&$message = ''): bool
{
// Allow pipe character in the token for compound tokens
if (($accessToken = SnapUtil::sanitizeDefaultInput(SnapUtil::INPUT_REQUEST, 'access_token', '')) === '') {
DupLog::trace('No access token found');
$message = __('No access token provided', 'duplicator-pro');
return false;
}
$this->name = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'name', '');
$this->notes = SnapUtil::sanitizeDefaultInput(SnapUtil::INPUT_REQUEST, 'notes', '');
try {
// Check if this is a compound token (new method) or direct token (legacy)
if (strpos($accessToken, '.') !== false && strpos($accessToken, '|') !== false) {
// New compound token authentication (contains both . and |)
$client = new DupCloudClient();
// Get site identifier
$siteIdentifier = UniqueId::getInstance()->getIdentifier();
DupLog::trace('Authenticating with compound token for site: ' . $siteIdentifier);
// Authenticate using compound token
$authResult = $client->authenticateSite($accessToken, $siteIdentifier);
// Store the permanent token and all data directly
$this->config['accessToken'] = $authResult['token'];
$this->config['authorized'] = true;
$this->config['userName'] = $authResult['user_name'] ?? '';
$this->config['userEmail'] = $authResult['user_email'] ?? '';
$this->config['totalSpace'] = $authResult['total_space'] ?? 0;
$this->config['freeSpace'] = $authResult['free_space'] ?? 0;
DupLog::trace('Website connected: ' . ($authResult['name'] ?? ''));
DupLog::trace('Storage info - Total: ' . $this->config['totalSpace'] . ', Free: ' . $this->config['freeSpace']);
DupLog::trace('User info - Name: ' . $this->config['userName'] . ', Email: ' . $this->config['userEmail']);
$currentPage = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'current_page', '');
$storagePageUrl = ControllersManager::getInstance()->getMenuLink(
ControllersManager::STORAGE_SUBMENU_SLUG
);
$message = TplMng::getInstance()->render(
'admin_pages/storages/parts/auth_success_message',
[
'storagePageUrl' => $storagePageUrl,
'storageName' => $this->getStypeName(),
'isSettingsPage' => $currentPage === ControllersManager::SETTINGS_SUBMENU_SLUG,
],
false
);
return true;
} else {
// Legacy method - direct token
return $this->authorize($accessToken);
}
} catch (\Exception $e) {
DupLog::trace('Authorization failed: ' . $e->getMessage());
$message = $e->getMessage();
return false;
}
}
/**
* Revokes authorization
*
* @param string $message Message
*
* @return bool True if revoked, false if failed
*/
public function revokeAuthorization(&$message = ''): bool
{
try {
if (!$this->isAuthorized()) {
return true;
}
if (!$this->getAdapter()->revokeAuthorization()) {
throw new Exception(__('Error revoking authorization.', 'duplicator-pro'));
}
} catch (Exception $e) {
DupLog::traceException($e, 'REVOKE AUTHORIZATION ERROR BUT COUNTINUE RESETTING CONFIG');
$message = $e->getMessage();
return false;
} finally {
$this->config = static::getDefaultConfig();
$this->save();
}
$message = __('Duplicator Cloud is disconnected successfully.', 'duplicator-pro');
return true;
}
/**
* Returns the config fields template path
*
* @return string
*/
protected function getConfigFieldsTemplatePath(): string
{
return 'dupcloudaddon/configs/dupcloud';
}
/**
* Returns the config fields template data
*
* @return array<string, mixed>
*/
protected function getConfigFieldsData(): array
{
return $this->getDefaultConfigFieldsData();
}
/**
* Returns the default config fields template data
*
* @return array<string, mixed>
*/
protected function getDefaultConfigFieldsData(): array
{
return [
'storage' => $this,
'maxPackages' => $this->config['max_packages'],
'userName' => $this->config['userName'],
'userEmail' => $this->config['userEmail'],
'totalSpace' => $this->config['totalSpace'],
'freeSpace' => $this->config['freeSpace'],
'authorized' => $this->config['authorized'],
'websiteUuid' => $this->config['websiteUuid'],
];
}
/**
* 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 = ''): bool
{
if ((parent::updateFromHttpRequest($message) === false)) {
return false;
}
$this->config['max_packages'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'dupcloud_max_files', 10);
$message = __('Dupicator Cloud Storage was updated.', 'duplicator-pro');
return true;
}
/**
* Get the storage adapter
*
* @return DupCloudStorageAdapter
*/
protected function getAdapter(): DupCloudStorageAdapter
{
if ($this->adapter === null) {
$this->adapter = new DupCloudStorageAdapter(
$this->config['accessToken'],
DupCloudClient::BACKUP_TYPE_STANDARD,
$this->config['max_packages']
);
}
return $this->adapter;
}
/**
* Get general data for backup
*
* @param AbstractPackage $package the Backup
*
* @return array<string,mixed>
*/
protected function getGeneralExtraData(AbstractPackage $package): array
{
return array_merge(
parent::getGeneralExtraData($package),
['backup_details' => self::getBackupDetails($package) ]
);
}
/**
* Returns the Backup details
*
* @param AbstractPackage $package the Backup
*
* @return array<string, mixed>
*/
public static function getBackupDetails(AbstractPackage $package): array
{
$data = [
'system' => [
'php_version' => $package->VersionPHP,
'wp_version' => $package->VersionWP,
'plugin_type' => 'pro',
'plugin_version' => $package->getVersion(),
],
'file_info' => [
'backup_filename' => $package->getArchiveFilename(), // full file name
'backup_name' => $package->getName(), // name, first part of file name
'backup_hash' => $package->getHash(), // hash [a-z0-9]{20}_[0-9]{14}
'created' => $package->getCreated(),
'type' => PackageUtils::getExecTypeString($package->getExecutionType(), $package->template_id),
'engine' => PackageUtils::getEngineTypeString(
$package->build_progress->current_build_mode,
$package->ziparchive_mode
),
'secure_mode' => $package->Installer->OptsSecureOn,
'runtime' => $package->Runtime,
'notes' => $package->notes,
'installer' => $package->Installer->getInstallerName(),
'backup_size_from_plugin' => $package->Archive->Size, // Used on upload start to know the estimated file size
'filters' => [
'enabled' => $package->Archive->FilterOn,
'directories' => [
'user' => $package->Archive->FilterInfo->Dirs->Instance,
'unreadable' => $package->Archive->FilterInfo->Dirs->Unreadable,
],
'files' => [
'user' => $package->Archive->FilterInfo->Files->Instance,
'unreadable' => $package->Archive->FilterInfo->Files->Unreadable,
],
'extensions' => $package->Archive->FilterInfo->Exts->Instance,
],
'components' => array_map(fn($component): string => BuildComponents::getLabel($component), $package->components),
],
'db_info' => [
'engine' => $package->Database->info->dbEngine,
'version' => $package->VersionDB,
'name' => $package->Database->info->name,
'size' => $package->Database->info->tablesSizeOnDisk,
'filters' => [
'enabled' => $package->Database->FilterOn,
'tables' => explode(',', $package->Database->FilterTables),
],
'collations' => $package->Database->info->collationList,
],
];
$reasons = [];
$isEligible = self::isCloudRecoveryEligible($package, $reasons);
$data['file_info']['installer_params'] = PackageUtils::getOverwriteParamFileName($package->getPrimaryInternalHash());
$data['file_info']['is_recovery_eligible'] = $isEligible;
$data['file_info']['recovery_ineligibility_reasons'] = $reasons;
return $data;
}
/**
* Check if a package is eligible to be a cloud recovery point.
*
* This checks recovery eligibility without requiring local storage,
* since the package is being uploaded to cloud storage.
*
* When $reasons is passed, it is populated with the structured ineligibility data
* (keys present only when the condition causes ineligibility).
*
* @param AbstractPackage $package the Backup
* @param IneligibilityReasons $reasons Populated by reference with ineligibility data
*
* @return bool
*/
private static function isCloudRecoveryEligible(AbstractPackage $package, array &$reasons = []): bool
{
$cacheKey = $package->getPrimaryInternalHash();
if (!isset(self::$eligibilityCache[$cacheKey])) {
$cacheReasons = [];
self::$eligibilityCache[$cacheKey] = [
'result' => (new RecoveryStatus($package))->meetsRecoveryRequirements($cacheReasons),
'reasons' => $cacheReasons,
];
}
$reasons = self::$eligibilityCache[$cacheKey]['reasons'];
return self::$eligibilityCache[$cacheKey]['result'];
}
/**
* Return Backup transfer files
*
* Overrides parent to add the restore config file required by the cloud installer for any backup.
*
* @param AbstractPackage $package the Backup
*
* @return array<string,string> return array from => to
*/
protected function getPackageUploadFiles(AbstractPackage $package): array
{
$files = parent::getPackageUploadFiles($package);
$configPath = $this->createRestoreConfigFile($package);
if ($configPath !== false) {
// All direct files must be uploaded before the archive, order important
$files = array_merge([$configPath => basename($configPath)], $files);
}
return $files;
}
/**
* Create temporary restore config file
*
* @param AbstractPackage $package the Backup
*
* @return string|false Path to temp file or false on failure
*/
protected function createRestoreConfigFile(AbstractPackage $package)
{
try {
$archivePath = $package->getLocalPackageFilePath(AbstractPackage::FILE_TYPE_ARCHIVE);
if ($archivePath === false || !file_exists($archivePath)) {
throw new Exception('Archive file not found');
}
$params = (new BackupPackage($archivePath, $package))->getOverwriteParams();
return PackageUtils::writeOverwriteParams(DUPLICATOR_SSDIR_PATH_TMP, $package->getPrimaryInternalHash(), $params);
} catch (Exception $e) {
DupLog::infoTrace('Failed to create restore Backup config file: ' . $e->getMessage());
return false;
}
}
/**
* Get upload chunk timeout in seconds
*
* @return int timeout in microseconds, 0 unlimited
*/
public function getUploadChunkTimeout(): int
{
$global = GlobalEntity::getInstance();
return (int) ($global->php_max_worker_time_in_sec <= 0 ? 0 : $global->php_max_worker_time_in_sec * SECONDS_IN_MICROSECONDS);
}
/**
* Duplicator cloud storage handles the pruning of old backups automatically
*
* @param array<string> $exclude List of Backups to exclude from deletion
*
* @return false|string[] false on failure or array of deleted files of Backups
*/
public function purgeOldPackages(array $exclude = [])
{
DupLog::infoTrace("Old backups are purged automatically by Duplicator Cloud storage");
return [];
}
/**
* Get upload chunk size in bytes
*
* @return int bytes
*/
public function getUploadChunkSize(): int
{
$dGlobal = DynamicGlobalEntity::getInstance();
return $dGlobal->getValInt(
'dupcloud_upload_chunk_size_in_kb',
self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB
) * KB_IN_BYTES;
}
/**
* Get download chunk size in bytes
*
* @return int bytes
*/
public function getDownloadChunkSize(): int
{
$dGlobal = DynamicGlobalEntity::getInstance();
return $dGlobal->getValInt(
'dupcloud_download_chunk_size_in_kb',
self::DEFAULT_DOWNLOAD_CHUNK_SIZE_IN_KB
) * KB_IN_BYTES;
}
/**
* Get user name
*
* @return string Return user name or false if not available
*/
public function getUserName(): string
{
return $this->config['userName'] ?? __('unknown', 'duplicator-pro');
}
/**
* Get user email
*
* @return string Return user email or false if not available
*/
public function getUserEmail(): string
{
return $this->config['userEmail'] ?? __('unknown', 'duplicator-pro');
}
/**
* Get total space
*
* @return int
*/
public function getTotalSpace(): int
{
return $this->config['totalSpace'] ?? 0;
}
/**
* Get free space
*
* @return int
*/
public function getUsedSpace(): int
{
$freeSpace = $this->config['freeSpace'] ?? 0;
$totalSpace = $this->config['totalSpace'] ?? 0;
return $totalSpace - $freeSpace;
}
/**
* Get free space
*
* @return int
*/
public function getFreeSpace(): int
{
return $this->config['freeSpace'] ?? 0;
}
/**
* Get website UUID
*
* @return string
*/
public function getWebsiteUuid(): string
{
return $this->config['websiteUuid'] ?? '';
}
/**
* Get unique DupCloud storasge or new tempalte if not exists
*
* @return self
*/
public static function getUniqueStorage(): self
{
$storages = static::getAllBySType(self::getSType());
if (is_array($storages) && count($storages) > 0) {
return $storages[0];
} else {
$storage = new self();
return $storage;
}
}
/**
* Register storage type
*
* @return void
*/
public static function registerType(): void
{
parent::registerType();
add_action('duplicator_update_global_storage_settings', function (): void {
$dGlobal = DynamicGlobalEntity::getInstance();
foreach (static::getDefaultSettings() as $key => $default) {
$value = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, $key, $default);
$dGlobal->setValInt($key, $value);
}
});
}
/**
* Get default settings
*
* @return array<string, scalar>
*/
protected static function getDefaultSettings(): array
{
return [
'dupcloud_upload_chunk_size_in_kb' => self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB,
'dupcloud_download_chunk_size_in_kb' => self::DEFAULT_DOWNLOAD_CHUNK_SIZE_IN_KB,
];
}
/**
* Render global options
*
* @return void
*/
public static function renderGlobalOptions(): void
{
if (static::isHidden()) {
return;
}
$dGlobal = DynamicGlobalEntity::getInstance();
TplMng::getInstance()->render(
'dupcloudaddon/configs/global_options',
[
'uploadChunkSizeInKb' => $dGlobal->getValInt(
'dupcloud_upload_chunk_size_in_kb',
self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB
),
'downloadChunkSizeInKb' => $dGlobal->getValInt(
'dupcloud_download_chunk_size_in_kb',
self::DEFAULT_DOWNLOAD_CHUNK_SIZE_IN_KB
),
]
);
}
/**
* Check if config data has changed
*
* @param array<string,mixed> $newData New data to compare
*
* @return bool True if changed, false otherwise
*/
private function hasConfigChanged(array $newData): bool
{
foreach ($newData as $key => $value) {
if (!isset($this->config[$key]) || $this->config[$key] !== $value) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* Handler for DupCloud storage tokens via license integration
*
* @package Duplicator\Addons\DupCloudAddon
* @copyright (c) 2024, Snap Creek LLC
*/
declare(strict_types=1);
namespace Duplicator\Addons\DupCloudAddon\Models;
use Duplicator\Addons\ProBase\Models\LicenseData;
use Duplicator\Utils\Logging\DupLog;
/**
* Handles storage tokens for quick connect from duplicator pro license
*/
class QuickConnect
{
/** @var array<string,mixed> */
private static $tokens = [];
/**
* Initialize hooks
*
* @return void
*/
public static function init(): void
{
add_action('duplicator_license_check_remote_data_success', [self::class, 'handleLicenseDataSuccess'], 10, 2);
}
/**
* Update storage tokens
*
* @param LicenseData $license The license data instance
* @param object $remoteData The remote data from license check
*
* @return void
*/
public static function handleLicenseDataSuccess(LicenseData $license, $remoteData): void
{
if (
!isset($remoteData->storage_tokens) ||
!is_array($remoteData->storage_tokens)
) {
return;
}
foreach ($remoteData->storage_tokens as $tokenObj) {
self::$tokens[] = [
'license_key' => $tokenObj->license_key,
'token' => $tokenObj->token,
'product_name' => $tokenObj->product_name,
'price_name' => $tokenObj->price_name,
'expiration' => $tokenObj->expiration,
'is_lifetime' => $tokenObj->is_lifetime,
];
}
}
/**
* Get storage tokens by requesting fresh license data
*
* @return array<string,mixed>
*/
public static function getStorageTokens(): array
{
$licenseData = LicenseData::getInstance();
// Tokens are set with funciton handleLicenseDataSuccess inside getLicenseData using hooks
add_filter('duplicator_license_request_params', [self::class, 'addStorageTokensRequest']);
$licenseData->getLicenseData(true);
remove_filter('duplicator_license_request_params', [self::class, 'addStorageTokensRequest']);
return self::$tokens;
}
/**
* Add storage tokens request parameter to license request
*
* @param array<string,mixed> $params Request parameters
*
* @return array<string,mixed>
*/
public static function addStorageTokensRequest($params)
{
$params['request_storage_tokens'] = 'true';
return $params;
}
/**
* Check if license-based connection is available
*
* @return bool
*/
public static function isLicenseConnectionAvailable(): bool
{
return class_exists(LicenseData:: class);
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* Auloader calsses
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Addons\DupCloudAddon\Utils;
use Duplicator\Addons\DupCloudAddon\DupCloudAddon;
use Duplicator\Utils\AbstractAutoloader;
/**
* Autoloader calss, dont user Duplicator library here
*/
final class Autoloader extends AbstractAutoloader
{
const VENDOR_PATH = DupCloudAddon::ADDON_PATH . '/vendor-prefixed/';
/**
* Register autoloader function
*
* @return void
*/
public static function register(): void
{
spl_autoload_register([self::class, 'load']);
self::loadFiles();
}
/**
* Load class
*
* @param string $className class name
*
* @return void
*/
public static function load($className): void
{
foreach (self::getNamespacesVendorMapping() as $namespace => $mappedPath) {
if (strpos($className, (string) $namespace) !== 0) {
continue;
}
$filepath = self::getFilenameFromClass($className, $namespace, $mappedPath);
if (file_exists($filepath)) {
include $filepath;
return;
}
}
}
/**
* Load necessary files
*
* @return void
*/
private static function loadFiles(): void
{
$files = [
'/guzzlehttp/promises/src/functions_include.php',
'/guzzlehttp/psr7/src/functions_include.php',
'/guzzlehttp/guzzle/src/functions_include.php',
];
foreach ($files as $file) {
require_once self::VENDOR_PATH . $file;
}
}
/**
* Return namespace mapping
*
* @return string[]
*/
protected static function getNamespacesVendorMapping(): array
{
return [
self::ROOT_VENDOR . 'Psr\\Http\\Message' => self::VENDOR_PATH . '/psr/http-message/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',
];
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Addons\DupCloudAddon\Utils;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Utils\ExpireOptions;
/**
* DupCloud rate limit handler
*/
final class DupCloudRateLimitHandler
{
const EXPIRE_DUPCLOUD_RATE_LIMIT_PREFIX = 'dup_cloud_rate_limit_route_';
const EXPIRE_DUPCLOUD_DEFAULT_RATE_LIMIT_ERROR_KEY = 'dup_cloud_rate_limit_error';
/**
* Init
*
* @return void
*/
public static function init(): void
{
add_action('duplicator_dup_cloud_rate_limit_error', [self::class, 'handleRateLimitError'], 10, 2);
}
/**
* Check if any rate limit error is set
*
* @return bool
*/
public static function hasRateLimitError(): bool
{
return ExpireOptions::get(self::EXPIRE_DUPCLOUD_DEFAULT_RATE_LIMIT_ERROR_KEY, false);
}
/**
* Get retry after in seconds
*
* @return int
*/
public static function retryAfter(): int
{
if (!ExpireOptions::get(self::EXPIRE_DUPCLOUD_DEFAULT_RATE_LIMIT_ERROR_KEY, false)) {
return 0;
}
return ExpireOptions::getExpireTime(self::EXPIRE_DUPCLOUD_DEFAULT_RATE_LIMIT_ERROR_KEY) - time();
}
/**
* Handle rate limit error
*
* @param string $url URL
* @param int $retryAfter Retry after
*
* @return void
*/
public static function handleRateLimitError(string $url, int $retryAfter): void
{
if (!ExpireOptions::get(self::EXPIRE_DUPCLOUD_DEFAULT_RATE_LIMIT_ERROR_KEY, false)) {
ExpireOptions::set(self::EXPIRE_DUPCLOUD_DEFAULT_RATE_LIMIT_ERROR_KEY, true, $retryAfter);
}
ExpireOptions::set(self::getKey($url), true, $retryAfter);
}
/**
* Check if route is rate limited
*
* @param string $url The URL to check
*
* @return bool
*/
public static function isBlocked(string $url): bool
{
return ExpireOptions::get(self::getKey($url), false);
}
/**
* Get expire key from route
*
* @param string $url URL
*
* @return string
*/
private static function getKey(string $url): string
{
$url = SnapIO::untrailingslashit($url);
$id = sprintf('%u', crc32($url));
return self::EXPIRE_DUPCLOUD_RATE_LIMIT_PREFIX . $id;
}
}

View File

@@ -0,0 +1,930 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Addons\DupCloudAddon\Utils;
use Duplicator\Addons\DupCloudAddon\Exceptions\PresignedUrlExpiredException;
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudClient;
use Duplicator\Models\Storages\StoragePathInfo;
use Duplicator\Utils\Logging\DupLog;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Models\Storages\AbstractStorageAdapter;
use Duplicator\Package\Create\PackInstaller;
use Exception;
/**
* Storage adapter to connect with Duplicator Cloud
*/
class DupCloudStorageAdapter extends AbstractStorageAdapter
{
/** @var int */
const DEFAULT_CHUNK_SIZE = 6 * MB_IN_BYTES;
private string $accessToken;
private \Duplicator\Addons\DupCloudAddon\Utils\DupCloudClient $client;
/** @var resource */
private $sourceFileHandle;
/** @var string */
private $lastSourceFilePath;
/** @var resource */
private $destFileHandle;
/** @var string */
private $lastDestFilePath = '';
/** @var ?RemoteStorageInfo */
private $remoteInfo;
/** @var string */
protected string $backupType = DupCloudClient::BACKUP_TYPE_STANDARD;
/** @var int */
private int $maxBackups = 0;
/**
* Class constructor
*
* @param string $accessToken The access accessKey
* @param string $backupType The backup type
* @param int $maxBackups The max backups
*
* @return void
*/
public function __construct(string $accessToken = '', string $backupType = DupCloudClient::BACKUP_TYPE_STANDARD, int $maxBackups = 0)
{
$this->accessToken = $accessToken;
$this->client = new DupCloudClient($this->accessToken);
$this->backupType = $backupType;
$this->maxBackups = $maxBackups;
}
/**
* Destructor
*
* @return void
*/
public function __destruct()
{
if (is_resource($this->sourceFileHandle)) {
fclose($this->sourceFileHandle);
}
if (is_resource($this->destFileHandle)) {
fclose($this->destFileHandle);
}
}
/**
* 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 = ''): bool
{
return $this->isValid();
}
/**
* Destroy the storage on deletion.
*
* @return bool true on success or false on failure.
*/
public function destroy(): bool
{
// Cloud storage is not destroyable
if ($this->delete('/', true) === false) {
return false;
}
return true;
}
/**
* Check if storage is valid and ready to use.
*
* @param string $errorMsg The error message if storage is invalid.
*
* @return bool
*/
protected function realIsValid(string &$errorMsg = ''): bool
{
$remoteInfo = $this->remoteStorageInfo();
if (!$remoteInfo->isSuccess()) {
$errorMsg = __('Remote storage connection failed', 'duplicator-pro');
return false;
} elseif (!$remoteInfo->isAuthorized()) {
$errorMsg = __('Storage configuration is invalid', 'duplicator-pro');
return false;
} elseif ($remoteInfo->getFreeSpace() <= 0) {
$errorMsg = __('Storage is full', 'duplicator-pro');
return false;
}
return true;
}
/**
* Get remote Storage info
*
* @param string $errorMsg The error message if storage is invalid.
*
* @return RemoteStorageInfo
*/
protected function remoteStorageInfo(string &$errorMsg = ''): RemoteStorageInfo
{
if ($this->remoteInfo === null) {
$this->remoteInfo = $this->client->remoteStorageInfo($errorMsg);
}
return $this->remoteInfo;
}
/**
* Get free space in bytes
*
* @return int
*/
public function getFreeSpace(): int
{
return $this->remoteStorageInfo()->getFreeSpace();
}
/**
* Get total available space in bytes
*
* @return int
*/
public function getTotalSpace(): int
{
return $this->remoteStorageInfo()->getTotalSpace();
}
/**
* Get user email associated with the storage
*
* @return string
*/
public function getUserEmail(): string
{
return $this->remoteStorageInfo()->getUserEmail();
}
/**
* Get user name associated with the storage
*
* @return string
*/
public function getUserName(): string
{
return $this->remoteStorageInfo()->getUserName();
}
/**
* Get whether the request was successful
*
* @return bool
*/
public function isAuthorized(): bool
{
return $this->remoteStorageInfo()->isAuthorized();
}
/**
* Get whether the storage is ready for upload
*
* @return bool
*/
public function isReady(): bool
{
return $this->remoteStorageInfo()->isReady();
}
/**
* Get whether the request was successful
*
* @return bool
*/
public function isSuccess(): bool
{
return $this->remoteStorageInfo()->isSuccess();
}
/**
* Get website uuid
*
* @return string
*/
public function getWebsiteUuid(): string
{
return $this->remoteStorageInfo()->getWebsiteUuid();
}
/**
* 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(string $path): bool
{
if (DupCloudClient::isRootDir($path)) {
return true;
}
return false;
}
/**
* Create file with content.
*
* @param string $path The path to file.
* @param string $content The content of file.
*
* @return false The number of bytes that were written to the file, or false on failure.
*/
protected function realCreateFile(string $path, string $content): bool
{
return false;
}
/**
* 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(string $path, bool $recursive = false): bool
{
try {
if ($this->isIncremental()) {
throw new Exception('This method is not supported in incremental backup mode');
}
// Extract backup name from path (get filename only)
if (DupCloudClient::isRootDir($path)) {
$result = $this->client->deleteAllBackups();
if ($result) {
return true;
} else {
DupLog::trace('Failed to delete all backups from website storage.');
return false;
}
}
if (DupCloudClient::isAllowedFileName($path)) {
$result = $this->client->deleteFile($path);
if ($result) {
return true;
} else {
DupLog::trace('Failed to delete backup from cloud storage: ' . $path);
return false;
}
}
throw new Exception("Can only delete root dir or backup. Invalid Path: $path");
} catch (Exception $e) {
DupLog::traceException($e, 'Error deleting backup from cloud storage: ' . $path);
return false;
}
}
/**
* Get file content.
*
* @param string $path The path to file.
*
* @return bool The content of file or false on failure.
*/
public function getFileContent(string $path): bool
{
return false;
}
/**
* 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(string $oldPath, string $newPath): bool
{
return false;
}
/**
* Get path info and cache it, is path not exists return path info with exists property set to false.
*
* @param string $path Relative storage path, if empty, return root path info.
*
* @return StoragePathInfo|false The path info or false on error.
*/
protected function getRealPathInfo(string $path)
{
try {
if ($this->isIncremental()) {
throw new Exception('This method is not supported in incremental backup mode');
}
// For root path, return directory info
if (DupCloudClient::isRootDir($path)) {
$info = new StoragePathInfo();
$info->path = '/';
$info->exists = true;
$info->isDir = true;
$info->size = 0;
$info->modified = time();
return $info;
}
if (DupCloudClient::isAllowedFileName($path)) {
return $this->client->getFileInfo($path);
} else {
$info = new StoragePathInfo();
$info->path = $path;
return $info;
}
} catch (Exception $e) {
DupLog::traceException($e, 'Error getting path info for: ' . $path);
return false;
}
}
/**
* 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(string $path, bool $files = true, bool $folders = true): array
{
try {
if ($this->isIncremental()) {
throw new Exception('This method is not supported in incremental backup mode');
}
if (!DupCloudClient::isRootDir($path)) {
DupLog::trace('Duplicator Cloud storage only supports scanning root directory, path provided: ' . $path);
return [];
}
if (!$files) {
DupLog::trace('Duplicator Cloud storage only contains files not directories.');
return [];
}
$fileList = [];
$storageInfolist = $this->client->getFileList();
foreach ($storageInfolist as $storageInfo) {
$fileList[] = $storageInfo->path;
}
return $fileList;
} catch (Exception $e) {
DupLog::traceException($e, 'Error scanning cloud storage directory');
return [];
}
}
/**
* 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(string $path, array $filters = []): bool
{
try {
if ($this->isIncremental()) {
throw new Exception('This method is not supported in incremental backup mode');
}
if (!DupCloudClient::isRootDir($path)) {
return true;
}
$backupList = $this->scanDir('/');
if (count($backupList) === 0) {
return true;
}
$regexFilters = [];
$normalFilters = [];
foreach ($filters as $filter) {
if (preg_match('/^\/.*\/$/', $filter) === 1) {
$regexFilters[] = $filter;
} else {
$normalFilters[] = $filter;
}
}
$filtered = [];
foreach ($backupList as $backupName) {
if (in_array($backupName, $normalFilters)) {
continue;
}
foreach ($regexFilters as $regexFilter) {
if (preg_match($regexFilter, $backupName) === 1) {
continue 2;
}
}
$filtered[] = $backupName;
}
return count($filtered) === 0;
} catch (Exception $e) {
DupLog::traceException($e, 'Error checking if cloud storage directory is empty');
// If we can't determine, assume it's not empty to be safe
return false;
}
}
/**
* 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.
* This data is intended to be per-file and may be reset between files.
* @param array<string,mixed> $generalExtraData Extra data to pass to copy function that persists across files
* during the entire transfer operation.
*
* @return false|int The number of bytes that were written to the file, or false on failure.
*/
protected function realCopyToStorage(
string $sourceFile,
string $storageFile,
int $offset = 0,
int $length = -1,
int $timeout = 0,
array &$extraData = [],
array &$generalExtraData = []
) {
DupLog::infoTrace("Copying to Storage file " . $sourceFile . " to " . $storageFile);
$startTime = microtime(true);
$filename = basename($sourceFile);
$fileSize = filesize($sourceFile);
try {
if ($this->isFile($storageFile)) {
$remainingBytes = ($fileSize - $offset) > 0 ? ($fileSize - $offset) : 0;
DupLog::info("File already exists in cloud storage.");
DupLog::info("Going to return remaining bytes: {$remainingBytes}bytes to satisfy the copy request");
return $remainingBytes;
}
if (!DupCloudClient::isAllowedFileName($storageFile)) {
throw new Exception('Invalid backup name: ' . $storageFile);
}
if (!is_file($sourceFile)) {
throw new Exception("File not found at path: {$sourceFile}");
}
// Only 2 types of files are allowed to be uploaded
// 1. All the files that pass direct upload file type check will be uploaded directly
// 2. All other files will have to be backup archives
if (($type = self::getDirectUploadFileType($filename)) !== null) {
if ($this->client->directUpload($sourceFile, $type, $storageFile) === false) {
throw new Exception('Failed to upload installer file to cloud direcly');
}
return $fileSize;
} elseif (preg_match(DUPLICATOR_ARCHIVE_REGEX_PATTERN, $filename) !== 1) {
throw new Exception('Invalid backup name: ' . $storageFile);
}
if ($offset === 0) {
if (!isset($generalExtraData['backup_details'])) {
throw new Exception('Backup details not found in extra data');
}
$backupDetails = $generalExtraData['backup_details'];
if ($backupDetails['file_info']['backup_filename'] !== $storageFile) {
throw new Exception('Backup name in backup details does not match the storage file name');
}
if (($timeout === 0 && $length < 0) || ($fileSize <= $length)) {
if ($this->client->upload($sourceFile, $backupDetails, $this->maxBackups) === false) {
throw new Exception('Failed to upload file to cloud storage in single request.');
}
return $fileSize;
}
if (!isset($extraData['UploadUuid']) || !isset($extraData['UploadUrls'])) {
$result = $this->client->startMultipart($backupDetails, $this->backupType);
$extraData['UploadUuid'] = $result['uuid'];
$extraData['UploadUrls'] = $result['urls'];
}
} elseif (!isset($extraData['UploadUuid']) || $extraData['UploadUuid'] === false) {
//the upload ID must exist if it's not the first chunk
throw new Exception('Upload UUID has to be set to continue multipart upload');
}
$partNumber = isset($extraData['Parts']) ? count($extraData['Parts']) + 1 : 1;
if (($sourceFileHandle = $this->getSourceFileHandle($sourceFile)) === false) {
throw new Exception('Couldn\'t open source file for reading');
}
$bytesWritten = 0;
$length = $length > 0 ? $length : self::DEFAULT_CHUNK_SIZE;
do {
if (
fseek($sourceFileHandle, $offset) === -1 ||
($content = fread($sourceFileHandle, $length)) === false
) {
throw new Exception('Couldn\'t read from source file');
}
$this->uploadPart($partNumber, $content, $extraData);
if ($timeout === 0) {
$bytesWritten = $length;
break;
}
$bytesWritten += strlen($content);
$offset += $length;
$partNumber++;
} while (self::getElapsedTime($startTime) < $timeout && !feof($sourceFileHandle));
//finished upload
if (feof($sourceFileHandle)) {
if (!$this->client->completeMultipart($extraData['UploadUuid'], $extraData['Parts'], $this->maxBackups)) {
throw new Exception('Failed to complete multipart upload');
}
}
} catch (Exception $e) {
DupLog::infoTrace('DupCloudStorageAdapter::realCopyToStorage: ' . $e->getMessage());
DupLog::infoTraceException($e, 'DupCloudStorageAdapter::realCopyToStorage: ');
return false;
}
return $bytesWritten;
}
/**
* Upload a part
*
* @param int $partNumber The part number
* @param string $content The content
* @param array<string, mixed> $extraData Extra data
*
* @return void
*
* @throws Exception If the upload fails for other reasons
*/
protected function uploadPart(int $partNumber, string $content, array &$extraData): void
{
if (!isset($extraData['UploadUrls'][$partNumber])) {
$extraData['UploadUrls'] = $this->client->getPartUrls($extraData['UploadUuid'], $partNumber);
}
try {
$this->client->uploadPart($extraData['UploadUrls'][$partNumber], $content);
} catch (PresignedUrlExpiredException $e) {
$extraData['UploadUrls'] = $this->client->getPartUrls($extraData['UploadUuid'], $partNumber);
$this->client->uploadPart($extraData['UploadUrls'][$partNumber], $content);
}
$extraData['Parts'][] = [
'ETag' => md5($content),
'PartNumber' => $partNumber,
];
}
/**
* Check if direct upload file name
*
* @param string $filename The file name
*
* @return ?string The file type or null if not a direct upload file
*/
public static function getDirectUploadFileType(string $filename): ?string
{
$localInstallerRegex = '/(^.+_[a-z0-9]{7,}_[0-9]{14})_(.*)' . preg_quote(PackInstaller::INSTALLER_SERVER_EXTENSION, '/') . '$/';
if (preg_match($localInstallerRegex, $filename) === 1) {
return 'installer';
}
if (strpos($filename, DUPLICATOR_LOCAL_OVERWRITE_PARAMS) === 0) {
return 'installer-params';
}
return null;
}
/**
* 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.
* This data is intended to be per-file and may be reset between files.
* @param array<string,mixed> $generalExtraData Extra data to pass to copy function that persists across files
* during the entire transfer operation.
*
* @return false|int The number of bytes that were written to the file, or false on failure.
*/
public function copyFromStorage(
string $storageFile,
string $destFile,
int $offset = 0,
int $length = -1,
int $timeout = 0,
array &$extraData = [],
array &$generalExtraData = []
) {
$startTime = microtime(true);
try {
if (!DupCloudClient::isAllowedFileName($storageFile)) {
throw new Exception('Invalid backup name: ' . $storageFile);
}
$filename = basename($storageFile);
if ($offset > 0 && !file_exists($destFile)) {
throw new Exception('Destination file doesn\'t exist');
}
if (wp_mkdir_p(dirname($destFile)) == false) {
throw new Exception('Can\'t create parent folder');
}
if (is_file($destFile) && $offset === 0 && !unlink($destFile)) {
throw new Exception('Can\'t delete destination file');
}
if (!$this->isFile($filename)) {
throw new Exception('Backup file doesn\'t exist');
}
if (!isset($extraData['download_url'])) {
$extraData = $this->client->getDownloadData($filename);
if (empty($extraData['download_url'])) {
throw new Exception('Download URL not found');
}
}
if ($timeout === 0 && $offset === 0 && $length < 0) {
if (($content = $this->client->downloadChunk($extraData['download_url'], 0, -1)) === false) {
DupLog::infoTrace('Error downloading chunk: ' . print_r(
[
'url' => $extraData['download_url'],
'offset' => $offset,
'length' => $extraData['size'],
],
true
));
throw new Exception('Error downloading whole file');
}
return file_put_contents($destFile, $content);
}
if (($handle = $this->getDestFileHandle($destFile)) === false) {
return false;
}
$bytesWritten = 0;
$length = $length > 0 ? $length : self::DEFAULT_CHUNK_SIZE;
$downloadLenght = min($length, $extraData['size'] - $offset);
do {
if (($content = $this->client->downloadChunk($extraData['download_url'], $offset, $downloadLenght)) === false) {
DupLog::infoTrace('Error downloading chunk: ' . print_r(
[
'url' => $extraData['download_url'],
'offset' => $offset,
'length' => $downloadLenght,
],
true
));
throw new Exception('Error downloading chunk');
}
if (
@ftruncate($handle, $offset) === false ||
@fseek($handle, $offset) === -1 ||
@fwrite($handle, $content) === false ||
@fflush($handle) === false
) {
return false;
}
if ($timeout === 0) {
return $length;
}
$bytesWritten += strlen($content);
$offset += $length;
} while (self::getElapsedTime($startTime) < $timeout && $offset < $extraData['size']);
} catch (Exception $e) {
DupLog::infoTraceException($e, 'DupCloudStorageAdapter::copyFromStorage ');
return false;
}
return $bytesWritten;
}
/**
* Get the elapsed time in microseconds
*
* @param float $startTime The start time
*
* @return float The elapsed time in microseconds
*/
private static function getElapsedTime(float $startTime): float
{
return (microtime(true) - $startTime) * SECONDS_IN_MICROSECONDS;
}
/**
* Get the storage usage stats
*
* @return array{name:string,email:string,email_verified_at:string,created_at:string}
*/
public function getUserInfo(): array
{
return $this->client->getUserInfo();
}
/**
* Revoke authorization
*
* @return bool
*/
public function revokeAuthorization(): bool
{
return $this->client->revoke();
}
/**
* Mark an upload as failed on the remote server
*
* @param string $backupName The backup name to cancel
*
* @return bool True if cancelled, false otherwise
*/
public function failUploadByName(string $backupName): bool
{
try {
if (strlen($backupName) === 0) {
throw new Exception('Backup name is empty');
}
return $this->client->failUploadByName($backupName);
} catch (Exception $e) {
DupLog::infoTraceException($e, 'DupCloudStorageAdapter::failUpload');
return false;
}
}
/**
* Mark an upload as canceled on the remote server
*
* @param string $backupName The backup name to cancel
*
* @return bool True if cancelled, false otherwise
*/
public function cancelUploadByName(string $backupName): bool
{
try {
if (strlen($backupName) === 0) {
throw new Exception('Backup name is empty');
}
return $this->client->cancelUploadByName($backupName);
} catch (Exception $e) {
DupLog::infoTraceException($e, 'DupCloudStorageAdapter::cancelUpload');
return false;
}
}
/**
* Mark an upload as failed on the remote server
*
* @param string $uploadUuid The upload UUID to cancel
*
* @return bool True if cancelled, false otherwise
*/
public function failUpload(string $uploadUuid): bool
{
try {
if (strlen($uploadUuid) === 0) {
throw new Exception('Upload UUID is empty');
}
return $this->client->failUpload($uploadUuid);
} catch (Exception $e) {
DupLog::infoTraceException($e, 'DupCloudStorageAdapter::failUpload');
return false;
}
}
/**
* Cancel an upload on the remote server
*
* @param string $uploadUuid The upload UUID to cancel
*
* @return bool True if cancelled, false otherwise
*/
public function cancelUpload(string $uploadUuid): bool
{
try {
if (strlen($uploadUuid) === 0) {
throw new Exception('Upload UUID is empty');
}
return $this->client->cancelUpload($uploadUuid);
} catch (Exception $e) {
DupLog::infoTraceException($e, 'DupCloudStorageAdapter::cancelUpload');
return false;
}
}
/**
* Returns true if we are in incremental mode
*
* @return bool
*/
private function isIncremental(): bool
{
return $this->backupType === DupCloudClient::BACKUP_TYPE_INCREMENTAL;
}
/**
* Returns the source file handle
*
* @param string $sourceFilePath The source file path
*
* @return resource
*/
private function getSourceFileHandle(string $sourceFilePath)
{
if ($this->lastSourceFilePath === $sourceFilePath) {
return $this->sourceFileHandle;
}
if (is_resource($this->sourceFileHandle)) {
fclose($this->sourceFileHandle);
}
if (($this->sourceFileHandle = SnapIO::fopen($sourceFilePath, 'r')) === false) {
throw new Exception('Can\'t open ' . $sourceFilePath . ' file');
}
$this->lastSourceFilePath = $sourceFilePath;
return $this->sourceFileHandle;
}
/**
* Returns the dest file handle
*
* @param string $destFilePath The dest file path
*
* @return resource|false The dest file handle or false on failure
*/
private function getDestFileHandle(string $destFilePath)
{
if ($this->lastDestFilePath === $destFilePath && is_resource($this->destFileHandle)) {
return $this->destFileHandle;
}
if (@is_resource($this->destFileHandle)) {
@fclose($this->destFileHandle);
}
if (($this->destFileHandle = @fopen($destFilePath, 'cb')) === false) {
return false;
}
$this->lastDestFilePath = $destFilePath;
return $this->destFileHandle;
}
}

View File

@@ -0,0 +1,151 @@
<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Addons\DupCloudAddon\Utils;
/**
* Class to store and provide access to remote storage information
*/
class RemoteStorageInfo
{
/** @var bool Whether the request was successful */
private bool $success = false;
/** @var bool Whether the storage is authorized */
private bool $authorized = false;
/** @var bool Whether the storage is ready for upload */
private bool $ready = false;
/** @var int Total available space in bytes */
private int $totalSpace = 0;
/** @var int Free space in bytes */
private int $freeSpace = 0;
/** @var string User name associated with the storage */
private string $userName = '';
/** @var string User email associated with the storage */
private string $userEmail = '';
/** @var string Website UUID */
private string $websiteUuid = '';
/**
* Constructor
*
* @param bool $success Whether the request was successful
* @param bool $authorized Whether the storage is authorized
* @param bool $ready Whether the storage is ready for upload
* @param int $totalSpace Total available space in bytes
* @param int $freeSpace Free space in bytes
* @param string $userName User name associated with the storage
* @param string $userEmail User email associated with the storage
* @param string $websiteUuid Website UUID
*/
public function __construct(
bool $success = false,
bool $authorized = false,
bool $ready = false,
int $totalSpace = 0,
int $freeSpace = 0,
string $userName = '',
string $userEmail = '',
string $websiteUuid = ''
) {
$this->success = $success;
$this->authorized = $authorized;
$this->ready = $ready;
$this->totalSpace = $totalSpace;
$this->freeSpace = $freeSpace;
$this->userName = $userName;
$this->userEmail = $userEmail;
$this->websiteUuid = $websiteUuid;
}
/**
* Get whether the request was successful
*
* @return bool
*/
public function isSuccess(): bool
{
return $this->success;
}
/**
* Get whether the storage is authorized
*
* @return bool
*/
public function isAuthorized(): bool
{
return $this->authorized;
}
/**
* Get whether the storage is ready for upload
*
* @return bool
*/
public function isReady(): bool
{
return $this->ready;
}
/**
* Get total available space in bytes
*
* @return int
*/
public function getTotalSpace(): int
{
return $this->totalSpace;
}
/**
* Get free space in bytes
*
* @return int
*/
public function getFreeSpace(): int
{
return $this->freeSpace;
}
/**
* Get user name associated with the storage
*
* @return string
*/
public function getUserName(): string
{
return $this->userName;
}
/**
* Get user email associated with the storage
*
* @return string
*/
public function getUserEmail(): string
{
return $this->userEmail;
}
/**
* Get website UUID
*
* @return string
*/
public function getWebsiteUuid(): string
{
return $this->websiteUuid;
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* Duplicator Cloud Storage Provider Configs
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
use Duplicator\Addons\DupCloudAddon\Models\DupCloudStorage;
defined("ABSPATH") or die("");
/**
* Variables
*
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
* @var \Duplicator\Core\Views\TplMng $tplMng
*/
$storage = $tplMng->getDataValueObjRequired('storage', DupCloudStorage::class);
$maxPackages = $tplMng->getDataValueIntRequired("maxPackages");
$tplMng->render('admin_pages/storages/parts/provider_head');
?>
<tr>
<th scope="row"><label for=""><?php esc_html_e("Authorization", 'duplicator-pro'); ?></label></th>
<td class="dupcloud-authorize">
<?php
$tplMng->render(
'dupcloudaddon/connect/storage_auth',
['token_connection' => true]
);
?>
</td>
</tr>
<tr>
<th scope="row"><label for="dupcloud_max_files"><?php esc_html_e("Max Backups", 'duplicator-pro'); ?></label></th>
<td>
<div class="horizontal-input-row">
<input
id="dupcloud_max_files"
class="dupcloud_max_files margin-0"
name="dupcloud_max_files"
type="number"
value="<?php echo (int) $maxPackages; ?>"
min="0"
maxlength="4"
data-parsley-errors-container="#dupcloud_max_files_error_container"
data-parsley-required="true"
data-parsley-type="number"
data-parsley-min="0"
>
<label for="dupcloud_max_files">
<?php esc_html_e("Number of Backups to keep.", 'duplicator-pro'); ?><br/>
</label>
</div>
<?php $tplMng->render('admin_pages/storages/parts/max_backups_description'); ?>
<div id="dupcloud_max_files_error_container" class="duplicator-error-container"></div>
</td>
</tr>
<?php
$tplMng->render('admin_pages/storages/parts/provider_foot');

View File

@@ -0,0 +1,71 @@
<?php
/**
* Template for Duplicator Cloud Connect Step 1
*
* @package Duplicator\Addons\DupCloudAddon
* @copyright (c) 2024, Snap Creek LLC
*/
use Duplicator\Addons\DupCloudAddon\Models\DupCloudStorage;
use Duplicator\Core\CapMng;
defined('ABSPATH') || exit;
/**
* Variables
*
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
* @var Duplicator\Core\Views\TplMng $tplMng
*/
$storage = $tplMng->getDataValueObjRequired('storage', DupCloudStorage::class);
$autoActivate = $tplMng->getDataValueBool('auto_activate_storage', false);
?>
<div class="dup-settings-wrapper" >
<h3 class="title">
<?php esc_html_e('Duplicator Cloud', 'duplicator-pro') ?>
</h3>
<hr size="1">
<?php if ($storage->isAuthorized()) { ?>
<label class="lbl-larger">
<?php esc_html_e("Cloud Info", 'duplicator-pro'); ?>
</label>
<?php } else { ?>
<p>
<?php esc_html_e('Connect to Duplicator Cloud Storage.', 'duplicator-pro'); ?>
</p>
<label class="lbl-larger">
<?php esc_html_e("Authorization", 'duplicator-pro'); ?>
</label>
<?php } ?>
<div class="margin-bottom-1">
<form
id="dup-storage-form"
method="post"
data-parsley-ui-enabled="true"
target="_self"
>
<input type="hidden" name="storage_id" id="storage_id" value="<?php echo (int) $storage->getId(); ?>">
<input type="hidden" id="name" name="name" value="<?php echo esc_attr($storage->getName()); ?>">
<?php $tplMng->render('dupcloudaddon/connect/storage_auth'); ?>
</form>
</div>
</div>
<?php
$tplMng->render('admin_pages/storages/storage_scripts');
if ($autoActivate && CapMng::can(CapMng::CAP_STORAGE, false)) {
?>
<script>
jQuery(document).ready(function($) {
var $button = $('#dupli-dupcloud-license-connect-btn');
if ($button.length) {
DupliJs.Storage.DupCloud.LicenseConnect($button, true);
}
});
</script>
<?php
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* Duplicator Cloud global storage options
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
use Duplicator\Addons\DupCloudAddon\Models\DupCloudStorage;
defined("ABSPATH") or die("");
/**
* Variables
*
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
* @var \Duplicator\Core\Views\TplMng $tplMng
* @var array<string, mixed> $tplData
*/
?>
<div class="dup-accordion-wrapper display-separators close" >
<div class="accordion-header" >
<h3 class="title"><?php echo esc_html(DupCloudStorage::getStypeName()); ?></h3>
</div>
<div class="accordion-content">
<label class="lbl-larger" >
<?php esc_html_e("Upload Chunk Size", 'duplicator-pro'); ?>
</label>
<div class="margin-bottom-1" >
<input
class="text-right inline-display width-small margin-bottom-0"
name="dupcloud_upload_chunk_size_in_kb"
id="dupcloud_upload_chunk_size_in_kb"
type="number"
min="<?php echo (int) DupCloudStorage::UPLOAD_CHUNK_MIN_SIZE_IN_KB; ?>"
max="<?php echo (int) DupCloudStorage::UPLOAD_CHUNK_MAX_SIZE_IN_KB; ?>"
data-parsley-required
data-parsley-type="number"
data-parsley-errors-container="#dupcloud_upload_chunk_size_in_kb_error_container"
value="<?php echo (int) $tplData['uploadChunkSizeInKb']; ?>"
>&nbsp;<b>KB</b>
<div id="dupcloud_upload_chunk_size_in_kb_error_container" class="duplicator-error-container"></div>
<p class="description">
<?php esc_html_e('How much should be uploaded to Duplicator Cloud per attempt. Higher=faster but less reliable.', 'duplicator-pro'); ?>
<?php echo esc_html(sprintf(__('Min size %skb.', 'duplicator-pro'), DupCloudStorage::UPLOAD_CHUNK_MIN_SIZE_IN_KB)); ?>
</p>
</div>
<label class="lbl-larger" >
<?php esc_html_e("Download Chunk Size", 'duplicator-pro'); ?>
</label>
<div class="margin-bottom-1" >
<input
class="text-right inline-display width-small margin-bottom-0"
name="dupcloud_download_chunk_size_in_kb"
id="dupcloud_download_chunk_size_in_kb"
type="number"
min="<?php echo (int) DupCloudStorage::DOWNLOAD_CHUNK_MIN_SIZE_IN_KB; ?>"
max="<?php echo (int) DupCloudStorage::DOWNLOAD_CHUNK_MAX_SIZE_IN_KB; ?>"
data-parsley-required
data-parsley-type="number"
data-parsley-errors-container="#dupcloud_download_chunk_size_in_kb_error_container"
value="<?php echo (int) $tplData['downloadChunkSizeInKb']; ?>"
>&nbsp;<b>KB</b>
<div id="dupcloud_download_chunk_size_in_kb_error_container" class="duplicator-error-container"></div>
<p class="description">
<?php esc_html_e('How much should be downloaded from Duplicator Cloud per attempt. Higher=faster but less reliable.', 'duplicator-pro'); ?>
<?php echo esc_html(sprintf(__('Min size %skb.', 'duplicator-pro'), DupCloudStorage::DOWNLOAD_CHUNK_MIN_SIZE_IN_KB)); ?>
</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,89 @@
<?php
/**
* Template for Duplicator Cloud Connect Step 1
*
* @package Duplicator\Addons\DupCloudAddon
* @copyright (c) 2024, Snap Creek LLC
*/
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudClient;
defined('ABSPATH') || exit;
/**
* Variables
*
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
* @var Duplicator\Core\Views\TplMng $tplMng
*/
$tokens = $tplMng->getDataValueArrayRequired('tokens');
switch (count($tokens)) {
case 0:
?>
<div class="dup-box">
<div class="dup-box-title">
<i class="fa fa-cloud-off"></i>&nbsp;
<?php esc_html_e('No Storage Available', 'duplicator-pro'); ?>
</div>
<div class="dup-box-panel" >
<p>
<?php esc_html_e(
'No Duplicator Cloud storage is available for your license. Purchase cloud storage to use this feature.',
'duplicator-pro'
); ?>
</p>
<a
href="<?php echo esc_url(DupCloudClient::getManageLicenseStorageUrl()); ?>"
class="button button-primary"
target="_blank"
>
<i class="fa fa-shopping-cart"></i> <?php esc_html_e('Purchase Storage', 'duplicator-pro'); ?>
</a>
</div>
</div>
<?php
break;
case 1:
// 1 token found, auto connect - no HTML needed, handled by JS
break;
default:
?>
<div class="dup-box">
<div class="dup-box-title">
<?php esc_html_e('Select Storage', 'duplicator-pro'); ?>
</div>
<div class="dup-box-panel" >
<p>
<?php esc_html_e(
'Multiple storage options are available for your license. Please select one:',
'duplicator-pro'
); ?>
</p>
<div id="storage-selection-list">
<?php foreach ($tokens as $index => $token) : ?>
<?php
$maskedLicense = substr($token['license_key'], 0, 4) . '***';
$expiration = $token['is_lifetime'] ?
__('Lifetime', 'duplicator-pro') :
date_i18n(get_option('date_format'), strtotime($token['expiration']));
?>
<div class="dup-storage-item margin-bottom-1" >
<strong><?php echo esc_html($token['price_name']); ?></strong>&nbsp;|&nbsp;
<?php esc_html_e('License:', 'duplicator-pro'); ?> <?php echo esc_html($maskedLicense); ?>&nbsp;|&nbsp;
<?php esc_html_e('Expires:', 'duplicator-pro'); ?> <?php echo esc_html($expiration); ?>&nbsp;&nbsp;
<button type="button" class="button button-primary small margin-bottom-0" data-token-select="<?php echo esc_attr($index); ?>">
<?php esc_html_e('Use This Storage', 'duplicator-pro'); ?>
</button>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php
break;
}

View File

@@ -0,0 +1,298 @@
<?php
/**
* Template for Duplicator Cloud Connect Step 1
*
* @package Duplicator\Addons\DupCloudAddon
* @copyright (c) 2024, Snap Creek LLC
*/
use Duplicator\Addons\DupCloudAddon\Ajax\ServicesDupCloud;
use Duplicator\Addons\DupCloudAddon\Models\DupCloudStorage;
use Duplicator\Addons\DupCloudAddon\Models\QuickConnect;
use Duplicator\Addons\DupCloudAddon\Utils\DupCloudClient;
use Duplicator\Libs\Snap\SnapString;
use Duplicator\Views\UI\UiDialog;
defined('ABSPATH') || exit;
/**
* Variables
*
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
* @var Duplicator\Core\Views\TplMng $tplMng
*/
$storage = $tplMng->getDataValueObjRequired('storage', DupCloudStorage::class);
$errorMsg = '';
$showLicenseConnection = QuickConnect::isLicenseConnectionAvailable();
$showTokenConnection = $tplMng->getDataValueBool('token_connection', false);
$bothConnection = $showLicenseConnection && $showTokenConnection;
if (!$storage->isAuthorized()) : ?>
<div class='dupcloud-authorization-state' id="dupcloud-state-unauthorized">
<?php if ($showLicenseConnection) { ?>
<button
id="dupli-dupcloud-license-connect-btn"
type="button"
class="button secondary hollow margin-bottom-0"
>
<i class="fa fa-key"></i> <?php esc_html_e('Connect via Duplicator Pro License', 'duplicator-pro'); ?>
</button>
<?php
}
if ($bothConnection) { ?>
&nbsp;<strong>or</strong>&nbsp;
<?php
}
if ($showTokenConnection) { ?>
<button
id="dupli-dupcloud-connect-btn"
type="button"
class="button secondary hollow margin-bottom-0"
onclick="DupliJs.Storage.DupCloud.ShowTokenInput();">
<i class="fa fa-plug"></i> <?php esc_html_e('Connect Duplicator Cloud Token', 'duplicator-pro'); ?>
</button>
<?php } ?>
<div id="dupli-dupcloud-token-area" style="display:none;">
<div class="storage-auth-step">
<p>
<b><?php esc_html_e('Step 1:', 'duplicator-pro'); ?></b>&nbsp;
<?php esc_html_e('Get your authentication token from Duplicator.com', 'duplicator-pro'); ?>
</p>
<button
type="button"
class="button secondary hollow margin-bottom-0"
onclick="window.open('<?php echo esc_js(DupCloudClient::getManageLicenseStorageUrl()); ?>', '_blank');">
<i class="fa fa-external-link"></i> <?php esc_html_e('Get Connection Token', 'duplicator-pro'); ?>
</button>
</div>
<div class="storage-auth-step">
<p>
<b><?php esc_html_e('Step 2:', 'duplicator-pro'); ?></b>&nbsp;
<?php esc_html_e('Paste your authentication token below:', 'duplicator-pro'); ?>
</p>
<input id="dupcloud-compound-token" name="dupcloud-compound-token" style="width: 500px" type="text">
</div>
<div class="storage-auth-step">
<button
id="dupcloud-finalize-setup"
type="button"
class="button secondary margin-bottom-0">
<i class="fa fa-check-square"></i> <?php esc_html_e('Finalize Setup', 'duplicator-pro'); ?>
</button>
</div>
</div>
</div>
<?php else : ?>
<div class='dupcloud-authorization-state' id="dupcloud-state-authorized" style="margin-top:-10px">
<?php if (strlen($storage->getUserName()) > 0) : ?>
<h3>
<?php esc_html_e('Duplicator Cloud Account', 'duplicator-pro'); ?><br />
<i class="dupli-edit-info">
<?php esc_html_e('Duplicator has been authorized to access this user\'s Duplicator Cloud account', 'duplicator-pro'); ?>
</i>
</h3>
<?php if (!$storage->isValid($errorMsg, true)) : ?>
<div class="alert-color margin-bottom-1">
<p><b><?php esc_html_e('The storage is currently not in a valid state.', 'duplicator-pro'); ?></b></p>
<p><b><?php esc_html_e('Error:', 'duplicator-pro'); ?></b> <?php echo esc_html($errorMsg); ?></p>
</div>
<?php endif; ?>
<div id="dupcloud-account-info">
<label><?php esc_html_e('Name', 'duplicator-pro'); ?>:</label>
<?php echo esc_html($storage->getUserName()); ?><br />
<label><?php esc_html_e('Email', 'duplicator-pro'); ?>:</label> <?php echo esc_html($storage->getUserEmail()); ?><br />
<label><?php esc_html_e('Space', 'duplicator-pro'); ?>:</label>
<?php if ($storage->getFreeSpace() > 0) : ?>
<?php printf(
'%1$s of %2$s is is used',
esc_html(SnapString::byteSize($storage->getUsedSpace())),
esc_html(SnapString::byteSize($storage->getTotalSpace()))
); ?>
<?php else : ?>
<b class="alert-color">
<?php printf(
'%1$s of %2$s is used',
esc_html(SnapString::byteSize($storage->getUsedSpace())),
esc_html(SnapString::byteSize($storage->getTotalSpace()))
); ?>
</b>
<br />
<br />
<b class="alert-color"><?php esc_html_e('Warning! Storage is full and cannot be used.', 'duplicator-pro'); ?></b>
<?php endif ?>
</div><br />
<?php else : ?>
<div><?php esc_html_e('Error retrieving user information.', 'duplicator-pro'); ?></div>
<?php endif ?>
<a href="<?php echo esc_url($storage->getBackupsUrl()); ?>" target="_blank"
id="dup-dupcloud-manage-website"
class="button margin-right-1 button-primary"
target="_blank"
>
<?php esc_html_e('Manage Backups', 'duplicator-pro'); ?>
</a>
<button
id="dup-dupcloud-cancel-authorization"
type="button"
class="button gray hollow">
<?php esc_html_e('Cancel Authorization', 'duplicator-pro'); ?>
</button><br />
<i class="dupli-edit-info">
<?php
esc_html_e(
'Disassociation of storage provider will require re-authorization.',
'duplicator-pro'
); ?>
</i>
</div>
<?php endif;
$alertConnStatus = new UiDialog();
$alertConnStatus->title = __('Duplicator Cloud Authorization Error', 'duplicator-pro');
$alertConnStatus->message = ''; // javascript inserted message
$alertConnStatus->initAlert();
?>
<script>
jQuery(document).ready(function($) {
DupliJs.Storage.DupCloud = DupliJs.Storage.DupCloud || {};
DupliJs.Storage.DupCloud.ShowTokenInput = function() {
$('#dupli-dupcloud-connect-btn-area').hide();
$('#dupli-dupcloud-token-area').show();
}
DupliJs.Storage.DupCloud.LicenseConnect = function($button, $skipNoLicenses = false) {
var originalText = $button.html();
$button.html('<i class="fa fa-spinner fa-spin"></i> <?php esc_html_e("Getting tokens...", "duplicator-pro"); ?>').prop('disabled', true);
// AJAX call to get storage tokens
DupliJs.Util.ajaxWrapper({
action: '<?php echo esc_js(ServicesDupCloud::AJAX_ACTION_QUICK_CONNECT); ?>',
nonce: '<?php echo esc_js(wp_create_nonce(ServicesDupCloud::AJAX_ACTION_QUICK_CONNECT)); ?>'
},
function(result, data, funcData, textStatus, jqXHR) {
if (result.success) {
var tokens = funcData.tokens;
var modalHtml = funcData.html;
if (tokens.length === 0) {
$button.html(originalText).prop('disabled', false);
if ($skipNoLicenses == false) {
// No storage tokens available - show modal with purchase message
DupliJs.Storage.DupCloudLicense.showModal(modalHtml);
}
} else if (tokens.length === 1) {
// Single storage - auto connect (no modal)
DupliJs.Storage.DupCloudLicense.autoConnect(tokens[0]);
} else {
// Multiple storages - show selection modal
DupliJs.Storage.DupCloudLicense.showModal(modalHtml, tokens);
}
} else {
$button.html(originalText).prop('disabled', false);
// Skip error messages in silence mode
if (data.message && $skipNoLicenses == false) {
DupliJs.addAdminMessage(data.message, 'error');
}
}
});
}
$('#dup-dupcloud-manage-website').click(function(e) {
e.stopPropagation();
window.open('<?php echo esc_js(DupCloudClient::manageWebsitesUrl()); ?>', '_blank');
return false;
});
$('#dup-dupcloud-cancel-authorization').click(function(e) {
e.stopPropagation();
DupliJs.Storage.RevokeAuth(<?php echo (int) $storage->getId(); ?>);
return false;
});
$('#dupcloud-finalize-setup').click(function(event) {
event.stopPropagation();
var compoundToken = $('#dupcloud-compound-token').val().trim();
if (compoundToken.length > 0) {
// Validate token format (should contain a dot)
if (compoundToken.indexOf('.') === -1) {
<?php $alertConnStatus->showAlert(); ?>
let alertMsg = "<i class='fas fa-exclamation-triangle'></i> " +
"<?php esc_html_e('Invalid token format. Please ensure you copied the complete authentication token.', 'duplicator-pro'); ?>";
<?php $alertConnStatus->updateMessage("alertMsg"); ?>
return false;
}
DupliJs.Storage.PrepareForSubmit();
DupliJs.Storage.Authorize(
<?php echo (int) $storage->getId(); ?>,
<?php echo (int) $storage->getSType(); ?>, {
'name': $('#name').val(),
'notes': $('#notes').val(),
'access_token': compoundToken // Send compound token as access_token
}
);
} else {
<?php $alertConnStatus->showAlert(); ?>
let alertMsg = "<i class='fas fa-exclamation-triangle'></i> " +
"<?php esc_html_e('Please paste your authentication token!', 'duplicator-pro'); ?>";
<?php $alertConnStatus->updateMessage("alertMsg"); ?>
}
return false;
});
// License-based connection handler
$('#dupli-dupcloud-license-connect-btn').click(function(event) {
event.stopPropagation();
var $button = $(this);
DupliJs.Storage.DupCloud.LicenseConnect($button);
return false;
});
});
// License connection namespace
DupliJs.Storage.DupCloudLicense = DupliJs.Storage.DupCloudLicense || {};
DupliJs.Storage.DupCloudLicense.showModal = function(htmlContent, tokens = null) {
var modal = new DuplicatorModalBox({
htmlContent: htmlContent,
closeInContent: true,
closeColor: "#000",
openCallback: function(content, modalObj) {
// If tokens provided, setup click handlers for storage selection
if (tokens && tokens.length > 1) {
$(content).find('[data-token-select]').click(function() {
var tokenIndex = $(this).data('token-select');
if (tokens[tokenIndex]) {
modalObj.close();
DupliJs.Storage.DupCloudLicense.autoConnect(tokens[tokenIndex]);
}
});
}
}
});
modal.open();
};
DupliJs.Storage.DupCloudLicense.autoConnect = function(storageToken) {
// Set the token in the input field
$('#dupcloud-compound-token').val(storageToken.token);
// Automatically trigger finalize setup
setTimeout(function() {
$('#dupcloud-finalize-setup').click();
}, 100);
};
</script>

View File

@@ -0,0 +1,39 @@
<?php
/**
* Template for Duplicator Cloud Connect Step 1
*
* @package Duplicator\Addons\DupCloudAddon
* @copyright (c) 2024, Snap Creek LLC
*/
use Duplicator\Addons\DupCloudAddon\Models\DupCloudStorage;
use Duplicator\Core\CapMng;
defined('ABSPATH') || exit;
/**
* Variables
*
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
* @var Duplicator\Core\Views\TplMng $tplMng
*/
$storage = DupCloudStorage::getUniqueStorage();
if (
!CapMng::can(CapMng::CAP_STORAGE, false) ||
!$storage->isAuthorized()
) {
return;
}
?>
<span>
<a href="<?php echo esc_url($storage->getBackupsUrl()); ?>" target="_blank"
id="dup-dupcloud-manage-website"
class="button button-primary hollow tiny font-bold margin-bottom-0"
target="_blank"
>
<i class="fa-solid fa-cloud"></i>&nbsp;
<?php esc_html_e('Cloud Dashboard', 'duplicator-pro'); ?>
</a>
</span>