first commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<Files *.php>
|
||||
Order Deny,Allow
|
||||
Deny from all
|
||||
</Files>
|
||||
|
||||
<Files index.php>
|
||||
Order Allow,Deny
|
||||
Allow from all
|
||||
</Files>
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* FTP/SFTP ADDON
|
||||
*
|
||||
* Name: Duplicator PRO base
|
||||
* Version: 1
|
||||
* Author: Duplicator
|
||||
* Author URI: https://duplicator.com/
|
||||
*
|
||||
* PHP version 5.3
|
||||
*
|
||||
* @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\AmazonS3Addon;
|
||||
|
||||
use Duplicator\Addons\AmazonS3Addon\Models\AmazonS3Storage;
|
||||
use Duplicator\Addons\AmazonS3Addon\Models\AmazonS3CompatibleStorage;
|
||||
use Duplicator\Addons\AmazonS3Addon\Models\BackblazeStorage;
|
||||
use Duplicator\Addons\AmazonS3Addon\Models\CloudflareStorage;
|
||||
use Duplicator\Addons\AmazonS3Addon\Models\DigitalOceanStorage;
|
||||
use Duplicator\Addons\AmazonS3Addon\Models\DreamStorage;
|
||||
use Duplicator\Addons\AmazonS3Addon\Models\GoogleCloudStorage;
|
||||
use Duplicator\Addons\AmazonS3Addon\Models\VultrStorage;
|
||||
use Duplicator\Addons\AmazonS3Addon\Models\WasabiStorage;
|
||||
use Duplicator\Addons\AmazonS3Addon\Utils\Autoloader;
|
||||
use Duplicator\Core\Addons\AbstractAddonCore;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
|
||||
/**
|
||||
* Storage ftp/sftp addon class
|
||||
*/
|
||||
class AmazonS3Addon extends AbstractAddonCore
|
||||
{
|
||||
const ADDON_PATH = __DIR__;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
Autoloader::register();
|
||||
|
||||
add_action('duplicator_pro_daily_actions', [__CLASS__, 'purgeOldS3MultipartUploads']);
|
||||
add_action('duplicator_pro_register_storage_types', [$this, 'registerStorages']);
|
||||
add_filter('duplicator_template_file', [__CLASS__, 'getTemplateFile'], 10, 2);
|
||||
add_filter('duplicator_usage_stats_storages_infos', [__CLASS__, 'getStorageUsageStats'], 10, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register storages
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerStorages()
|
||||
{
|
||||
AmazonS3Storage::registerType();
|
||||
AmazonS3CompatibleStorage::registerType();
|
||||
GoogleCloudStorage::registerType();
|
||||
BackblazeStorage::registerType();
|
||||
DreamStorage::registerType();
|
||||
DigitalOceanStorage::registerType();
|
||||
VultrStorage::registerType();
|
||||
CloudflareStorage::registerType();
|
||||
WasabiStorage::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, 'amazons3addon/') === 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_s3_count'] = 0;
|
||||
$storageNums['storages_s3_compatible_count'] = 0;
|
||||
|
||||
foreach ($storages as $index => $storage) {
|
||||
switch ($storage->getStype()) {
|
||||
case AmazonS3Storage::getSType():
|
||||
$storageNums['storages_s3_count']++;
|
||||
break;
|
||||
case AmazonS3CompatibleStorage::getSType():
|
||||
case GoogleCloudStorage::getSType():
|
||||
case BackblazeStorage::getSType():
|
||||
case DreamStorage::getSType():
|
||||
case DigitalOceanStorage::getSType():
|
||||
case VultrStorage::getSType():
|
||||
case CloudflareStorage::getSType():
|
||||
case WasabiStorage::getSType():
|
||||
$storageNums['storages_s3_compatible_count']++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $storageNums;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Purge old S3 multipart uploads
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function purgeOldS3MultipartUploads()
|
||||
{
|
||||
if (($storages = AbstractStorageEntity::getAll()) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($storages as $storage) {
|
||||
if (!$storage instanceof \Duplicator\Addons\AmazonS3Addon\Models\AmazonS3Storage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$storage->purgeMultipartUpload();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonPath()
|
||||
{
|
||||
return __DIR__;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonFile()
|
||||
{
|
||||
return __FILE__;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\AmazonS3Addon\Models;
|
||||
|
||||
use Duplicator\Core\Views\TplMng;
|
||||
use Duplicator\Libs\Snap\SnapUtil;
|
||||
|
||||
class AmazonS3CompatibleStorage extends AmazonS3Storage
|
||||
{
|
||||
/**
|
||||
* Get default config
|
||||
*
|
||||
* @return array<string,scalar>
|
||||
*/
|
||||
protected static function getDefaultConfig()
|
||||
{
|
||||
$config = parent::getDefaultConfig();
|
||||
$config = array_merge($config, ['ACL_full_control' => false]);
|
||||
return $config;
|
||||
}
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('Amazon S3 Compatible', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an html anchor tag of location or a string
|
||||
*
|
||||
* @return string Returns an html anchor tag with the storage location as a hyperlink or just a plain string
|
||||
*/
|
||||
public function getHtmlLocationLink()
|
||||
{
|
||||
if (preg_match('/^http(s)?:\\/\\//i', $this->getLocationString())) {
|
||||
return '<a href="' . esc_url($this->getLocationString()) . '" target="_blank" >' . esc_html($this->getLocationLabel()) . '</a>';
|
||||
} else {
|
||||
return '<span>' . esc_html($this->getLocationString()) . '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
return '/' . $this->config['bucket'] . $this->getStorageFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage location label.
|
||||
*
|
||||
* @return string The storage location label
|
||||
*/
|
||||
protected function getLocationLabel()
|
||||
{
|
||||
return '/' . $this->config['bucket'] . $this->getStorageFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of S3 compatible providers
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getCompatibleProviders()
|
||||
{
|
||||
return array(
|
||||
'Aruba',
|
||||
'Cloudian',
|
||||
'Cloudn',
|
||||
'Connectria',
|
||||
'Constant',
|
||||
'Exoscal',
|
||||
'Eucalyptus',
|
||||
'Nifty',
|
||||
'Nimbula',
|
||||
'Minio',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority, used to sort storages.
|
||||
* 100 is neutral value, 0 is the highest priority
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getPriority()
|
||||
{
|
||||
return 160;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render form config fields
|
||||
*
|
||||
* @param bool $echo Echo or return
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderConfigFields($echo = true)
|
||||
{
|
||||
return TplMng::getInstance()->render(
|
||||
'amazons3addon/configs/all_s3_compatible',
|
||||
[
|
||||
'storage' => $this,
|
||||
'maxPackages' => $this->config['max_packages'],
|
||||
'storageFolder' => $this->config['storage_folder'],
|
||||
'accessKey' => $this->config['access_key'],
|
||||
'bucket' => $this->config['bucket'],
|
||||
'region' => $this->config['region'],
|
||||
'endpoint' => $this->config['endpoint'],
|
||||
'secretKey' => $this->config['secret_key'],
|
||||
'storageClass' => $this->config['storage_class'],
|
||||
'aclFullControl' => $this->config['ACL_full_control'],
|
||||
'isAutofillEndpoint' => $this->isAutofillEndpoint(),
|
||||
'isAutofillRegion' => $this->isAutofillRegion(),
|
||||
'isAclSupported' => $this->isACLSupported(),
|
||||
'aclDescription' => $this->getACLDescription(),
|
||||
'documentationLinks' => $this->getDocumentationLinks(),
|
||||
],
|
||||
$echo
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documentation links
|
||||
*
|
||||
* @return array<int,array<string,string>>
|
||||
*/
|
||||
protected static function getDocumentationLinks()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'label' => __('S3 Compatibility API', 'duplicator-pro'),
|
||||
'url' => 'https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the endpoint is generated automatically
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAutofillEndpoint()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the region is generated automatically
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAutofillRegion()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the ACL is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isACLSupported()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ACL description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getACLDescription()
|
||||
{
|
||||
return __(
|
||||
"This option only works if the storage provider supports the 'bucket-owner-full-control' object-level canned ACL.",
|
||||
'duplicator-pro'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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['endpoint'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 's3_endpoint');
|
||||
$this->config['ACL_full_control'] = $this->isACLSupported() ?
|
||||
SnapUtil::sanitizeBoolInput(SnapUtil::INPUT_REQUEST, 's3_ACL_full_control') : false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register storage type
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function registerType()
|
||||
{
|
||||
parent::registerType();
|
||||
|
||||
if (self::class === static::class) {
|
||||
// only add filter for current storage and not inherited
|
||||
add_filter('duplicator_pro_storage_type_class', function ($class, $type, $data) {
|
||||
if ($type == AmazonS3Storage::getSType()) {
|
||||
$isLegacy = (!isset($data['legacyEntity']) || $data['legacyEntity'] === true);
|
||||
$provider = (isset($data['s3_provider']) ? $data['s3_provider'] : '');
|
||||
if ($isLegacy && $provider == 'other') {
|
||||
$class = __CLASS__;
|
||||
}
|
||||
}
|
||||
return $class;
|
||||
}, 10, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\AmazonS3Addon\Models;
|
||||
|
||||
use DUP_PRO_Global_Entity;
|
||||
use Duplicator\Core\Views\TplMng;
|
||||
use Duplicator\Libs\Snap\SnapUtil;
|
||||
use Duplicator\Models\DynamicGlobalEntity;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
use Exception;
|
||||
|
||||
class AmazonS3Storage extends AbstractStorageEntity
|
||||
{
|
||||
/** @var int */
|
||||
const DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB = 6000;
|
||||
/** @var int */
|
||||
const UPLOAD_CHUNK_MIN_SIZE_IN_KB = 5 * 1024;
|
||||
|
||||
/**
|
||||
* Get default config
|
||||
*
|
||||
* @return array<string,scalar>
|
||||
*/
|
||||
protected static function getDefaultConfig()
|
||||
{
|
||||
$config = parent::getDefaultConfig();
|
||||
$config = array_merge(
|
||||
$config,
|
||||
[
|
||||
'access_key' => '',
|
||||
'bucket' => '',
|
||||
'region' => '',
|
||||
'endpoint' => '',
|
||||
'secret_key' => '',
|
||||
'storage_class' => 'STANDARD',
|
||||
'ACL_full_control' => true,
|
||||
]
|
||||
);
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the field label
|
||||
*
|
||||
* @param string $field Field name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getFieldLabel($field)
|
||||
{
|
||||
switch ($field) {
|
||||
case 'accessKey':
|
||||
return __('Access Key', 'duplicator-pro');
|
||||
case 'secretKey':
|
||||
return __('Secret Key', 'duplicator-pro');
|
||||
case 'region':
|
||||
return __('Region', 'duplicator-pro');
|
||||
case 'endpoint':
|
||||
return __('Endpoint', 'duplicator-pro');
|
||||
case 'bucket':
|
||||
return __('Bucket', 'duplicator-pro');
|
||||
case 'aclFullControl':
|
||||
return __('Additional Settings', 'duplicator-pro');
|
||||
default:
|
||||
throw new Exception("Unknown field: $field");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [
|
||||
'storage_folder' => $this->s3_storage_folder,
|
||||
'max_packages' => $this->s3_max_files,
|
||||
'access_key' => $this->s3_access_key,
|
||||
'bucket' => $this->s3_bucket,
|
||||
'region' => $this->s3_region,
|
||||
'endpoint' => $this->s3_endpoint,
|
||||
'secret_key' => $this->s3_secret_key,
|
||||
'storage_class' => $this->s3_storage_class,
|
||||
'ACL_full_control' => $this->s3_ACL_full_control,
|
||||
];
|
||||
// reset old values
|
||||
$this->s3_storage_folder = '';
|
||||
$this->s3_max_files = 10;
|
||||
$this->s3_access_key = '';
|
||||
$this->s3_bucket = '';
|
||||
$this->s3_provider = 'amazon';
|
||||
$this->s3_region = '';
|
||||
$this->s3_endpoint = '';
|
||||
$this->s3_secret_key = '';
|
||||
$this->s3_storage_class = 'STANDARD';
|
||||
$this->s3_ACL_full_control = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type icon.
|
||||
*
|
||||
* @return string Returns the icon
|
||||
*/
|
||||
public static function getStypeIcon()
|
||||
{
|
||||
return '<img src="' . esc_url(static::getIconUrl()) . '" class="dup-storage-icon" alt="' . esc_attr(static::getStypeName()) . '" />';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type icon url.
|
||||
*
|
||||
* @return string The icon url
|
||||
*/
|
||||
protected static function getIconUrl()
|
||||
{
|
||||
return DUPLICATOR_PRO_IMG_URL . '/aws.svg';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('Amazon S3', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority, used to sort storages.
|
||||
* 100 is neutral value, 0 is the highest priority
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getPriority()
|
||||
{
|
||||
return 150;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
$params = [
|
||||
'region' => $this->config['region'],
|
||||
'bucket' => $this->config['bucket'],
|
||||
'prefix' => $this->getStorageFolder(),
|
||||
];
|
||||
|
||||
return 'https://console.aws.amazon.com/s3/home' . '?' . http_build_query($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an html anchor tag of location
|
||||
*
|
||||
* @return string Returns an html anchor tag with the storage location as a hyperlink.
|
||||
*
|
||||
* @example
|
||||
* OneDrive Example return
|
||||
* <a target="_blank" href="https://1drv.ms/f/sAFrQtasdrewasyghg">https://1drv.ms/f/sAFrQtasdrewasyghg</a>
|
||||
*/
|
||||
public function getHtmlLocationLink()
|
||||
{
|
||||
return '<a href="' . esc_url($this->getLocationString()) . '" target="_blank" >' . esc_html($this->getLocationLabel()) . '</a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage location label.
|
||||
*
|
||||
* @return string The storage location label
|
||||
*/
|
||||
protected function getLocationLabel()
|
||||
{
|
||||
return 's3://' . $this->config['bucket'] . $this->getStorageFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
return SnapUtil::isCurlEnabled(true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported notice, displayed if storage isn't supported
|
||||
*
|
||||
* @return string html string or empty if storage is supported
|
||||
*/
|
||||
public static function getNotSupportedNotice()
|
||||
{
|
||||
if (static::isSupported()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!SnapUtil::isCurlEnabled()) {
|
||||
$result = sprintf(
|
||||
__(
|
||||
"The Storage %s requires the PHP cURL extension and related functions to be enabled.",
|
||||
'duplicator-pro'
|
||||
),
|
||||
static::getStypeName()
|
||||
);
|
||||
} elseif (!SnapUtil::isCurlEnabled(true, true)) {
|
||||
$result = sprintf(
|
||||
__(
|
||||
"The Storage %s requires 'curl_multi_' type functions to be enabled. One or more are disabled on your server.",
|
||||
'duplicator-pro'
|
||||
),
|
||||
static::getStypeName()
|
||||
);
|
||||
} else {
|
||||
$result = sprintf(
|
||||
__(
|
||||
'The Storage %s is not supported on this server.',
|
||||
'duplicator-pro'
|
||||
),
|
||||
static::getStypeName()
|
||||
);
|
||||
}
|
||||
|
||||
return esc_html($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is valid
|
||||
*
|
||||
* @return bool Return true if storage is valid and ready to use, false otherwise
|
||||
*/
|
||||
public function isValid()
|
||||
{
|
||||
return $this->getAdapter()->isValid();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 %1$s folder:<br/> <i>%2$s</i>', "duplicator-pro"),
|
||||
$this->getStypeName(),
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'pending':
|
||||
return sprintf(
|
||||
__('Transfer to %1$s folder %2$s is pending', "duplicator-pro"),
|
||||
$this->getStypeName(),
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'failed':
|
||||
return sprintf(
|
||||
__('Failed to transfer to %1$s folder %2$s', "duplicator-pro"),
|
||||
$this->getStypeName(),
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'cancelled':
|
||||
return sprintf(
|
||||
__('Cancelled before could transfer to %1$s folder %2$s', "duplicator-pro"),
|
||||
$this->getStypeName(),
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'success':
|
||||
return sprintf(
|
||||
__('Transferred package to %1$s folder %2$s', "duplicator-pro"),
|
||||
$this->getStypeName(),
|
||||
$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(
|
||||
'amazons3addon/configs/amazon_s3',
|
||||
[
|
||||
'storage' => $this,
|
||||
'maxPackages' => $this->config['max_packages'],
|
||||
'storageFolder' => $this->config['storage_folder'],
|
||||
'accessKey' => $this->config['access_key'],
|
||||
'bucket' => $this->config['bucket'],
|
||||
'region' => $this->config['region'],
|
||||
'endpoint' => $this->config['endpoint'],
|
||||
'secretKey' => $this->config['secret_key'],
|
||||
'storageClass' => $this->config['storage_class'],
|
||||
'aclFullControl' => $this->config['ACL_full_control'],
|
||||
'regionOptions' => self::regionOptions(),
|
||||
],
|
||||
$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, 's3_max_files', 10);
|
||||
$this->config['storage_folder'] = self::getSanitizedInputFolder('_s3_storage_folder');
|
||||
|
||||
$this->config['access_key'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 's3_access_key');
|
||||
$secretKey = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 's3_secret_key');
|
||||
if (strlen($secretKey) > 0) {
|
||||
$this->config['secret_key'] = $secretKey;
|
||||
}
|
||||
$this->config['region'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 's3_region');
|
||||
$this->config['storage_class'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 's3_storage_class');
|
||||
$this->config['bucket'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 's3_bucket');
|
||||
|
||||
|
||||
$message = sprintf(
|
||||
__('Storage Updated.', 'duplicator-pro'),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full s3 client
|
||||
*
|
||||
* @return S3StorageAdapter
|
||||
*/
|
||||
protected function getAdapter()
|
||||
{
|
||||
$global = DUP_PRO_Global_Entity::getInstance();
|
||||
return new S3StorageAdapter(
|
||||
$this->config['access_key'],
|
||||
$this->config['secret_key'],
|
||||
$this->config['region'],
|
||||
$this->config['bucket'],
|
||||
$this->config['storage_folder'],
|
||||
$this->config['endpoint'],
|
||||
$this->config['storage_class'],
|
||||
$global->ipv4_only,
|
||||
!$global->ssl_disableverify,
|
||||
($global->ssl_useservercerts ? '' : DUPLICATOR_PRO_CERT_PATH),
|
||||
$this->config['ACL_full_control']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns value => label pairs for region drop-down options for S3 Amazon Direct storage type
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected static function regionOptions()
|
||||
{
|
||||
return array(
|
||||
"us-east-1" => __("US East (N. Virginia)", 'duplicator-pro'),
|
||||
"us-east-2" => __("US East (Ohio)", 'duplicator-pro'),
|
||||
"us-west-1" => __("US West (N. California)", 'duplicator-pro'),
|
||||
"us-west-2" => __("US West (Oregon)", 'duplicator-pro'),
|
||||
"af-south-1" => __("Africa (Cape Town)", 'duplicator-pro'),
|
||||
"ap-east-1" => __("Asia Pacific (Hong Kong)", 'duplicator-pro'),
|
||||
"ap-south-1" => __("Asia Pacific (Mumbai)", 'duplicator-pro'),
|
||||
"ap-northeast-1" => __("Asia Pacific (Tokyo)", 'duplicator-pro'),
|
||||
"ap-northeast-2" => __("Asia Pacific (Seoul)", 'duplicator-pro'),
|
||||
"ap-northeast-3" => __("Asia Pacific (Osaka-Local)", 'duplicator-pro'),
|
||||
"ap-southeast-1" => __("Asia Pacific (Singapore)", 'duplicator-pro'),
|
||||
"ap-southeast-2" => __("Asia Pacific (Sydney)", 'duplicator-pro'),
|
||||
"ap-southeast-3" => __("Asia Pacific (Jakarta)", 'duplicator-pro'),
|
||||
"ca-central-1" => __("Canada (Central)", 'duplicator-pro'),
|
||||
"cn-north-1" => __("China (Beijing)", 'duplicator-pro'),
|
||||
"cn-northwest-1" => __("China (Ningxia)", 'duplicator-pro'),
|
||||
"eu-central-1" => __("EU (Frankfurt)", 'duplicator-pro'),
|
||||
"eu-west-1" => __("EU (Ireland)", 'duplicator-pro'),
|
||||
"eu-west-2" => __("EU (London)", 'duplicator-pro'),
|
||||
"eu-west-3" => __("EU (Paris)", 'duplicator-pro'),
|
||||
"eu-south-1" => __("Europe (Milan)", 'duplicator-pro'),
|
||||
"eu-north-1" => __("Europe (Stockholm)", 'duplicator-pro'),
|
||||
"me-south-1" => __("Middle East (Bahrain)", 'duplicator-pro'),
|
||||
"sa-east-1" => __("South America (Sao Paulo)", 'duplicator-pro'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register storage type
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
$dGlobal->save();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk size in bytes
|
||||
*
|
||||
* @return int bytes
|
||||
*/
|
||||
public function getUploadChunkSize()
|
||||
{
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
return $dGlobal->getVal('s3_upload_part_size_in_kb', self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_KB) * 1024;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default settings
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*/
|
||||
protected static function getDefaultSettings()
|
||||
{
|
||||
return ['s3_upload_part_size_in_kb' => 6000];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function renderGlobalOptions()
|
||||
{
|
||||
if (self::class !== static::class) {
|
||||
return;
|
||||
}
|
||||
$values = static::getDefaultSettings();
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
foreach ($values as $key => $default) {
|
||||
$values[$key] = $dGlobal->getVal($key, $default);
|
||||
}
|
||||
?>
|
||||
<h3 class="title"><?php esc_html_e("Amazon S3", 'duplicator-pro') ?></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="s3_upload_part_size_in_kb"
|
||||
id="s3_upload_part_size_in_kb"
|
||||
type="number"
|
||||
min="<?php echo self::UPLOAD_CHUNK_MIN_SIZE_IN_KB; ?>"
|
||||
max="5243000"
|
||||
data-parsley-required
|
||||
data-parsley-type="number"
|
||||
data-parsley-errors-container="#s3_upload_chunksize_in_kb_error_container"
|
||||
value="<?php echo $values['s3_upload_part_size_in_kb']; ?>"
|
||||
> <b>KB</b>
|
||||
<div id="s3_upload_chunksize_in_kb_error_container" class="duplicator-error-container"></div>
|
||||
<p class="description">
|
||||
<?php esc_html_e('How much should be uploaded to Amazon S3 per attempt. Higher=faster but less reliable.', 'duplicator-pro'); ?>
|
||||
<?php echo esc_html(sprintf(__('Min size %skb.', 'duplicator-pro'), self::UPLOAD_CHUNK_MIN_SIZE_IN_KB)); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge old multipart uploads
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function purgeMultipartUpload()
|
||||
{
|
||||
$this->getAdapter()->abortMultipartUploads(2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\AmazonS3Addon\Models;
|
||||
|
||||
class BackblazeStorage extends AmazonS3CompatibleStorage
|
||||
{
|
||||
/**
|
||||
* Get default config
|
||||
*
|
||||
* @return array<string,scalar>
|
||||
*/
|
||||
protected static function getDefaultConfig()
|
||||
{
|
||||
$config = parent::getDefaultConfig();
|
||||
$config['ACL_full_control'] = false;
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 9;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
return 'https://secure.backblaze.com/b2_buckets.htm';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the storage location label.
|
||||
*
|
||||
* @return string The storage location label
|
||||
*/
|
||||
protected function getLocationLabel()
|
||||
{
|
||||
return __('Bucket List', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('Backblaze B2', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority, used to sort storages.
|
||||
* 100 is neutral value, 0 is the highest priority
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getPriority()
|
||||
{
|
||||
return 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type icon url.
|
||||
*
|
||||
* @return string The icon url
|
||||
*/
|
||||
protected static function getIconUrl()
|
||||
{
|
||||
return DUPLICATOR_PRO_IMG_URL . '/backblaze.svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documentation links
|
||||
*
|
||||
* @return array<int,array<string,string>>
|
||||
*/
|
||||
protected static function getDocumentationLinks()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'label' => __('Overview', 'duplicator-pro'),
|
||||
'url' => 'https://www.backblaze.com/b2/docs/',
|
||||
],
|
||||
[
|
||||
'label' => __('S3 Compatible API', 'duplicator-pro'),
|
||||
'url' => 'https://www.backblze.com/b2/docs/s3_compatible_api.html',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the field label
|
||||
*
|
||||
* @param string $field Field name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getFieldLabel($field)
|
||||
{
|
||||
switch ($field) {
|
||||
case 'accessKey':
|
||||
return __('Key ID', 'duplicator-pro');
|
||||
case 'secretKey':
|
||||
return __('Application Key', 'duplicator-pro');
|
||||
}
|
||||
return parent::getFieldLabel($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if ACL is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isACLSupported()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the region is generated automatically
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAutofillRegion()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register storage type
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function registerType()
|
||||
{
|
||||
parent::registerType();
|
||||
add_filter('duplicator_pro_storage_type_class', function ($class, $type, $data) {
|
||||
if ($type == AmazonS3Storage::getSType()) {
|
||||
$isLegacy = (!isset($data['legacyEntity']) || $data['legacyEntity'] === true);
|
||||
$provider = (isset($data['s3_provider']) ? $data['s3_provider'] : '');
|
||||
if ($isLegacy && $provider == 'backblaze') {
|
||||
$class = __CLASS__;
|
||||
}
|
||||
}
|
||||
return $class;
|
||||
}, 10, 3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\AmazonS3Addon\Models;
|
||||
|
||||
class CloudflareStorage extends AmazonS3CompatibleStorage
|
||||
{
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 11;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('Cloudflare R2', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
return 'https://dash.cloudflare.com/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage location label.
|
||||
*
|
||||
* @return string The storage location label
|
||||
*/
|
||||
protected function getLocationLabel()
|
||||
{
|
||||
return __('Dashboard', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type icon url.
|
||||
*
|
||||
* @return string The icon url
|
||||
*/
|
||||
protected static function getIconUrl()
|
||||
{
|
||||
return DUPLICATOR_PRO_IMG_URL . '/cloudflare.svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the ACL is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isACLSupported()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documentation links
|
||||
*
|
||||
* @return array<int,array<string,string>>
|
||||
*/
|
||||
protected static function getDocumentationLinks()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'label' => __('Overview', 'duplicator-pro'),
|
||||
'url' => 'https://developers.cloudflare.com/r2/',
|
||||
],
|
||||
[
|
||||
'label' => __('S3 Compatible API', 'duplicator-pro'),
|
||||
'url' => 'https://developers.cloudflare.com/r2/api/s3/api/',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\AmazonS3Addon\Models;
|
||||
|
||||
class DigitalOceanStorage extends AmazonS3CompatibleStorage
|
||||
{
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 14;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('Digital Ocean Spaces', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the region is generated automatically
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAutofillRegion()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the ACL is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isACLSupported()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type icon url.
|
||||
*
|
||||
* @return string The icon url
|
||||
*/
|
||||
protected static function getIconUrl()
|
||||
{
|
||||
return DUPLICATOR_PRO_IMG_URL . '/digital-ocean.svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
return 'https://cloud.digitalocean.com/spaces/' . $this->config['bucket'] . $this->config['storage_folder'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documentation links
|
||||
*
|
||||
* @return array<int,array<string,string>>
|
||||
*/
|
||||
protected static function getDocumentationLinks()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'label' => __('Spaces Object Storage', 'duplicator-pro'),
|
||||
'url' => 'https://docs.digitalocean.com/products/spaces/',
|
||||
],
|
||||
[
|
||||
'label' => __('Spaces API', 'duplicator-pro'),
|
||||
'url' => 'https://docs.digitalocean.com/reference/api/spaces-api/',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\AmazonS3Addon\Models;
|
||||
|
||||
class DreamStorage extends AmazonS3CompatibleStorage
|
||||
{
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 13;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('Dream Objects', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type icon url.
|
||||
*
|
||||
* @return string The icon url
|
||||
*/
|
||||
protected static function getIconUrl()
|
||||
{
|
||||
return DUPLICATOR_PRO_IMG_URL . '/dreamhost.svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the region is generated automatically
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAutofillRegion()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the ACL is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isACLSupported()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
return 'https://panel.dreamhost.com/index.cgi?tree=cloud.objects';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage location label.
|
||||
*
|
||||
* @return string The storage location label
|
||||
*/
|
||||
protected function getLocationLabel()
|
||||
{
|
||||
return __('Bucket List', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documentation links
|
||||
*
|
||||
* @return array<int,array<string,string>>
|
||||
*/
|
||||
protected static function getDocumentationLinks()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'label' => __('Overview', 'duplicator-pro'),
|
||||
'url' => 'https://help.dreamhost.com/hc/en-us/articles/214823108-DreamObjects-overview',
|
||||
],
|
||||
[
|
||||
'label' => __('S3 Compatible API', 'duplicator-pro'),
|
||||
'url' => 'https://help.dreamhost.com/hc/en-us/articles/217590537-How-To-Use-DreamObjects-S3-compatible-API',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\AmazonS3Addon\Models;
|
||||
|
||||
class GoogleCloudStorage extends AmazonS3CompatibleStorage
|
||||
{
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 16;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('Google Cloud Storage', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
return 'https://console.cloud.google.com/storage/browser/' . $this->config['bucket'] . $this->config['storage_folder'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type icon url.
|
||||
*
|
||||
* @return string The icon url
|
||||
*/
|
||||
protected static function getIconUrl()
|
||||
{
|
||||
return DUPLICATOR_PRO_IMG_URL . '/google-cloud.svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ACL description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getACLDescription()
|
||||
{
|
||||
return __(
|
||||
"Make sure to change the 'Access Control' to 'Fine Grained' for this setting to work.",
|
||||
'duplicator-pro'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documentation links
|
||||
*
|
||||
* @return array<int,array<string,string>>
|
||||
*/
|
||||
protected static function getDocumentationLinks()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'label' => __('Interoperability with S3 API', 'duplicator-pro'),
|
||||
'url' => 'https://cloud.google.com/storage/docs/interoperability',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\AmazonS3Addon\Models;
|
||||
|
||||
class VultrStorage extends AmazonS3CompatibleStorage
|
||||
{
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 12;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('Vultr', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type icon url.
|
||||
*
|
||||
* @return string The icon url
|
||||
*/
|
||||
protected static function getIconUrl()
|
||||
{
|
||||
return DUPLICATOR_PRO_IMG_URL . '/vultr.svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the region is generated automatically
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAutofillRegion()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
return 'https://my.vultr.com/objectstorage/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage location label.
|
||||
*
|
||||
* @return string The storage location label
|
||||
*/
|
||||
protected function getLocationLabel()
|
||||
{
|
||||
return __('Bucket List', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documentation links
|
||||
*
|
||||
* @return array<int,array<string,string>>
|
||||
*/
|
||||
protected static function getDocumentationLinks()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'label' => __('Vultr Object Storage', 'duplicator-pro'),
|
||||
'url' => 'https://www.vultr.com/docs/vultr-object-storage/',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\AmazonS3Addon\Models;
|
||||
|
||||
class WasabiStorage extends AmazonS3CompatibleStorage
|
||||
{
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('Wasabi', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type icon url.
|
||||
*
|
||||
* @return string The icon url
|
||||
*/
|
||||
protected static function getIconUrl()
|
||||
{
|
||||
return DUPLICATOR_PRO_IMG_URL . '/wasabi.svg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the endpoint is generated automatically
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAutofillEndpoint()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationURL()
|
||||
{
|
||||
return 'https://console.wasabisys.com/file_manager/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage location label.
|
||||
*
|
||||
* @return string The storage location label
|
||||
*/
|
||||
protected function getLocationLabel()
|
||||
{
|
||||
return __('Bucket List', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documentation links
|
||||
*
|
||||
* @return array<int,array<string,string>>
|
||||
*/
|
||||
protected static function getDocumentationLinks()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'label' => __('Wasabi Academy', 'duplicator-pro'),
|
||||
'url' => 'https://docs.wasabi.com/',
|
||||
],
|
||||
[
|
||||
'label' => __('S3 Compatible API', 'duplicator-pro'),
|
||||
'url' => 'https://docs.wasabi.com/docs/wasabi-api',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Auloader calsses
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\AmazonS3Addon\Utils;
|
||||
|
||||
use Duplicator\Addons\AmazonS3Addon\AmazonS3Addon;
|
||||
use Duplicator\Utils\AbstractAutoloader;
|
||||
|
||||
/**
|
||||
* Autoloader calss, dont user Duplicator library here
|
||||
*/
|
||||
final class Autoloader extends AbstractAutoloader
|
||||
{
|
||||
const VENDOR_PATH = AmazonS3Addon::ADDON_PATH . '/vendor-prefixed/';
|
||||
|
||||
/**
|
||||
* Register autoloader function
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register()
|
||||
{
|
||||
spl_autoload_register([__CLASS__, 'load']);
|
||||
|
||||
self::loadFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load class
|
||||
*
|
||||
* @param string $className class name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function load($className)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load necessary files
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function loadFiles()
|
||||
{
|
||||
$files = [
|
||||
'/paragonie/random_compat/lib/random.php',
|
||||
'/ralouphie/getallheaders/src/getallheaders.php',
|
||||
'/guzzlehttp/promises/src/functions_include.php',
|
||||
'/guzzlehttp/psr7/src/functions_include.php',
|
||||
'/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||
'/symfony/polyfill-php70/bootstrap.php',
|
||||
'/symfony/polyfill-php72/bootstrap.php',
|
||||
'/symfony/polyfill-intl-idn/bootstrap.php',
|
||||
'/symfony/polyfill-mbstring/bootstrap.php',
|
||||
'/guzzlehttp/guzzle/src/functions_include.php',
|
||||
'/mtdowling/jmespath.php/src/JmesPath.php',
|
||||
'/aws/aws-sdk-php/src/functions.php',
|
||||
];
|
||||
|
||||
foreach ($files as $file) {
|
||||
require_once self::VENDOR_PATH . $file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return namespace mapping
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected static function getNamespacesVendorMapping()
|
||||
{
|
||||
return [
|
||||
self::ROOT_VENDOR . 'Symfony\\Polyfill\\Intl\\Idn' => AmazonS3Addon::getAddonPath() . '/vendor-prefixed/symfony/polyfill-intl-idn',
|
||||
self::ROOT_VENDOR . 'Symfony\\Polyfill\\Intl\\Normalizer' => AmazonS3Addon::getAddonPath() . '/vendor-prefixed/symfony/polyfill-intl-normalizer',
|
||||
self::ROOT_VENDOR . 'Symfony\\Polyfill\\Mbstring' => AmazonS3Addon::getAddonPath() . '/vendor-prefixed/symfony/polyfill-mbstring',
|
||||
self::ROOT_VENDOR . 'Symfony\\Polyfill\\Php70' => AmazonS3Addon::getAddonPath() . '/vendor-prefixed/symfony/polyfill-php70',
|
||||
self::ROOT_VENDOR . 'Symfony\\Polyfill\\Php72' => AmazonS3Addon::getAddonPath() . '/vendor-prefixed/symfony/polyfill-php72',
|
||||
self::ROOT_VENDOR . 'JmesPath' => AmazonS3Addon::getAddonPath() . '/vendor-prefixed/mtdowling/jmespath.php/src',
|
||||
self::ROOT_VENDOR . 'Psr\\Http\\Message' => AmazonS3Addon::getAddonPath() . '/vendor-prefixed/psr/http-message/src',
|
||||
self::ROOT_VENDOR . 'GuzzleHttp\\Promise' => AmazonS3Addon::getAddonPath() . '/vendor-prefixed/guzzlehttp/promises/src',
|
||||
self::ROOT_VENDOR . 'GuzzleHttp\\Psr7' => AmazonS3Addon::getAddonPath() . '/vendor-prefixed/guzzlehttp/psr7/src',
|
||||
self::ROOT_VENDOR . 'GuzzleHttp' => AmazonS3Addon::getAddonPath() . '/vendor-prefixed/guzzlehttp/guzzle/src',
|
||||
self::ROOT_VENDOR . 'Aws' => AmazonS3Addon::getAddonPath() . '/vendor-prefixed/aws/aws-sdk-php/src',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Duplicator messages sections
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\AmazonS3Addon\Models\AmazonS3CompatibleStorage;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var \Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
* @var AmazonS3CompatibleStorage $storage
|
||||
*/
|
||||
$storage = $tplData["storage"];
|
||||
/** @var int */
|
||||
$maxPackages = $tplData["maxPackages"];
|
||||
/** @var string */
|
||||
$storageFolder = $tplData["storageFolder"];
|
||||
/** @var string */
|
||||
$accessKey = $tplData["accessKey"];
|
||||
/** @var string */
|
||||
$bucket = $tplData["bucket"];
|
||||
/** @var string */
|
||||
$region = $tplData["region"];
|
||||
/** @var string */
|
||||
$secretKey = $tplData["secretKey"];
|
||||
/** @var string */
|
||||
$storageClass = $tplData["storageClass"];
|
||||
/** @var string */
|
||||
$endpoint = $tplData["endpoint"];
|
||||
/** @var string */
|
||||
$aclFullControl = $tplData["aclFullControl"];
|
||||
/** @var bool */
|
||||
$isAutofillEndpoint = $tplData["isAutofillEndpoint"];
|
||||
/** @var bool */
|
||||
$isAutofillRegion = $tplData["isAutofillRegion"];
|
||||
/** @var bool */
|
||||
$isAclSupported = $tplData["isAclSupported"];
|
||||
/** @var string */
|
||||
$aclDescription = $tplData["aclDescription"];
|
||||
/** @var array<int,array<string,string>> */
|
||||
$documentationLinks = $tplData["documentationLinks"];
|
||||
|
||||
$tplMng->render('admin_pages/storages/parts/provider_head');
|
||||
?>
|
||||
<tr>
|
||||
<td colspan="2" style="padding-left:0">
|
||||
<i><?php printf(
|
||||
esc_html_x(
|
||||
'S3 Setup Guide: %1$sStep-by-Step%2$s and %3$sUser Bucket Policy%4$s.',
|
||||
'1%$s and %3$s are opening and %2$s and %4$s are closing <a> tags',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a target="_blank" href="' . esc_url(DUPLICATOR_PRO_DUPLICATOR_DOCS_URL . 'amazon-s3-step-by-step') . '">',
|
||||
'</a>',
|
||||
'<a href="' . esc_url(DUPLICATOR_PRO_DUPLICATOR_DOCS_URL . 'amazon-s3-policy-setup') . '" target="_blank">',
|
||||
'</a>'
|
||||
); ?>
|
||||
</i>
|
||||
<i>
|
||||
<?php if (count($documentationLinks) > 0) {
|
||||
printf(
|
||||
esc_html_x(
|
||||
'Documentation for %s: ',
|
||||
'%s is the provider name',
|
||||
'duplicator-pro'
|
||||
),
|
||||
esc_html($storage->getStypeName())
|
||||
);
|
||||
|
||||
echo implode(', ', array_map(function ($link) {
|
||||
return '<a target="_blank" href="' . $link['url'] . '">' . $link['label'] . '</a>';
|
||||
}, $documentationLinks));
|
||||
} ?>
|
||||
</i>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for=""><?php esc_html_e("Authorization", 'duplicator-pro'); ?></label></th>
|
||||
<td class="dup-s3-auth-account">
|
||||
<h3>
|
||||
<?php
|
||||
if ($storage->getId() < 0) {
|
||||
echo $storage->getStypeIcon();
|
||||
}
|
||||
?>
|
||||
<?php echo esc_html($storage->getStypeName()) . ' ' . esc_html__('Account', 'duplicator-pro'); ?>
|
||||
</h3>
|
||||
<?php if ($storage->getSType() === AmazonS3CompatibleStorage::getSType()) {
|
||||
$tplMng->render('amazons3addon/parts/s3_compatible_msg');
|
||||
} ?>
|
||||
<table class="dup-form-sub-area margin-top-1">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="s3_access_key_<?php echo (int) $storage->getSType(); ?>">
|
||||
<?php echo esc_html($storage->getFieldLabel('accessKey')); ?>:
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
id="s3_access_key_<?php echo (int) $storage->getSType(); ?>"
|
||||
name="s3_access_key"
|
||||
data-parsley-errors-container="#s3_access_key_<?php echo (int) $storage->getSType(); ?>_error_container"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
value="<?php echo esc_attr($accessKey); ?>"
|
||||
data-parsley-required="true"
|
||||
>
|
||||
<div id="s3_access_key_<?php echo (int) $storage->getSType(); ?>_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="s3_secret_key_<?php echo (int) $storage->getSType(); ?>">
|
||||
<?php echo esc_html($storage->getFieldLabel('secretKey')); ?>:
|
||||
</label>
|
||||
</th>
|
||||
|
||||
<td>
|
||||
<input
|
||||
id="s3_secret_key_<?php echo (int) $storage->getSType(); ?>"
|
||||
name="s3_secret_key"
|
||||
type="password"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($secretKey))); ?>"
|
||||
data-parsley-errors-container="#s3_secret_key_<?php echo (int) $storage->getSType(); ?>_error_container"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
>
|
||||
<div id="s3_secret_key_<?php echo (int) $storage->getSType(); ?>_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"></th>
|
||||
<td>
|
||||
<table class="dup-form-sub-area dup-s3-auth-provider">
|
||||
<tr>
|
||||
<th>
|
||||
<label for="s3_endpoint_<?php echo (int) $storage->getSType(); ?>">
|
||||
<?php echo esc_html($storage->getFieldLabel('endpoint')); ?>:
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="s3_endpoint_<?php echo (int) $storage->getSType(); ?>"
|
||||
name="s3_endpoint"
|
||||
value="<?php echo esc_attr($endpoint); ?>"
|
||||
<?php echo ($isAutofillEndpoint ? 'readonly="true"' : ''); ?>
|
||||
data-parsley-required="true"
|
||||
>
|
||||
<?php if ($isAutofillEndpoint) : ?>
|
||||
<p class="description">
|
||||
<i><?php esc_html_e('The endpoint URL will be autofilled based on the region.', 'duplicator-pro'); ?></i>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<label for="s3_region_<?php echo (int) $storage->getSType(); ?>">
|
||||
<?php echo esc_html($storage->getFieldLabel('region')); ?>:
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
id="s3_region_<?php echo (int) $storage->getSType(); ?>"
|
||||
name="s3_region"
|
||||
<?php echo ($isAutofillRegion ? 'readonly="true"' : ''); ?>
|
||||
value="<?php echo esc_attr($region); ?>"
|
||||
data-parsley-required="true"
|
||||
data-parsley-pattern="[0-9a-zA-Z-_]+"
|
||||
>
|
||||
<?php if ($isAutofillRegion) : ?>
|
||||
<p class="description">
|
||||
<i><?php esc_html_e('The region will be autodetected from the endpoint URL.', 'duplicator-pro'); ?></i>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="invisible_out_of_screen">
|
||||
<th>
|
||||
<label for="s3_storage_class_<?php echo (int) $storage->getSType(); ?>">
|
||||
<?php esc_html_e("Storage Class", 'duplicator-pro'); ?>:
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<select id="s3_storage_class_<?php echo (int) $storage->getSType(); ?>" name="s3_storage_class">
|
||||
<option <?php selected(true); ?> value="STANDARD"><?php esc_html_e("Standard", 'duplicator-pro'); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="s3_bucket_<?php echo (int) $storage->getSType(); ?>">
|
||||
<?php echo esc_html($storage->getFieldLabel('bucket')); ?>
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
id="s3_bucket_<?php echo (int) $storage->getSType(); ?>"
|
||||
name="s3_bucket"
|
||||
type="text"
|
||||
value="<?php echo esc_attr($bucket); ?>"
|
||||
data-parsley-required="true"
|
||||
>
|
||||
<p class="description">
|
||||
<i><?php esc_html_e("S3 Bucket where you want to save the backups.", 'duplicator-pro'); ?></i>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="_s3_storage_folder_<?php echo (int) $storage->getSType(); ?>"><?php esc_html_e("Storage Folder", 'duplicator-pro'); ?>:</label></th>
|
||||
<td>
|
||||
<input
|
||||
id="_s3_storage_folder_<?php echo (int) $storage->getSType(); ?>"
|
||||
name="_s3_storage_folder"
|
||||
type="text"
|
||||
value="<?php echo esc_attr($storageFolder); ?>"
|
||||
>
|
||||
<p class="description">
|
||||
<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="s3_max_files_<?php echo (int) $storage->getSType(); ?>"><?php esc_html_e("Max Packages", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<label for="s3_max_files_<?php echo (int) $storage->getSType(); ?>">
|
||||
<input
|
||||
id="s3_max_files_<?php echo (int) $storage->getSType(); ?>"
|
||||
class="s3_max_files"
|
||||
name="s3_max_files"
|
||||
type="number"
|
||||
value="<?php echo (int) $maxPackages; ?>"
|
||||
min="0"
|
||||
maxlength="4"
|
||||
data-parsley-errors-container="#s3_max_files_<?php echo (int) $storage->getSType(); ?>_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'); ?>
|
||||
<p class="description">
|
||||
<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>
|
||||
</p>
|
||||
</label>
|
||||
<div id="s3_max_files_<?php echo (int) $storage->getSType(); ?>_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php if ($isAclSupported) : ?>
|
||||
<tr class="s3-acl-row" valign="top">
|
||||
<th scope="row"><label><?php echo esc_html($storage->getFieldLabel('aclFullControl')); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="s3_ACL_full_control"
|
||||
id="s3_ACL_full_control_<?php echo (int) $storage->getSType(); ?>"
|
||||
value="1"
|
||||
<?php checked($aclFullControl, true); ?>
|
||||
>
|
||||
<label for="s3_ACL_full_control_<?php echo (int) $storage->getSType(); ?>">
|
||||
<?php esc_html_e("Give bucket owner full control (ACL) to all files uploaded by Duplicator Pro.", 'duplicator-pro'); ?>
|
||||
</label><br />
|
||||
<p class="description">
|
||||
<i>
|
||||
<?php echo esc_html($aclDescription); ?>
|
||||
</i>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php $tplMng->render('admin_pages/storages/parts/provider_foot');
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Duplicator messages sections
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\AmazonS3Addon\Models\AmazonS3Storage;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var \Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
* @var AmazonS3Storage $storage
|
||||
*/
|
||||
$storage = $tplData["storage"];
|
||||
/** @var int */
|
||||
$maxPackages = $tplData["maxPackages"];
|
||||
/** @var string */
|
||||
$storageFolder = $tplData["storageFolder"];
|
||||
/** @var string */
|
||||
$accessKey = $tplData["accessKey"];
|
||||
/** @var string */
|
||||
$bucket = $tplData["bucket"];
|
||||
/** @var string */
|
||||
$region = $tplData["region"];
|
||||
/** @var string */
|
||||
$secretKey = $tplData["secretKey"];
|
||||
/** @var string */
|
||||
$storageClass = $tplData["storageClass"];
|
||||
/** @var string */
|
||||
$endpoint = $tplData["endpoint"];
|
||||
/** @var string */
|
||||
$aclFullControl = $tplData["aclFullControl"];
|
||||
/** @var array<string,string> */
|
||||
$regionOptions = $tplData["regionOptions"];
|
||||
|
||||
$tplMng->render('admin_pages/storages/parts/provider_head');
|
||||
?>
|
||||
<tr>
|
||||
<td colspan="2" style="padding-left:0">
|
||||
<i>
|
||||
<?php
|
||||
printf(
|
||||
esc_html_x(
|
||||
'Amazon S3 Setup Guide: %1$sStep-by-Step%2$s and %3$sUser Bucket Policy%4$s.',
|
||||
'1,3 represents <a> tag, 2,4 represents </a> tag',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a target="_blank" href="' . esc_url(DUPLICATOR_PRO_DUPLICATOR_DOCS_URL . 'amazon-s3-step-by-step') . '">',
|
||||
'</a>',
|
||||
'<a href="' . esc_url(DUPLICATOR_PRO_DUPLICATOR_DOCS_URL . 'amazon-s3-policy-setup') . '" target="_blank">',
|
||||
'</a>'
|
||||
);
|
||||
?>
|
||||
</i>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for=""><?php esc_html_e("Authorization", 'duplicator-pro'); ?></label></th>
|
||||
<td class="dup-s3-auth-account">
|
||||
<h3>
|
||||
<?php esc_html_e('Amazon Account', 'duplicator-pro'); ?><br/>
|
||||
</h3>
|
||||
<table class="dup-form-sub-area">
|
||||
<tr>
|
||||
<th scope="row"><label for="s3_access_key_amazon"><?php esc_html_e("Access Key", 'duplicator-pro'); ?>:</label></th>
|
||||
<td>
|
||||
<input
|
||||
id="s3_access_key_amazon"
|
||||
name="s3_access_key"
|
||||
data-parsley-errors-container="#s3_access_key_amazon_error_container"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
value="<?php echo esc_attr($accessKey); ?>"
|
||||
>
|
||||
<div id="s3_access_key_amazon_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="s3_secret_key_amazon"><?php esc_html_e("Secret Key", 'duplicator-pro'); ?>:</label>
|
||||
</th>
|
||||
|
||||
<td>
|
||||
<input
|
||||
id="s3_secret_key_amazon"
|
||||
name="s3_secret_key"
|
||||
type="password"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($secretKey))); ?>"
|
||||
data-parsley-errors-container="#s3_secret_key_amazon_error_container"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
>
|
||||
<div id="s3_secret_key_amazon_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"></th>
|
||||
<td>
|
||||
<table class="dup-form-sub-area dup-s3-auth-provider">
|
||||
<tr>
|
||||
<th><label for="s3_region_amazon"><?php esc_html_e("Region", 'duplicator-pro'); ?>:</label></th>
|
||||
<td>
|
||||
<select id="s3_region_amazon" name="s3_region">
|
||||
<?php
|
||||
foreach ($regionOptions as $value => $label) {
|
||||
?>
|
||||
<option
|
||||
<?php selected($region, $value); ?>
|
||||
value="<?php echo esc_attr($value); ?>"
|
||||
>
|
||||
<?php echo esc_html($label . " - '" . $value . "'"); ?>
|
||||
</option>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="s3_storage_class_amazon"><?php esc_html_e("Storage Class", 'duplicator-pro'); ?>:</label></th>
|
||||
<td>
|
||||
<select id="s3_storage_class_amazon" name="s3_storage_class">
|
||||
<option <?php selected($storageClass == 'REDUCED_REDUNDANCY'); ?> value="REDUCED_REDUNDANCY">
|
||||
<?php esc_html_e("Reduced Redundancy", 'duplicator-pro'); ?>
|
||||
</option>
|
||||
<option <?php selected($storageClass == 'STANDARD'); ?> value="STANDARD">
|
||||
<?php esc_html_e("Standard", 'duplicator-pro'); ?>
|
||||
</option>
|
||||
<option <?php selected($storageClass == 'STANDARD_IA'); ?> value="STANDARD_IA">
|
||||
<?php esc_html_e("Standard IA", 'duplicator-pro'); ?>
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="_s3_storage_folder_amazon"><?php esc_html_e("Storage Folder", 'duplicator-pro'); ?>:</label></th>
|
||||
<td>
|
||||
<input
|
||||
id="_s3_storage_folder_amazon"
|
||||
name="_s3_storage_folder"
|
||||
type="text"
|
||||
value="<?php echo esc_attr($storageFolder); ?>"
|
||||
>
|
||||
<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>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="s3_bucket_amazon"><?php esc_html_e("Bucket", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input id="s3_bucket_amazon" name="s3_bucket" type="text" value="<?php echo esc_attr($bucket); ?>">
|
||||
<p><i><?php esc_html_e("S3 Bucket where you want to save the backups.", 'duplicator-pro'); ?></i></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="s3_max_files_amazon"><?php esc_html_e("Max Packages", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<label for="s3_max_files_amazon">
|
||||
<input
|
||||
id="s3_max_files_amazon"
|
||||
class="s3_max_files"
|
||||
name="s3_max_files"
|
||||
type="number"
|
||||
value="<?php echo (int) $maxPackages; ?>"
|
||||
min="0"
|
||||
maxlength="4"
|
||||
data-parsley-errors-container="#s3_max_files_amazon_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="s3_max_files_amazon_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php $tplMng->render('admin_pages/storages/parts/provider_foot'); ?>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
use Duplicator\Addons\AmazonS3Addon\Models\AmazonS3CompatibleStorage;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var \Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
* @var AbstractStorageEntity $storage
|
||||
*/
|
||||
$storage = $tplData["storage"];
|
||||
?>
|
||||
|
||||
<p>
|
||||
<?php
|
||||
esc_html_e(
|
||||
'The Amazon S3 compatible storage option allows you to connect to any object storage that is compatible with the S3 API.',
|
||||
'duplicator-pro'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
esc_attr_x(
|
||||
'Examples of compatible providers are %s.',
|
||||
'%s is a comma seperated list of providers',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<b>' . esc_html(implode(', ', AmazonS3CompatibleStorage::getCompatibleProviders())) . '</b>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* FTP/SFTP ADDON
|
||||
*
|
||||
* Name: Duplicator PRO base
|
||||
* Version: 1
|
||||
* Author: Duplicator
|
||||
* Author URI: https://duplicator.com/
|
||||
*
|
||||
* PHP version 5.3
|
||||
*
|
||||
* @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\FtpAddon;
|
||||
|
||||
use Duplicator\Addons\FtpAddon\Models\FTPStorage;
|
||||
use Duplicator\Addons\FtpAddon\Models\SFTPStorage;
|
||||
use Duplicator\Core\Addons\AbstractAddonCore;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
|
||||
/**
|
||||
* Storage ftp/sftp addon class
|
||||
*/
|
||||
class FtpAddon extends AbstractAddonCore
|
||||
{
|
||||
const ADDON_PATH = __DIR__;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
add_action('init', [$this, 'hookInit']);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function calle on duplicator_addons_loaded hook
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function hookInit()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Register storages
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerStorages()
|
||||
{
|
||||
FTPStorage::registerType();
|
||||
SFTPStorage::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, 'ftpaddon/') === 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_ftp_count'] = 0;
|
||||
$storageNums['storages_sftp_count'] = 0;
|
||||
|
||||
foreach ($storages as $index => $storage) {
|
||||
switch ($storage->getSType()) {
|
||||
case FTPStorage::getSType():
|
||||
$storageNums['storages_ftp_count']++;
|
||||
break;
|
||||
case SFTPStorage::getSType():
|
||||
$storageNums['storages_sftp_count']++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $storageNums;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonPath()
|
||||
{
|
||||
return __DIR__;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonFile()
|
||||
{
|
||||
return __FILE__;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,958 @@
|
||||
<?php
|
||||
|
||||
namespace Duplicator\Addons\FtpAddon\Models;
|
||||
|
||||
use CurlHandle;
|
||||
use Duplicator\Addons\FtpAddon\Utils\FTPUtils;
|
||||
use Duplicator\Models\Storages\AbstractStorageAdapter;
|
||||
use Duplicator\Models\Storages\StoragePathInfo;
|
||||
use Duplicator\Libs\Snap\SnapIO;
|
||||
use Exception;
|
||||
|
||||
class FTPCurlStorageAdapter extends AbstractStorageAdapter
|
||||
{
|
||||
/** @var int */
|
||||
const DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024;
|
||||
/** @var string */
|
||||
private $root = '';
|
||||
/** @var string */
|
||||
private $server = '';
|
||||
/** @var int */
|
||||
private $port = 21;
|
||||
/** @var string */
|
||||
private $username = '';
|
||||
/** @var string */
|
||||
private $password = '';
|
||||
/** @var int */
|
||||
private $timeoutInSec = 15;
|
||||
/** @var bool */
|
||||
private $ssl = false;
|
||||
/** @var bool */
|
||||
private $passiveMode = false;
|
||||
/** @var resource */
|
||||
private $sourceFileHandle = null;
|
||||
/** @var string */
|
||||
private $lastSourceFilePath = '';
|
||||
/** @var resource */
|
||||
private $destFileHandle = null;
|
||||
/** @var string */
|
||||
private $lastDestFilePath = '';
|
||||
/** @var resource */
|
||||
private $tempFileHandle = null;
|
||||
/** @var string */
|
||||
private $lastTempFilePath = '';
|
||||
/** @var null|resource|CurlHandle */
|
||||
private $connection = null;
|
||||
/** @var int */
|
||||
private $throttle = 0;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param string $server The server to connect to
|
||||
* @param int $port The port to connect to
|
||||
* @param string $username The username to use
|
||||
* @param string $password The password to use
|
||||
* @param string $root The root directory to use
|
||||
* @param int $timeoutInSec The timeout in seconds
|
||||
* @param bool $ssl Whether to use SSL
|
||||
* @param bool $passiveMode Whether to use passive mode
|
||||
* @param int $throttle The throttle in microseconds
|
||||
*/
|
||||
public function __construct(
|
||||
$server,
|
||||
$port = 21,
|
||||
$username = '',
|
||||
$password = '',
|
||||
$root = '/',
|
||||
$timeoutInSec = 15,
|
||||
$ssl = false,
|
||||
$passiveMode = false,
|
||||
$throttle = 0
|
||||
) {
|
||||
$this->server = $server;
|
||||
$this->port = (int) $port;
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
$this->root = SnapIO::trailingslashit($root);
|
||||
$this->timeoutInSec = max(1, (int) $timeoutInSec);
|
||||
$this->ssl = (bool) $ssl;
|
||||
$this->passiveMode = (bool) $passiveMode;
|
||||
$this->throttle = max(0, (int) $throttle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the FTP connection and initializes root directory
|
||||
*
|
||||
* @param string $errorMsg The error message to return
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function initialize(&$errorMsg = '')
|
||||
{
|
||||
if (!$this->isDir('/') && !$this->createDir('/')) {
|
||||
$errorMsg = "Couldn't create root directory.";
|
||||
return false;
|
||||
}
|
||||
$this->wait();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttle the connection
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function wait()
|
||||
{
|
||||
if ($this->throttle > 0) {
|
||||
usleep($this->throttle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = '')
|
||||
{
|
||||
if (!$this->isConnectionInfoValid($errorMsg)) {
|
||||
$errorMsg = "FTP connection info is invalid: " . $errorMsg;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->testConnection($errorMsg) === false) {
|
||||
$errorMsg = "FTP connection failed: " . $errorMsg;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isDir('/')) {
|
||||
$errorMsg = "FTP root directory doesn't exist.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the connection info is valid
|
||||
*
|
||||
* @param string $errorMsg The error message to return
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isConnectionInfoValid(&$errorMsg = '')
|
||||
{
|
||||
if (strlen($this->server) < 1) {
|
||||
$errorMsg = "FTP server is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($this->username) < 1) {
|
||||
$errorMsg = "FTP username is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($this->password) < 1) {
|
||||
$errorMsg = "FTP password is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->port < 1) {
|
||||
$errorMsg = "FTP port is invalid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($this->root) < 1) {
|
||||
$errorMsg = "FTP root directory is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* test ftp connection
|
||||
*
|
||||
* @param string $errorMsg error message
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function testConnection(&$errorMsg = '')
|
||||
{
|
||||
$path = $this->getFullPath('/', true);
|
||||
return $this->curlCall($path, [CURLOPT_TIMEOUT => $this->timeoutInSec]) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
try {
|
||||
$path = SnapIO::trailingslashit($this->getFullPath($path, true));
|
||||
return $this->curlCall($path, [CURLOPT_FTP_CREATE_MISSING_DIRS => true]) !== false;
|
||||
} finally {
|
||||
$this->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
try {
|
||||
if (($fullPath = $this->getFullPath($path)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->exists($path) && !$this->delete($path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'duplicator-pro-');
|
||||
if (($bytesWritten = file_put_contents($tmpFile, $content)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($stream = @fopen($tmpFile, 'r')) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$success = $this->curlCall(
|
||||
$fullPath,
|
||||
[
|
||||
CURLOPT_UPLOAD => true,
|
||||
CURLOPT_NOPROGRESS => true,
|
||||
CURLOPT_FTP_CREATE_MISSING_DIRS => true,
|
||||
CURLOPT_INFILE => $stream,
|
||||
CURLOPT_INFILESIZE => $bytesWritten,
|
||||
]
|
||||
);
|
||||
|
||||
@fclose($stream);
|
||||
if ($success === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $bytesWritten;
|
||||
} finally {
|
||||
$this->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
if (($path = $this->getFullPath($path)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($content = $this->curlCall($path)) === false) {
|
||||
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)
|
||||
{
|
||||
try {
|
||||
if (($fullOldPath = $this->getFullPath($oldPath)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($fullNewPath = $this->getFullPath($newPath)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->curlCall('/', [CURLOPT_QUOTE => ["RNFR " . $fullOldPath, "RNTO " . $fullNewPath]]) !== false;
|
||||
} finally {
|
||||
$this->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete reletative 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)
|
||||
{
|
||||
try {
|
||||
$fullPath = $this->getFullPath($path, true);
|
||||
if ($this->isDir($path)) {
|
||||
$fullPath = SnapIO::trailingslashit($fullPath);
|
||||
if ($recursive) {
|
||||
foreach ($this->scanDir($path) as $item) {
|
||||
if (!$this->realDelete(SnapIO::trailingslashit($path) . $item, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->curlCall('/', [CURLOPT_QUOTE => ["RMD " . $fullPath]]) !== false;
|
||||
} elseif ($this->isFile($path)) {
|
||||
return $this->curlCall('/', [CURLOPT_QUOTE => ["DELE " . $fullPath]]) !== false;
|
||||
} else {
|
||||
//path doesn't exist
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
$this->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [])
|
||||
{
|
||||
try {
|
||||
$startTime = microtime(true);
|
||||
|
||||
if (($storageFileFullPath = $this->getFullPath($storageFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_file($sourceFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($offset === 0 && $this->isFile($storageFile) && !$this->delete($storageFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Uplaod file at once without any other operation
|
||||
if (($timeout === 0 && $offset === 0 && $length < 0) || filesize($sourceFile) < $length) {
|
||||
if (($content = @file_get_contents($sourceFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->createFile($storageFile, $content);
|
||||
}
|
||||
|
||||
if (
|
||||
($sourceFileHandle = $this->getSourceFileHandle($sourceFile)) === false ||
|
||||
($tempFileHandle = $this->getTempFileHandle()) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bytesWritten = 0;
|
||||
$length = $length < 0 ? self::DEFAULT_CHUNK_SIZE : $length;
|
||||
do {
|
||||
if (
|
||||
@fseek($sourceFileHandle, $offset) === -1 ||
|
||||
($chunk = @fread($sourceFileHandle, $length)) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
@ftruncate($tempFileHandle, 0) === false ||
|
||||
@rewind($tempFileHandle) === false ||
|
||||
@fwrite($tempFileHandle, $chunk) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@rewind($tempFileHandle);
|
||||
|
||||
$result = $this->curlCall(
|
||||
$storageFileFullPath,
|
||||
[
|
||||
CURLOPT_FTPAPPEND => true,
|
||||
CURLOPT_UPLOAD => true,
|
||||
CURLOPT_FTP_CREATE_MISSING_DIRS => true,
|
||||
CURLOPT_INFILE => $tempFileHandle,
|
||||
CURLOPT_INFILESIZE => strlen($chunk),
|
||||
]
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//abort on first chunk if no timeout
|
||||
if ($timeout === 0) {
|
||||
return $length;
|
||||
}
|
||||
|
||||
$bytesWritten += strlen($chunk);
|
||||
$offset += strlen($chunk);
|
||||
} while ($timeout !== 0 && self::getElapsedTime($startTime) < $timeout && !feof($sourceFileHandle));
|
||||
|
||||
return $bytesWritten;
|
||||
} finally {
|
||||
$this->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [])
|
||||
{
|
||||
try {
|
||||
$startTime = microtime(true);
|
||||
|
||||
if (($fullPath = $this->getFullPath($storageFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wp_mkdir_p(dirname($destFile)) == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($offset === 0 && @file_exists($destFile) && !@unlink($destFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isFile($storageFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($timeout === 0 && $offset === 0 && $length < 0) {
|
||||
if (($content = $this->getFileContent($storageFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return @file_put_contents($destFile, $content);
|
||||
}
|
||||
|
||||
if (($handle = $this->getDestFileHandle($destFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (@fseek($handle, $offset) === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bytesWritten = 0;
|
||||
$length = $length < 0 ? self::DEFAULT_CHUNK_SIZE : $length;
|
||||
do {
|
||||
if (@fseek($handle, $offset) === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$content = $this->curlCall(
|
||||
$fullPath,
|
||||
[
|
||||
CURLOPT_RANGE => sprintf('%d-%d', $offset, $offset + $length - 1),
|
||||
]
|
||||
);
|
||||
|
||||
if (
|
||||
$content === false ||
|
||||
(strlen($content) > 0 && @fwrite($handle, $content) === false)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($timeout === 0) {
|
||||
return $length;
|
||||
}
|
||||
|
||||
$bytesWritten += strlen($content);
|
||||
$offset += strlen($content);
|
||||
} while ($timeout !== 0 && self::getElapsedTime($startTime) < $timeout && strlen($content) > 0);
|
||||
|
||||
return $bytesWritten;
|
||||
} finally {
|
||||
$this->wait();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files meta information in a folder
|
||||
*
|
||||
* @param string $path remote dir path
|
||||
* @param bool $filterDots filters . and .. from the list
|
||||
*
|
||||
* @return array{array{name: string, size: int, modified: int, created: int, isDir: bool}}|array{}
|
||||
*/
|
||||
private function getRawListInfo($path, $filterDots = true)
|
||||
{
|
||||
//direactories need the trailing slash to be recognized as such
|
||||
$path = SnapIO::trailingslashit($this->getFullPath($path, true));
|
||||
$res = $this->curlCall($path, [CURLOPT_CUSTOMREQUEST => 'LIST']);
|
||||
|
||||
if ($res === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$items = explode("\n", $res);
|
||||
$items = array_filter($items, function ($item) {
|
||||
return !empty($item);
|
||||
});
|
||||
|
||||
if (empty($items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (strpos($items[0], 'total') !== false) {
|
||||
array_shift($items);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($items as $key => $item) {
|
||||
if (($parsed = FTPUtils::parseRawListString($item, $this->getSystemType())) !== false) {
|
||||
if ($filterDots && ($parsed['name'] === '.' || $parsed['name'] === '..')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = $parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get System type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getSystemType()
|
||||
{
|
||||
if (($res = $this->curlCall('/', [CURLOPT_CUSTOMREQUEST => 'SYST'])) === false) {
|
||||
return 'UNIX';
|
||||
}
|
||||
|
||||
$res = strtoupper($res);
|
||||
if (strpos($res, 'WINDOWS') !== false) {
|
||||
return 'WINDOWS_NT';
|
||||
}
|
||||
|
||||
return 'UNIX';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path info.
|
||||
*
|
||||
* @param string $path Relative storage path, if empty, return root path info.
|
||||
*
|
||||
* @return StoragePathInfo|false The path info or false if path is invalid.
|
||||
*/
|
||||
public function getRealPathInfo($path)
|
||||
{
|
||||
if (($fullPath = $this->getFullPath($path, true)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
$info = new StoragePathInfo();
|
||||
$info->path = $path;
|
||||
|
||||
if (
|
||||
($response = $this->curlCall($fullPath, [CURLOPT_HEADER => true, CURLOPT_NOBODY => true])) !== false &&
|
||||
preg_match('/^Content-Length:\s*(\d+)/im', $response, $matches) === 1
|
||||
) {
|
||||
// Is file
|
||||
$info->exists = true;
|
||||
$info->isDir = false;
|
||||
$info->size = (int) $matches[1];
|
||||
|
||||
$response = $this->curlCall($fullPath, [CURLOPT_CUSTOMREQUEST => 'MDTM']);
|
||||
$matches = [];
|
||||
if (preg_match('/^(\d{14})/', $response, $matches) === 1) {
|
||||
$info->modified = strtotime($matches[1]);
|
||||
} else {
|
||||
$info->modified = time();
|
||||
}
|
||||
|
||||
$info->created = $info->modified;
|
||||
} elseif ($this->curlCall(trailingslashit($fullPath), [CURLOPT_CUSTOMREQUEST => 'NLST']) !== false) {
|
||||
// Is folder
|
||||
$info = new StoragePathInfo();
|
||||
$info->path = $path;
|
||||
$info->exists = true;
|
||||
$info->isDir = true;
|
||||
$info->size = 0;
|
||||
$info->created = time();
|
||||
$info->modified = time();
|
||||
} else {
|
||||
// Not exists
|
||||
$info->exists = false;
|
||||
$info->isDir = false;
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$infoList = $this->getRawListInfo($path);
|
||||
$result = [];
|
||||
foreach ($infoList as $item) {
|
||||
if ($item['isDir'] && !$folders) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$item['isDir'] && !$files) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[] = $item['name'];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [])
|
||||
{
|
||||
if (!$this->isDir($path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$regexFilters = [];
|
||||
$normalFilters = [];
|
||||
foreach ($filters as $filter) {
|
||||
if (preg_match('/^\/.*\/$/', $filter) === 1) {
|
||||
$regexFilters[] = $filter;
|
||||
} 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the storage on deletion.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
public function destroy()
|
||||
{
|
||||
// Don't delete if root directory
|
||||
if (
|
||||
preg_match('/^[a-zA-Z]:\/$/', $this->root) === 1 ||
|
||||
preg_match('/^\/$/', $this->root) === 1
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->delete('/', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destruct
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (is_resource($this->sourceFileHandle)) {
|
||||
fclose($this->sourceFileHandle);
|
||||
}
|
||||
|
||||
if (is_resource($this->tempFileHandle)) {
|
||||
fclose($this->tempFileHandle);
|
||||
}
|
||||
|
||||
if (is_resource($this->destFileHandle)) {
|
||||
fclose($this->destFileHandle);
|
||||
}
|
||||
|
||||
if ($this->connection !== null) {
|
||||
curl_close($this->connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a curl call
|
||||
*
|
||||
* @param string $path where the curl call occur
|
||||
* @param array<int,mixed> $options configuration options
|
||||
* @param string $errorMsg error message
|
||||
*
|
||||
* @return string|false response or false on failure
|
||||
*/
|
||||
private function curlCall($path = '/', $options = [], &$errorMsg = '')
|
||||
{
|
||||
if ($this->connection === null) {
|
||||
$this->connection = curl_init();
|
||||
} else {
|
||||
curl_reset($this->connection);
|
||||
}
|
||||
|
||||
if ($this->connection === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$path = ltrim($path, '/\\');
|
||||
$options[CURLOPT_URL] = sprintf('ftp://%s:%d/%s', $this->server, $this->port, $path);
|
||||
$options = array_replace($this->getDefaultOptions(), $options);
|
||||
|
||||
curl_setopt_array($this->connection, $options);
|
||||
|
||||
if (($response = curl_exec($this->connection)) === false) {
|
||||
if (($errno = curl_errno($this->connection))) {
|
||||
switch ($errno) {
|
||||
case 6:
|
||||
case 7:
|
||||
$errorMsg = 'Unable to connect to FTP server. Please check your FTP hostname, port, and active mode settings. Error code: ' . $errno;
|
||||
break;
|
||||
case 8:
|
||||
$errorMsg = 'Got an unexpected reply from FTP server. Error code: ' . $errno;
|
||||
break;
|
||||
case 9:
|
||||
$errorMsg = 'Unable to change FTP directory. Please ensure that you have permission on the server. Error code: ' . $errno;
|
||||
break;
|
||||
case 23:
|
||||
$errorMsg = 'Unable to download file from FTP server. Please ensure that you have enough disk space. Error code: ' . $errno;
|
||||
break;
|
||||
case 28:
|
||||
$errorMsg = 'Connecting to FTP server timed out. Please check FTP hostname, port, username, password, and active mode ' .
|
||||
'settings. Error code: ' . $errno;
|
||||
break;
|
||||
case 67:
|
||||
$errorMsg = 'Unable to login to FTP server. Please check your username and password. Error code: ' . $errno;
|
||||
break;
|
||||
default:
|
||||
$errorMsg = 'Unable to connect to FTP. Error code: ' . $errno . '. Error message: ' . curl_error($this->connection);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$http_code = curl_getinfo($this->connection, CURLINFO_HTTP_CODE);
|
||||
if ($http_code >= 400) {
|
||||
$errorMsg = sprintf('Error code: %s.', $http_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns default options for cURL
|
||||
*
|
||||
* @return array<int,mixed>
|
||||
*/
|
||||
private function getDefaultOptions()
|
||||
{
|
||||
$options = [
|
||||
CURLOPT_USERPWD => sprintf('%s:%s', $this->username, $this->password),
|
||||
CURLOPT_HEADER => false,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_BINARYTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => $this->timeoutInSec,
|
||||
CURLOPT_TIMEOUT => $this->timeoutInSec,
|
||||
];
|
||||
|
||||
|
||||
if ($this->ssl) {
|
||||
$options[CURLOPT_SSL_VERIFYPEER] = false;
|
||||
$options[CURLOPT_SSL_VERIFYHOST] = false;
|
||||
$options[CURLOPT_FTP_SSL] = CURLFTPSSL_TRY;
|
||||
$options[CURLOPT_FTPSSLAUTH] = CURLFTPAUTH_TLS;
|
||||
}
|
||||
|
||||
if ($this->passiveMode) {
|
||||
$options[CURLOPT_FTP_USE_EPSV] = true;
|
||||
} else {
|
||||
$options[CURLOPT_FTP_USE_EPRT] = true;
|
||||
$options[CURLOPT_FTPPORT] = 0;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source file handle
|
||||
*
|
||||
* @param string $destFilePath The source file path
|
||||
*
|
||||
* @return resource|false returns the file handle or false on failure
|
||||
*/
|
||||
private function getDestFileHandle($destFilePath)
|
||||
{
|
||||
if ($this->lastDestFilePath === $destFilePath) {
|
||||
return $this->destFileHandle;
|
||||
}
|
||||
|
||||
if (is_resource($this->destFileHandle)) {
|
||||
fclose($this->destFileHandle);
|
||||
}
|
||||
|
||||
if (($this->destFileHandle = SnapIO::fopen($destFilePath, 'cb')) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->lastDestFilePath = $destFilePath;
|
||||
return $this->destFileHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source file handle
|
||||
*
|
||||
* @param string $sourceFilePath The source file path
|
||||
*
|
||||
* @return resource|false returns the file handle or false on failure
|
||||
*/
|
||||
private function getSourceFileHandle($sourceFilePath)
|
||||
{
|
||||
if ($this->lastSourceFilePath === $sourceFilePath) {
|
||||
return $this->sourceFileHandle;
|
||||
}
|
||||
|
||||
if (is_resource($this->sourceFileHandle)) {
|
||||
fclose($this->sourceFileHandle);
|
||||
}
|
||||
|
||||
if (($this->sourceFileHandle = SnapIO::fopen($sourceFilePath, 'r')) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->lastSourceFilePath = $sourceFilePath;
|
||||
return $this->sourceFileHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an empty file stream to temporarlly store chunk data.
|
||||
*
|
||||
* @return resource|false
|
||||
*/
|
||||
private function getTempFileHandle()
|
||||
{
|
||||
if (is_resource($this->tempFileHandle)) {
|
||||
if (ftruncate($this->tempFileHandle, 0) === false) {
|
||||
return false;
|
||||
}
|
||||
if (rewind($this->tempFileHandle) === false) {
|
||||
return false;
|
||||
}
|
||||
return $this->tempFileHandle;
|
||||
}
|
||||
|
||||
if (@file_exists($this->lastTempFilePath)) {
|
||||
@unlink($this->lastTempFilePath);
|
||||
}
|
||||
|
||||
$this->lastTempFilePath = tempnam(sys_get_temp_dir(), 'duplicator_ftp_curl_tmpfile_');
|
||||
if (($this->tempFileHandle = @fopen($this->lastTempFilePath, 'r+')) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->tempFileHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the full path of storage from relative path.
|
||||
*
|
||||
* @param string $path The relative storage path
|
||||
* @param bool $acceptEmpty If true, return root path if path is empty. Default to false.
|
||||
*
|
||||
* @return string|false The full path or false if path is invalid.
|
||||
*/
|
||||
protected function getFullPath($path, $acceptEmpty = false)
|
||||
{
|
||||
$path = ltrim((string) $path, '/\\');
|
||||
if (strlen($path) === 0) {
|
||||
return $acceptEmpty ? SnapIO::trailingslashit($this->root) : false;
|
||||
}
|
||||
return $this->root . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elapsed time in microseconds
|
||||
*
|
||||
* @param float $startTime start time in microseconds
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private static function getElapsedTime($startTime)
|
||||
{
|
||||
return (microtime(true) - $startTime) * SECONDS_IN_MICROSECONDS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\FtpAddon\Models;
|
||||
|
||||
use Duplicator\Core\Views\TplMng;
|
||||
use Duplicator\Libs\Snap\SnapUtil;
|
||||
use Duplicator\Models\DynamicGlobalEntity;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
use Duplicator\Models\Storages\AbstractStorageAdapter;
|
||||
use Exception;
|
||||
|
||||
class FTPStorage extends AbstractStorageEntity
|
||||
{
|
||||
const MIN_UPLOAD_CHUNK_SIZE_IN_MB = 1;
|
||||
const DEFAULT_UPLOAD_CHUNK_SIZE_IN_MB = 5;
|
||||
const MAX_UPLOAD_CHUNK_SIZE_IN_MB = 100;
|
||||
|
||||
/**
|
||||
* Get default config
|
||||
*
|
||||
* @return array<string,scalar>
|
||||
*/
|
||||
protected static function getDefaultConfig()
|
||||
{
|
||||
$config = parent::getDefaultConfig();
|
||||
$config = array_merge(
|
||||
$config,
|
||||
[
|
||||
'server' => '',
|
||||
'port' => 21,
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'use_curl' => false,
|
||||
'timeout_in_secs' => 15,
|
||||
'ssl' => false,
|
||||
'passive_mode' => true,
|
||||
]
|
||||
);
|
||||
return $config;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get stoage adapter
|
||||
*
|
||||
* @return AbstractStorageAdapter
|
||||
*/
|
||||
protected function getAdapter()
|
||||
{
|
||||
if ($this->adapter !== null) {
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
if ($this->config['use_curl']) {
|
||||
$this->adapter = new FTPCurlStorageAdapter(
|
||||
$this->config['server'],
|
||||
$this->config['port'],
|
||||
$this->config['username'],
|
||||
$this->config['password'],
|
||||
$this->config['storage_folder'],
|
||||
$this->config['timeout_in_secs'],
|
||||
$this->config['ssl'],
|
||||
$this->config['passive_mode']
|
||||
);
|
||||
} else {
|
||||
$this->adapter = new FTPStorageAdapter(
|
||||
$this->config['server'],
|
||||
$this->config['port'],
|
||||
$this->config['username'],
|
||||
$this->config['password'],
|
||||
$this->config['storage_folder'],
|
||||
$this->config['timeout_in_secs'],
|
||||
$this->config['ssl'],
|
||||
$this->config['passive_mode']
|
||||
);
|
||||
}
|
||||
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be called, automatically, when Serialize
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function __serialize() // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__serializeFound
|
||||
{
|
||||
$data = parent::__serialize();
|
||||
unset($data['client']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [
|
||||
'server' => $this->ftp_server,
|
||||
'port' => $this->ftp_port,
|
||||
'username' => $this->ftp_username,
|
||||
'password' => $this->ftp_password,
|
||||
'use_curl' => $this->ftp_use_curl,
|
||||
'storage_folder' => '/' . ltrim($this->ftp_storage_folder, '/\\'),
|
||||
'max_packages' => $this->ftp_max_files,
|
||||
'timeout_in_secs' => $this->ftp_timeout_in_secs,
|
||||
'ssl' => $this->ftp_ssl,
|
||||
'passive_mode' => $this->ftp_passive_mode,
|
||||
];
|
||||
// reset old values
|
||||
$this->ftp_server = '';
|
||||
$this->ftp_port = 21;
|
||||
$this->ftp_username = '';
|
||||
$this->ftp_password = '';
|
||||
$this->ftp_use_curl = false;
|
||||
$this->ftp_storage_folder = '';
|
||||
$this->ftp_max_files = 10;
|
||||
$this->ftp_timeout_in_secs = 15;
|
||||
$this->ftp_ssl = false;
|
||||
$this->ftp_passive_mode = false;
|
||||
}
|
||||
|
||||
// For legacy entities, we need to make sure the config is up to date
|
||||
$this->config['port'] = (int) $this->config['port'];
|
||||
$this->config['max_packages'] = (int) $this->config['max_packages'];
|
||||
$this->config['timeout_in_secs'] = (int) $this->config['timeout_in_secs'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the FontAwesome storage type icon.
|
||||
*
|
||||
* @return string Returns the font-awesome icon
|
||||
*/
|
||||
public static function getStypeIcon()
|
||||
{
|
||||
return '<i class="fas fa-network-wired fa-fw"></i>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('FTP', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority, used to sort storages.
|
||||
* 100 is neutral value, 0 is the highest priority
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getPriority()
|
||||
{
|
||||
return 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
return "ftp://" . $this->config['server'] . ":" . $this->config['port'] . $this->getStorageFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
return apply_filters('duplicator_pro_ftp_connect_exists', function_exists('ftp_connect'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported notice, displayed if storage isn't supported
|
||||
*
|
||||
* @return string html string or empty if storage is supported
|
||||
*/
|
||||
public static function getNotSupportedNotice()
|
||||
{
|
||||
if (static::isSupported()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
esc_html__(
|
||||
'FTP Storage requires FTP module enabled. Please install the FTP module as described in the %s.',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a href="https://secure.php.net/manual/en/ftp.installation.php" target="_blank">https://secure.php.net/manual/en/ftp.installation.php</a>'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is valid
|
||||
*
|
||||
* @return bool Return true if storage is valid and ready to use, false otherwise
|
||||
*/
|
||||
public function isValid()
|
||||
{
|
||||
return $this->getAdapter()->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk size in bytes
|
||||
*
|
||||
* @return int bytes
|
||||
*/
|
||||
public function getUploadChunkSize()
|
||||
{
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
return $dGlobal->getVal('ftp_upload_chunksize_in_mb', self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_MB) * 1024 * 1024;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk timeout in seconds
|
||||
*
|
||||
* @return int timeout in microseconds, 0 unlimited
|
||||
*/
|
||||
public function getUploadChunkTimeout()
|
||||
{
|
||||
return (int) ($this->config['timeout_in_secs'] <= 0 ? 0 : $this->config['timeout_in_secs'] * SECONDS_IN_MICROSECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 FTP server %1$s in folder:<br/> <i>%2$s</i>', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'pending':
|
||||
return sprintf(
|
||||
__('Transfer to FTP server %1$s in folder %2$s is pending', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'failed':
|
||||
return sprintf(
|
||||
__('Failed to transfer to FTP server %1$s in folder %2$s', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'cancelled':
|
||||
return sprintf(
|
||||
__('Cancelled before could transfer to FTP server:<br/>%1$s in folder %2$s', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'success':
|
||||
return sprintf(
|
||||
__('Transferred package to FTP server:<br/>%1$s in folder %2$s', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
default:
|
||||
throw new Exception('Invalid key');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List quick view
|
||||
*
|
||||
* @param bool $echo Echo or return
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getListQuickView($echo = true)
|
||||
{
|
||||
ob_start();
|
||||
?>
|
||||
<div>
|
||||
<label><?php esc_html_e('Server', 'duplicator-pro'); ?>:</label>
|
||||
<?php echo esc_html($this->config['server']); ?>: <?php echo intval($this->config['port']); ?> <br/>
|
||||
<label><?php esc_html_e('Location', 'duplicator-pro') ?>:</label>
|
||||
<?php
|
||||
echo wp_kses(
|
||||
$this->getHtmlLocationLink(),
|
||||
[
|
||||
'a' => [
|
||||
'href' => [],
|
||||
'target' => [],
|
||||
],
|
||||
]
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
if ($echo) {
|
||||
ob_end_flush();
|
||||
return '';
|
||||
} else {
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render form config fields
|
||||
*
|
||||
* @param bool $echo Echo or return
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderConfigFields($echo = true)
|
||||
{
|
||||
return TplMng::getInstance()->render(
|
||||
'ftpaddon/configs/ftp',
|
||||
[
|
||||
'storage' => $this,
|
||||
'server' => $this->config['server'],
|
||||
'port' => $this->config['port'],
|
||||
'username' => $this->config['username'],
|
||||
'password' => $this->config['password'],
|
||||
'storageFolder' => $this->config['storage_folder'],
|
||||
'maxPackages' => $this->config['max_packages'],
|
||||
'timeout' => $this->config['timeout_in_secs'],
|
||||
'useCurl' => $this->config['use_curl'],
|
||||
'isPassive' => $this->config['passive_mode'],
|
||||
'useSSL' => $this->config['ssl'],
|
||||
],
|
||||
$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, 'ftp_max_files', 10);
|
||||
$this->config['server'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'ftp_server', '');
|
||||
$this->config['port'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'ftp_port', 10);
|
||||
$this->config['username'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'ftp_username', '');
|
||||
$password = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'ftp_password', '');
|
||||
$password2 = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'ftp_password2', '');
|
||||
if (strlen($password) > 0) {
|
||||
if ($password !== $password2) {
|
||||
$message = __('Passwords do not match', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
$this->config['password'] = $password;
|
||||
}
|
||||
$this->config['storage_folder'] = self::getSanitizedInputFolder('_ftp_storage_folder', 'add');
|
||||
$this->config['timeout_in_secs'] = max(10, SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'ftp_timeout_in_secs', 15));
|
||||
$this->config['use_curl'] = SnapUtil::sanitizeBoolInput(SnapUtil::INPUT_REQUEST, '_ftp_use_curl', false);
|
||||
$this->config['ssl'] = SnapUtil::sanitizeBoolInput(SnapUtil::INPUT_REQUEST, '_ftp_ssl', false);
|
||||
$this->config['passive_mode'] = SnapUtil::sanitizeBoolInput(SnapUtil::INPUT_REQUEST, '_ftp_passive_mode', false);
|
||||
|
||||
$errorMsg = '';
|
||||
if ($this->getAdapter()->initialize($errorMsg) === false) {
|
||||
$message = sprintf(
|
||||
__('Failed to connect to FTP server with message: %1$s', 'duplicator-pro'),
|
||||
$errorMsg
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
__('FTP Storage Updated - Server %1$s, Folder %2$s was created.', 'duplicator-pro'),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register storage type
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
$dGlobal->save();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default settings
|
||||
*
|
||||
* @return array<string, scalar>
|
||||
*/
|
||||
protected static function getDefaultSettings()
|
||||
{
|
||||
return ['ftp_upload_chunksize_in_mb' => self::DEFAULT_UPLOAD_CHUNK_SIZE_IN_MB];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the settings page for this storage.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function renderGlobalOptions()
|
||||
{
|
||||
$values = self::getDefaultSettings();
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
foreach ($values as $key => $default) {
|
||||
$values[$key] = $dGlobal->getVal($key, $default);
|
||||
}
|
||||
?>
|
||||
<h3 class="title"><?php esc_html_e("FTP", 'duplicator-pro') ?></h3>
|
||||
<hr size="1" />
|
||||
<table class="form-table">
|
||||
<tr valign="top">
|
||||
<th scope="row"><label><?php esc_html_e("Upload Size (MB)", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input class="dup-narrow-input"
|
||||
type="number"
|
||||
min="<?php echo self::MIN_UPLOAD_CHUNK_SIZE_IN_MB; ?>"
|
||||
max="<?php echo self::MAX_UPLOAD_CHUNK_SIZE_IN_MB; ?>"
|
||||
name="ftp_upload_chunksize_in_mb"
|
||||
id="ftp_upload_chunksize_in_mb"
|
||||
data-parsley-required
|
||||
data-parsley-type="number"
|
||||
data-parsley-errors-container="#ftp_upload_chunksize_in_mb_error_container"
|
||||
value="<?php echo (int) $values['ftp_upload_chunksize_in_mb']; ?>" />
|
||||
<div id="ftp_upload_chunksize_in_mb_error_container" class="duplicator-error-container"></div>
|
||||
<p class="description">
|
||||
<?php esc_html_e('How much should be uploaded to the server per attempt.', 'duplicator-pro'); ?>
|
||||
<?php echo esc_html(sprintf(__('Min size %smb.', 'duplicator-pro'), self::MIN_UPLOAD_CHUNK_SIZE_IN_MB)); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,385 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\FtpAddon\Models;
|
||||
|
||||
use Duplicator\Core\Views\TplMng;
|
||||
use Duplicator\Libs\Snap\SnapUtil;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
use Exception;
|
||||
|
||||
class SFTPStorage extends AbstractStorageEntity
|
||||
{
|
||||
const MIN_UPLOAD_CHUNK_SIZE_IN_MB = 1;
|
||||
const DEFAULT_UPLOAD_CHUNK_SIZE_IN_MB = 5;
|
||||
const MAX_UPLOAD_CHUNK_SIZE_IN_MB = 100;
|
||||
|
||||
/**
|
||||
* Get stoage adapter
|
||||
*
|
||||
* @return SFTPStorageAdapter
|
||||
*/
|
||||
protected function getAdapter()
|
||||
{
|
||||
return new SFTPStorageAdapter(
|
||||
$this->config['server'],
|
||||
$this->config['port'],
|
||||
$this->config['username'],
|
||||
$this->config['password'],
|
||||
$this->config['storage_folder'],
|
||||
$this->config['private_key'],
|
||||
$this->config['private_key_password'],
|
||||
$this->config['timeout_in_secs']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default config
|
||||
*
|
||||
* @return array<string,scalar>
|
||||
*/
|
||||
protected static function getDefaultConfig()
|
||||
{
|
||||
$config = parent::getDefaultConfig();
|
||||
$config = array_merge(
|
||||
$config,
|
||||
[
|
||||
'server' => '',
|
||||
'port' => 22,
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'private_key' => '',
|
||||
'private_key_password' => '',
|
||||
'timeout_in_secs' => 15,
|
||||
]
|
||||
);
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize
|
||||
*
|
||||
* Wakeup method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
parent::__wakeup();
|
||||
|
||||
if ($this->legacyEntity) {
|
||||
// Old storage entity
|
||||
$this->legacyEntity = false;
|
||||
// Make sure the storage type is right from the old entity
|
||||
$this->storage_type = $this->getSType();
|
||||
$this->config = [
|
||||
'server' => $this->sftp_server,
|
||||
'port' => $this->sftp_port,
|
||||
'username' => $this->sftp_username,
|
||||
'password' => $this->sftp_password,
|
||||
'private_key' => $this->sftp_private_key,
|
||||
'private_key_password' => $this->sftp_private_key_password,
|
||||
'storage_folder' => '/' . ltrim($this->sftp_storage_folder, '/\\'),
|
||||
'max_packages' => $this->sftp_max_files,
|
||||
'timeout_in_secs' => $this->sftp_timeout_in_secs,
|
||||
];
|
||||
// reset old values
|
||||
$this->sftp_server = '';
|
||||
$this->sftp_port = 22;
|
||||
$this->sftp_username = '';
|
||||
$this->sftp_password = '';
|
||||
$this->sftp_private_key = '';
|
||||
$this->sftp_private_key_password = '';
|
||||
$this->sftp_storage_folder = '';
|
||||
$this->sftp_max_files = 10;
|
||||
$this->sftp_timeout_in_secs = 15;
|
||||
$this->sftp_disable_chunking_mode = false;
|
||||
}
|
||||
|
||||
// For legacy entities, we need to make sure the config is up to date
|
||||
$this->config['port'] = (int) $this->config['port'];
|
||||
$this->config['max_packages'] = (int) $this->config['max_packages'];
|
||||
$this->config['timeout_in_secs'] = (int) $this->config['timeout_in_secs'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the FontAwesome storage type icon.
|
||||
*
|
||||
* @return string Returns the font-awesome icon
|
||||
*/
|
||||
public static function getStypeIcon()
|
||||
{
|
||||
return '<i class="fas fa-network-wired fa-fw"></i>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('SFTP', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
return $this->config['server'] . ":" . $this->config['port'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority, used to sort storages.
|
||||
* 100 is neutral value, 0 is the highest priority
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getPriority()
|
||||
{
|
||||
return 90;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
return extension_loaded('gmp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported notice, displayed if storage isn't supported
|
||||
*
|
||||
* @return string html string or empty if storage is supported
|
||||
*/
|
||||
public static function getNotSupportedNotice()
|
||||
{
|
||||
if (static::isSupported()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
_x(
|
||||
'SFTP requires the %1$sgmp extension%2$s. Please contact your host to install.',
|
||||
'1: <a> tag, 2: </a> tag',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a href="http://php.net/manual/en/book.gmp.php" target="_blank">',
|
||||
'</a>'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is valid
|
||||
*
|
||||
* @return bool Return true if storage is valid and ready to use, false otherwise
|
||||
*/
|
||||
public function isValid()
|
||||
{
|
||||
return $this->getAdapter()->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 SFTP server %1$s in folder:<br/> <i>%2$s</i>', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'pending':
|
||||
return sprintf(
|
||||
__('Transfer to SFTP server %1$s in folder %2$s is pending', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'failed':
|
||||
return sprintf(
|
||||
__('Failed to transfer to SFTP server %1$s in folder %2$s', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'cancelled':
|
||||
return sprintf(
|
||||
__('Cancelled before could transfer to SFTP server:<br/>%1$s in folder %2$s', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'success':
|
||||
return sprintf(
|
||||
__('Transferred package to SFTP server:<br/>%1$s in folder %2$s', "duplicator-pro"),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
default:
|
||||
throw new Exception('Invalid key');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List quick view
|
||||
*
|
||||
* @param bool $echo Echo or return
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getListQuickView($echo = true)
|
||||
{
|
||||
ob_start();
|
||||
?>
|
||||
<div>
|
||||
<label><?php esc_html_e('Server', 'duplicator-pro'); ?>:</label>
|
||||
<?php echo esc_html($this->config['server']); ?>: <?php echo intval($this->config['port']); ?> <br/>
|
||||
<label><?php esc_html_e('Location', 'duplicator-pro') ?>:</label>
|
||||
<?php
|
||||
echo wp_kses(
|
||||
$this->getHtmlLocationLink(),
|
||||
[
|
||||
'a' => [
|
||||
'href' => [],
|
||||
'target' => [],
|
||||
],
|
||||
]
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
if ($echo) {
|
||||
ob_end_flush();
|
||||
return '';
|
||||
} else {
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render form config fields
|
||||
*
|
||||
* @param bool $echo Echo or return
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderConfigFields($echo = true)
|
||||
{
|
||||
return TplMng::getInstance()->render(
|
||||
'ftpaddon/configs/sftp',
|
||||
[
|
||||
'storage' => $this,
|
||||
'server' => $this->config['server'],
|
||||
'port' => $this->config['port'],
|
||||
'username' => $this->config['username'],
|
||||
'password' => $this->config['password'],
|
||||
'privateKey' => $this->config['private_key'],
|
||||
'privateKeyPwd' => $this->config['private_key_password'],
|
||||
'storageFolder' => $this->config['storage_folder'],
|
||||
'maxPackages' => $this->config['max_packages'],
|
||||
'timeout' => $this->config['timeout_in_secs'],
|
||||
],
|
||||
$echo
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk size in bytes
|
||||
*
|
||||
* @return int bytes
|
||||
*/
|
||||
public function getUploadChunkSize()
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk timeout in seconds
|
||||
*
|
||||
* @return int timeout in microseconds, 0 unlimited
|
||||
*/
|
||||
public function getUploadChunkTimeout()
|
||||
{
|
||||
return (int) ($this->config['timeout_in_secs'] <= 0 ? 0 : $this->config['timeout_in_secs'] * SECONDS_IN_MICROSECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, 'sftp_max_files', 10);
|
||||
$this->config['server'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'sftp_server', '');
|
||||
$this->config['port'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'sftp_port', 10);
|
||||
$this->config['username'] = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'sftp_username', '');
|
||||
$password = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'sftp_password', '');
|
||||
$password2 = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'sftp_password2', '');
|
||||
$this->config['private_key'] = SnapUtil::sanitizeDefaultInput(SnapUtil::INPUT_REQUEST, 'sftp_private_key', '');
|
||||
$keyPassword = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'sftp_private_key_password', '');
|
||||
$keyPassword2 = SnapUtil::sanitizeTextInput(SnapUtil::INPUT_REQUEST, 'sftp_private_key_password2', '');
|
||||
if (strlen($password) > 0) {
|
||||
if ($password !== $password2) {
|
||||
$message = __('Passwords do not match', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
$this->config['password'] = $password;
|
||||
} elseif (strlen($keyPassword) > 0) {
|
||||
if ($keyPassword !== $keyPassword2) {
|
||||
$message = __('Priva key Passwords do not match', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
$this->config['private_key_password'] = $keyPassword;
|
||||
}
|
||||
$this->config['storage_folder'] = self::getSanitizedInputFolder('_sftp_storage_folder', 'add');
|
||||
$this->config['timeout_in_secs'] = max(10, SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'sftp_timeout_in_secs', 15));
|
||||
|
||||
$errorMsg = '';
|
||||
if ($this->getAdapter()->initialize($errorMsg) === false) {
|
||||
$message = sprintf(
|
||||
__('Failed to connect to SFTP server with message: %1$s', 'duplicator-pro'),
|
||||
$errorMsg
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
__('SFTP Storage Updated - Server %1$s, Folder %2$s was created.', 'duplicator-pro'),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,779 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snapcreek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\FtpAddon\Models;
|
||||
|
||||
use Exception;
|
||||
use Duplicator\Libs\Snap\SnapIO;
|
||||
use Duplicator\Models\Storages\AbstractStorageAdapter;
|
||||
use Duplicator\Models\Storages\StoragePathInfo;
|
||||
use VendorDuplicator\phpseclib3\Crypt\Common\PrivateKey;
|
||||
use VendorDuplicator\phpseclib3\Crypt\PublicKeyLoader;
|
||||
use VendorDuplicator\phpseclib3\Net\SFTP;
|
||||
|
||||
/**
|
||||
* SFTP class adapter
|
||||
*/
|
||||
class SFTPStorageAdapter extends AbstractStorageAdapter
|
||||
{
|
||||
/** @var int */
|
||||
const DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024;
|
||||
/** @var string */
|
||||
protected $server = '';
|
||||
/** @var int */
|
||||
protected $port = 22;
|
||||
/** @var string */
|
||||
protected $username = '';
|
||||
/** @var string */
|
||||
protected $password = '';
|
||||
/** @var string */
|
||||
protected $root = '';
|
||||
/** @var string */
|
||||
protected $privateKey = '';
|
||||
/** @var string */
|
||||
protected $privateKeyPassword = '';
|
||||
/** @var int */
|
||||
protected $timeout = 15;
|
||||
/** @var resource */
|
||||
private $sourceFileHandle = null;
|
||||
/** @var string */
|
||||
private $lastSourceFilePath = '';
|
||||
/** @var resource */
|
||||
private $destFileHandle = null;
|
||||
/** @var string */
|
||||
private $lastDestFilePath = '';
|
||||
/** @var ?SFTP */
|
||||
private $connection = null;
|
||||
|
||||
/**
|
||||
* Class contructor
|
||||
*
|
||||
* @param string $server hosting domain or ip address
|
||||
* @param int $port hosting port
|
||||
* @param string $username hosting username
|
||||
* @param string $password hosting password
|
||||
* @param string $root hosting root path
|
||||
* @param string $privateKey hosting private key
|
||||
* @param string $privateKeyPassword hosting private key password
|
||||
* @param int $timeout hosting timeout
|
||||
*/
|
||||
public function __construct(
|
||||
$server,
|
||||
$port = 22,
|
||||
$username = '',
|
||||
$password = '',
|
||||
$root = '',
|
||||
$privateKey = '',
|
||||
$privateKeyPassword = '',
|
||||
$timeout = 15
|
||||
) {
|
||||
$this->server = $server;
|
||||
$this->port = (int) $port;
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
$this->root = SnapIO::trailingslashit($root);
|
||||
$this->privateKey = $privateKey;
|
||||
$this->privateKeyPassword = $privateKeyPassword;
|
||||
$this->timeout = (int) $timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->isDir('/') && !$this->createDir('/')) {
|
||||
$errorMsg = 'Could not create root directory';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the storage on deletion.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
public function destroy()
|
||||
{
|
||||
return $this->delete('/', 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 = '')
|
||||
{
|
||||
if (!$this->isConnectionInfoValid($errorMsg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getConnection($errorMsg) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isDir('/')) {
|
||||
$errorMsg = 'Root path is invalid';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connection info is valid
|
||||
*
|
||||
* @param string $errorMsg error message
|
||||
*
|
||||
* @return bool true if connection info is valid, false otherwise
|
||||
*/
|
||||
private function isConnectionInfoValid(&$errorMsg = '')
|
||||
{
|
||||
try {
|
||||
if (strlen($this->server) == 0) {
|
||||
throw new Exception('Server name is required to make sftp connection');
|
||||
}
|
||||
|
||||
if ($this->port < 1) {
|
||||
throw new Exception('Server port is required to make sftp connection');
|
||||
}
|
||||
|
||||
if (strlen($this->username) == 0) {
|
||||
throw new Exception('Username is required to make sftp connection');
|
||||
}
|
||||
|
||||
if (strlen($this->password) == 0 && strlen($this->privateKey) == 0) {
|
||||
throw new Exception('You should provide either sftp user pasword or the private key to make sftp connection');
|
||||
}
|
||||
|
||||
if (strlen($this->privateKey) > 0 && strlen($this->privateKeyPassword) == 0) {
|
||||
throw new Exception('You should provide private key password');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$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.
|
||||
*/
|
||||
public function realCreateDir($path)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isDir($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$path = $this->getFullPath($path, true);
|
||||
try {
|
||||
return $conn->mkdir($path, -1, true) !== false;
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public function realCreateFile($path, $content)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($fullPath = $this->getFullPath($path)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$parentDir = dirname($path);
|
||||
if ($this->createDir($parentDir) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($conn->put($fullPath, $content) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return strlen($content);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($path = $this->getFullPath($path)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $conn->get($path);
|
||||
} catch (Exception $e) {
|
||||
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.
|
||||
*/
|
||||
public function realMove($oldPath, $newPath)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($oldPath = $this->getFullPath($oldPath)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($newPath = $this->getFullPath($newPath)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $conn->rename($oldPath, $newPath);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path info.
|
||||
*
|
||||
* @param string $path Relative storage path, if empty, return root path info.
|
||||
*
|
||||
* @return StoragePathInfo|false The path info or false if path is invalid.
|
||||
*/
|
||||
public function getRealPathInfo($path)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fullPath = $this->getFullPath($path, true);
|
||||
try {
|
||||
$info = $conn->stat($fullPath);
|
||||
if ($info === false) {
|
||||
throw new Exception('Could not get path info');
|
||||
}
|
||||
|
||||
$pathInfo = new StoragePathInfo();
|
||||
$pathInfo->exists = true;
|
||||
$pathInfo->path = $path;
|
||||
$pathInfo->isDir = $info['type'] === 2;
|
||||
$pathInfo->size = $pathInfo->isDir ? 0 : $info['size'];
|
||||
$pathInfo->modified = $info['mtime'];
|
||||
$pathInfo->created = isset($info['ctime']) ? $info['ctime'] : $info['mtime'];
|
||||
|
||||
return $pathInfo;
|
||||
} catch (Exception $e) {
|
||||
$pathInfo = new StoragePathInfo();
|
||||
$pathInfo->path = $path;
|
||||
|
||||
return $pathInfo;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$path = $this->getFullPath($path, true);
|
||||
try {
|
||||
$list = $conn->nlist($path);
|
||||
if ($list === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($list as $item) {
|
||||
if ($item === '.' || $item === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$itemPath = SnapIO::trailingslashit($path) . $item;
|
||||
if ($conn->is_dir($itemPath)) {
|
||||
if ($folders) {
|
||||
$result[] = $item;
|
||||
}
|
||||
} else {
|
||||
if ($files) {
|
||||
$result[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (Exception $e) {
|
||||
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($path, $filters = [])
|
||||
{
|
||||
if ($this->isDir($path) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$regexFilters = [];
|
||||
$normalFilters = [];
|
||||
foreach ($filters as $filter) {
|
||||
if (preg_match('/^\/.*\/$/', $filter) === 1) {
|
||||
$regexFilters[] = $filter;
|
||||
} 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 = [])
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($storageFileFullPath = $this->getFullPath($storageFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parentDir = dirname($storageFile);
|
||||
if ($offset === 0 && !$this->createDir($parentDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//Uplaod file at once without any other operation
|
||||
if (($timeout === 0 && $offset === 0 && $length < 0) || filesize($sourceFile) < $length) {
|
||||
if (($content = file_get_contents($sourceFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->createFile($storageFile, $content);
|
||||
}
|
||||
|
||||
if (($sourceFileHandle = $this->getSourceFileHandle($sourceFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (@fseek($sourceFileHandle, $offset) === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bytesWritten = 0;
|
||||
$length = $length < 0 ? self::DEFAULT_CHUNK_SIZE : $length;
|
||||
try {
|
||||
$result = $conn->put(
|
||||
$storageFileFullPath,
|
||||
function ($size) use ($timeout, $startTime, &$bytesWritten, $sourceFileHandle, $length) {
|
||||
if ($timeout !== 0 && (microtime(true) - $startTime) * SECONDS_IN_MICROSECONDS > $timeout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($timeout === 0 && $bytesWritten >= $length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (feof($sourceFileHandle)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (($data = @fread($sourceFileHandle, $size)) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
},
|
||||
SFTP::SOURCE_CALLBACK | SFTP::RESUME,
|
||||
$offset,
|
||||
-1,
|
||||
function ($sent) use (&$bytesWritten) {
|
||||
$bytesWritten = $sent;
|
||||
}
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $timeout === 0 ? $length : $bytesWritten;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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 = [])
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($fullPath = $this->getFullPath($storageFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wp_mkdir_p(dirname($destFile)) == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($offset === 0 && @file_exists($destFile) && !@unlink($destFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isFile($storageFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($offset === 0 && $length < 0) {
|
||||
if (($content = $this->getFileContent($storageFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return @file_put_contents($destFile, $content);
|
||||
}
|
||||
|
||||
if (($handle = $this->getDestFileHandle($destFile)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bytesWritten = 0;
|
||||
$length = $length < 0 ? self::DEFAULT_CHUNK_SIZE : $length;
|
||||
try {
|
||||
do {
|
||||
$content = $conn->get($fullPath, false, $offset, $length);
|
||||
|
||||
if (
|
||||
$content === false ||
|
||||
@fseek($handle, $offset) === -1 ||
|
||||
@fwrite($handle, $content) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($timeout === 0) {
|
||||
return $length;
|
||||
}
|
||||
|
||||
$bytesWritten += strlen($content);
|
||||
$offset += strlen($content);
|
||||
} while (self::getElapsedTime($startTime) < $timeout && $content !== false);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $bytesWritten;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an SFTP object
|
||||
*
|
||||
* @param string $errorMsg error message
|
||||
*
|
||||
* @return ?SFTP
|
||||
*/
|
||||
private function getConnection(&$errorMsg = '')
|
||||
{
|
||||
|
||||
if ($this->connection instanceof SFTP) {
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$this->isConnectionInfoValid($errorMsg)) {
|
||||
throw new Exception($errorMsg);
|
||||
}
|
||||
|
||||
$this->connection = new SFTP($this->server, $this->port);
|
||||
|
||||
if (strlen($this->privateKey) > 0) {
|
||||
if (($key = $this->getPrivateKey()) === null) {
|
||||
throw new Exception('Invalid private key');
|
||||
}
|
||||
|
||||
if (!$this->connection->login($this->username, $key)) {
|
||||
throw new Exception('Invalid username or private key');
|
||||
}
|
||||
} else {
|
||||
if (!$this->connection->login($this->username, $this->password)) {
|
||||
throw new Exception('Invalid username or password');
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
if ($this->connection !== null && $this->connection->isConnected()) {
|
||||
$this->connection->disconnect();
|
||||
}
|
||||
$this->connection = null;
|
||||
$errorMsg = $e->getMessage();
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an SFTP Private Key
|
||||
*
|
||||
* @return ?PrivateKey return key object or null
|
||||
*/
|
||||
protected function getPrivateKey()
|
||||
{
|
||||
if (strlen($this->privateKey) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$password = strlen($this->privateKeyPassword) > 0 ? $this->privateKeyPassword : false;
|
||||
$key = PublicKeyLoader::load($this->privateKey, $password);
|
||||
|
||||
if ($key instanceof PrivateKey) {
|
||||
return $key;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete reletative 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.
|
||||
*/
|
||||
public function realDelete($path, $recursive = false)
|
||||
{
|
||||
if (($conn = $this->getConnection()) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->exists($path) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$fullPath = $this->getFullPath($path, true);
|
||||
try {
|
||||
// have to use hack below because phpseclib doesn't work well with
|
||||
// directories in none recursive mode
|
||||
$isDir = $this->isDir($path);
|
||||
$isEmptyDir = $isDir && $this->isDirEmpty($path);
|
||||
if ($isDir) {
|
||||
if ($isEmptyDir === false && $recursive === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $conn->delete($fullPath, true);
|
||||
} else {
|
||||
return $conn->delete($fullPath);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the full path of storage from relative path.
|
||||
*
|
||||
* @param string $path The relative storage path
|
||||
* @param bool $acceptEmpty If true, return root path if path is empty. Default to false.
|
||||
*
|
||||
* @return string|false The full path or false if path is invalid.
|
||||
*/
|
||||
protected function getFullPath($path, $acceptEmpty = false)
|
||||
{
|
||||
$path = ltrim((string) $path, '/\\');
|
||||
if (strlen($path) === 0) {
|
||||
return $acceptEmpty ? SnapIO::untrailingslashit($this->root) : false;
|
||||
}
|
||||
return $this->root . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source file handle
|
||||
*
|
||||
* @param string $destFilePath The source file path
|
||||
*
|
||||
* @return resource|false returns the file handle or false on failure
|
||||
*/
|
||||
private function getDestFileHandle($destFilePath)
|
||||
{
|
||||
if ($this->lastDestFilePath === $destFilePath) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source file handle
|
||||
*
|
||||
* @param string $sourceFilePath The source file path
|
||||
*
|
||||
* @return resource|false
|
||||
*/
|
||||
private function getSourceFileHandle($sourceFilePath)
|
||||
{
|
||||
if ($this->lastSourceFilePath === $sourceFilePath) {
|
||||
return $this->sourceFileHandle;
|
||||
}
|
||||
|
||||
if (is_resource($this->sourceFileHandle)) {
|
||||
@fclose($this->sourceFileHandle);
|
||||
}
|
||||
|
||||
if (($this->sourceFileHandle = @fopen($sourceFilePath, 'r')) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->lastSourceFilePath = $sourceFilePath;
|
||||
return $this->sourceFileHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get elapsed time in microseconds
|
||||
*
|
||||
* @param float $startTime start time
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private function getElapsedTime($startTime)
|
||||
{
|
||||
return (microtime(true) - $startTime) * SECONDS_IN_MICROSECONDS;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class destructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->connection !== null && $this->connection->isConnected()) {
|
||||
$this->connection->disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snapcreek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\FtpAddon\Utils;
|
||||
|
||||
class FTPUtils
|
||||
{
|
||||
const SYS_TYPE_UNIX = 'UNIX';
|
||||
const SYS_TYPE_WINDOWS_NT = 'WINDOWS_NT';
|
||||
|
||||
/**
|
||||
* Parses a raw list item string into an array
|
||||
*
|
||||
* @param string $rawListString Raw list string
|
||||
* @param string $systemType System type (UNIX or WINDOWS_NT)
|
||||
*
|
||||
* @return false|array{name: string, size: int, modified: int, created: int, isDir: bool}|false
|
||||
*/
|
||||
public static function parseRawListString($rawListString, $systemType)
|
||||
{
|
||||
switch (strtoupper($systemType)) {
|
||||
case self::SYS_TYPE_UNIX:
|
||||
return self::parseUnixRawListString($rawListString);
|
||||
case self::SYS_TYPE_WINDOWS_NT:
|
||||
return self::parseWindowsRawListString($rawListString);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the info in the raw list item for Windows
|
||||
*
|
||||
* @param string $rawListString Raw list string
|
||||
*
|
||||
* @return false|array{name: string, size: int, modified: int, created: int, isDir: bool}|false
|
||||
*/
|
||||
private static function parseWindowsRawListString($rawListString)
|
||||
{
|
||||
//Regex below gets info from raw string on Windows systems
|
||||
//Dir: 01-01-70 12:00AM <DIR> folder
|
||||
//File: 01-01-70 12:00AM 4096 file.txt
|
||||
//Groups: 1 3 4
|
||||
$regex = '/^(\d{2}-\d{2}-\d{2,4}\s+\d{1,2}:\d{1,2}(?:AM|PM))\s+(?:<DIR>|(\d+))\s+(.+)$/';
|
||||
if (preg_match($regex, $rawListString, $matches) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$info = [];
|
||||
$info['name'] = $matches[4];
|
||||
$info['isDir'] = strtoupper($matches[3]) === '<DIR>';
|
||||
$info['size'] = $info['isDir'] ? 0 : (int) $matches[3];
|
||||
$info['modified'] = strtotime($matches[1]);
|
||||
$info['created'] = $info['modified'];
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the info in the raw list item for Unix
|
||||
*
|
||||
* @param string $rawListString Raw list string
|
||||
*
|
||||
* @return false|array{name: string, size: int, modified: int, created: int, isDir: bool}|false
|
||||
*/
|
||||
private static function parseUnixRawListString($rawListString)
|
||||
{
|
||||
//Regex below gets info from raw string on UNIX systems
|
||||
//Example: drwxr-xr-x 2 user group 4096 Jan 1 1970 folder
|
||||
//Groups: 1 2 3 4 5 6 7
|
||||
$regex = '/^([drwx\-]{10})\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+\d{1,2}:\d{1,2}|\w{3}\s+\d{1,2}\s+\d{4})\s+(.+)$/';
|
||||
if (preg_match($regex, $rawListString, $matches) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$info = [];
|
||||
$info['name'] = $matches[7];
|
||||
$info['isDir'] = $matches[1][0] === 'd';
|
||||
$info['size'] = $info['isDir'] ? 0 : (int) $matches[5];
|
||||
$info['modified'] = strtotime($matches[6]);
|
||||
$info['created'] = $info['modified'];
|
||||
|
||||
return $info;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Duplicator messages sections
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\FtpAddon\Models\FTPStorage;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var \Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
* @var FTPStorage $storage
|
||||
*/
|
||||
$storage = $tplData["storage"];
|
||||
/** @var string */
|
||||
$server = $tplData["server"];
|
||||
/** @var int */
|
||||
$port = $tplData["port"];
|
||||
/** @var string */
|
||||
$username = $tplData["username"];
|
||||
/** @var string */
|
||||
$password = $tplData["password"];
|
||||
/** @var string */
|
||||
$storageFolder = $tplData["storageFolder"];
|
||||
/** @var int */
|
||||
$maxPackages = $tplData["maxPackages"];
|
||||
/** @var int */
|
||||
$timeout = $tplData["timeout"];
|
||||
/** @var bool */
|
||||
$useCurl = $tplData["useCurl"];
|
||||
/** @var bool */
|
||||
$isPassive = $tplData["isPassive"];
|
||||
/** @var bool */
|
||||
$useSSL = $tplData["useSSL"];
|
||||
|
||||
$tplMng->render('admin_pages/storages/parts/provider_head');
|
||||
?>
|
||||
<tr>
|
||||
<td class="dpro-sub-title" colspan="2"><b><?php esc_html_e("Credentials", 'duplicator-pro'); ?></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_server"><?php esc_html_e("Server", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="ftp_server"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="ftp_server"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
value="<?php echo esc_attr($server); ?>"
|
||||
data-parsley-errors-container="#ftp_server_error_container"
|
||||
data-parsley-required="true"
|
||||
>
|
||||
<label for="ftp_server">
|
||||
<?php esc_html_e("Port", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<input
|
||||
name="ftp_port"
|
||||
id="ftp_port"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
style="width:75px"
|
||||
value="<?php echo (int) $port; ?>"
|
||||
data-parsley-errors-container="#ftp_server_error_container"
|
||||
data-parsley-required="true"
|
||||
data-parsley-type="number"
|
||||
data-parsley-range="[1, 65535]"
|
||||
>
|
||||
<div id="ftp_server_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_username"><?php esc_html_e("Username", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="ftp_username"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="ftp_username"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
value="<?php echo esc_attr($username); ?>"
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_password"><?php esc_html_e("Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="ftp_password"
|
||||
name="ftp_password"
|
||||
type="password"
|
||||
class="dup-empty-field-on-submit"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($password))); ?>"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_password2"><?php esc_html_e("Retype Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="ftp_password2"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="ftp_password2"
|
||||
type="password"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($password))); ?>"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
data-parsley-errors-container="#ftp_password2_error_container"
|
||||
data-parsley-trigger="change"
|
||||
data-parsley-equalto="#ftp_password"
|
||||
data-parsley-equalto-message="<?php esc_html_e("Passwords do not match", 'duplicator-pro'); ?>"
|
||||
><br/>
|
||||
<div id="ftp_password2_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="dpro-sub-title" colspan="2"><b><?php esc_html_e("Settings", 'duplicator-pro'); ?></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="_ftp_storage_folder"><?php esc_html_e("Storage Folder", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="_ftp_storage_folder"
|
||||
name="_ftp_storage_folder"
|
||||
type="text"
|
||||
value="<?php echo esc_attr($storageFolder); ?>"
|
||||
>
|
||||
<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="ftp_max_files"><?php esc_html_e("Max Packages", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<label for="ftp_max_files">
|
||||
<input
|
||||
id="ftp_max_files"
|
||||
name="ftp_max_files"
|
||||
type="number"
|
||||
value="<?php echo (int) $maxPackages; ?>"
|
||||
min="0"
|
||||
maxlength="4"
|
||||
data-parsley-errors-container="#ftp_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="ftp_max_files_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_timeout_in_secs"><?php esc_html_e("Timeout", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
|
||||
<label for="ftp_timeout_in_secs">
|
||||
<input
|
||||
id="ftp_timeout"
|
||||
name="ftp_timeout_in_secs"
|
||||
type="number"
|
||||
min="10"
|
||||
value="<?php echo (int) $timeout; ?>"
|
||||
data-parsley-errors-container="#ftp_timeout_error_container"
|
||||
data-parsley-required="true"
|
||||
data-parsley-type="number"
|
||||
data-parsley-min="10"
|
||||
>
|
||||
<label for="ftp_timeout_in_secs">
|
||||
<?php esc_html_e("seconds", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<br>
|
||||
<i>
|
||||
<?php
|
||||
esc_html_e(
|
||||
"Do not modify this setting unless you know the expected result or have talked to support.",
|
||||
'duplicator-pro'
|
||||
); ?>
|
||||
</i>
|
||||
</label>
|
||||
<div id="ftp_timeout_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="ftp_ssl"><?php esc_html_e("Explicit SSL", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input name="_ftp_ssl" <?php checked($useSSL); ?> class="checkbox" value="1" type="checkbox" id="_ftp_ssl" >
|
||||
<label for="_ftp_ssl"><?php esc_html_e("Enable", 'duplicator-pro'); ?></label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="_ftp_passive_mode"><?php esc_html_e("Passive Mode", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
<?php checked($isPassive); ?>
|
||||
class="checkbox"
|
||||
value="1"
|
||||
type="checkbox"
|
||||
name="_ftp_passive_mode"
|
||||
id="_ftp_passive_mode"
|
||||
>
|
||||
<label for="_ftp_passive_mode"><?php esc_html_e("Enable", 'duplicator-pro'); ?></label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="_ftp_use_curl"><?php esc_html_e("cURL", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input <?php checked($useCurl); ?> class="checkbox" value="1" type="checkbox" name="_ftp_use_curl" id="_ftp_use_curl">
|
||||
<label for="_ftp_use_curl"><?php esc_html_e("Enable", 'duplicator-pro'); ?></label>
|
||||
<p><i><?php esc_html_e("PHP cURL. Only check if connection test recommends it.", 'duplicator-pro'); ?></i></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label> </label></th>
|
||||
<td>
|
||||
<p>
|
||||
<?php
|
||||
echo wp_kses(
|
||||
__(
|
||||
"<b>Note:</b> This setting is for FTP and FTPS (FTP/SSL) only.
|
||||
To use SFTP (SSH File Transfer Protocol) change the type dropdown above.",
|
||||
'duplicator-pro'
|
||||
),
|
||||
array(
|
||||
'b' => array(),
|
||||
)
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php $tplMng->render('admin_pages/storages/parts/provider_foot'); ?>
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Duplicator messages sections
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\FtpAddon\Models\SFTPStorage;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var \Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
* @var SFTPStorage $storage
|
||||
*/
|
||||
$storage = $tplData["storage"];
|
||||
/** @var string */
|
||||
$server = $tplData["server"];
|
||||
/** @var int */
|
||||
$port = $tplData["port"];
|
||||
/** @var string */
|
||||
$username = $tplData["username"];
|
||||
/** @var string */
|
||||
$password = $tplData["password"];
|
||||
/** @var string */
|
||||
$privateKey = $tplData["privateKey"];
|
||||
/** @var string */
|
||||
$privateKeyPwd = $tplData["privateKeyPwd"];
|
||||
/** @var string */
|
||||
$storageFolder = $tplData["storageFolder"];
|
||||
/** @var int */
|
||||
$maxPackages = $tplData["maxPackages"];
|
||||
/** @var int */
|
||||
$timeout = $tplData["timeout"];
|
||||
|
||||
$tplMng->render('admin_pages/storages/parts/provider_head');
|
||||
?>
|
||||
<tr>
|
||||
<td class="dpro-sub-title" colspan="2"><b><?php esc_html_e("Credentials", 'duplicator-pro'); ?></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_server"><?php esc_html_e("Server", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input id="sftp_server" class="dup-empty-field-on-submit" name="sftp_server"
|
||||
data-parsley-errors-container="#sftp_server_error_container" type="text" a
|
||||
utocomplete="off" value="<?php echo esc_attr($server); ?>">
|
||||
<label for="sftp_server">
|
||||
<?php esc_html_e("Port", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<input
|
||||
name="sftp_port"
|
||||
id="sftp_port"
|
||||
data-parsley-errors-container="#sftp_server_error_container"
|
||||
data-parsley-required="true"
|
||||
data-parsley-type="number"
|
||||
data-parsley-range="[1, 65535]"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
style="width:75px"
|
||||
value="<?php echo (int) $port; ?>"
|
||||
>
|
||||
<div id="sftp_server_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_username"><?php esc_html_e("Username", 'duplicator-pro'); ?></label></th>
|
||||
<td><input id="sftp_username" class="dup-empty-field-on-submit" name="sftp_username"
|
||||
type="text" autocomplete="off" value="<?php echo esc_attr($username); ?>" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_password"><?php esc_html_e("Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="sftp_password"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="sftp_password"
|
||||
type="password"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($password))); ?>"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_password2"><?php esc_html_e("Retype Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="sftp_password2"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="sftp_password2"
|
||||
type="password"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($password))); ?>"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
data-parsley-errors-container="#sftp_password2_error_container"
|
||||
data-parsley-trigger="change"
|
||||
data-parsley-equalto="#sftp_password"
|
||||
data-parsley-equalto-message="<?php esc_attr_e("Passwords do not match", 'duplicator-pro'); ?>"
|
||||
><br/>
|
||||
<div id="sftp_password2_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_private_key"><?php esc_html_e("Private Key (PuTTY)", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="sftp_private_key_file"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="sftp_private_key_file"
|
||||
onchange="DuplicatorReadPrivateKey(this);"
|
||||
type="file"
|
||||
accept="ppk"
|
||||
value=""
|
||||
data-parsley-errors-container="#sftp_private_key_error_container"
|
||||
><br/>
|
||||
<input type="hidden" name="sftp_private_key" id="sftp_private_key" value="<?php echo esc_attr($privateKey); ?>" />
|
||||
<div id="sftp_private_key_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_private_key_password"><?php esc_html_e("Private Key Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="sftp_private_key_password"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="sftp_private_key_password"
|
||||
type="password"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($privateKeyPwd))); ?>"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
data-parsley-errors-container="#sftp_private_key_password_error_container"
|
||||
>
|
||||
<br/>
|
||||
<div id="sftp_private_key_password_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_private_key_password2"><?php esc_html_e("Private Key Retype Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
id="sftp_private_key_password2"
|
||||
class="dup-empty-field-on-submit"
|
||||
name="sftp_private_key_password2"
|
||||
type="password"
|
||||
placeholder="<?php echo esc_attr(str_repeat("*", strlen($privateKeyPwd))); ?>"
|
||||
autocomplete="off"
|
||||
value=""
|
||||
data-parsley-errors-container="#sftp_private_key_password2_error_container"
|
||||
data-parsley-trigger="change"
|
||||
data-parsley-equalto="#sftp_private_key_password"
|
||||
data-parsley-equalto-message="<?php esc_html_e("Passwords do not match", 'duplicator-pro'); ?>"
|
||||
><br/>
|
||||
<div id="sftp_private_key_password2_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="dpro-sub-title" colspan="2"><b><?php esc_html_e("Settings", 'duplicator-pro'); ?></b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="_sftp_storage_folder"><?php esc_html_e("Storage Folder", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input id="_sftp_storage_folder" name="_sftp_storage_folder" type="text" value="<?php echo esc_attr($storageFolder); ?>">
|
||||
<p>
|
||||
<i>
|
||||
<?php
|
||||
printf(
|
||||
esc_html_x(
|
||||
'Folder where packages will be stored. This should be %1$san absolute path, not a relative path%2$s
|
||||
and be unique for each web-site using Duplicator.',
|
||||
'%1$s representes the opening and %2$s the closing bold (<b>) tag',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<b>',
|
||||
'</b>'
|
||||
);
|
||||
?>
|
||||
</i>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_max_files"><?php esc_html_e("Max Packages", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<label for="sftp_max_files">
|
||||
<input id="sftp_max_files" name="sftp_max_files" data-parsley-errors-container="#sftp_max_files_error_container"
|
||||
type="text" value="<?php echo (int) $maxPackages; ?>">
|
||||
<?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="sftp_max_files_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="sftp_timeout_in_secs"><?php esc_html_e("Timeout", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<label for="sftp_timeout_in_secs">
|
||||
<input
|
||||
id="sftp_timeout"
|
||||
name="sftp_timeout_in_secs"
|
||||
data-parsley-errors-container="#sftp_timeout_error_container"
|
||||
type="text"
|
||||
value="<?php echo (int) $timeout; ?>"
|
||||
>
|
||||
<label for="sftp_timeout_in_secs">
|
||||
<?php esc_html_e("seconds", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<br>
|
||||
<i>
|
||||
<?php
|
||||
esc_html_e(
|
||||
"Do not modify this setting unless you know the expected result or have talked to support.",
|
||||
'duplicator-pro'
|
||||
); ?>
|
||||
</i>
|
||||
</label>
|
||||
<div id="sftp_timeout_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php $tplMng->render('admin_pages/storages/parts/provider_foot'); ?>
|
||||
<script>
|
||||
jQuery(document).ready(function ($) {
|
||||
DuplicatorReadPrivateKey = function (file_obj)
|
||||
{
|
||||
var files = file_obj.files;
|
||||
var private_key = files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
$("#sftp_private_key").val(e.target.result);
|
||||
}
|
||||
reader.readAsText(private_key);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Google Drive ADDON
|
||||
*
|
||||
* Name: Google Drive ADDON
|
||||
* 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\GDriveAddon;
|
||||
|
||||
use Duplicator\Addons\GDriveAddon\Models\GDriveStorage;
|
||||
use Duplicator\Addons\GDriveAddon\Utils\Autoloader;
|
||||
use Duplicator\Core\Addons\AbstractAddonCore;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
|
||||
class GDriveAddon 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 void
|
||||
*/
|
||||
public function registerStorages()
|
||||
{
|
||||
GDriveStorage::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, 'gdriveaddon/') === 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_gdrive_count'] = 0;
|
||||
|
||||
foreach ($storages as $index => $storage) {
|
||||
if ($storage->getSType() === GDriveStorage::getSType()) {
|
||||
$storageNums['storages_gdrive_count']++;
|
||||
}
|
||||
}
|
||||
|
||||
return $storageNums;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonPath()
|
||||
{
|
||||
return __DIR__;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonFile()
|
||||
{
|
||||
return __FILE__;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\GDriveAddon\Models;
|
||||
|
||||
use Duplicator\Addons\GDriveAddon\Utils\GoogleClient;
|
||||
use Duplicator\Models\Storages\AbstractStorageAdapter;
|
||||
use Duplicator\Utils\OAuth\TokenEntity;
|
||||
use Exception;
|
||||
use VendorDuplicator\Psr\Http\Message\RequestInterface;
|
||||
use VendorDuplicator\Google\Client;
|
||||
use VendorDuplicator\Google\Http\MediaFileUpload;
|
||||
use VendorDuplicator\Google\Service\Drive;
|
||||
use VendorDuplicator\GuzzleHttp\Psr7\Request;
|
||||
use VendorDuplicator\GuzzleHttp\Psr7\Response;
|
||||
|
||||
/**
|
||||
* @method GDriveStoragePathInfo getPathInfo(string $path)
|
||||
*/
|
||||
class GDriveAdapter extends AbstractStorageAdapter
|
||||
{
|
||||
const FOLDER_MIME_TYPE = 'application/vnd.google-apps.folder';
|
||||
const CHUNK_SIZE_STEP = 256 * KB_IN_BYTES;
|
||||
|
||||
/** @var Drive The Google Drive service */
|
||||
protected $drive = null;
|
||||
/** @var string The root storage path */
|
||||
protected $storagePath = '';
|
||||
/** @var string The root storage path id */
|
||||
protected $storagePathId = '';
|
||||
/** @var TokenEntity The OAuth token entity */
|
||||
protected $token = null;
|
||||
/** @var int */
|
||||
protected $startTime = 0;
|
||||
/** @var bool */
|
||||
protected $sslVerify = true;
|
||||
/** @var string If empty use server cert else use custom cert path */
|
||||
protected $sslCert = '';
|
||||
/** @var bool */
|
||||
protected $ipv4Only = false;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param TokenEntity $token The OAuth token entity.
|
||||
* @param string $storagePath The root storage path.
|
||||
* @param string $storagePathId The root storage path id.
|
||||
* @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(
|
||||
TokenEntity $token,
|
||||
$storagePath,
|
||||
$storagePathId = '',
|
||||
$sslVerify = true,
|
||||
$sslCert = '',
|
||||
$ipv4Only = false
|
||||
) {
|
||||
$this->token = $token;
|
||||
$this->storagePath = $storagePath;
|
||||
$this->storagePathId = $storagePathId;
|
||||
$this->sslVerify = $sslVerify;
|
||||
$this->sslCert = $sslCert;
|
||||
$this->ipv4Only = $ipv4Only;
|
||||
|
||||
if ($token->isAboutToExpire()) {
|
||||
$token->refresh(true);
|
||||
}
|
||||
|
||||
$httpOptions = [];
|
||||
if ($this->sslVerify === false) {
|
||||
$verify = false;
|
||||
} elseif (strlen($this->sslCert) === 0) {
|
||||
$verify = true;
|
||||
} else {
|
||||
$verify = $this->sslCert;
|
||||
}
|
||||
$httpOptions['verify'] = $verify;
|
||||
if ($this->ipv4Only) {
|
||||
$httpOptions['force_ip_resolve'] = 'v4';
|
||||
}
|
||||
$client = new GoogleClient();
|
||||
$client->setHttpClientOptions($httpOptions);
|
||||
|
||||
$client->setAccessToken([
|
||||
'created' => $token->getCreated(),
|
||||
'access_token' => $token->getAccessToken(),
|
||||
'refresh_token' => $token->getRefreshToken(),
|
||||
'expires_in' => $token->getExpiresIn(),
|
||||
'scope' => $token->getScope(),
|
||||
]);
|
||||
|
||||
$this->drive = new Drive($client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Google Drive service.
|
||||
*
|
||||
* @return Drive
|
||||
*/
|
||||
public function getService()
|
||||
{
|
||||
return $this->drive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->token->isValid()) {
|
||||
$errorMsg = __('Invalid token supplied for google drive', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
if (! $this->exists('/') && ! $this->createDir('/')) {
|
||||
$errorMsg = __('Unable to create root directory on google drive', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
if (empty($this->storagePathId)) {
|
||||
$storage = $this->getPathInfo('/');
|
||||
if ($storage->exists) {
|
||||
$this->storagePathId = $storage->id;
|
||||
} else {
|
||||
$errorMsg = __('Unable to fetch root directory info from Google Drive', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the storage on deletion.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
*/
|
||||
public function destroy()
|
||||
{
|
||||
$this->storagePathId = '';
|
||||
return $this->delete('/', 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 = '')
|
||||
{
|
||||
if (! $this->token->isValid()) {
|
||||
$errorMsg = 'Invalid token supplied for google drive';
|
||||
return false;
|
||||
}
|
||||
|
||||
$root = $this->getPathInfo('/');
|
||||
if (! $root || !$root->exists) {
|
||||
$errorMsg = 'Root directory does not exist on google drive, false for isValid';
|
||||
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 = trim($path, '/');
|
||||
|
||||
if (empty($this->storagePathId)) {
|
||||
// if we don't have the storage path id set, we fetch it
|
||||
$storageFolder = $this->getPathInfo('/');
|
||||
if ($storageFolder->exists) {
|
||||
$this->storagePathId = $storageFolder->id;
|
||||
} else {
|
||||
$path = $this->storagePath . '/' . $path;
|
||||
}
|
||||
}
|
||||
|
||||
$parts = array_filter(explode('/', $path));
|
||||
$parent = $this->storagePathId;
|
||||
|
||||
// At this point, if we don't have a parent, we need to create from the root path.
|
||||
|
||||
// We assume that a partial path may exist
|
||||
// So we try to search for the path and create it if it doesn't exist
|
||||
// But once we create one directory, we assume that the rest of the path doesn't exist
|
||||
// This saves us a lot of calls to the Google Drive API
|
||||
$pathMayExist = true;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$query = "name = '{$part}' and trashed = false and mimeType = '" . self::FOLDER_MIME_TYPE . "'";
|
||||
if ($parent) {
|
||||
$query .= " and '{$parent}' in parents";
|
||||
}
|
||||
|
||||
if ($pathMayExist) {
|
||||
// At first, we try to find the directory
|
||||
$response = $this->drive->files->listFiles([
|
||||
'q' => $query,
|
||||
'fields' => 'files(id)',
|
||||
]);
|
||||
if ($response->count() > 0) {
|
||||
$file = $response->getFiles()[0];
|
||||
$parent = $file->getId();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$pathMayExist = false;
|
||||
// If we didn't find the directory, we create it
|
||||
$file = new Drive\DriveFile([
|
||||
'name' => $part,
|
||||
'mimeType' => self::FOLDER_MIME_TYPE,
|
||||
]);
|
||||
|
||||
if (! empty($parent)) {
|
||||
$file->setParents([$parent]);
|
||||
}
|
||||
|
||||
try {
|
||||
$file = $this->drive->files->create($file, ['fields' => 'id']);
|
||||
} catch (\Exception $e) {
|
||||
\DUP_PRO_Log::traceObject('[GDriveAdapter] Unable to create directory: ' . $e->getMessage(), $e);
|
||||
return false;
|
||||
}
|
||||
$parent = $file->getId();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
$response = $this->createNewFile($path, [
|
||||
'data' => $content,
|
||||
'uploadType' => 'multipart',
|
||||
'fields' => 'id,size',
|
||||
]);
|
||||
|
||||
if (!$response) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (int) $response->getSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$info = $this->getPathInfo($path);
|
||||
|
||||
if (! $info->exists) {
|
||||
return true; // if the path doesn't exist, we can consider it deleted
|
||||
}
|
||||
|
||||
if ($info->isDir && ! $recursive && ! $this->isDirEmpty($path)) {
|
||||
return false; // if it's a directory and, we are not deleting recursively, we can't delete it
|
||||
}
|
||||
|
||||
try {
|
||||
$this->drive->files->delete($info->id);
|
||||
} catch (\Exception $e) {
|
||||
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)
|
||||
{
|
||||
$info = $this->getPathInfo($path);
|
||||
|
||||
if (! $info->exists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var Response $response */
|
||||
$response = $this->drive->files->get($info->id, [
|
||||
'alt' => 'media',
|
||||
'acknowledgeAbuse' => true,
|
||||
]);
|
||||
|
||||
return $response->getBody()->getContents();
|
||||
} catch (\Exception $e) {
|
||||
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($oldPath, $newPath)
|
||||
{
|
||||
$fileInfo = $this->getPathInfo($oldPath);
|
||||
$oldDirInfo = $this->getPathInfo(dirname($oldPath));
|
||||
$newDirInfo = $this->getPathInfo(dirname($newPath));
|
||||
$file = $fileInfo->file;
|
||||
|
||||
try {
|
||||
$this->drive->files->update($fileInfo->id, $file, [
|
||||
'addParents' => $newDirInfo->id,
|
||||
'removeParents' => $oldDirInfo->id,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 GDriveStoragePathInfo|false The path info or false on error.
|
||||
*/
|
||||
protected function getRealPathInfo($path)
|
||||
{
|
||||
try {
|
||||
$info = $this->nestedPathInfo($path);
|
||||
} catch (\Exception $e) {
|
||||
$info = false;
|
||||
}
|
||||
|
||||
return $this->buildPathInfo($info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$info = $this->getPathInfo($path);
|
||||
|
||||
if (! $info->exists) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = "'{$info->id}' in parents and trashed = false";
|
||||
|
||||
if (! $files) {
|
||||
$query .= " and mimeType = '" . self::FOLDER_MIME_TYPE . "'";
|
||||
}
|
||||
if (! $folders) {
|
||||
$query .= " and mimeType != '" . self::FOLDER_MIME_TYPE . "'";
|
||||
}
|
||||
|
||||
$nextPageToken = null;
|
||||
$result = [];
|
||||
do {
|
||||
$response = $this->drive->files->listFiles([
|
||||
'q' => $query,
|
||||
'pageToken' => $nextPageToken,
|
||||
]);
|
||||
|
||||
$result = array_merge($result, array_map(function ($file) {
|
||||
$info = $this->buildPathInfo($file);
|
||||
return $info->path;
|
||||
}, $response->getFiles()));
|
||||
} while ($nextPageToken = $response->getNextPageToken());
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [])
|
||||
{
|
||||
$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 = [])
|
||||
{
|
||||
$this->startTrackingTime();
|
||||
|
||||
$chunkSize = max(self::CHUNK_SIZE_STEP, floor($length / self::CHUNK_SIZE_STEP) * self::CHUNK_SIZE_STEP);
|
||||
|
||||
$source = fopen($sourceFile, 'rb');
|
||||
if (! $source) {
|
||||
\DUP_PRO_Log::info(sprintf('[GDriveAdapter] Unable to open source file %s', $sourceFile));
|
||||
return false;
|
||||
}
|
||||
|
||||
fseek($source, $offset);
|
||||
|
||||
$storageFile = '/' . trim($storageFile, '/');
|
||||
$targetPath = dirname($storageFile);
|
||||
|
||||
if ($targetPath === '/' && ! empty($this->storagePathId)) {
|
||||
$target = new Drive\DriveFile();
|
||||
$target->setId($this->storagePathId);
|
||||
} else {
|
||||
$target = $this->getPathInfo($targetPath);
|
||||
if (! $target->exists) {
|
||||
$this->createDir($targetPath);
|
||||
$target = $this->getPathInfo($targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (! $target) {
|
||||
\DUP_PRO_Log::info(sprintf('[GDriveAdapter] Unable to get target path info for %s', $targetPath));
|
||||
return false;
|
||||
}
|
||||
|
||||
$client = $this->drive->getClient();
|
||||
$client->setDefer(true);
|
||||
$originalHttpClient = $client->getHttpClient();
|
||||
|
||||
$file = new Drive\DriveFile([
|
||||
'name' => basename($storageFile),
|
||||
'parents' => [$target->id],
|
||||
]);
|
||||
|
||||
// The file create call returns the request object as we have set client defer to true
|
||||
/** @var Request $request */
|
||||
$request = $this->drive->files->create($file);
|
||||
|
||||
$media = new MediaFileUpload(
|
||||
$client,
|
||||
$request,
|
||||
'application/octet-stream',
|
||||
'',
|
||||
true,
|
||||
$chunkSize
|
||||
);
|
||||
|
||||
$firstChunk = true;
|
||||
|
||||
$media->setFileSize($filesize = filesize($sourceFile));
|
||||
|
||||
if (! empty($extraData['resume_uri'])) {
|
||||
$resumeUri = $extraData['resume_uri'];
|
||||
try {
|
||||
$this->forceSet($media, 'progress', $offset);
|
||||
$this->forceSet($media, 'resumeUri', $resumeUri);
|
||||
} catch (\Exception $e) {
|
||||
$client->setHttpClient($originalHttpClient);
|
||||
\DUP_PRO_Log::info('[GDriveAdapter] Unable to set resume uri: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
$firstChunk = false;
|
||||
\DUP_PRO_Log::trace(sprintf('[GDriveAdapter] Resuming upload for %s from offset %s timeout %d', $sourceFile, $offset, $timeout));
|
||||
}
|
||||
|
||||
do {
|
||||
if ($timeout > 0) {
|
||||
// Set the timeout for the client (in seconds)
|
||||
$client->setHttpClient(new \VendorDuplicator\GuzzleHttp\Client([
|
||||
'base_uri' => $originalHttpClient->getConfig('base_uri'),
|
||||
'http_errors' => false,
|
||||
'timeout' => $timeout / SECONDS_IN_MICROSECONDS, // convert microseconds to seconds
|
||||
]));
|
||||
}
|
||||
$chunk = fread($source, $chunkSize);
|
||||
if (! $chunk) {
|
||||
\DUP_PRO_Log::trace(sprintf('[GDriveAdapter] Unable to read chunk from %s', $sourceFile));
|
||||
$status = true; // we can't set it to false, because drive sdk returns false when chunk upload is successful.
|
||||
break;
|
||||
}
|
||||
$status = $media->nextChunk($chunk);
|
||||
if ($firstChunk) {
|
||||
\DUP_PRO_Log::trace(sprintf('[GDriveAdapter] Created resume uri for %s and it\'s %s', $sourceFile, $media->getResumeUri()));
|
||||
$extraData['resume_uri'] = $media->getResumeUri(); // we need to cache the resume uri for the next chunk
|
||||
$firstChunk = false;
|
||||
}
|
||||
$message = '[GDriveAdapter] Uploaded %d/%d bytes, requested [%d, %d] of %s';
|
||||
\DUP_PRO_Log::trace(sprintf($message, $media->getProgress(), $filesize, $offset, $length, $sourceFile));
|
||||
if ($length > 0) {
|
||||
// if we have a length, we need to stop when we reach it
|
||||
break;
|
||||
}
|
||||
} while (!feof($source) && ! $status && ! $this->hasReachedTimeout($timeout));
|
||||
|
||||
if (feof($source)) {
|
||||
// if we reached the end of the file, we can delete the cached resume uri
|
||||
unset($extraData['resume_uri']);
|
||||
\DUP_PRO_Log::trace(sprintf('[GDriveAdapter] File %s copied successfully to %s', $sourceFile, $storageFile));
|
||||
}
|
||||
|
||||
$client->setDefer(false);
|
||||
$client->setHttpClient($originalHttpClient);
|
||||
|
||||
// If we have false as status, it means the upload is not finished yet
|
||||
// On the final chunk upload, we get the file info
|
||||
if ($status === false || ($status instanceof Drive\DriveFile)) {
|
||||
return $length > 0 ? $length : $filesize;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate info on create dir, this method is exendable by child classes if StoragePathInfo is extended.
|
||||
*
|
||||
* @param string $path Dir path
|
||||
*
|
||||
* @return GDriveStoragePathInfo
|
||||
*/
|
||||
protected function generateCreateDirInfo($path)
|
||||
{
|
||||
return $this->getRealPathInfo($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start tracking the time for the current operation
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function startTrackingTime()
|
||||
{
|
||||
$this->startTime = (int) (microtime(true) * 1000000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the elapsed time since the start of the current operation
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getElapsedTime()
|
||||
{
|
||||
return (int) (microtime(true) * 1000000) - $this->startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the operation has reached the timeout
|
||||
*
|
||||
* @param int $timeout The timeout in microseconds
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasReachedTimeout($timeout)
|
||||
{
|
||||
return $timeout > 0 && $this->getElapsedTime() >= ($timeout - 1000000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate info on delete item, this methos is exendable by child classes if StoragePathInfo is extended.
|
||||
*
|
||||
* @param string $path Item path
|
||||
*
|
||||
* @return GDriveStoragePathInfo
|
||||
*/
|
||||
protected function generateDeleteInfo($path)
|
||||
{
|
||||
$info = new GDriveStoragePathInfo();
|
||||
$info->path = $path;
|
||||
$info->exists = false;
|
||||
$info->isDir = false;
|
||||
$info->size = 0;
|
||||
$info->created = 0;
|
||||
$info->modified = 0;
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new file in the specified path.
|
||||
*
|
||||
* @param string $path The path to create the file in
|
||||
* @param array<string, string> $options The options to create the file with
|
||||
*
|
||||
* @return false|Drive\DriveFile
|
||||
*/
|
||||
protected function createNewFile($path, $options = [])
|
||||
{
|
||||
$path = '/' . trim($path, '/');
|
||||
|
||||
$parent = $this->getPathInfo(dirname($path));
|
||||
|
||||
if (! $parent->exists) {
|
||||
if ($this->createDir(dirname($path))) {
|
||||
$parent = $this->getPathInfo(dirname($path));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$file = new Drive\DriveFile([
|
||||
'name' => basename($path),
|
||||
'parents' => [$parent->id],
|
||||
]);
|
||||
|
||||
try {
|
||||
return $this->drive->files->create($file, $options);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse the path folder by folder and fetch the file info.
|
||||
*
|
||||
* @param string $path The path get information for
|
||||
* @param ?string $parent The parent folder id
|
||||
*
|
||||
* @return false|Drive\DriveFile
|
||||
*/
|
||||
protected function nestedPathInfo($path, $parent = null)
|
||||
{
|
||||
$path = trim($path, '/');
|
||||
$traversed = explode('/', $this->storagePath); // keep track of the traversed path, by default the root folder is traversed
|
||||
|
||||
if (! $parent) {
|
||||
$parent = $this->storagePathId;
|
||||
}
|
||||
if (! $parent) {
|
||||
// if we don't have a parent, we need to traverse from the root folder
|
||||
$path = $this->storagePath . '/' . $path;
|
||||
$traversed = []; // we are traversing from the root folder, so we reset the traversed path
|
||||
$info = false;
|
||||
} else {
|
||||
$info = $this->drive->files->get($parent, ['fields' => 'id,name,mimeType,size,createdTime,modifiedTime,md5Checksum,webViewLink']);
|
||||
}
|
||||
|
||||
$parts = array_filter(explode('/', $path));
|
||||
|
||||
foreach ($parts as $index => $part) {
|
||||
$query = "name = '{$part}' and trashed = false";
|
||||
if ($parent) {
|
||||
$query .= " and '{$parent}' in parents";
|
||||
}
|
||||
if ($index < count($parts) - 1) {
|
||||
// if we are not in the last iteration, it's most definitely a folder
|
||||
$query .= " and mimeType = '" . self::FOLDER_MIME_TYPE . "'";
|
||||
}
|
||||
|
||||
$result = $this->drive->files->listFiles([
|
||||
'q' => $query,
|
||||
'fields' => 'files(id,name,mimeType,size,createdTime,modifiedTime,md5Checksum,webViewLink)',
|
||||
]);
|
||||
$traversed[] = $part;
|
||||
|
||||
if ($result->count() === 0) {
|
||||
// if we didn't find anything, we can stop here
|
||||
return false;
|
||||
}
|
||||
foreach ($result->getFiles() as $file) {
|
||||
if ($file->getName() !== $part) {
|
||||
continue; // we are looking for a file/folder with the same name
|
||||
}
|
||||
if ($index < (count($parts) - 1) && $file->getMimeType() !== self::FOLDER_MIME_TYPE) {
|
||||
// if we are not in the last iteration, we are most definitely looking for a folder, so we skip if it's not
|
||||
continue;
|
||||
}
|
||||
// At this point we have found the file or folder we were looking for
|
||||
$props = $file->getProperties();
|
||||
$props['path'] = implode('/', $traversed);
|
||||
$file->setProperties($props); // add the path to the file properties
|
||||
$parent = $file->getId();
|
||||
$info = $file;
|
||||
|
||||
// we need to keep looking for the next part
|
||||
continue 2;
|
||||
}
|
||||
// if we got here, we didn't find the file or folder we were looking for
|
||||
$info = false;
|
||||
break;
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the path info object from Google Drive's file info.
|
||||
*
|
||||
* @param Drive\DriveFile|false $file The file info
|
||||
*
|
||||
* @return GDriveStoragePathInfo
|
||||
*/
|
||||
protected function buildPathInfo($file)
|
||||
{
|
||||
$info = new GDriveStoragePathInfo();
|
||||
|
||||
if (! $file) {
|
||||
$info->exists = false;
|
||||
return $info;
|
||||
}
|
||||
|
||||
$props = $file->getProperties();
|
||||
|
||||
$info->exists = true;
|
||||
$info->id = $file->getId();
|
||||
$info->name = $file->getName();
|
||||
$info->mimeType = $file->getMimeType();
|
||||
$info->isDir = $info->mimeType === self::FOLDER_MIME_TYPE;
|
||||
$info->size = (int) $file->getSize();
|
||||
$info->webUrl = $file->getWebViewLink();
|
||||
$info->created = $file->getCreatedTime() ? strtotime($file->getCreatedTime()) : time();
|
||||
$info->modified = $file->getModifiedTime() ? strtotime($file->getModifiedTime()) : time();
|
||||
$info->md5Checksum = $file->getMd5Checksum();
|
||||
|
||||
if (isset($props['path'])) {
|
||||
// if we have the path in the properties, that's a path from the storage folder
|
||||
// so we remove the storage folder and the slash after that from the path
|
||||
$info->path = substr($props['path'], strlen($this->storagePath) + 1);
|
||||
} else {
|
||||
// if we don't have the path in the properties, we "assume" it's under the storage folder
|
||||
$info->path = $file->getName();
|
||||
}
|
||||
|
||||
$info->file = $file;
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcefully set a property on an object
|
||||
*
|
||||
* @param object $object The object to set the property on
|
||||
* @param string $property The property to set
|
||||
* @param mixed $value The value to set
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function forceSet($object, $property, $value)
|
||||
{
|
||||
if (!is_object($object)) {
|
||||
throw new Exception('Object must be an object');
|
||||
}
|
||||
if (!property_exists($object, $property)) {
|
||||
throw new Exception('Property ' . $property . ' does not exist on object ' . get_class($object));
|
||||
}
|
||||
$reflection = new \ReflectionProperty($object, $property);
|
||||
$reflection->setAccessible(true);
|
||||
$reflection->setValue($object, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [])
|
||||
{
|
||||
$client = $this->drive->getClient();
|
||||
$originalHttpClient = $client->getHttpClient();
|
||||
if ($timeout > 0) {
|
||||
$baseUri = $originalHttpClient->getConfig('base_uri');
|
||||
$client->setHttpClient(new \VendorDuplicator\GuzzleHttp\Client([
|
||||
'base_uri' => $baseUri,
|
||||
'http_errors' => \false,
|
||||
'timeout' => $timeout / 1000000, // convert microseconds to seconds
|
||||
]));
|
||||
}
|
||||
|
||||
if ($offset == 0 && $length < 0) {
|
||||
// If we are copying the entire file, we can use the export link
|
||||
$contents = $this->getFileContent($storageFile);
|
||||
if ($contents === false) {
|
||||
$client->setHttpClient($originalHttpClient);
|
||||
return false;
|
||||
}
|
||||
// This may return 0 if the file is empty, so we specifically check for false
|
||||
return file_put_contents($destFile, $contents);
|
||||
}
|
||||
|
||||
if (! isset($extraData['fileId'])) {
|
||||
// this is the first chunk, we need to get the file id & make sure the destination is writable & empty.
|
||||
if (file_put_contents($destFile, '') === false) {
|
||||
\DUP_PRO_Log::trace('[GDriveAdapter] Unable to write to destination file: ' . $destFile);
|
||||
$client->setHttpClient($originalHttpClient);
|
||||
return false;
|
||||
}
|
||||
if (! $this->exists($storageFile)) {
|
||||
$client->setHttpClient($originalHttpClient);
|
||||
return false;
|
||||
}
|
||||
$extraData['fileId'] = $this->getPathInfo($storageFile)->id;
|
||||
}
|
||||
$fileId = $extraData['fileId'];
|
||||
|
||||
try {
|
||||
$client->setDefer(true);
|
||||
/** @var RequestInterface $request */
|
||||
$request = $this->drive->files->get($fileId, [
|
||||
'alt' => 'media',
|
||||
'acknowledgeAbuse' => true,
|
||||
]);
|
||||
$client->setDefer(false);
|
||||
|
||||
$request = $request->withHeader('Range', 'bytes=' . $offset . '-' . ($length > 0 ? ($offset + $length - 1) : ''));
|
||||
|
||||
$response = $client->execute($request, null);
|
||||
$contents = $response->getBody()->getContents();
|
||||
|
||||
$client->setHttpClient($originalHttpClient);
|
||||
if (file_put_contents($destFile, $contents, FILE_APPEND) !== false) {
|
||||
return $length;
|
||||
}
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
$client->setHttpClient($originalHttpClient);
|
||||
\DUP_PRO_Log::trace('[GDriveAdapter] Unable to get file content: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,617 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\GDriveAddon\Models;
|
||||
|
||||
use DUP_PRO_Global_Entity;
|
||||
use DUP_PRO_Google_Drive_Transfer_Mode;
|
||||
use DUP_PRO_Log;
|
||||
use DUP_PRO_Package_Upload_Info;
|
||||
use DUP_PRO_Server;
|
||||
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\OAuth\TokenEntity;
|
||||
use Duplicator\Utils\OAuth\TokenService;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @property GDriveAdapter $adapter
|
||||
*/
|
||||
class GDriveStorage extends AbstractStorageEntity implements StorageAuthInterface
|
||||
{
|
||||
// These numbers represent clients created in Google Cloud Console
|
||||
const GDRIVE_CLIENT_NATIVE = 1; // Native client 1
|
||||
const GDRIVE_CLIENT_WEB0722 = 2; // Web client 07/2022
|
||||
const GDRIVE_CLIENT_LATEST = 2; // Latest out of these above
|
||||
|
||||
const REQUIRED_SCOPES = [
|
||||
"openid",
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
// The drive.file scope limits access to just those files created by the plugin
|
||||
"https://www.googleapis.com/auth/drive.file",
|
||||
];
|
||||
|
||||
/**
|
||||
* Get default config
|
||||
*
|
||||
* @return array<string,scalar>
|
||||
*/
|
||||
protected static function getDefaultConfig()
|
||||
{
|
||||
$config = parent::getDefaultConfig();
|
||||
$config = array_merge(
|
||||
$config,
|
||||
[
|
||||
'storage_folder_id' => '',
|
||||
'storage_folder_web_url' => '',
|
||||
'token_json' => '',
|
||||
'refresh_token' => '',
|
||||
'client_number' => -1,
|
||||
'authorized' => false,
|
||||
]
|
||||
);
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize
|
||||
*
|
||||
* Wakeup method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
parent::__wakeup();
|
||||
|
||||
if ($this->legacyEntity) {
|
||||
// Old storage entity
|
||||
$this->legacyEntity = false;
|
||||
// Make sure the storage type is right from the old entity
|
||||
$this->storage_type = $this->getSType();
|
||||
$this->config = [
|
||||
'token_json' => $this->gdrive_access_token_set_json,
|
||||
'refresh_token' => $this->gdrive_refresh_token,
|
||||
'storage_folder' => ltrim($this->gdrive_storage_folder, '/\\'),
|
||||
'client_number' => $this->gdrive_client_number,
|
||||
'max_packages' => $this->gdrive_max_files,
|
||||
'authorized' => ($this->gdrive_authorization_state == 1),
|
||||
];
|
||||
// reset old values
|
||||
$this->gdrive_access_token_set_json = '';
|
||||
$this->gdrive_refresh_token = '';
|
||||
$this->gdrive_storage_folder = '';
|
||||
$this->gdrive_client_number = -1;
|
||||
$this->gdrive_max_files = 10;
|
||||
$this->gdrive_authorization_state = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type icon.
|
||||
*
|
||||
* @return string Returns the storage icon
|
||||
*/
|
||||
public static function getStypeIcon()
|
||||
{
|
||||
$imgUrl = DUPLICATOR_PRO_IMG_URL . '/google-drive.svg';
|
||||
return '<img src="' . esc_url($imgUrl) . '" class="dup-storage-icon" alt="' . esc_attr(static::getStypeName()) . '" />';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('Google Drive', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
if ($this->isAuthorized()) {
|
||||
return $this->config['storage_folder_web_url'];
|
||||
} else {
|
||||
return __('Not Authenticated', 'duplicator-pro');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
return (SnapUtil::isCurlEnabled() || SnapUtil::isUrlFopenEnabled());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported notice, displayed if storage isn't supported
|
||||
*
|
||||
* @return string html string or empty if storage is supported
|
||||
*/
|
||||
public static function getNotSupportedNotice()
|
||||
{
|
||||
if (static::isSupported()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!SnapUtil::isCurlEnabled() && !SnapUtil::isUrlFopenEnabled()) {
|
||||
return esc_html__(
|
||||
'Google Drive requires either the PHP CURL extension enabled or the allow_url_fopen runtime configuration to be enabled.',
|
||||
'duplicator-pro'
|
||||
);
|
||||
} elseif (!SnapUtil::isCurlEnabled()) {
|
||||
return esc_html__('Google Drive requires the PHP CURL extension enabled.', 'duplicator-pro');
|
||||
} else {
|
||||
return esc_html__('Google Drive requires the allow_url_fopen runtime configuration to be enabled.', 'duplicator-pro');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk size in bytes
|
||||
*
|
||||
* @return int bytes
|
||||
*/
|
||||
public function getUploadChunkSize()
|
||||
{
|
||||
$dGlobal = DynamicGlobalEntity::getInstance();
|
||||
$chunkSizeKb = $dGlobal->getVal('gdrive_upload_chunksize_in_kb', 256);
|
||||
|
||||
return $chunkSizeKb * KB_IN_BYTES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upload chunk timeout in seconds
|
||||
*
|
||||
* @return int timeout in microseconds, 0 unlimited
|
||||
*/
|
||||
public function getUploadChunkTimeout()
|
||||
{
|
||||
// @todo: fixed to 10 seconds for historical reasons, make it configurable.
|
||||
return 10 * 1000000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an HTML anchor tag of location
|
||||
*
|
||||
* @return string Returns an HTML anchor tag with the storage location as a hyperlink.
|
||||
*/
|
||||
public function getHtmlLocationLink()
|
||||
{
|
||||
if (! $this->isAuthorized() || empty($this->config['storage_folder_web_url'])) {
|
||||
return '<span>' . esc_html($this->getStorageFolder()) . '</span>';
|
||||
}
|
||||
|
||||
return sprintf("<a href=\"%s\" target=\"_blank\">%s</a>", esc_url($this->config['storage_folder_web_url']), esc_html($this->getStorageFolder()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorized from HTTP request
|
||||
*
|
||||
* @param string $message Message
|
||||
*
|
||||
* @return bool True if authorized, false if failed
|
||||
*/
|
||||
public function authorizeFromRequest(&$message = '')
|
||||
{
|
||||
$tokenPairString = '';
|
||||
try {
|
||||
if (($refreshToken = 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();
|
||||
|
||||
$token = (new TokenEntity(static::getSType(), ['refresh_token' => $refreshToken]));
|
||||
if (!$token->refresh(true)) {
|
||||
throw new Exception(__('Failed to fetch information from Google Drive. Make sure the token is valid.', 'duplicator-pro'));
|
||||
}
|
||||
|
||||
if (empty($token->getScope())) {
|
||||
throw new Exception(__("Couldn't connect. Google Drive scopes not found.", 'duplicator-pro'));
|
||||
}
|
||||
|
||||
if (! $token->hasScopes(static::REQUIRED_SCOPES)) {
|
||||
throw new Exception(
|
||||
__(
|
||||
"Authorization failed. You did not allow all required permissions. Try again and make sure that you checked all checkboxes.",
|
||||
'duplicator-pro'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->config['refresh_token'] = $token->getRefreshToken();
|
||||
$this->config['token_json'] = wp_json_encode([
|
||||
'created' => $token->getCreated(),
|
||||
'access_token' => $token->getAccessToken(),
|
||||
'refresh_token' => $token->getRefreshToken(),
|
||||
'expires_in' => $token->getExpiresIn(),
|
||||
'scope' => $token->getScope(),
|
||||
]);
|
||||
$this->config['client_number'] = self::GDRIVE_CLIENT_LATEST;
|
||||
|
||||
$this->config['authorized'] = $token->isValid();
|
||||
} catch (Exception $e) {
|
||||
DUP_PRO_Log::traceException($e, "Problem authorizing Google Drive access token");
|
||||
DUP_PRO_Log::traceObject('Token pair string from authorization:', $tokenPairString);
|
||||
$message = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
$this->save();
|
||||
|
||||
$message = __('Google Drive 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 = __('Google Drive isn\'t authorized.', 'duplicator-pro');
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$client = $this->getAdapter()->getService()->getClient();
|
||||
|
||||
if (!empty($this->config['refresh_token'])) {
|
||||
$client->revokeToken($this->config['refresh_token']);
|
||||
}
|
||||
|
||||
$accessTokenObj = json_decode($this->config['token_json']);
|
||||
if (is_object($accessTokenObj) && property_exists($accessTokenObj, 'access_token')) {
|
||||
$gdrive_access_token = $accessTokenObj->access_token;
|
||||
} else {
|
||||
$gdrive_access_token = false;
|
||||
}
|
||||
|
||||
if (!empty($gdrive_access_token)) {
|
||||
$client->revokeToken($gdrive_access_token);
|
||||
}
|
||||
|
||||
$this->config['token_json'] = '';
|
||||
$this->config['refresh_token'] = '';
|
||||
$this->config['client_number'] = -1;
|
||||
$this->config['authorized'] = false;
|
||||
} catch (Exception $e) {
|
||||
DUP_PRO_Log::trace("Problem revoking Google Drive access token msg: " . $e->getMessage());
|
||||
$message = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
$message = __('Google Drive is disconnected successfully.', 'duplicator-pro');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorizationUrl()
|
||||
{
|
||||
return (new TokenService(static::getSType()))->getRedirectUri();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage adapter
|
||||
*
|
||||
* @return GDriveAdapter
|
||||
*/
|
||||
public function getAdapter()
|
||||
{
|
||||
$global = DUP_PRO_Global_Entity::getInstance();
|
||||
$token = $this->getTokenFromConfig();
|
||||
if (! $this->adapter) {
|
||||
if (! isset($this->config['storage_folder_id']) || empty($this->config['storage_folder_id'])) {
|
||||
$this->adapter = new GDriveAdapter(
|
||||
$token,
|
||||
$this->config['storage_folder'],
|
||||
'',
|
||||
!$global->ssl_disableverify,
|
||||
($global->ssl_useservercerts ? '' : DUPLICATOR_PRO_CERT_PATH),
|
||||
$global->ipv4_only
|
||||
);
|
||||
$this->adapter->initialize();
|
||||
$storageFolder = $this->adapter->getPathInfo('/');
|
||||
$this->config['storage_folder_id'] = $storageFolder->id;
|
||||
$this->config['storage_folder_web_url'] = $storageFolder->webUrl;
|
||||
$this->save();
|
||||
} else {
|
||||
$this->adapter = new GDriveAdapter(
|
||||
$token,
|
||||
$this->config['storage_folder'],
|
||||
$this->config['storage_folder_id'],
|
||||
!$global->ssl_disableverify,
|
||||
($global->ssl_useservercerts ? '' : DUPLICATOR_PRO_CERT_PATH),
|
||||
$global->ipv4_only
|
||||
);
|
||||
$this->adapter->initialize();
|
||||
}
|
||||
}
|
||||
$storageFolder = $this->adapter->getPathInfo('/');
|
||||
if ($storageFolder->name !== basename($this->getStorageFolder())) {
|
||||
// root folder id & storage folder name is different.
|
||||
$this->adapter = new GDriveAdapter(
|
||||
$token,
|
||||
$this->config['storage_folder'],
|
||||
'',
|
||||
!$global->ssl_disableverify,
|
||||
($global->ssl_useservercerts ? '' : DUPLICATOR_PRO_CERT_PATH),
|
||||
$global->ipv4_only
|
||||
);
|
||||
$this->adapter->initialize();
|
||||
$storageFolder = $this->adapter->getPathInfo('/');
|
||||
$this->config['storage_folder_id'] = $storageFolder->id;
|
||||
$this->config['storage_folder_web_url'] = $storageFolder->webUrl;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render form config fields
|
||||
*
|
||||
* @param bool $echo Echo or return
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderConfigFields($echo = true)
|
||||
{
|
||||
$userInfo = false;
|
||||
$quotaString = '';
|
||||
|
||||
if ($this->isAuthorized()) {
|
||||
$adapter = $this->getAdapter();
|
||||
try {
|
||||
$serviceDrive = $adapter->getService();
|
||||
$optParams = array('fields' => '*');
|
||||
$about = $serviceDrive->about->get($optParams);
|
||||
$storageQuota = $about->getStorageQuota();
|
||||
$quota_total = max($storageQuota->getLimit(), 1);
|
||||
$quota_used = $storageQuota->getUsage();
|
||||
$userInfo = $about->getUser();
|
||||
|
||||
if (is_numeric($quota_total) && is_numeric($quota_used)) {
|
||||
$available_quota = $quota_total - $quota_used;
|
||||
$used_perc = round($quota_used * 100 / $quota_total, 1);
|
||||
$quotaString = sprintf(
|
||||
__('%1$s%% used, %2$s available', 'duplicator-pro'),
|
||||
$used_perc,
|
||||
size_format($available_quota)
|
||||
);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
DUP_PRO_Log::info("Problem getting Google Drive user info and quota: " . $e->getMessage());
|
||||
$userInfo = $quotaString = null;
|
||||
}
|
||||
}
|
||||
|
||||
return TplMng::getInstance()->render(
|
||||
'gdriveaddon/configs/google_drive',
|
||||
[
|
||||
'storage' => $this,
|
||||
'storageFolder' => $this->config['storage_folder'],
|
||||
'maxPackages' => $this->config['max_packages'],
|
||||
'userInfo' => $userInfo,
|
||||
'quotaString' => $quotaString,
|
||||
],
|
||||
$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;
|
||||
}
|
||||
|
||||
$previousStorageFolder = $this->config['storage_folder'];
|
||||
$this->config['max_packages'] = SnapUtil::sanitizeIntInput(SnapUtil::INPUT_REQUEST, 'gdrive_max_files', 10);
|
||||
$this->config['storage_folder'] = self::getSanitizedInputFolder('_gdrive_storage_folder', 'remove');
|
||||
|
||||
if ($previousStorageFolder !== $this->config['storage_folder']) {
|
||||
$this->config['storage_folder_id'] = '';
|
||||
$this->config['storage_folder_web_url'] = '';
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
__('Google Drive Storage Updated.', 'duplicator-pro'),
|
||||
$this->config['server'],
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token entity from config
|
||||
*
|
||||
* @return TokenEntity
|
||||
*/
|
||||
protected function getTokenFromConfig()
|
||||
{
|
||||
$token = new TokenEntity(static::getSType(), $this->config['token_json']);
|
||||
if ($token->isAboutToExpire()) {
|
||||
try {
|
||||
$token->refresh(true);
|
||||
} catch (Exception $e) {
|
||||
DUP_PRO_Log::traceException($e, "Problem refreshing Google Drive access token");
|
||||
}
|
||||
$this->config['token_json'] = wp_json_encode([
|
||||
'created' => $token->getCreated(),
|
||||
'access_token' => $token->getAccessToken(),
|
||||
'refresh_token' => $token->getRefreshToken(),
|
||||
'expires_in' => $token->getExpiresIn(),
|
||||
'scope' => $token->getScope(),
|
||||
]);
|
||||
$this->save();
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 [
|
||||
'gdrive_upload_chunksize_in_kb' => 1024,
|
||||
'gdrive_transfer_mode' => DUP_PRO_Google_Drive_Transfer_Mode::Auto,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @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="gdrive_upload_chunksize_in_kb"
|
||||
id="gdrive_upload_chunksize_in_kb"
|
||||
type="number"
|
||||
min="256"
|
||||
step="256"
|
||||
data-parsley-required
|
||||
data-parsley-type="number"
|
||||
data-parsley-errors-container="#gdrive_upload_chunksize_in_kb_error_container"
|
||||
value="<?php echo (int) $values['gdrive_upload_chunksize_in_kb']; ?>"
|
||||
> <b>KB</b>
|
||||
<div id="gdrive_upload_chunksize_in_kb_error_container" class="duplicator-error-container"></div>
|
||||
<p class="description">
|
||||
<?php esc_html_e(
|
||||
'How much should be uploaded to Google Drive per attempt. Higher=faster but less reliable. It should be multiple of 256.',
|
||||
'duplicator-pro'
|
||||
); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr valign="top">
|
||||
<th scope="row"><label><?php esc_html_e("Transfer Mode", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
type="radio"
|
||||
value="<?php echo (int) DUP_PRO_Google_Drive_Transfer_Mode::Auto ?>"
|
||||
name="gdrive_transfer_mode" id="gdrive_transfer_mode_auto"
|
||||
<?php checked($values['gdrive_transfer_mode'], DUP_PRO_Google_Drive_Transfer_Mode::Auto); ?>
|
||||
>
|
||||
<label for="gdrive_transfer_mode_auto"><?php esc_html_e("Auto", 'duplicator-pro'); ?></label>
|
||||
|
||||
<input
|
||||
type="radio" <?php disabled(!DUP_PRO_Server::isURLFopenEnabled()) ?>
|
||||
value="<?php echo (int) DUP_PRO_Google_Drive_Transfer_Mode::FOpen_URL ?>"
|
||||
name="gdrive_transfer_mode"
|
||||
id="gdrive_transfer_mode_stream"
|
||||
<?php checked($values['gdrive_transfer_mode'], DUP_PRO_Google_Drive_Transfer_Mode::FOpen_URL); ?>
|
||||
>
|
||||
<label for="gdrive_transfer_mode_stream"><?php esc_html_e("FOpen URL", 'duplicator-pro'); ?></label>
|
||||
<?php if (!DUP_PRO_Server::isURLFopenEnabled()) : ?>
|
||||
<i
|
||||
class="fas fa-question-circle fa-sm"
|
||||
data-tooltip-title="<?php esc_attr_e("FOpen URL", 'duplicator-pro'); ?>"
|
||||
data-tooltip="<?php esc_attr_e('Not available because "allow_url_fopen" is turned off in the php.ini', 'duplicator-pro'); ?>">
|
||||
</i>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\GDriveAddon\Models;
|
||||
|
||||
use Duplicator\Models\Storages\StoragePathInfo;
|
||||
use VendorDuplicator\Google\Service\Drive\DriveFile;
|
||||
|
||||
class GDriveStoragePathInfo extends StoragePathInfo
|
||||
{
|
||||
/** @var string */
|
||||
public $id = '';
|
||||
|
||||
/** @var string */
|
||||
public $name = '';
|
||||
|
||||
/** @var string */
|
||||
public $mimeType = '';
|
||||
|
||||
/** @var string */
|
||||
public $webUrl = '';
|
||||
|
||||
/** @var string */
|
||||
public $md5Checksum = '';
|
||||
|
||||
/** @var DriveFile */
|
||||
public $file;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Duplicator\Addons\GDriveAddon\Utils;
|
||||
|
||||
use Duplicator\Addons\GDriveAddon\GDriveAddon;
|
||||
use Duplicator\Utils\AbstractAutoloader;
|
||||
|
||||
class Autoloader extends AbstractAutoloader
|
||||
{
|
||||
const VENDOR_PATH = GDriveAddon::ADDON_PATH . '/vendor-prefixed/';
|
||||
/**
|
||||
* Register autoloader function
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register()
|
||||
{
|
||||
spl_autoload_register([__CLASS__, 'load']);
|
||||
|
||||
require_once GDriveAddon::ADDON_PATH . '/vendor-prefixed/guzzlehttp/guzzle/src/functions_include.php';
|
||||
require_once GDriveAddon::ADDON_PATH . '/vendor-prefixed/guzzlehttp/promises/src/functions_include.php';
|
||||
require_once GDriveAddon::ADDON_PATH . '/vendor-prefixed/guzzlehttp/psr7/src/functions_include.php';
|
||||
require_once GDriveAddon::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 . 'Firebase\\JWT' => self::VENDOR_PATH . 'firebase/php-jwt/src/',
|
||||
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 . 'Monolog' => self::VENDOR_PATH . 'monolog/monolog/src/Monolog',
|
||||
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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Duplicator\Addons\GDriveAddon\Utils;
|
||||
|
||||
use VendorDuplicator\Google\Client;
|
||||
use VendorDuplicator\GuzzleHttp\Client as GuzzleClient;
|
||||
|
||||
class GoogleClient extends Client
|
||||
{
|
||||
/** @var array<string,mixed> */
|
||||
protected $customHttpOptions = [];
|
||||
|
||||
/**
|
||||
* Set http client options
|
||||
*
|
||||
* @param array<string,mixed> $options options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setHttpClientOptions(array $options)
|
||||
{
|
||||
$this->customHttpOptions = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new http client.
|
||||
*
|
||||
* @return GuzzleClient
|
||||
*/
|
||||
protected function createDefaultHttpClient()
|
||||
{
|
||||
$options = [
|
||||
'base_uri' => $this->getConfig('base_path'),
|
||||
'http_errors' => false,
|
||||
];
|
||||
$options = array_merge($options, $this->customHttpOptions);
|
||||
$guzzleClient = new GuzzleClient($options);
|
||||
|
||||
return $guzzleClient;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Duplicator messages sections
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\GDriveAddon\Models\GDriveStorage;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var \Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
* @var GDriveStorage $storage
|
||||
*/
|
||||
$storage = $tplData["storage"];
|
||||
/** @var string */
|
||||
$storageFolder = $tplData["storageFolder"];
|
||||
/** @var int */
|
||||
$maxPackages = $tplData["maxPackages"];
|
||||
/** @var \VendorDuplicator\Google\Service\Drive\User $userInfo */
|
||||
$userInfo = $tplData["userInfo"];
|
||||
/** @var string */
|
||||
$quotaString = $tplData["quotaString"];
|
||||
|
||||
$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="gdrive-authorize">
|
||||
<?php if (!$storage->isAuthorized()) : ?>
|
||||
<div class='gdrive-authorization-state' id="gdrive-state-unauthorized">
|
||||
<!-- CONNECT -->
|
||||
<div id="dpro-gdrive-connect-btn-area">
|
||||
<button
|
||||
id="dpro-gdrive-connect-btn"
|
||||
type="button"
|
||||
class="button button-large"
|
||||
onclick="DupPro.Storage.GDrive.GoogleGetAuthUrl();"
|
||||
>
|
||||
<i class="fa fa-plug"></i> <?php esc_html_e('Connect to Google Drive', 'duplicator-pro'); ?>
|
||||
<img
|
||||
src="<?php echo esc_url(DUPLICATOR_PRO_IMG_URL . '/google-drive.svg'); ?>"
|
||||
style='vertical-align: middle; margin:-2px 0 0 3px; height:18px; width:18px'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="authorization-state" id="dpro-gdrive-connect-progress">
|
||||
<div style="padding:10px">
|
||||
<i class="fas fa-circle-notch fa-spin"></i> <?php esc_html_e('Getting Google Drive Request Token...', 'duplicator-pro'); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEPS -->
|
||||
<div id="dpro-gdrive-steps">
|
||||
<div>
|
||||
<b><?php esc_html_e('Step 1:', 'duplicator-pro'); ?></b>
|
||||
<?php
|
||||
esc_html_e(
|
||||
"Duplicator needs to authorize Google Drive. Make sure to allow all required permissions.",
|
||||
'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'
|
||||
) . ' google_drive.php' .
|
||||
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="gdrive-auth-window-button" class="button" onclick="DupPro.Storage.GDrive.OpenAuthPage(); return false;">
|
||||
<i class="fa fa-user"></i> <?php esc_html_e("Authorize Google Drive", 'duplicator-pro'); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="gdrive-auth-code-area">
|
||||
<b><?php esc_html_e('Step 2:', 'duplicator-pro'); ?></b>
|
||||
<?php esc_html_e("Paste code from Google authorization page.", 'duplicator-pro'); ?> <br/>
|
||||
<input style="width:400px" id="gdrive-auth-code" name="gdrive-auth-code" />
|
||||
</div>
|
||||
|
||||
<b><?php esc_html_e('Step 3:', 'duplicator-pro'); ?></b>
|
||||
<?php esc_html_e('Finalize Google Drive setup by clicking the "Finalize Setup" button.', 'duplicator-pro') ?>
|
||||
<br/>
|
||||
<button
|
||||
id="gdrive-finalize-setup"
|
||||
type="button"
|
||||
class="button"
|
||||
>
|
||||
<i class="fa fa-check-square"></i> <?php esc_html_e('Finalize Setup', 'duplicator-pro'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class='gdrive-authorization-state' id="gdrive-state-authorized" style="margin-top:-10px">
|
||||
<?php if ($userInfo != null) : ?>
|
||||
<h3>
|
||||
<?php esc_html_e('Google Drive Account', 'duplicator-pro'); ?><br/>
|
||||
<i class="dpro-edit-info">
|
||||
<?php esc_html_e('Duplicator has been authorized to access this user\'s Google Drive account', 'duplicator-pro'); ?>
|
||||
</i>
|
||||
</h3>
|
||||
<div id="gdrive-account-info">
|
||||
<label><?php esc_html_e('Name', 'duplicator-pro'); ?>:</label>
|
||||
<?php echo esc_html($userInfo->getDisplayName()); ?><br/>
|
||||
|
||||
<label><?php esc_html_e('Email', 'duplicator-pro'); ?>:</label> <?php echo esc_html($userInfo->getEmailAddress()); ?>
|
||||
<?php if (strlen($quotaString) > 0) { ?>
|
||||
<br>
|
||||
<label><?php esc_html_e('Quota', 'duplicator-pro'); ?>:</label> <?php echo esc_html($quotaString); ?>
|
||||
<?php } ?>
|
||||
</div><br/>
|
||||
<?php else : ?>
|
||||
<div><?php esc_html_e('Error retrieving user information.', 'duplicator-pro'); ?></div>
|
||||
<?php endif ?>
|
||||
|
||||
<button
|
||||
id="dup-gdrive-cancel-authorization"
|
||||
type="button"
|
||||
class="button"
|
||||
>
|
||||
<?php esc_html_e('Cancel Authorization', 'duplicator-pro'); ?>
|
||||
</button><br/>
|
||||
<i class="dpro-edit-info">
|
||||
<?php
|
||||
esc_html_e(
|
||||
'Disassociates storage provider with the Google Drive account. Will require re-authorization.',
|
||||
'duplicator-pro'
|
||||
); ?>
|
||||
</i>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="_gdrive_storage_folder">
|
||||
<?php esc_html_e("Storage Folder", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<b>//Google Drive/</b>
|
||||
<input
|
||||
id="_gdrive_storage_folder"
|
||||
name="_gdrive_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>
|
||||
<?php
|
||||
$tipContent = __(
|
||||
'If the directory path above is already in Google Drive before connecting then a duplicate folder name will be made in the same path.',
|
||||
'duplicator-pro'
|
||||
) . ' google_drive.php' . __('This is because the plugin only has rights to folders it creates.', 'duplicator-pro');
|
||||
?>
|
||||
<i
|
||||
class="fas fa-question-circle fa-sm"
|
||||
data-tooltip-title="<?php esc_attr_e("Storage Folder Notice", 'duplicator-pro'); ?>"
|
||||
data-tooltip="<?php esc_attr($tipContent); ?>"
|
||||
>
|
||||
</i>
|
||||
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for=""><?php esc_html_e("Max Packages", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<label for="gdrive_max_files">
|
||||
<input
|
||||
id="gdrive_max_files"
|
||||
name="gdrive_max_files"
|
||||
type="number"
|
||||
value="<?php echo (int) $maxPackages; ?>"
|
||||
min="0"
|
||||
maxlength="4"
|
||||
data-parsley-errors-container="#gdrive_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="gdrive_max_files_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
$tplMng->render('admin_pages/storages/parts/provider_foot');
|
||||
|
||||
// Alerts for Google Drive
|
||||
$alertConnStatus = new DUP_PRO_UI_Dialog();
|
||||
$alertConnStatus->title = __('Google Drive Authorization Error', 'duplicator-pro');
|
||||
$alertConnStatus->message = ''; // javascript inserted message
|
||||
$alertConnStatus->initAlert();
|
||||
|
||||
?>
|
||||
<script>
|
||||
jQuery(document).ready(function ($) {
|
||||
DupPro.Storage.GDrive = DupPro.Storage.GDrive || {};
|
||||
|
||||
$('#dup-gdrive-cancel-authorization').click(function (event) {
|
||||
event.stopPropagation();
|
||||
DupPro.Storage.RevokeAuth(<?php echo (int) $storage->getId(); ?>);
|
||||
});
|
||||
|
||||
DupPro.Storage.GDrive.GoogleGetAuthUrl = function ()
|
||||
{
|
||||
$('#dpro-gdrive-connect-btn-area').hide();
|
||||
$('#dpro-gdrive-steps').show();
|
||||
DupPro.Storage.GDrive.AuthUrl = <?php echo json_encode($storage->getAuthorizationUrl()); ?>;
|
||||
}
|
||||
|
||||
DupPro.Storage.GDrive.OpenAuthPage = function ()
|
||||
{
|
||||
window.open(DupPro.Storage.GDrive.AuthUrl, '_blank');
|
||||
}
|
||||
|
||||
$('#gdrive-finalize-setup').click(function (event) {
|
||||
event.stopPropagation();
|
||||
|
||||
if ($('#gdrive-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': $('#_gdrive_storage_folder').val(),
|
||||
'max_packages': $('#gdrive_max_files').val(),
|
||||
'auth_code' : $('#gdrive-auth-code').val()
|
||||
}
|
||||
);
|
||||
} else {
|
||||
<?php $alertConnStatus->showAlert(); ?>
|
||||
let alertMsg = "<i class='fas fa-exclamation-triangle'></i> " +
|
||||
"<?php esc_html_e('Please enter your Google Drive authorization code!', 'duplicator-pro'); ?>";
|
||||
<?php $alertConnStatus->updateMessage("alertMsg"); ?>
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
// silent
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Duplicator\Addons\OneDriveAddon;
|
||||
|
||||
use Duplicator\Addons\OneDriveAddon\Models\OneDriveStorage;
|
||||
use Duplicator\Core\Addons\AbstractAddonCore;
|
||||
use Duplicator\Models\Storages\AbstractStorageEntity;
|
||||
|
||||
class OneDriveAddon extends AbstractAddonCore
|
||||
{
|
||||
const ADDON_PATH = __DIR__;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register storages
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function registerStorages()
|
||||
{
|
||||
OneDriveStorage::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, 'onedriveaddon/') === 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_onedrive_count'] = 0;
|
||||
|
||||
foreach ($storages as $storage) {
|
||||
if ($storage->getSType() === OneDriveStorage::getSType()) {
|
||||
$storageNums['storages_onedrive_count']++;
|
||||
}
|
||||
}
|
||||
|
||||
return $storageNums;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonPath()
|
||||
{
|
||||
return static::ADDON_PATH;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonFile()
|
||||
{
|
||||
return __FILE__;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace Duplicator\Addons\OneDriveAddon;
|
||||
|
||||
use DUP_PRO_Log;
|
||||
use Duplicator\Libs\Snap\SnapLog;
|
||||
|
||||
class HttpClient
|
||||
{
|
||||
/** @var string Base URL */
|
||||
protected $baseUrl = '';
|
||||
/** @var string Access token */
|
||||
protected $accessToken = '';
|
||||
/** @var int Timeout in seconds */
|
||||
protected $timeout = 1000;
|
||||
/** @var string Path to the certificate */
|
||||
protected $sslCert = '';
|
||||
/** @var bool Should verify the SSL certificate */
|
||||
protected $sslVerify = true;
|
||||
/** @var array<string,string> Default headers */
|
||||
protected $headers = [];
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param string $baseUrl Base URL
|
||||
* @param string $accesToken Access token
|
||||
* @param bool $sslVerify If true, use SSL
|
||||
* @param string $sslCert If empty use server cert
|
||||
* @param int $timeout Timeout in seconds
|
||||
*/
|
||||
public function __construct($baseUrl, $accesToken, $sslVerify = true, $sslCert = '', $timeout = 1000)
|
||||
{
|
||||
$this->baseUrl = $baseUrl;
|
||||
$this->accessToken = $accesToken;
|
||||
$this->timeout = $timeout;
|
||||
$this->sslCert = $sslCert;
|
||||
$this->sslVerify = $sslVerify;
|
||||
|
||||
$this->headers['Authorization'] = 'Bearer ' . $this->accessToken;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $url The URL to request
|
||||
* @param array<string, scalar> $data The data to send
|
||||
* @param array<string, string> $headers The headers to send
|
||||
*
|
||||
* @return array{headers: array<string, string>, body: string, code: int}
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function get($url, $data = [], $headers = [])
|
||||
{
|
||||
return $this->request('GET', $url, $data, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url The URL to request
|
||||
* @param array<string, mixed>|string $data The data to send
|
||||
* @param array<string, string> $headers The headers to send
|
||||
*
|
||||
* @return array{headers: array<string, string>, body: string, code: int}
|
||||
*/
|
||||
public function post($url, $data = [], $headers = [])
|
||||
{
|
||||
return $this->request('POST', $url, $data, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url The URL to request
|
||||
* @param array<string, mixed>|string $data The data to send
|
||||
* @param array<string, string> $headers The headers to send
|
||||
*
|
||||
* @return array{headers: array<string, string>, body: string, code: int}
|
||||
*/
|
||||
public function put($url, $data = [], $headers = [])
|
||||
{
|
||||
return $this->request('PUT', $url, $data, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url The URL to request
|
||||
* @param array<string, string> $headers The headers to send
|
||||
*
|
||||
* @return array{headers: array<string, string>, body: string, code: int}
|
||||
*/
|
||||
public function delete($url, $headers = [])
|
||||
{
|
||||
return $this->request('DELETE', $url, [], $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a HTTP request
|
||||
*
|
||||
* @param string $method The request verb
|
||||
* @param string $url The URL to request
|
||||
* @param array<string, mixed>|string $data The data to send, arrays will be json encoded, strings will be sent as is
|
||||
* @param array<string, string> $headers The headers to send
|
||||
* @param array<int, scalar> $overrideOptions Override the curl options
|
||||
*
|
||||
* @return array{headers: array<string, string>, body: string, code: int}
|
||||
*/
|
||||
public function request($method, $url, $data = [], $headers = [], $overrideOptions = [])
|
||||
{
|
||||
$headers = array_merge($this->headers, $headers);
|
||||
if (!empty($this->baseUrl) && strpos($url, 'http') !== 0) {
|
||||
$url = rtrim($this->baseUrl, '/') . '/' . ltrim($url, '/');
|
||||
}
|
||||
|
||||
$curl = curl_init();
|
||||
|
||||
if (!$curl) {
|
||||
throw new \Exception('Could not initialize remote request using curl.');
|
||||
}
|
||||
|
||||
if ($method === 'GET' && !empty($data)) {
|
||||
$url .= '?' . http_build_query($data);
|
||||
$data = [];
|
||||
}
|
||||
|
||||
$options = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_AUTOREFERER => true,
|
||||
CURLOPT_URL => $url,
|
||||
|
||||
CURLOPT_TIMEOUT => $this->timeout,
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => ($this->sslVerify ? 2 : false),
|
||||
CURLOPT_SSL_VERIFYPEER => $this->sslVerify,
|
||||
];
|
||||
|
||||
if (!empty($this->sslCert)) {
|
||||
$options[CURLOPT_CAINFO] = $this->sslCert;
|
||||
}
|
||||
|
||||
// We are sending a json payload
|
||||
if (!empty($data) && is_array($data)) {
|
||||
$options[CURLOPT_POSTFIELDS] = json_encode($data);
|
||||
$headers['Content-Type'] = 'application/json';
|
||||
} elseif (!empty($data) && is_string($data)) {
|
||||
// We are sending a string payload
|
||||
$options[CURLOPT_POST] = true;
|
||||
$options[CURLOPT_POSTFIELDS] = $data;
|
||||
}
|
||||
|
||||
// Format the headers for curl and set the option
|
||||
$options[CURLOPT_HTTPHEADER] = $this->formatRequestHeaders($headers);
|
||||
|
||||
// Override the options if needed
|
||||
if (!empty($overrideOptions)) {
|
||||
$options = array_merge($options, $overrideOptions);
|
||||
}
|
||||
|
||||
// Set the options
|
||||
if (!curl_setopt_array($curl, $options)) {
|
||||
// curl will return immediately if it fails to set one of the options
|
||||
throw new \Exception('Could not set curl options.');
|
||||
}
|
||||
|
||||
$response = curl_exec($curl);
|
||||
|
||||
// check for any error
|
||||
$error = curl_error($curl);
|
||||
|
||||
if ($error || $response === false) {
|
||||
throw new \Exception("Curl error: {$error}");
|
||||
}
|
||||
|
||||
// Get the header size and the http code
|
||||
$headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
|
||||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
|
||||
// Get the header and the body
|
||||
$header = substr($response, 0, $headerSize);
|
||||
$body = substr($response, $headerSize);
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
return [
|
||||
'headers' => $this->formatResponseHeaders($header),
|
||||
'body' => $body,
|
||||
'code' => (int) $httpCode,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the headers for curl
|
||||
*
|
||||
* @param array<string, string> $headers The headers to format
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function formatRequestHeaders($headers)
|
||||
{
|
||||
return array_map(function ($key, $value) {
|
||||
return $key . ': ' . $value;
|
||||
}, array_keys($headers), $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the response headers
|
||||
*
|
||||
* @param string $headers The headers to format
|
||||
*
|
||||
* @return false|string[]
|
||||
*/
|
||||
protected function formatResponseHeaders($headers)
|
||||
{
|
||||
$headers = explode("\r\n", $headers);
|
||||
foreach ($headers as $index => $item) {
|
||||
$item = explode(': ', $item);
|
||||
if (count($item) === 2) {
|
||||
$headers[trim($item[0])] = trim($item[1]);
|
||||
}
|
||||
unset($headers[$index]);
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\OneDriveAddon\Models;
|
||||
|
||||
use DUP_PRO_Global_Entity;
|
||||
use DUP_PRO_Log;
|
||||
use Duplicator\Addons\OneDriveAddon\OnedriveAdapter;
|
||||
use Duplicator\Addons\OneDriveAddon\OneDriveStoragePathInfo;
|
||||
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\OAuth\TokenEntity;
|
||||
use Duplicator\Utils\OAuth\TokenService;
|
||||
use Exception;
|
||||
|
||||
class OneDriveStorage extends AbstractStorageEntity implements StorageAuthInterface
|
||||
{
|
||||
const GRAPH_SCOPES = [
|
||||
'openid',
|
||||
'offline_access',
|
||||
'files.readwrite.appfolder',
|
||||
];
|
||||
|
||||
const CLIENT_ID = '15fa3a0d-b7ee-447c-8093-7bfcf30b0797';
|
||||
|
||||
const LOGOUT_REDIRECT_URI = 'https://snapcreek.com/misc/onedrive/redir3.php';
|
||||
|
||||
/**
|
||||
* @var null|OneDriveAdapter Storage adapter
|
||||
*/
|
||||
protected $adapter = null;
|
||||
|
||||
/**
|
||||
* Get default config
|
||||
*
|
||||
* @return array<string,scalar>
|
||||
*/
|
||||
protected static function getDefaultConfig()
|
||||
{
|
||||
$config = parent::getDefaultConfig();
|
||||
$config = array_merge(
|
||||
$config,
|
||||
[
|
||||
'endpoint_url' => '',
|
||||
'resource_id' => '',
|
||||
'access_token' => '',
|
||||
'refresh_token' => '',
|
||||
'token_obtained' => 0,
|
||||
'storage_folder_id' => '',
|
||||
'storage_folder_web_url' => '',
|
||||
'all_folders_perm' => false,
|
||||
'authorized' => false,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize
|
||||
*
|
||||
* Wakeup method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
parent::__wakeup();
|
||||
|
||||
if ($this->legacyEntity) {
|
||||
// Old storage entity
|
||||
$this->legacyEntity = false;
|
||||
// Make sure the storage type is right from the old entity
|
||||
$this->storage_type = $this->getSType();
|
||||
$this->config = [
|
||||
'endpoint_url' => $this->onedrive_endpoint_url,
|
||||
'resource_id' => $this->onedrive_resource_id,
|
||||
'access_token' => $this->onedrive_access_token,
|
||||
'refresh_token' => $this->onedrive_refresh_token,
|
||||
'token_obtained' => $this->onedrive_token_obtained,
|
||||
'storage_folder' => ltrim($this->onedrive_storage_folder, '/\\'),
|
||||
'max_packages' => $this->onedrive_max_files,
|
||||
'storage_folder_id' => $this->onedrive_storage_folder_id,
|
||||
'storage_folder_web_url' => $this->onedrive_storage_folder_web_url,
|
||||
'authorized' => ($this->onedrive_authorization_state == 1),
|
||||
];
|
||||
// reset old values
|
||||
$this->onedrive_endpoint_url = '';
|
||||
$this->onedrive_resource_id = '';
|
||||
$this->onedrive_access_token = '';
|
||||
$this->onedrive_refresh_token = '';
|
||||
$this->onedrive_token_obtained = 0;
|
||||
$this->onedrive_storage_folder = '';
|
||||
$this->onedrive_max_files = 10;
|
||||
$this->onedrive_storage_folder_id = '';
|
||||
$this->onedrive_authorization_state = 0;
|
||||
$this->onedrive_storage_folder_web_url = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be called, automatically, when Serialize
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function __serialize() // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__serializeFound
|
||||
{
|
||||
$data = parent::__serialize();
|
||||
unset($data['client']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the storage type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getSType()
|
||||
{
|
||||
return 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type icon.
|
||||
*
|
||||
* @return string Returns the storage icon
|
||||
*/
|
||||
public static function getStypeIcon()
|
||||
{
|
||||
$imgUrl = DUPLICATOR_PRO_IMG_URL . '/onedrive.svg';
|
||||
return '<img src="' . esc_url($imgUrl) . '" class="dup-storage-icon" alt="' . esc_attr(static::getStypeName()) . '" />';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the storage type name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getStypeName()
|
||||
{
|
||||
return __('OneDrive', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage location string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocationString()
|
||||
{
|
||||
if (!$this->isAuthorized()) {
|
||||
return __("Not Authenticated", "duplicator-pro");
|
||||
} else {
|
||||
return $this->config['storage_folder_web_url'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an html anchor tag of location
|
||||
*
|
||||
* @return string Returns an html anchor tag with the storage location as a hyperlink.
|
||||
*
|
||||
* @example
|
||||
* OneDrive Example return
|
||||
* <a target="_blank" href="https://1drv.ms/f/sAFrQtasdrewasyghg">https://1drv.ms/f/sAFrQtasdrewasyghg</a>
|
||||
*/
|
||||
public function getHtmlLocationLink()
|
||||
{
|
||||
if ($this->isAuthorized()) {
|
||||
return '<a href="' . esc_url($this->getLocationString()) . '" target="_blank" >' . esc_html($this->getLocationString()) . '</a>';
|
||||
} else {
|
||||
return $this->getLocationString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
return SnapUtil::isCurlEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported notice, displayed if storage isn't supported
|
||||
*
|
||||
* @return string html string or empty if storage is supported
|
||||
*/
|
||||
public static function getNotSupportedNotice()
|
||||
{
|
||||
if (static::isSupported()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return esc_html__('OneDrive requires the PHP CURL extension enabled.', 'duplicator-pro');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chunk size bytes
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUploadChunkSize()
|
||||
{
|
||||
return DynamicGlobalEntity::getInstance()->getVal('onedrive_upload_chunksize_in_kb') * 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorized from HTTP request
|
||||
*
|
||||
* @param string $message Message
|
||||
*
|
||||
* @return bool True if authorized, false if failed
|
||||
*/
|
||||
public function authorizeFromRequest(&$message = '')
|
||||
{
|
||||
try {
|
||||
if (($refreshToken = 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();
|
||||
|
||||
$token = (new TokenEntity(self::getSType(), ['refresh_token' => $refreshToken]));
|
||||
if (! $token->refresh(true)) {
|
||||
throw new Exception(__('Failed to fetch information from OneDrive. Make sure the token is valid.', 'duplicator-pro'));
|
||||
}
|
||||
|
||||
$this->config['access_token'] = $token->getAccessToken();
|
||||
$this->config['refresh_token'] = $token->getRefreshToken();
|
||||
$this->config['token_obtained'] = $token->getCreated();
|
||||
$this->config['endpoint_url'] = $this->config['resource_id'] = '';
|
||||
$this->config['authorized'] = true;
|
||||
|
||||
|
||||
DUP_PRO_Log::traceObject("OneDrive App folder: ", $storageFolder = $this->getOneDriveStorageFolder());
|
||||
|
||||
if (! $storageFolder) {
|
||||
throw new Exception("Failed to fetch information from OneDrive. Make sure the token is valid.");
|
||||
}
|
||||
|
||||
// Get the storage folder id
|
||||
$this->config['storage_folder_id'] = $storageFolder->id;
|
||||
$this->config['storage_folder_web_url'] = $storageFolder->webUrl;
|
||||
} catch (Exception $e) {
|
||||
DUP_PRO_Log::trace("Problem authorizing OneDrive access token msg: " . $e->getMessage());
|
||||
$message = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
$message = __('OneDrive 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 = __('Onedrive isn\'t authorized.', 'duplicator-pro');
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->config['endpoint_url'] = '';
|
||||
$this->config['resource_id'] = '';
|
||||
$this->config['access_token'] = '';
|
||||
$this->config['refresh_token'] = '';
|
||||
$this->config['token_obtained'] = 0;
|
||||
$this->config['storage_folder_id'] = '';
|
||||
$this->config['storage_folder_web_url'] = '';
|
||||
$this->config['authorized'] = false;
|
||||
|
||||
$message = __('Onedrive is disconnected successfully.', 'duplicator-pro');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external revoke url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getExternalRevokeUrl()
|
||||
{
|
||||
$base_url = "https://login.microsoftonline.com/common/oauth2/v2.0/logout";
|
||||
$fields_arr = [
|
||||
"client_id" => self::CLIENT_ID,
|
||||
"post_logout_redirect_uri" => self::LOGOUT_REDIRECT_URI,
|
||||
];
|
||||
$query = http_build_query($fields_arr);
|
||||
|
||||
return $base_url . "?$query";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorizationUrl()
|
||||
{
|
||||
return (new TokenService(static::getSType()))->getRedirectUri();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 %1$s folder:<br/> <i>%2$s</i>', "duplicator-pro"),
|
||||
$this->getStypeName(),
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'pending':
|
||||
return sprintf(
|
||||
__('Transfer to %1$s folder %2$s is pending', "duplicator-pro"),
|
||||
$this->getStypeName(),
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'failed':
|
||||
return sprintf(
|
||||
__('Failed to transfer to %1$s folder %2$s', "duplicator-pro"),
|
||||
$this->getStypeName(),
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'cancelled':
|
||||
return sprintf(
|
||||
__('Cancelled before could transfer to %1$s folder %2$s', "duplicator-pro"),
|
||||
$this->getStypeName(),
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
case 'success':
|
||||
return sprintf(
|
||||
__('Transferred package to %1$s folder %2$s', "duplicator-pro"),
|
||||
$this->getStypeName(),
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
default:
|
||||
throw new Exception('Invalid key');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all permissions
|
||||
*
|
||||
* @param bool $allFoldersPerm All folders permission
|
||||
*
|
||||
* @return bool True if success, false otherwise
|
||||
*/
|
||||
public function setAllPermissions($allFoldersPerm)
|
||||
{
|
||||
$this->config['all_folders_perm'] = $allFoldersPerm;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render form config fields
|
||||
*
|
||||
* @param bool $echo Echo or return
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderConfigFields($echo = true)
|
||||
{
|
||||
$hasError = $this->isAuthorized() && !$this->getAdapter()->isValid();
|
||||
|
||||
return TplMng::getInstance()->render(
|
||||
'onedriveaddon/configs/onedrive',
|
||||
[
|
||||
'storage' => $this,
|
||||
'storageFolder' => $this->config['storage_folder'],
|
||||
'maxPackages' => $this->config['max_packages'],
|
||||
'allFolderPers' => $this->config['all_folders_perm'],
|
||||
'accountInfo' => $this->getAccountInfo(),
|
||||
'hasError' => $hasError,
|
||||
'externalRevokeUrl' => $this->getExternalRevokeUrl(),
|
||||
],
|
||||
$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, 'onedrive_msgraph_max_files', 10);
|
||||
$oldFolder = $this->config['storage_folder'];
|
||||
$this->config['storage_folder'] = self::getSanitizedInputFolder('_onedrive_msgraph_storage_folder', 'remove');
|
||||
|
||||
if ($this->isAuthorized() && $oldFolder != $this->config['storage_folder']) {
|
||||
$this->config['storage_folder_id'] = '';
|
||||
// Create new folder
|
||||
$folder = $this->getOneDriveStorageFolder();
|
||||
$this->config['storage_folder_id'] = $folder->id;
|
||||
$this->config['storage_folder_web_url'] = $folder->webUrl;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
__('OneDrive Storage Updated.', 'duplicator-pro'),
|
||||
$this->getStorageFolder()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account info
|
||||
*
|
||||
* @return false|object
|
||||
*/
|
||||
protected function getAccountInfo()
|
||||
{
|
||||
if (! $this->isAuthorized()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$storageFolder = $this->getOneDriveStorageFolder();
|
||||
|
||||
if (!$storageFolder || empty($storageFolder->user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (object) $storageFolder->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get onedrive storage folder
|
||||
*
|
||||
* @return OneDriveStoragePathInfo|false
|
||||
*/
|
||||
protected function getOneDriveStorageFolder()
|
||||
{
|
||||
$adapter = $this->getAdapter();
|
||||
|
||||
if (! $adapter->isValid()) {
|
||||
DUP_PRO_Log::trace("OneDrive adapter is not valid, can't get storage folder.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->config['storage_folder_id']) {
|
||||
if (! $adapter->initialize($error)) {
|
||||
DUP_PRO_Log::trace("Failed to initialize OneDrive adapter: $error");
|
||||
return false;
|
||||
}
|
||||
$folder = $adapter->getPathInfo('/');
|
||||
$this->config['storage_folder_id'] = $folder->id;
|
||||
$this->save();
|
||||
} else {
|
||||
$folder = $adapter->getPathInfo('/');
|
||||
}
|
||||
|
||||
return $folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stoage adapter
|
||||
*
|
||||
* @return OnedriveAdapter
|
||||
*/
|
||||
protected function getAdapter()
|
||||
{
|
||||
if (!$this->adapter) {
|
||||
$global = DUP_PRO_Global_Entity::getInstance();
|
||||
$token = $this->getTokenFromConfig();
|
||||
$this->adapter = new OneDriveAdapter(
|
||||
$token,
|
||||
$this->config['storage_folder'],
|
||||
$this->config['storage_folder_id'],
|
||||
!$global->ssl_disableverify,
|
||||
($global->ssl_useservercerts ? '' : DUPLICATOR_PRO_CERT_PATH)
|
||||
);
|
||||
if (! $this->adapter->initialize($error)) {
|
||||
DUP_PRO_Log::trace("Failed to initialize OneDrive adapter: $error");
|
||||
}
|
||||
}
|
||||
return $this->adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token entity using config values
|
||||
*
|
||||
* @return TokenEntity|false
|
||||
*/
|
||||
public function getTokenFromConfig()
|
||||
{
|
||||
$token = new TokenEntity(self::getSType(), [
|
||||
'created' => $this->config['token_obtained'],
|
||||
'expires_in' => 3600,
|
||||
'scope' => self::GRAPH_SCOPES,
|
||||
'access_token' => $this->config['access_token'],
|
||||
'refresh_token' => $this->config['refresh_token'],
|
||||
]);
|
||||
|
||||
if ($token->isAboutToExpire()) {
|
||||
$token->refresh(true);
|
||||
if (!$token->isValid()) {
|
||||
return false;
|
||||
}
|
||||
$this->config['token_obtained'] = $token->getCreated();
|
||||
$this->config['refresh_token'] = $token->getRefreshToken();
|
||||
$this->config['access_token'] = $token->getAccessToken();
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
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 ['onedrive_upload_chunksize_in_kb' => DUPLICATOR_PRO_ONEDRIVE_UPLOAD_CHUNK_DEFAULT_SIZE_IN_KB];
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
type="number"
|
||||
name="onedrive_upload_chunksize_in_kb"
|
||||
id="onedrive_upload_chunksize_in_kb"
|
||||
class="dup-narrow-input text-right"
|
||||
step="320"
|
||||
min="<?php echo intval(DUPLICATOR_PRO_ONEDRIVE_UPLOAD_CHUNK_MIN_SIZE_IN_KB); ?>"
|
||||
data-parsley-required
|
||||
data-parsley-type="number"
|
||||
data-parsley-errors-container="#onedrive_upload_chunksize_in_kb_error_container"
|
||||
value="<?php echo (int) $values['onedrive_upload_chunksize_in_kb']; ?>"
|
||||
> <b>KB</b>
|
||||
<div id="onedrive_upload_chunksize_in_kb_error_container" class="duplicator-error-container"></div>
|
||||
<p class="description">
|
||||
<?php printf(esc_html__(
|
||||
'How much should be uploaded to OneDrive per attempt. It should be multiple of %dkb. Higher=faster but less reliable.',
|
||||
'duplicator-pro'
|
||||
), (int) DUPLICATOR_PRO_ONEDRIVE_UPLOAD_CHUNK_MIN_SIZE_IN_KB);
|
||||
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online#upload-bytes-to-the-upload-session
|
||||
printf(
|
||||
esc_html__('Default size %1$dkb. Min size %2$dkb.', 'duplicator-pro'),
|
||||
(int) DUPLICATOR_PRO_ONEDRIVE_UPLOAD_CHUNK_DEFAULT_SIZE_IN_KB,
|
||||
(int) DUPLICATOR_PRO_ONEDRIVE_UPLOAD_CHUNK_MIN_SIZE_IN_KB
|
||||
); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Duplicator\Addons\OneDriveAddon;
|
||||
|
||||
use Duplicator\Models\Storages\StoragePathInfo;
|
||||
|
||||
class OneDriveStoragePathInfo extends StoragePathInfo
|
||||
{
|
||||
/** @var string */
|
||||
public $id = '';
|
||||
|
||||
/** @var string */
|
||||
public $name = '';
|
||||
|
||||
/** @var string */
|
||||
public $webUrl = '';
|
||||
|
||||
/**
|
||||
* @var ?array{mimeType: string, hashes: array{sha1Hash: string, quickXorHash: string, sha256Hash: string}}
|
||||
*/
|
||||
public $file = null;
|
||||
|
||||
/**
|
||||
* @var array{id: string, displayName: string}|null
|
||||
*/
|
||||
public $user = null;
|
||||
}
|
||||
@@ -0,0 +1,760 @@
|
||||
<?php
|
||||
|
||||
namespace Duplicator\Addons\OneDriveAddon;
|
||||
|
||||
use DUP_PRO_Log;
|
||||
use Duplicator\Models\Storages\AbstractStorageAdapter;
|
||||
use Duplicator\Models\Storages\StoragePathInfo;
|
||||
use Duplicator\Utils\OAuth\TokenEntity;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @method OneDriveStoragePathInfo getPathInfo($path)
|
||||
*/
|
||||
class OnedriveAdapter extends AbstractStorageAdapter
|
||||
{
|
||||
/** @var TokenEntity The token object to use for authentication */
|
||||
protected $token = null;
|
||||
/** @var HttpClient */
|
||||
protected $http = null;
|
||||
/** @var string The ID of the folder to use for storage */
|
||||
protected $storageFolderId = '';
|
||||
/** @var string The name of the folder to use for storage */
|
||||
protected $storageFolderName = '';
|
||||
/** @var ?OneDriveStoragePathInfo The app folder object */
|
||||
protected $appFolder = null;
|
||||
/** @var string Base URL for API requests */
|
||||
protected $baseUrl = 'https://graph.microsoft.com/v1.0';
|
||||
/** @var bool */
|
||||
protected $sslVerify = true;
|
||||
/** @var string If empty use server cert else use custom cert path */
|
||||
protected $sslCert = '';
|
||||
/** @var int The microsecond at which the current operation started */
|
||||
protected $startTime = 0;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param TokenEntity $token The token object to use for authentication
|
||||
* @param string $storageFolder The folder to use for storage
|
||||
* @param string $storageFolderId The ID of the folder to use for storage
|
||||
* @param bool $sslVerify If true, use SSL
|
||||
* @param string $sslCert If empty use server cert
|
||||
*/
|
||||
public function __construct(
|
||||
TokenEntity $token,
|
||||
$storageFolder,
|
||||
$storageFolderId = '',
|
||||
$sslVerify = true,
|
||||
$sslCert = ''
|
||||
) {
|
||||
$this->token = $token;
|
||||
$this->storageFolderName = trim($storageFolder, '/');
|
||||
$this->storageFolderId = $storageFolderId;
|
||||
$this->sslVerify = $sslVerify;
|
||||
$this->sslCert = $sslCert;
|
||||
$this->http = new HttpClient($this->baseUrl, $token->getAccessToken(), $this->sslVerify, $this->sslCert);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the storage adapter
|
||||
*
|
||||
* @param string $errorMsg The error message to modify if initialization fails
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function initialize(&$errorMsg = '')
|
||||
{
|
||||
if (! $this->token->isValid()) {
|
||||
$errorMsg = __('Invalid token supplied for OneDrive', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
if (! $this->exists('/') && ! $this->createDir('/')) {
|
||||
$errorMsg = __('Unable to create root directory for OneDrive', 'duplicator-pro');
|
||||
return false;
|
||||
}
|
||||
if (empty($this->storageFolderId)) {
|
||||
$root = $this->getPathInfo('/');
|
||||
if (! $root || ! $root->exists) {
|
||||
$errorMsg = 'OneDrive root folder does not exist.';
|
||||
return false;
|
||||
}
|
||||
$this->storageFolderId = $root->id;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the storage adapter
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function destroy()
|
||||
{
|
||||
$storageFolder = $this->getPathInfo('/');
|
||||
if (! $storageFolder || ! $storageFolder->exists) {
|
||||
return true; // nothing to delete
|
||||
}
|
||||
|
||||
return $this->delete('/', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the storage adapter is valid
|
||||
*
|
||||
* @param string $errorMsg The error message to modify if validation fails
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(&$errorMsg = '')
|
||||
{
|
||||
if (!$this->token->isValid() && !$this->token->refresh()) {
|
||||
$errorMsg = 'Invalid token supplied or token refresh failed.';
|
||||
return false;
|
||||
}
|
||||
$root = $this->getPathInfo('/');
|
||||
if (! $root || ! $root->exists) {
|
||||
$errorMsg = 'OneDrive root folder does not exist.';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory in the storage
|
||||
* If the given path is a nested path, it will create all the parent directories
|
||||
*
|
||||
* @param string $path The path to create
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function realCreateDir($path)
|
||||
{
|
||||
if (empty($this->storageFolderId)) {
|
||||
$parentFolder = $this->getAppFolder()->id;
|
||||
$path = trim($this->storageFolderName . '/' . trim($path, '/'), '/');
|
||||
} else {
|
||||
$parentFolder = $this->storageFolderId;
|
||||
$path = trim($path, '/');
|
||||
}
|
||||
|
||||
$folders = explode('/', $this->formatPath($path));
|
||||
|
||||
$completed = '';
|
||||
foreach ($folders as $folder) {
|
||||
$item = $this->getItemDetailsByPath($completed . '/' . $folder);
|
||||
if (!isset($item['id'])) {
|
||||
try {
|
||||
$item = $this->createDriveDirectory($parentFolder, $folder);
|
||||
if (!isset($item['id'])) {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$completed .= '/' . $folder;
|
||||
$parentFolder = $item['id'];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file which is less that 4MB in the storage
|
||||
*
|
||||
* @see https://github.com/OneDrive/onedrive-api-docs/blob/live/docs/rest-api/api/driveitem_put_content.md
|
||||
*
|
||||
* @param string $path The path in which the file will be created
|
||||
* @param string $content The content of the file
|
||||
*
|
||||
* @return false|int
|
||||
*/
|
||||
protected function realCreateFile($path, $content)
|
||||
{
|
||||
// maximum content length is 4MB
|
||||
if (strlen($content) > 4 * 1024 * 1024) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$file = ltrim($this->formatPath($path), '/');
|
||||
|
||||
try {
|
||||
$response = $this->http->put("/me/drive/items/{$this->storageFolderId}:/{$file}:/content", $content, ['Content-Type' => 'text/plain']);
|
||||
} catch (Exception $e) {
|
||||
// Request failed from curl
|
||||
DUP_PRO_Log::infoTrace("Failed to create file in OneDrive: {$e->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
$response = json_decode($response['body'], true);
|
||||
|
||||
if (!isset($response['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (int) $response['size'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete relative path from storage root.
|
||||
*
|
||||
* @see https://github.com/OneDrive/onedrive-api-docs/blob/live/docs/rest-api/api/driveitem_delete.md
|
||||
*
|
||||
* @param string $path The path or drive item id to delete
|
||||
* @param bool $recursive Whether to delete recursively
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function realDelete($path, $recursive = false)
|
||||
{
|
||||
if (! $this->exists($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$info = $this->getItemDetailsByPath($path);
|
||||
|
||||
if (! $recursive && isset($info['folder']) && $info['folder']['childCount'] > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->http->delete("/me/drive/items/{$info['id']}");
|
||||
} catch (Exception $e) {
|
||||
// Request failed from curl
|
||||
DUP_PRO_Log::infoTrace("Failed to delete file in OneDrive: {$e->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response['code'] === 204;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a file
|
||||
*
|
||||
* @param string $path The path to the file
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public function getFileContent($path)
|
||||
{
|
||||
$item = $this->getItemDetailsByPath($path);
|
||||
if (!isset($item['@microsoft.graph.downloadUrl'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return file_get_contents($item['@microsoft.graph.downloadUrl']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 OneDriveStoragePathInfo|false The path info or false on error.
|
||||
*/
|
||||
protected function getRealPathInfo($path)
|
||||
{
|
||||
$path = '/' . ltrim($path, '/');
|
||||
|
||||
$item = $this->getItemDetailsByPath($path);
|
||||
|
||||
return $this->buildStoragePathInfo($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a file or directory. The destination path must not exist.
|
||||
*
|
||||
* @see https://github.com/OneDrive/onedrive-api-docs/blob/live/docs/rest-api/api/driveitem_move.md
|
||||
*
|
||||
* @param string $oldPath The path to the file or directory to move
|
||||
* @param string $newPath The destination path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function realMove($oldPath, $newPath)
|
||||
{
|
||||
$oldItem = $this->getPathInfo($oldPath);
|
||||
$newDirectoryItem = $this->getPathInfo(dirname($newPath));
|
||||
if (!$oldItem || !$newDirectoryItem) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$response = $this->http->request('PATCH', "/me/drive/items/{$oldItem->id}", [
|
||||
'parentReference' => [
|
||||
'id' => $newDirectoryItem->id,
|
||||
],
|
||||
'name' => basename($newPath),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
// Request failed error from curl
|
||||
DUP_PRO_Log::infoTrace("Failed to move file in OneDrive: {$e->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
$response = json_decode($response['body'], true);
|
||||
|
||||
return isset($response['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path The path to the directory to scan
|
||||
* @param bool $files Whether to include files
|
||||
* @param bool $folders Whether to include folders
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function scanDir($path, $files = true, $folders = true)
|
||||
{
|
||||
$path = '/' . ltrim($path, '/');
|
||||
if ($path !== '/') {
|
||||
// Paths under the storage folder must be prefixed with a colon, no need to do this for the storage folder itself
|
||||
$path = ":{$path}:";
|
||||
}
|
||||
try {
|
||||
$response = $this->http->get("/me/drive/items/{$this->storageFolderId}{$path}/children");
|
||||
} catch (Exception $e) {
|
||||
DUP_PRO_Log::infoTrace("Failed to scan dir in OneDrive: {$e->getMessage()}");
|
||||
return [];
|
||||
}
|
||||
$items = json_decode($response['body'], true);
|
||||
|
||||
if (!isset($items['value'])) {
|
||||
return [];
|
||||
} else {
|
||||
$items = $items['value'];
|
||||
}
|
||||
|
||||
foreach ($items as $index => $item) {
|
||||
$item = $this->buildStoragePathInfo($item);
|
||||
$items[$index] = $item->name;
|
||||
if (!$folders && $item->isDir) {
|
||||
unset($items[$index]);
|
||||
}
|
||||
if (!$files && !$item->isDir) {
|
||||
unset($items[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a directory is empty
|
||||
*
|
||||
* @param string $path The path to the directory
|
||||
* @param string[] $filters An array of filters to apply
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isDirEmpty($path, $filters = [])
|
||||
{
|
||||
$item = $this->getItemDetailsByPath($path);
|
||||
if (!isset($item['folder'])) {
|
||||
return false;
|
||||
}
|
||||
if ($item['folder']['childCount'] === 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start tracking the time for the current operation
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function startTrackingTime()
|
||||
{
|
||||
$this->startTime = (int) (microtime(true) * SECONDS_IN_MICROSECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the elapsed time since the start of the current operation
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function getElapsedTime()
|
||||
{
|
||||
return (int) (microtime(true) * SECONDS_IN_MICROSECONDS) - $this->startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the operation has reached the timeout
|
||||
*
|
||||
* @param int $timeout The timeout in microseconds
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasReachedTimeout($timeout)
|
||||
{
|
||||
return $timeout > 0 && $this->getElapsedTime() > $timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [])
|
||||
{
|
||||
$this->startTrackingTime();
|
||||
|
||||
if (! isset($extraData['uploadSession'])) {
|
||||
$extraData['uploadSession'] = $this->createUploadSession($storageFile);
|
||||
}
|
||||
$uploadSession = $extraData['uploadSession'];
|
||||
if (! $uploadSession) {
|
||||
DUP_PRO_Log::infoTrace("Failed to create upload session for {$storageFile}, try uploading again.");
|
||||
return false;
|
||||
}
|
||||
$expiration = strtotime($uploadSession['expirationDateTime']);
|
||||
$fileSize = filesize($sourceFile);
|
||||
$defaultChunkSize = MB_IN_BYTES; // 1MB
|
||||
$chunkSize = $length > 0 ? $length : $defaultChunkSize;
|
||||
$stream = fopen($sourceFile, 'rb');
|
||||
$bytesRemaining = $fileSize - $offset;
|
||||
$bytesUploaded = $offset;
|
||||
fseek($stream, $bytesUploaded);
|
||||
|
||||
// We stop uploading if we have reached the timeout or if we have uploaded the entire file.
|
||||
while ($bytesRemaining > 0 && ! $this->hasReachedTimeout($timeout)) {
|
||||
if (time() > $expiration) {
|
||||
DUP_PRO_Log::infoTrace("OneDrive Upload session expired for {$storageFile}, try uploading again.");
|
||||
unset($extraData['uploadSession']);
|
||||
return false;
|
||||
}
|
||||
$chunkSize = min($chunkSize, $bytesRemaining);
|
||||
$chunk = fread($stream, $chunkSize);
|
||||
try {
|
||||
//$this->http->setTimeout($timeout / SECONDS_IN_MICROSECONDS);
|
||||
$response = $this->http->put($uploadSession['uploadUrl'], $chunk, [
|
||||
'Content-Length' => $chunkSize,
|
||||
'Content-Range' => sprintf('bytes %d-%d/%d', $bytesUploaded, $bytesUploaded + $chunkSize - 1, $fileSize),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
DUP_PRO_Log::infoTrace("Failed to copy file to OneDrive: {$e->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$bytesUploaded += $chunkSize;
|
||||
$bytesRemaining -= $chunkSize;
|
||||
|
||||
if (in_array($response['code'], [200, 201])) {
|
||||
// We have finished uploading the file
|
||||
unset($extraData['uploadSession']);
|
||||
return $length > 0 ? $length : $fileSize; // We return the original requested chunksize for historical reasons.
|
||||
}
|
||||
// At this point only 202 is expected, which means we have to continue uploading
|
||||
if ($response['code'] !== 202) {
|
||||
// 4XX means we cannot resume uploading. 5XX means we can retry later.
|
||||
DUP_PRO_Log::infoTrace("OneDrive Upload error for {$storageFile}, try uploading again.");
|
||||
unset($extraData['uploadSession']);
|
||||
return false;
|
||||
}
|
||||
|
||||
// A specific length was requested, we have uploaded the requested length.
|
||||
if ($length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// We return the original requested chunksize for historical reasons.
|
||||
// @todo: This SHOULD be the number of bytes that were written to the file.
|
||||
return $length > 0 ? $length : $fileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the app folder object
|
||||
*
|
||||
* @see https://github.com/OneDrive/onedrive-api-docs/blob/live/docs/rest-api/api/drive_get_specialfolder.md
|
||||
*
|
||||
* @return OneDriveStoragePathInfo|false
|
||||
*/
|
||||
protected function getAppFolder()
|
||||
{
|
||||
if ($this->appFolder !== null) {
|
||||
return $this->appFolder;
|
||||
}
|
||||
try {
|
||||
$response = $this->http->get('/me/drive/special/approot');
|
||||
} catch (Exception $e) {
|
||||
DUP_PRO_Log::infoTrace("Failed to get app folder in OneDrive: {$e->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
$item = json_decode($response['body'], true);
|
||||
|
||||
return $this->appFolder = $this->buildStoragePathInfo($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details of an item by path
|
||||
*
|
||||
* @see https://github.com/OneDrive/onedrive-api-docs/blob/live/docs/rest-api/api/driveitem_get.md#http-request
|
||||
*
|
||||
* @param string $path The path to the item
|
||||
*
|
||||
* @return array<string, mixed>|false
|
||||
*/
|
||||
public function getItemDetailsByPath($path)
|
||||
{
|
||||
$path = '/' . ltrim($this->formatPath($path), '/');
|
||||
if (empty($this->storageFolderId)) {
|
||||
$parent = $this->getAppFolder()->id;
|
||||
$path = '/' . trim($this->storageFolderName . $path, '/');
|
||||
} else {
|
||||
$parent = $this->storageFolderId;
|
||||
}
|
||||
try {
|
||||
$response = $this->http->get("me/drive/items/{$parent}:{$path}");
|
||||
} catch (Exception $e) {
|
||||
DUP_PRO_Log::infoTrace("Failed to get item details in OneDrive: {$e->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return json_decode($response['body'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory in the storage
|
||||
*
|
||||
* @see https://github.com/OneDrive/onedrive-api-docs/blob/live/docs/rest-api/api/driveitem_post_children.md
|
||||
*
|
||||
* @param string $parent The ID of the parent directory
|
||||
* @param string $directory The name of the directory to create
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function createDriveDirectory($parent, $directory)
|
||||
{
|
||||
try {
|
||||
$response = $this->http->post('me/drive/items/' . $parent . '/children', [
|
||||
'name' => $this->formatPath($directory),
|
||||
'folder' => new \stdClass(),
|
||||
'@microsoft.graph.conflictBehavior' => 'fail',
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
DUP_PRO_Log::infoTrace("Failed to create directory in OneDrive: {$e->getMessage()}");
|
||||
return [];
|
||||
}
|
||||
|
||||
return json_decode($response['body'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new upload session
|
||||
*
|
||||
* @see https://github.com/OneDrive/onedrive-api-docs/blob/live/docs/rest-api/api/driveitem_createuploadsession.md
|
||||
*
|
||||
* @param string $targetFile The path to the destination file
|
||||
*
|
||||
* @return array{uploadUrl: string, expirationDateTime: string}|false
|
||||
*/
|
||||
protected function createUploadSession($targetFile)
|
||||
{
|
||||
$targetFile = $this->formatPath($targetFile);
|
||||
$parent = $this->storageFolderId;
|
||||
$file = basename($targetFile);
|
||||
if ($file !== $targetFile) {
|
||||
$directory = dirname($targetFile);
|
||||
$this->createDir($directory);
|
||||
$targetDir = $this->getPathInfo($directory);
|
||||
$parent = $targetDir->id;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->http->post("me/drive/items/{$parent}:/{$file}:/createUploadSession", [
|
||||
'item' => ["name" => $file],
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
// Request failed from curl
|
||||
DUP_PRO_Log::infoTrace("Failed to create upload session in OneDrive: {$e->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($response['code'] !== 200) {
|
||||
// Failed to create the upload session, error exists in the response body
|
||||
return false;
|
||||
}
|
||||
return json_decode($response['body'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a StoragePathInfo object from the given array
|
||||
*
|
||||
* @param array<string, mixed> $item The array to build the object from
|
||||
*
|
||||
* @return OneDriveStoragePathInfo|false
|
||||
*/
|
||||
protected function buildStoragePathInfo($item)
|
||||
{
|
||||
$info = new OneDriveStoragePathInfo();
|
||||
|
||||
if (!isset($item['id'])) {
|
||||
return $info;
|
||||
}
|
||||
$info->exists = true;
|
||||
$info->id = $item['id'];
|
||||
$info->name = $item['name'];
|
||||
$info->isDir = isset($item['folder']);
|
||||
$info->created = isset($item['createdDateTime']) ? strtotime($item['createdDateTime']) : 0;
|
||||
$info->modified = isset($item['lastModifiedDateTime']) ? strtotime($item['lastModifiedDateTime']) : 0;
|
||||
$info->size = isset($item['size']) ? $item['size'] : 0;
|
||||
$info->webUrl = isset($item['webUrl']) ? $item['webUrl'] : '';
|
||||
if (isset($item['file'])) {
|
||||
$info->file = $item['file'];
|
||||
}
|
||||
if (isset($item['createdBy']['user'])) {
|
||||
$info->user = $item['createdBy']['user'];
|
||||
}
|
||||
if (isset($item['parentReference']['path'])) {
|
||||
// path can be different from the name, e.g. when the file is in a subdirectory
|
||||
$fullPath = $item['parentReference']['path'] . '/' . $info->name;
|
||||
$storagePosition = strpos($fullPath, $this->storageFolderName); // calculate the position of the storage folder name
|
||||
$filePathStartPosition = $storagePosition + strlen($this->storageFolderName) + 1; // file path starts after the storage folder name & the slash
|
||||
$info->path = substr($fullPath, $filePathStartPosition);
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate info on create dir
|
||||
*
|
||||
* @param string $path Dir path
|
||||
*
|
||||
* @return StoragePathInfo
|
||||
*/
|
||||
protected function generateCreateDirInfo($path)
|
||||
{
|
||||
return $this->getRealPathInfo($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate info on delete item
|
||||
*
|
||||
* @param string $path Item path
|
||||
*
|
||||
* @return StoragePathInfo
|
||||
*/
|
||||
protected function generateDeleteInfo($path)
|
||||
{
|
||||
$info = new OneDriveStoragePathInfo();
|
||||
$info->path = $path;
|
||||
$info->exists = false;
|
||||
$info->isDir = false;
|
||||
$info->size = 0;
|
||||
$info->created = 0;
|
||||
$info->modified = 0;
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a path for OneDrive. This handles special characters in the path
|
||||
*
|
||||
* @param string $path The path to format
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function formatPath($path)
|
||||
{
|
||||
return implode('/', array_map('rawurlencode', explode('/', $path)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [])
|
||||
{
|
||||
$this->startTrackingTime();
|
||||
//$this->http->setTimeout($timeout / SECONDS_IN_MICROSECONDS);
|
||||
if ($length < 0) {
|
||||
// We can use the download URL to download the file in one go
|
||||
$content = $this->getFileContent($storageFile);
|
||||
if ($content === false) {
|
||||
return false;
|
||||
}
|
||||
$content = substr($content, $offset);
|
||||
if (file_put_contents($destFile, $content) === false) {
|
||||
return false;
|
||||
}
|
||||
return strlen($content);
|
||||
}
|
||||
|
||||
if (! isset($extraData['downloadUrl'])) {
|
||||
$item = $this->getItemDetailsByPath($storageFile);
|
||||
if (!isset($item['@microsoft.graph.downloadUrl'])) {
|
||||
return false;
|
||||
}
|
||||
$extraData['downloadUrl'] = $item['@microsoft.graph.downloadUrl'];
|
||||
if (file_put_contents($destFile, '') === false) {
|
||||
DUP_PRO_Log::infoTrace("[OnedriveAddon] Failed to open file for writing: {$destFile}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$downloadUrl = $extraData['downloadUrl'];
|
||||
|
||||
$range = "bytes={$offset}-" . ($offset + $length - 1);
|
||||
try {
|
||||
$response = $this->http->get($downloadUrl, [], ['Range' => $range]);
|
||||
} catch (Exception $e) {
|
||||
DUP_PRO_Log::infoTrace("[OnedriveAddon] Failed to download file: {$storageFile}. Error: {$e->getMessage()}");
|
||||
return false;
|
||||
}
|
||||
if ($response['code'] !== 206 && $response['code'] !== 200) {
|
||||
DUP_PRO_Log::infoTrace("[OnedriveAddon] Failed to download file: {$storageFile}. Response " . $response['body']);
|
||||
return false;
|
||||
}
|
||||
file_put_contents($destFile, $response['body'], FILE_APPEND);
|
||||
|
||||
return $length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Duplicator messages sections
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\OneDriveAddon\Models\OneDriveStorage;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var \Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var \Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
* @var OneDriveStorage $storage
|
||||
*/
|
||||
$storage = $tplData["storage"];
|
||||
/** @var string */
|
||||
$storageFolder = $tplData["storageFolder"];
|
||||
/** @var int */
|
||||
$maxPackages = $tplData["maxPackages"];
|
||||
/** @var bool */
|
||||
$allFolderPers = $tplData["allFolderPers"];
|
||||
/** @var false|object */
|
||||
$accountInfo = $tplData["accountInfo"];
|
||||
/** @var false|object */
|
||||
$hasError = $tplData["hasError"];
|
||||
/** @var string */
|
||||
$externalRevokeUrl = $tplData["externalRevokeUrl"];
|
||||
|
||||
$tplMng->render('admin_pages/storages/parts/provider_head');
|
||||
?>
|
||||
<tr>
|
||||
<th scope="row"><label><?php esc_html_e("Authorization", 'duplicator-pro'); ?></label></th>
|
||||
<td class="onedrive-authorize">
|
||||
<?php if (!$storage->isAuthorized()) : ?>
|
||||
<div class='onedrive-msgraph-authorization-state' id="onedrive-msgraph-state-unauthorized">
|
||||
<div id="dup-all-onedrive-allperms-wrapper" >
|
||||
<?php esc_html_e('All folders read write permission:', 'duplicator-pro'); ?>
|
||||
<label class="switch">
|
||||
<input
|
||||
id="onedrive_msgraph_all_folders_read_write_perm"
|
||||
name="onedrive_msgraph_all_folders_read_write_perm"
|
||||
type="checkbox"
|
||||
value="1"
|
||||
<?php checked($allFolderPers); ?>
|
||||
>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<div class="auth-code-popup-note" style="margin-top:1px; margin-left: 0;">
|
||||
<?php
|
||||
echo esc_html__('There is only Apps folder permission scope by default.', 'duplicator-pro') . ' ' .
|
||||
esc_html__('If your OneDrive Business is not working, Please switch on this option.', 'duplicator-pro'); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONNECT -->
|
||||
<button
|
||||
id="dpro-onedrive-msgraph-connect-btn"
|
||||
type="button"
|
||||
class="button button-large"
|
||||
onclick="DupPro.Storage.OneDrive.GetAuthUrl(); return false;"
|
||||
>
|
||||
<i class="fa fa-plug"></i> <?php esc_html_e('Connect to OneDrive', 'duplicator-pro'); ?>
|
||||
<img
|
||||
src="<?php echo esc_url(DUPLICATOR_PRO_IMG_URL . '/onedrive.svg'); ?>"
|
||||
style='vertical-align: middle; margin:-2px 0 0 3px; height:18px; width:18px'
|
||||
>
|
||||
</button>
|
||||
|
||||
<div class='onedrive-msgraph-auth-container' style="display: none;">
|
||||
<!-- STEP 2 -->
|
||||
<b><?php esc_html_e("Step 1:", 'duplicator-pro'); ?></b>
|
||||
<?php esc_html_e(' Duplicator needs to authorize at OneDrive.', 'duplicator-pro'); ?>
|
||||
<div class="auth-code-popup-note" style="margin-top:1px">
|
||||
<?php
|
||||
esc_html_e(
|
||||
'Note: Clicking the button below will open a new tab/window.
|
||||
Please be sure your browser does not block popups.
|
||||
If a new tab/window does not open check your browsers address bar to allow popups from this URL.',
|
||||
'duplicator-pro'
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<button
|
||||
id="dpro-onedrive-msgraph-auth-btn"
|
||||
type="button"
|
||||
class="button button-large"
|
||||
data-auth-url="<?php echo esc_attr($storage->getAuthorizationUrl()); ?>"
|
||||
>
|
||||
<i class="fa fa-user"></i> <?php esc_html_e('Authorize Onedrive', 'duplicator-pro'); ?>
|
||||
</button>
|
||||
<br/><br/>
|
||||
|
||||
<div id="onedrive-msgraph-auth-container">
|
||||
<b><?php esc_html_e('Step 2:', 'duplicator-pro'); ?></b>
|
||||
<?php esc_html_e("Paste code from OneDrive authorization page.", 'duplicator-pro'); ?> <br/>
|
||||
<input style="width:400px" id="onedrive-msgraph-auth-code" name="onedrive-msgraph-auth-code" />
|
||||
</div>
|
||||
<br><br>
|
||||
<!-- STEP 3 -->
|
||||
<b><?php esc_html_e("Step 3:", 'duplicator-pro'); ?></b>
|
||||
<?php esc_html_e('Finalize OneDrive validation by clicking the "Finalize Setup" button.', 'duplicator-pro'); ?>
|
||||
<br/>
|
||||
<button
|
||||
type="button"
|
||||
id="onedrive-msgraph-finalize-setup"
|
||||
class="button"
|
||||
>
|
||||
<i class="fa fa-check-square"></i> <?php esc_html_e('Finalize Setup', 'duplicator-pro'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="onedrive-msgraph-authorization-state" id="onedrive-msgraph-state-authorized">
|
||||
<?php if ($storage->isAuthorized()) : ?>
|
||||
<h3>
|
||||
<?php esc_html_e('OneDrive Account', 'duplicator-pro'); ?><br/>
|
||||
<i class="dpro-edit-info">
|
||||
<?php esc_html_e('Duplicator has been authorized to access this user\'s OneDrive account', 'duplicator-pro'); ?>
|
||||
</i>
|
||||
</h3>
|
||||
|
||||
<?php
|
||||
if ($accountInfo !== false) {
|
||||
?>
|
||||
<div id="onedrive-account-info">
|
||||
<label><?php esc_html_e('Name', 'duplicator-pro'); ?>:</label>
|
||||
<?php echo esc_html($accountInfo->displayName); ?> <br/>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
} elseif ($hasError) {
|
||||
?>
|
||||
<div class="error-txt">
|
||||
<?php
|
||||
echo '<strong>';
|
||||
esc_html_e('Please click on the "Cancel Authorization" button and reauthorize the OneDrive storage', 'duplicator-pro');
|
||||
echo '</strong>';
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
<br/>
|
||||
<button type="button" class="button" onclick='DupPro.Storage.OneDrive.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 OneDrive account. Will require re-authorization.',
|
||||
'duplicator-pro'
|
||||
);
|
||||
?>
|
||||
</i>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="_onedrive_msgraph_storage_folder"><?php esc_html_e("Storage Folder", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<b>//OneDrive/Apps/Duplicator Pro/</b>
|
||||
<input
|
||||
id="_onedrive_msgraph_storage_folder"
|
||||
name="_onedrive_msgraph_storage_folder"
|
||||
type="text"
|
||||
value="<?php echo esc_attr($storageFolder); ?>"
|
||||
class="dpro-storeage-folder-path" data-parsley-pattern="^((?!\:).)*[^\.\:]$"
|
||||
data-parsley-errors-container="#onedrive_msgraph_storage_folder_error_container"
|
||||
data-parsley-pattern-message="<?php
|
||||
echo esc_attr__(
|
||||
'The folder path shouldn\'t include the special character colon(":") or shouldn\'t end with a dot(".").',
|
||||
'duplicator-pro'
|
||||
); ?>"
|
||||
>
|
||||
<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>
|
||||
<div id="onedrive_msgraph_storage_folder_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for=""><?php esc_html_e("Max Packages", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<label for="onedrive_msgraph_max_files">
|
||||
<input
|
||||
data-parsley-errors-container="#onedrive_msgraph_max_files_error_container"
|
||||
id="onedrive_msgraph_max_files"
|
||||
name="onedrive_msgraph_max_files"
|
||||
type="text"
|
||||
value="<?php echo absint($maxPackages); ?>" maxlength="4"
|
||||
>
|
||||
<?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="onedrive_msgraph_max_files_error_container" class="duplicator-error-container"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php $tplMng->render('admin_pages/storages/parts/provider_foot');
|
||||
|
||||
// Alerts for OneDrive
|
||||
$alertConnStatus = new DUP_PRO_UI_Dialog();
|
||||
$alertConnStatus->title = __('OneDrive Connection Status', 'duplicator-pro');
|
||||
$alertConnStatus->message = ''; // javascript inserted message
|
||||
$alertConnStatus->initAlert();
|
||||
?>
|
||||
<script>
|
||||
jQuery(document).ready(function ($) {
|
||||
let storageId = <?php echo (int) $storage->getId(); ?>;
|
||||
|
||||
DupPro.Storage.OneDrive.GetAuthUrl = function ()
|
||||
{
|
||||
$("#dpro-onedrive-msgraph-connect-btn").hide();
|
||||
$(".onedrive-msgraph-auth-container").show();
|
||||
};
|
||||
|
||||
$('#dpro-onedrive-msgraph-auth-btn').click(function() {
|
||||
let authUrl = $(this).data('auth-url');
|
||||
// console.log(DupPro.Storage.OneDrive.AuthUrl);
|
||||
window.open(authUrl, '_blank');
|
||||
});
|
||||
|
||||
DupPro.Storage.OneDrive.CancelAuthorization = function ()
|
||||
{
|
||||
window.open(<?php echo json_encode($externalRevokeUrl); ?>, '_blank');
|
||||
DupPro.Storage.RevokeAuth(storageId);
|
||||
}
|
||||
|
||||
DupPro.Storage.OneDrive.FinalizeSetup = function () {
|
||||
if ($('#onedrive-msgraph-auth-code').val().length > 5) {
|
||||
$("#dup-storage-form").submit();
|
||||
} else {
|
||||
<?php $alertConnStatus->showAlert(); ?>
|
||||
let alertMsg = "<i class='fas fa-exclamation-triangle'></i> " +
|
||||
"<?php esc_html_e('Please enter your OneDrive authorization code!', 'duplicator-pro'); ?>";
|
||||
<?php $alertConnStatus->updateMessage("alertMsg"); ?>
|
||||
}
|
||||
}
|
||||
|
||||
$('#onedrive_msgraph_all_folders_read_write_perm').change(function (event) {
|
||||
event.stopPropagation();
|
||||
let allPerms = $(this).is(':checked');
|
||||
Duplicator.Util.ajaxWrapper(
|
||||
{
|
||||
action: 'duplicator_pro_onedrive_all_perms_update',
|
||||
storage_id: storageId,
|
||||
all_perms: allPerms,
|
||||
nonce: '<?php echo esc_js(wp_create_nonce('duplicator_pro_onedrive_all_perms_update')); ?>'
|
||||
},
|
||||
function (result, data, funcData, textStatus, jqXHR) {
|
||||
if (funcData.success) {
|
||||
$('#dpro-onedrive-msgraph-auth-btn').data('auth-url', funcData.auth_url);
|
||||
} else {
|
||||
DupPro.addAdminMessage(funcData.message, 'error');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#onedrive-msgraph-finalize-setup').click(function (event) {
|
||||
event.stopPropagation();
|
||||
|
||||
if ($('#onedrive-msgraph-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': $('#_onedrive_msgraph_storage_folder').val(),
|
||||
'max_packages': $('#onedrive_msgraph_max_files').val(),
|
||||
'auth_code' : $('#onedrive-msgraph-auth-code').val()
|
||||
}
|
||||
);
|
||||
} else {
|
||||
<?php $alertConnStatus->showAlert(); ?>
|
||||
let alertMsg = "<i class='fas fa-exclamation-triangle'></i> " +
|
||||
"<?php esc_html_e('Please enter your Onedrive authorization code!', 'duplicator-pro'); ?>";
|
||||
<?php $alertConnStatus->updateMessage("alertMsg"); ?>
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Version Pro Base addon class
|
||||
*
|
||||
* Name: Duplicator PRO base
|
||||
* Version: 1
|
||||
* Author: Duplicator
|
||||
* Author URI: http://snapcreek.com
|
||||
*
|
||||
* PHP version 5.6.20
|
||||
*
|
||||
* @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\ProBase;
|
||||
|
||||
// phpcs:disable
|
||||
require_once __DIR__ . '/vendor/edd/EDD_SL_Plugin_Updater.php';
|
||||
// phpcs:enable
|
||||
|
||||
use Duplicator\Controllers\SchedulePageController;
|
||||
use Duplicator\Addons\ProBase\License\License;
|
||||
use Duplicator\Addons\ProBase\License\LicenseNotices;
|
||||
use Duplicator\Addons\ProBase\Models\LicenseData;
|
||||
use Duplicator\Core\Controllers\AbstractMenuPageController;
|
||||
use Duplicator\Core\MigrationMng;
|
||||
use Duplicator\Installer\Models\MigrateData;
|
||||
use Duplicator\Libs\Snap\SnapLog;
|
||||
|
||||
/**
|
||||
* Version Pro Base addon class
|
||||
*
|
||||
* @category Duplicator
|
||||
* @package Plugin
|
||||
* @author Snapcreek <admin@snapcreek.com>
|
||||
* @license https://www.gnu.org/licenses/gpl-3.0.html GPLv3
|
||||
* @link http://snapcreek.com
|
||||
*/
|
||||
class ProBase extends \Duplicator\Core\Addons\AbstractAddonCore
|
||||
{
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
add_action('init', array($this, 'hookInit'));
|
||||
add_action('duplicator_unistall', array($this, 'unistall'));
|
||||
|
||||
add_filter('duplicator_main_menu_label', function () {
|
||||
return 'Duplicator Pro';
|
||||
});
|
||||
|
||||
add_filter('duplicator_menu_pages', array($this, 'addScheduleMenuField'));
|
||||
|
||||
add_action(MigrationMng::HOOK_FIRST_LOGIN_AFTER_INSTALL, function (MigrateData $migrationData) {
|
||||
License::clearVersionCache();
|
||||
});
|
||||
|
||||
add_action('duplicator_pro_on_upgrade_version', [$this, 'onUpgradePlugin'], 10, 2);
|
||||
|
||||
add_action('duplicator_before_update_crypt_setting', [__CLASS__, 'beforeCryptUpdateSettings']);
|
||||
add_action('duplicator_after_update_crypt_setting', [__CLASS__, 'afterCryptUpdateSettings']);
|
||||
|
||||
LicenseNotices::init();
|
||||
LicensingController::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unistall
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unistall()
|
||||
{
|
||||
if (strlen(LicenseData::getInstance()->getKey()) > 0) {
|
||||
switch (LicenseData::getInstance()->deactivate()) {
|
||||
case LicenseData::ACTIVATION_RESPONSE_OK:
|
||||
break;
|
||||
case LicenseData::ACTIVATION_REQUEST_ERROR:
|
||||
SnapLog::phpErr("Error deactivate license: ACTIVATION_RESPONSE_POST_ERROR");
|
||||
break;
|
||||
case LicenseData::ACTIVATION_RESPONSE_INVALID:
|
||||
default:
|
||||
SnapLog::phpErr("Error deactivate license: ACTIVATION_RESPONSE_INVALID");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add schedule menu page
|
||||
*
|
||||
* @param array<string, AbstractMenuPageController> $basicMenuPages menu pages
|
||||
*
|
||||
* @return array<string, AbstractMenuPageController>
|
||||
*/
|
||||
public function addScheduleMenuField($basicMenuPages)
|
||||
{
|
||||
$page = SchedulePageController::getInstance();
|
||||
|
||||
$basicMenuPages[$page->getSlug()] = $page;
|
||||
return $basicMenuPages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function calle on duplicator_addons_loaded hook
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function hookInit()
|
||||
{
|
||||
License::check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called on plugin upgrade
|
||||
*
|
||||
* @param false|string $currentVersion current version
|
||||
* @param string $newVersion new version
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function onUpgradePlugin($currentVersion, $newVersion)
|
||||
{
|
||||
if ($currentVersion !== false && version_compare($currentVersion, '4.5.16-beta1', '<')) {
|
||||
$legacyKey = get_option(LicenseData::LICENSE_OLD_KEY_OPTION_NAME, '');
|
||||
if (!empty($legacyKey)) {
|
||||
LicenseData::getInstance()->setKey($legacyKey);
|
||||
}
|
||||
delete_option(LicenseData::LICENSE_OLD_KEY_OPTION_NAME);
|
||||
}
|
||||
License::clearVersionCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Before crypt update settings
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function beforeCryptUpdateSettings()
|
||||
{
|
||||
// make sure the license date si reade before the settings are updated
|
||||
LicenseData::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* After crypt update settings
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function afterCryptUpdateSettings()
|
||||
{
|
||||
LicenseData::getInstance()->save();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonPath()
|
||||
{
|
||||
return __DIR__;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonFile()
|
||||
{
|
||||
return __FILE__;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Version Pro Base Installer addon class
|
||||
*
|
||||
* Name: Duplicator PRO base
|
||||
* Version: 1
|
||||
* Author: Snap Creek
|
||||
* Author URI: http://snapcreek.com
|
||||
*
|
||||
* @category Duplicator
|
||||
* @package Installer
|
||||
* @author Snapcreek <admin@snapcreek.com>
|
||||
* @copyright 2011-2021 Snapcreek LLC
|
||||
* @license https://www.gnu.org/licenses/gpl-3.0.html GPLv3
|
||||
* @version GIT: $Id$
|
||||
* @link http://snapcreek.com
|
||||
*/
|
||||
|
||||
namespace Duplicator\Installer\Addons\ProBase;
|
||||
|
||||
use Duplicator\Installer\Core\Hooks\HooksMng;
|
||||
use Duplicator\Installer\Core\Params\Items\ParamItem;
|
||||
|
||||
/**
|
||||
* Version Pro Base Installer addon class
|
||||
*
|
||||
* @category Duplicator
|
||||
* @package Installer
|
||||
* @author Snapcreek <admin@snapcreek.com>
|
||||
* @license https://www.gnu.org/licenses/gpl-3.0.html GPLv3
|
||||
* @link http://snapcreek.com
|
||||
*/
|
||||
class ProBase extends \Duplicator\Installer\Core\Addons\InstAbstractAddonCore
|
||||
{
|
||||
/**
|
||||
* Main init addon
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
HooksMng::getInstance()->addFilter(
|
||||
'dupx_main_header',
|
||||
function ($value) {
|
||||
return 'Duplicator PRO';
|
||||
}
|
||||
);
|
||||
|
||||
HooksMng::getInstance()->addFilter('installer_get_init_params', array(__CLASS__, 'getInitParams'));
|
||||
HooksMng::getInstance()->addAction(
|
||||
'after_params_overwrite',
|
||||
array(
|
||||
'\\Duplicator\\Installer\\Addons\\ProBase\\AdvancedParams',
|
||||
'updateParamsAfterOverwrite',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getInitParams
|
||||
*
|
||||
* @param ParamItem[] $params params list
|
||||
*
|
||||
* @return ParamItem[]
|
||||
*/
|
||||
public static function getInitParams($params)
|
||||
{
|
||||
$advParams = array();
|
||||
AdvancedParams::init($advParams);
|
||||
return array_merge($params, $advParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get addon main folder
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonPath()
|
||||
{
|
||||
return __DIR__;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get addon main file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonFile()
|
||||
{
|
||||
return __FILE__;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Installer\Addons\ProBase;
|
||||
|
||||
use Exception;
|
||||
|
||||
abstract class AbstractLicense
|
||||
{
|
||||
const TYPE_UNKNOWN = -1;
|
||||
const TYPE_UNLICENSED = 0;
|
||||
const TYPE_PERSONAL = 1;
|
||||
const TYPE_FREELANCER = 2;
|
||||
const TYPE_BUSINESS = 3;
|
||||
const TYPE_PERSONAL_AUTO = 4;
|
||||
const TYPE_FREELANCER_AUTO = 5;
|
||||
const TYPE_BUSINESS_AUTO = 6;
|
||||
const TYPE_GOLD = 7;
|
||||
const TYPE_BASIC = 8;
|
||||
const TYPE_PLUS = 9;
|
||||
const TYPE_PRO = 10;
|
||||
const TYPE_ELITE = 11;
|
||||
|
||||
const CAPABILITY_BRAND = 1000;
|
||||
const CAPABILITY_IMPORT_SETTINGS = 1001;
|
||||
const CAPABILITY_SHEDULE_HOURLY = 1002;
|
||||
const CAPABILITY_MULTISITE = 1003;
|
||||
const CAPABILITY_MULTISITE_PLUS = 1004;
|
||||
const CAPABILITY_POWER_TOOLS = 1005;
|
||||
const CAPABILITY_CHANGE_TABLE_PREFIX = 1006;
|
||||
const CAPABILITY_UPDATE_AUTH = 1007;
|
||||
const CAPABILITY_CAPABILITIES_MNG = 1008;
|
||||
const CAPABILITY_CAPABILITIES_MNG_PLUS = 1009;
|
||||
const CAPABILITY_PRO_BASE = 1010;
|
||||
const CAPABILITY_PACKAGE_COMPONENTS_PLUS = 1011;
|
||||
const CAPABILITY_IMPORT = 1012;
|
||||
const CAPABILITY_SCHEDULE = 1013;
|
||||
const CAPABILITY_STORAGE = 1014;
|
||||
const CAPABILITY_TEMPLATE = 1015;
|
||||
|
||||
/**
|
||||
* Returns the license type this installer file is made of.
|
||||
*
|
||||
* @return int Returns an enum type of License
|
||||
*/
|
||||
public static function getType()
|
||||
{
|
||||
// This is to avoid warnings in PHP 5.6 because isn't possibile declare an abstract static method.
|
||||
throw new Exception('This method must be extended');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return license limit
|
||||
*
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public static function getLimit()
|
||||
{
|
||||
// This is to avoid warnings in PHP 5.6 because isn't possibile declare an abstract static method.
|
||||
throw new Exception('This method must be extended');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return upsell URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getUpsellURL()
|
||||
{
|
||||
// This is to avoid warnings in PHP 5.6 because isn't possibile declare an abstract static method.
|
||||
throw new Exception('This method must be extended');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if license is unlimited
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isUnlimited()
|
||||
{
|
||||
return in_array(
|
||||
static::getType(),
|
||||
[
|
||||
self::TYPE_BUSINESS,
|
||||
self::TYPE_BUSINESS_AUTO,
|
||||
self::TYPE_GOLD,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if license have the capability
|
||||
*
|
||||
* @param int $capability capability key
|
||||
* @param ?int $license ENUM license type, if null Get currnt licnse type
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function can($capability, $license = null)
|
||||
{
|
||||
if (is_null($license)) {
|
||||
$license = static::getType();
|
||||
}
|
||||
|
||||
switch ($capability) {
|
||||
case self::CAPABILITY_PRO_BASE:
|
||||
return true;
|
||||
case self::CAPABILITY_IMPORT:
|
||||
return $license > 0;
|
||||
case self::CAPABILITY_MULTISITE:
|
||||
return $license > 0;
|
||||
case self::CAPABILITY_SCHEDULE:
|
||||
return $license > 0;
|
||||
case self::CAPABILITY_STORAGE:
|
||||
return $license > 0;
|
||||
case self::CAPABILITY_TEMPLATE:
|
||||
return $license > 0;
|
||||
case self::CAPABILITY_BRAND:
|
||||
case self::CAPABILITY_IMPORT_SETTINGS:
|
||||
case self::CAPABILITY_SHEDULE_HOURLY:
|
||||
case self::CAPABILITY_POWER_TOOLS:
|
||||
case self::CAPABILITY_UPDATE_AUTH:
|
||||
case self::CAPABILITY_CHANGE_TABLE_PREFIX:
|
||||
return in_array(
|
||||
$license,
|
||||
[
|
||||
self::TYPE_FREELANCER,
|
||||
self::TYPE_FREELANCER_AUTO,
|
||||
self::TYPE_BUSINESS,
|
||||
self::TYPE_BUSINESS_AUTO,
|
||||
self::TYPE_GOLD,
|
||||
self::TYPE_PLUS,
|
||||
self::TYPE_PRO,
|
||||
self::TYPE_ELITE,
|
||||
]
|
||||
);
|
||||
case self::CAPABILITY_MULTISITE_PLUS:
|
||||
case self::CAPABILITY_CAPABILITIES_MNG:
|
||||
case self::CAPABILITY_CAPABILITIES_MNG_PLUS:
|
||||
case self::CAPABILITY_PACKAGE_COMPONENTS_PLUS:
|
||||
return in_array(
|
||||
$license,
|
||||
[
|
||||
self::TYPE_BUSINESS,
|
||||
self::TYPE_BUSINESS_AUTO,
|
||||
self::TYPE_GOLD,
|
||||
self::TYPE_PRO,
|
||||
self::TYPE_ELITE,
|
||||
]
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if license can be upgraded
|
||||
*
|
||||
* @param ?int $license ENUM license type, if null Get currnt licnse type
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function canBeUpgraded($license = null)
|
||||
{
|
||||
if (is_null($license)) {
|
||||
$license = static::getType();
|
||||
}
|
||||
|
||||
return !in_array($license, [
|
||||
self::TYPE_BUSINESS,
|
||||
self::TYPE_BUSINESS_AUTO,
|
||||
self::TYPE_GOLD,
|
||||
self::TYPE_ELITE,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return license description
|
||||
*
|
||||
* @param ?int $license ENUM license type, if null Get currnt licnse type
|
||||
* @param bool $article if true add article before description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getLicenseToString($license = null, $article = false)
|
||||
{
|
||||
if (is_null($license)) {
|
||||
$license = static::getType();
|
||||
}
|
||||
|
||||
switch ($license) {
|
||||
case self::TYPE_UNLICENSED:
|
||||
return ($article ? 'an ' : '') . 'unlicensed';
|
||||
case self::TYPE_PERSONAL:
|
||||
case self::TYPE_PERSONAL_AUTO:
|
||||
return ($article ? 'a ' : '') . 'Personal';
|
||||
case self::TYPE_FREELANCER:
|
||||
case self::TYPE_FREELANCER_AUTO:
|
||||
return ($article ? 'a ' : '') . 'Freelancer';
|
||||
case self::TYPE_BUSINESS:
|
||||
case self::TYPE_BUSINESS_AUTO:
|
||||
return ($article ? 'a ' : '') . 'Business';
|
||||
case self::TYPE_GOLD:
|
||||
return ($article ? 'a ' : '') . 'Gold';
|
||||
case self::TYPE_BASIC:
|
||||
return ($article ? 'a ' : '') . 'Basic';
|
||||
case self::TYPE_PLUS:
|
||||
return ($article ? 'a ' : '') . 'Plus';
|
||||
case self::TYPE_PRO:
|
||||
return ($article ? 'a ' : '') . 'Pro';
|
||||
case self::TYPE_ELITE:
|
||||
return ($article ? 'a ' : '') . 'Elite';
|
||||
default:
|
||||
return ($article ? 'an ' : '') . 'unknown license type';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get best license from two given
|
||||
*
|
||||
* @param int $l1 ENUM license
|
||||
* @param int $l2 ENUM license
|
||||
*
|
||||
* @return int ENUM license
|
||||
*/
|
||||
protected static function getBestLicense($l1, $l2)
|
||||
{
|
||||
$l1Weight = 0;
|
||||
$l2Weight = 0;
|
||||
|
||||
$wheigts = [
|
||||
self::TYPE_UNLICENSED => -1,
|
||||
self::TYPE_BASIC => 0,
|
||||
self::TYPE_PERSONAL => 1,
|
||||
self::TYPE_PERSONAL_AUTO => 1,
|
||||
self::TYPE_PLUS => 2,
|
||||
self::TYPE_FREELANCER => 3,
|
||||
self::TYPE_FREELANCER_AUTO => 3,
|
||||
self::TYPE_PRO => 4,
|
||||
self::TYPE_ELITE => 5,
|
||||
self::TYPE_BUSINESS => 6,
|
||||
self::TYPE_BUSINESS_AUTO => 6,
|
||||
self::TYPE_GOLD => 7,
|
||||
];
|
||||
|
||||
$l1Weight = (isset($wheigts[$l1]) ? $wheigts[$l1] : -1 );
|
||||
$l2Weight = (isset($wheigts[$l2]) ? $wheigts[$l2] : -1 );
|
||||
|
||||
return ($l1Weight >= $l2Weight ? $l1 : $l2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* License class
|
||||
*
|
||||
* @category Duplicator
|
||||
* @package Installer
|
||||
* @author Snapcreek <admin@snapcreek.com>
|
||||
* @copyright 2011-2021 Snapcreek LLC
|
||||
* @license https://www.gnu.org/licenses/gpl-3.0.html GPLv3
|
||||
*/
|
||||
|
||||
namespace Duplicator\Installer\Addons\ProBase;
|
||||
|
||||
use Duplicator\Installer\Core\Params\Descriptors\DescriptorInterface;
|
||||
use Duplicator\Installer\Core\Params\Items\ParamForm;
|
||||
use Duplicator\Installer\Core\Params\PrmMng;
|
||||
use DUPX_U;
|
||||
|
||||
class AdvancedParams implements DescriptorInterface
|
||||
{
|
||||
/**
|
||||
* Init advanced params
|
||||
*
|
||||
* @param \Duplicator\Installer\Core\Params\Items\ParamItem[] $params params list
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(&$params)
|
||||
{
|
||||
$params[PrmMng::PARAM_GEN_WP_AUTH_KEY] = new ParamForm(
|
||||
PrmMng::PARAM_GEN_WP_AUTH_KEY,
|
||||
ParamForm::TYPE_BOOL,
|
||||
ParamForm::FORM_TYPE_CHECKBOX,
|
||||
array('default' => false),
|
||||
array(
|
||||
'label' => 'Auth Keys:',
|
||||
'checkboxLabel' => 'Generate New Unique Authentication Keys and Salts',
|
||||
'status' => (License::can(License::CAPABILITY_UPDATE_AUTH) ? ParamForm::STATUS_ENABLED : ParamForm::STATUS_DISABLED),
|
||||
'subNote' => (License::can(License::CAPABILITY_UPDATE_AUTH) ? '' : License::getLicenseUpdateText()),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update params after overwrite
|
||||
*
|
||||
* @param \Duplicator\Installer\Core\Params\Items\ParamItem[] $params params list
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function updateParamsAfterOverwrite($params)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Installer\Addons\ProBase;
|
||||
|
||||
use Duplicator\Installer\Core\Params\PrmMng;
|
||||
use DUPX_U;
|
||||
|
||||
class License extends AbstractLicense
|
||||
{
|
||||
/**
|
||||
* Returns the license type this installer file is made of.
|
||||
*
|
||||
* @return int Returns an enum type of License
|
||||
*/
|
||||
public static function getType()
|
||||
{
|
||||
return self::getBestLicense(self::getImporterLicense(), self::getInstallerLicense());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return license limit
|
||||
*
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public static function getLimit()
|
||||
{
|
||||
return (int) max(0, (int) \DUPX_ArchiveConfig::getInstance()->license_limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return upsell URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getUpsellURL()
|
||||
{
|
||||
return 'https://duplicator.com/my-account/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license on installer from package data
|
||||
*
|
||||
* @return int Returns an enum type of License
|
||||
*/
|
||||
protected static function getInstallerLicense()
|
||||
{
|
||||
return \DUPX_ArchiveConfig::getInstance()->license_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get importer license from params data
|
||||
*
|
||||
* @return int Returns an enum type of License
|
||||
*/
|
||||
protected static function getImporterLicense()
|
||||
{
|
||||
$overwriteData = PrmMng::getInstance()->getValue(PrmMng::PARAM_OVERWRITE_SITE_DATA);
|
||||
return isset($overwriteData['dupLicense']) ? $overwriteData['dupLicense'] : self::TYPE_UNLICENSED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return license required note
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getLicenseUpdateText()
|
||||
{
|
||||
return 'This option isn\'t available at the <b>' . static::getLicenseToString() . '</b> license level.' .
|
||||
'To enable this option ' .
|
||||
'<a href="' . DUPX_U::esc_url(static::getUpsellURL()) . '" target="_blank">' . 'upgrade' . '</a>' .
|
||||
' the License.';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\ProBase;
|
||||
|
||||
use Duplicator\Addons\ProBase\License\License;
|
||||
use Duplicator\Addons\ProBase\Models\LicenseData;
|
||||
|
||||
class DrmHandler
|
||||
{
|
||||
const SCHEDULE_DRM_DELAY_DAYS = 14;
|
||||
|
||||
/**
|
||||
* Return DRM activation days
|
||||
*
|
||||
* @return int -1 if has already expired, days left otherwise
|
||||
*/
|
||||
public static function getDaysTillDRM()
|
||||
{
|
||||
$status = LicenseData::getInstance()->getStatus();
|
||||
if ($status !== LicenseData::STATUS_VALID && $status !== LicenseData::STATUS_EXPIRED) {
|
||||
return -1;
|
||||
}
|
||||
if (($expiresDays = LicenseData::getInstance()->getExpirationDays()) === false) {
|
||||
return -1;
|
||||
}
|
||||
return (self::SCHEDULE_DRM_DELAY_DAYS + $expiresDays);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\ProBase\License;
|
||||
|
||||
use Duplicator\Addons\ProBase\Vendor\EDD\EDD_SL_Plugin_Updater;
|
||||
use DUP_PRO_Schedule_Entity;
|
||||
use Duplicator\Addons\ProBase\DrmHandler;
|
||||
use Duplicator\Addons\ProBase\Models\LicenseData;
|
||||
use Duplicator\Installer\Addons\ProBase\AbstractLicense;
|
||||
use Duplicator\Utils\ExpireOptions;
|
||||
|
||||
final class License extends AbstractLicense
|
||||
{
|
||||
/**
|
||||
* GENERAL SETTINGS
|
||||
*/
|
||||
const EDD_DUPPRO_STORE_URL = 'https://duplicator.com';
|
||||
const EDD_DUPPRO_ITEM_NAME = 'Duplicator Pro';
|
||||
const FRONTEND_CHECK_DELAY = 61; // Seconds, different fromgeneral frontend check to unsync
|
||||
const FRONTEND_CHECK_DELAY_OPTION_KEY = 'license_check';
|
||||
|
||||
const VISIBILITY_INFO = 0;
|
||||
const VISIBILITY_ALL = 1;
|
||||
const VISIBILITY_NONE = 2;
|
||||
|
||||
|
||||
/** @var ?EDD_SL_Plugin_Updater */
|
||||
private static $edd_updater = null;
|
||||
|
||||
/**
|
||||
* License check
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function check()
|
||||
{
|
||||
if (
|
||||
!is_admin() &&
|
||||
ExpireOptions::getUpdate(
|
||||
self::FRONTEND_CHECK_DELAY_OPTION_KEY,
|
||||
true,
|
||||
self::FRONTEND_CHECK_DELAY
|
||||
) !== false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
in_array(
|
||||
LicenseData::getInstance()->getStatus(),
|
||||
[
|
||||
LicenseData::STATUS_INVALID,
|
||||
LicenseData::STATUS_UNKNOWN,
|
||||
]
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::initEddUpdater();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if license have the capability
|
||||
*
|
||||
* @param int $capability capability key
|
||||
* @param ?int $license ENUM license type, if null Get currnt licnse type
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function can($capability, $license = null)
|
||||
{
|
||||
if (is_null($license)) {
|
||||
$license = static::getType();
|
||||
}
|
||||
|
||||
switch ($capability) {
|
||||
case self::CAPABILITY_SCHEDULE:
|
||||
if ($license > 0) {
|
||||
return true;
|
||||
}
|
||||
if (count(DUP_PRO_Schedule_Entity::get_active()) > 0 && DrmHandler::getDaysTillDRM() > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return parent::can($capability, $license);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force upgrade check
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function forceUpgradeCheck()
|
||||
{
|
||||
if (
|
||||
in_array(
|
||||
LicenseData::getInstance()->getStatus(),
|
||||
[
|
||||
LicenseData::STATUS_INVALID,
|
||||
LicenseData::STATUS_UNKNOWN,
|
||||
]
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::clearVersionCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return latest version of the plugin
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public static function getLatestVersion()
|
||||
{
|
||||
$version_info = null;
|
||||
$edd_updater = self::getEddUpdater();
|
||||
|
||||
/** @var false|object */
|
||||
$version_info = $edd_updater->get_cached_version_info();
|
||||
|
||||
if (is_object($version_info) && isset($version_info->new_version)) {
|
||||
return $version_info->new_version;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear version cache
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function clearVersionCache()
|
||||
{
|
||||
LicenseData::getInstance()->clearCache();
|
||||
self::getEddUpdater()->set_version_info_cache(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return license key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getLicenseKey()
|
||||
{
|
||||
return LicenseData::getInstance()->getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license status
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getLicenseStatus()
|
||||
{
|
||||
return LicenseData::getInstance()->getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return license type
|
||||
*
|
||||
* @return int ENUM AbstractLicense::TYPE_*
|
||||
*/
|
||||
public static function getType()
|
||||
{
|
||||
return (
|
||||
LicenseData::getInstance()->getStatus() == LicenseData::STATUS_VALID ?
|
||||
LicenseData::getInstance()->getLicenseType() :
|
||||
self::TYPE_UNLICENSED
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the EDD Updater
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function initEddUpdater()
|
||||
{
|
||||
if (self::$edd_updater !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dpro_edd_opts = array(
|
||||
'version' => DUPLICATOR_PRO_VERSION,
|
||||
'license' => self::getLicenseKey(),
|
||||
'item_name' => self::EDD_DUPPRO_ITEM_NAME,
|
||||
'author' => 'Snap Creek Software',
|
||||
'wp_override' => true,
|
||||
);
|
||||
|
||||
self::$edd_updater = new EDD_SL_Plugin_Updater(
|
||||
self::EDD_DUPPRO_STORE_URL,
|
||||
DUPLICATOR____FILE,
|
||||
$dpro_edd_opts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor that returns the EDD Updater singleton
|
||||
*
|
||||
* @return EDD_SL_Plugin_Updater
|
||||
*/
|
||||
private static function getEddUpdater()
|
||||
{
|
||||
if (self::$edd_updater === null) {
|
||||
self::initEddUpdater();
|
||||
}
|
||||
|
||||
return self::$edd_updater;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return upsell URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getUpsellURL()
|
||||
{
|
||||
return DUPLICATOR_PRO_BLOG_URL . 'my-account/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return no activation left message
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getNoActivationLeftMessage()
|
||||
{
|
||||
if (self::isUnlimited()) {
|
||||
$result = sprintf(__('%1$s site licenses are granted in batches of 500.', 'duplicator-pro'), License::getLicenseToString());
|
||||
$result .= ' ';
|
||||
$result .= sprintf(
|
||||
_x(
|
||||
'Please submit a %1$sticket request%2$s and we will grant you another batch.',
|
||||
'%1$s and %2$s represents the opening and closing HTML tags for an anchor or link',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a href="' . DUPLICATOR_PRO_BLOG_URL . 'my-account/support/" target="_blank">',
|
||||
'</a>'
|
||||
);
|
||||
$result .= '<br>';
|
||||
$result .= __('This process helps to ensure that licenses are not stolen or abused for users.', 'duplicator-pro');
|
||||
return $result;
|
||||
} else {
|
||||
return __(
|
||||
'Use the link above to login to your duplicator.com dashboard to manage your licenses or upgrade to a higher license.',
|
||||
'duplicator-pro'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\ProBase\License;
|
||||
|
||||
use Duplicator\Addons\ProBase\DrmHandler;
|
||||
use Duplicator\Addons\ProBase\LicensingController;
|
||||
use Duplicator\Addons\ProBase\Models\LicenseData;
|
||||
use Duplicator\Controllers\SettingsPageController;
|
||||
use Duplicator\Core\CapMng;
|
||||
use Duplicator\Core\Controllers\ControllersManager;
|
||||
use Duplicator\Core\Views\TplMng;
|
||||
use Duplicator\Views\AdminNotices;
|
||||
|
||||
class LicenseNotices
|
||||
{
|
||||
/**
|
||||
* Init notice actions
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init()
|
||||
{
|
||||
add_action('admin_init', array(__CLASS__, 'adminInit'));
|
||||
|
||||
$path = plugin_basename(DUPLICATOR____FILE);
|
||||
|
||||
// Important to make this priority 11 or greater to ensure the version cache is up to date by EDD
|
||||
add_action("after_plugin_row_{$path}", array(__CLASS__, 'noLicenseDisplay'), 11, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called on hook admin_init
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function adminInit()
|
||||
{
|
||||
$action = is_multisite() ? 'network_admin_notices' : 'admin_notices';
|
||||
add_action($action, array(__CLASS__, 'licenseAlertCheck'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called on hook admin_init
|
||||
*
|
||||
* @param string $file Path to the plugin file relative to the plugins directory
|
||||
* @param array<string, mixed> $plugin An array of plugin data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function noLicenseDisplay($file, $plugin)
|
||||
{
|
||||
$latest_version = License::getLatestVersion();
|
||||
|
||||
// Only display this message when there is no update message
|
||||
if (($latest_version === false) || version_compare(DUPLICATOR_PRO_VERSION, $latest_version, '>=')) {
|
||||
$error_string = null;
|
||||
|
||||
$licenseSettingsUrl = SettingsPageController::getInstance()->getMenuLink(
|
||||
LicensingController::L2_SLUG_LICENSING
|
||||
);
|
||||
|
||||
$licenseStatus = LicenseData::getInstance()->getStatus();
|
||||
|
||||
if ($licenseStatus === LicenseData::STATUS_INVALID || $licenseStatus === LicenseData::STATUS_SITE_INACTIVE) {
|
||||
$error_string = sprintf(
|
||||
esc_html_x(
|
||||
'Your Duplicator Pro license key is invalid so you aren\'t getting important updates!
|
||||
%1$sActivate your license%2$s or %3$spurchase a license%4$s.',
|
||||
'1,3: <a> tag, 2,4: </a> tag',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a href="' . esc_url($licenseSettingsUrl) . '">',
|
||||
'</a>',
|
||||
'<a target="_blank" href="' . esc_url(DUPLICATOR_PRO_BLOG_URL . 'pricing/') . '">',
|
||||
'</a>'
|
||||
);
|
||||
} elseif ($licenseStatus === LicenseData::STATUS_EXPIRED) {
|
||||
$license_key = License::getLicenseKey();
|
||||
|
||||
if ($license_key !== false) {
|
||||
$renewal_url = DUPLICATOR_PRO_BLOG_URL . 'checkout?edd_license_key=' . $license_key;
|
||||
|
||||
$error_string = sprintf(
|
||||
__(
|
||||
'Your Duplicator Pro license key has expired so you aren\'t getting important updates! %1$sRenew your license now%2$s',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a target="_blank" href="' . esc_url($renewal_url) . '">',
|
||||
'</a>'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($error_string != null) {
|
||||
ob_start();
|
||||
?>
|
||||
<script>jQuery("[data-slug=\'duplicator-pro\']").addClass("update");</script>
|
||||
<tr style="border-top-color:black" class="plugin-update-tr active" >
|
||||
<td colspan="4" class="plugin-update colspanchange">
|
||||
<div class="update-message notice inline notice-error notice-alt">
|
||||
<p>
|
||||
<?php
|
||||
echo wp_kses(
|
||||
$error_string,
|
||||
[
|
||||
'a' => [
|
||||
'href' => [],
|
||||
'target' => [],
|
||||
],
|
||||
]
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
ob_end_flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the WP action hook to detect the state of the endpoint license
|
||||
* which calls the various show* methods for which alert to display
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function licenseAlertCheck()
|
||||
{
|
||||
if (
|
||||
!CapMng::can(CapMng::CAP_BASIC, false) ||
|
||||
ControllersManager::isCurrentPage(
|
||||
ControllersManager::SETTINGS_SUBMENU_SLUG,
|
||||
LicensingController::L2_SLUG_LICENSING
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (file_exists(DUPLICATOR_PRO_SSDIR_PATH . "/ovr.dup")) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Style needs to be loaded here because css is global across wp-admin
|
||||
wp_enqueue_style(
|
||||
'dup-pro-plugin-style-notices',
|
||||
DUPLICATOR_PRO_PLUGIN_URL . 'assets/css/admin-notices.css',
|
||||
[],
|
||||
DUPLICATOR_PRO_VERSION
|
||||
);
|
||||
|
||||
$licenseData = LicenseData::getInstance();
|
||||
|
||||
switch ($licenseData->getStatus()) {
|
||||
case LicenseData::STATUS_VALID:
|
||||
break;
|
||||
case LicenseData::STATUS_EXPIRED:
|
||||
self::showExpired();
|
||||
break;
|
||||
case LicenseData::STATUS_UNKNOWN:
|
||||
case LicenseData::STATUS_INVALID:
|
||||
case LicenseData::STATUS_INACTIVE:
|
||||
case LicenseData::STATUS_DISABLED:
|
||||
case LicenseData::STATUS_SITE_INACTIVE:
|
||||
default:
|
||||
if ($licenseData->haveNoActivationsLeft()) {
|
||||
self::showNoActivationsLeft();
|
||||
} else {
|
||||
self::showInvalidStandardNag();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the smaller standard nag screen
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function showInvalidStandardNag()
|
||||
{
|
||||
$problem_text = 'missing';
|
||||
|
||||
$htmlMsg = TplMng::getInstance()->render(
|
||||
'licensing/notices/inactive_message',
|
||||
['problem' => $problem_text],
|
||||
false
|
||||
);
|
||||
|
||||
AdminNotices::displayGeneralAdminNotice(
|
||||
$htmlMsg,
|
||||
AdminNotices::GEN_ERROR_NOTICE,
|
||||
false,
|
||||
['dup-license-message'],
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the license count used up alert
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function showNoActivationsLeft()
|
||||
{
|
||||
$htmlMsg = TplMng::getInstance()->render(
|
||||
'licensing/notices/no_activation_left',
|
||||
[],
|
||||
false
|
||||
);
|
||||
AdminNotices::displayGeneralAdminNotice(
|
||||
$htmlMsg,
|
||||
AdminNotices::GEN_ERROR_NOTICE,
|
||||
false,
|
||||
['dup-license-message'],
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the expired message alert
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function showExpired()
|
||||
{
|
||||
$renewal_url = DUPLICATOR_PRO_BLOG_URL . 'checkout?edd_license_key=' . License::getLicenseKey();
|
||||
$htmlMsg = TplMng::getInstance()->render(
|
||||
'licensing/notices/expired',
|
||||
[
|
||||
'renewal_url' => $renewal_url,
|
||||
'schedule_disalbe_days_left' => DrmHandler::getDaysTillDRM(),
|
||||
'active_schedule_present' => count(\DUP_PRO_Schedule_Entity::get_active()) > 0,
|
||||
],
|
||||
false
|
||||
);
|
||||
AdminNotices::displayGeneralAdminNotice(
|
||||
$htmlMsg,
|
||||
AdminNotices::GEN_ERROR_NOTICE,
|
||||
false,
|
||||
['dup-license-message'],
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the upgrade link
|
||||
*
|
||||
* @param string $label The label of the link
|
||||
* @param bool $echo Whether to echo the link or return it
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getUpsellLinkHTML($label = 'Upgrade', $echo = true)
|
||||
{
|
||||
ob_start();
|
||||
?>
|
||||
<a class="dup-upgrade-license-link" href="<?php echo esc_url(License::getUpsellURL()); ?>" target="_blank">
|
||||
<?php echo esc_html($label); ?>
|
||||
</a>
|
||||
<?php
|
||||
if ($echo) {
|
||||
ob_end_flush();
|
||||
return '';
|
||||
} else {
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\ProBase\License;
|
||||
|
||||
class LicenseUtils
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Version Pro Base functionalities
|
||||
*
|
||||
* Name: Duplicator PRO base
|
||||
* Version: 1
|
||||
* Author: Snap Creek
|
||||
* Author URI: http://snapcreek.com
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\ProBase;
|
||||
|
||||
use DUP_PRO_Log;
|
||||
use Duplicator\Core\Controllers\ControllersManager;
|
||||
use Duplicator\Addons\ProBase\License\License;
|
||||
use Duplicator\Addons\ProBase\License\LicenseNotices;
|
||||
use Duplicator\Addons\ProBase\Models\LicenseData;
|
||||
use Duplicator\Controllers\SettingsPageController;
|
||||
use Duplicator\Core\CapMng;
|
||||
use Duplicator\Core\Controllers\PageAction;
|
||||
use Duplicator\Core\Controllers\SubMenuItem;
|
||||
use Duplicator\Core\Views\TplMng;
|
||||
use Duplicator\Libs\Snap\SnapUtil;
|
||||
use Duplicator\Libs\Snap\SnapWP;
|
||||
use Duplicator\Views\AdminNotices;
|
||||
use Exception;
|
||||
|
||||
class LicensingController
|
||||
{
|
||||
const L2_SLUG_LICENSING = 'licensing';
|
||||
|
||||
//License actions
|
||||
const ACTION_ACTIVATE_LICENSE = 'activate_license';
|
||||
const ACTION_DEACTIVATE_LICENSE = 'deactivate_license';
|
||||
const ACTION_CHANGE_VISIBILITY = 'change_visibility';
|
||||
const ACTION_CLEAR_KEY = 'clear_key';
|
||||
|
||||
const LICENSE_KEY_OPTION_AUTO_ACTIVE = 'duplicator_pro_license_auto_active';
|
||||
|
||||
/**
|
||||
* License controller init
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init()
|
||||
{
|
||||
add_action('admin_init', array(__CLASS__, 'licenseAutoActive'));
|
||||
add_action('admin_init', array(__CLASS__, 'forceUpgradeCheckAction'));
|
||||
add_filter('duplicator_sub_menu_items_' . ControllersManager::SETTINGS_SUBMENU_SLUG, array(__CLASS__, 'licenseSubMenu'));
|
||||
add_action('duplicator_render_page_content_' . ControllersManager::SETTINGS_SUBMENU_SLUG, array(__CLASS__, 'renderLicenseContent'), 10, 2);
|
||||
add_filter('duplicator_page_actions_' . ControllersManager::SETTINGS_SUBMENU_SLUG, array(__CLASS__, 'pageActions'));
|
||||
add_filter('duplicator_template_file', array(__CLASS__, 'getTemplateFile'), 10, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method call on admin_init hook
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function licenseAutoActive()
|
||||
{
|
||||
if (($lKey = get_option(self::LICENSE_KEY_OPTION_AUTO_ACTIVE, false)) === false) {
|
||||
return;
|
||||
}
|
||||
if (!CapMng::getInstance()->can(CapMng::CAP_LICENSE)) {
|
||||
return;
|
||||
}
|
||||
if (($action = SettingsPageController::getInstance()->getActionByKey(self::ACTION_ACTIVATE_LICENSE)) == false) {
|
||||
return;
|
||||
}
|
||||
delete_option(self::LICENSE_KEY_OPTION_AUTO_ACTIVE);
|
||||
$redirect = $action->getUrl(['_license_key' => $lKey]);
|
||||
if (wp_redirect($redirect)) {
|
||||
exit;
|
||||
} else {
|
||||
throw new Exception(__('Error redirecting to license activation page', 'duplicator-pro'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return force upgrade check URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getForceUpgradeCheckURL()
|
||||
{
|
||||
return SnapWP::adminUrl('update-core.php', ['force-check' => 1]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Force upgrade check action
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function forceUpgradeCheckAction()
|
||||
{
|
||||
global $pagenow;
|
||||
|
||||
if ($pagenow !== 'update-core.php') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SnapUtil::sanitizeBoolInput(SnapUtil::INPUT_REQUEST, 'force-check')) {
|
||||
return;
|
||||
}
|
||||
|
||||
License::forceUpgradeCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add license sub menu page
|
||||
*
|
||||
* @param SubMenuItem[] $subMenus sub menus
|
||||
*
|
||||
* @return SubMenuItem[]
|
||||
*/
|
||||
public static function licenseSubMenu($subMenus)
|
||||
{
|
||||
$subMenus[] = new SubMenuItem(self::L2_SLUG_LICENSING, __('Licensing', 'duplicator-pro'), '', CapMng::CAP_LICENSE, 100);
|
||||
return $subMenus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define actions related to the license
|
||||
*
|
||||
* @param PageAction[] $actions Page actions array from filter
|
||||
*
|
||||
* @return PageAction[] Updated page actions array
|
||||
*/
|
||||
public static function pageActions($actions)
|
||||
{
|
||||
$actions[] = new PageAction(
|
||||
self::ACTION_ACTIVATE_LICENSE,
|
||||
array(
|
||||
__CLASS__,
|
||||
'activateLicense',
|
||||
),
|
||||
array(
|
||||
ControllersManager::SETTINGS_SUBMENU_SLUG,
|
||||
self::L2_SLUG_LICENSING,
|
||||
)
|
||||
);
|
||||
$actions[] = new PageAction(
|
||||
self::ACTION_DEACTIVATE_LICENSE,
|
||||
array(
|
||||
__CLASS__,
|
||||
'deactivateLicense',
|
||||
),
|
||||
array(
|
||||
ControllersManager::SETTINGS_SUBMENU_SLUG,
|
||||
self::L2_SLUG_LICENSING,
|
||||
)
|
||||
);
|
||||
$actions[] = new PageAction(
|
||||
self::ACTION_CLEAR_KEY,
|
||||
array(
|
||||
__CLASS__,
|
||||
'clearLicenseKey',
|
||||
),
|
||||
array(
|
||||
ControllersManager::SETTINGS_SUBMENU_SLUG,
|
||||
self::L2_SLUG_LICENSING,
|
||||
)
|
||||
);
|
||||
$actions[] = new PageAction(
|
||||
self::ACTION_CHANGE_VISIBILITY,
|
||||
array(
|
||||
__CLASS__,
|
||||
'changeLicenseVisibility',
|
||||
),
|
||||
array(
|
||||
ControllersManager::SETTINGS_SUBMENU_SLUG,
|
||||
self::L2_SLUG_LICENSING,
|
||||
)
|
||||
);
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that changes the license visibility
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function changeLicenseVisibility()
|
||||
{
|
||||
$result = array(
|
||||
'license_success' => false,
|
||||
'license_message' => '',
|
||||
);
|
||||
$global = \DUP_PRO_Global_Entity::getInstance();
|
||||
$sglobal = \DUP_PRO_Secure_Global_Entity::getInstance();
|
||||
|
||||
$oldVisibility = $global->license_key_visible;
|
||||
$newVisibility = filter_input(INPUT_POST, 'license_key_visible', FILTER_VALIDATE_INT);
|
||||
$newPassword = SnapUtil::sanitizeInput(INPUT_POST, '_key_password', '');
|
||||
|
||||
if ($oldVisibility === $newVisibility) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
switch ($newVisibility) {
|
||||
case License::VISIBILITY_ALL:
|
||||
if ($sglobal->lkp !== $newPassword) {
|
||||
$result['license_message'] = __("Wrong password entered. Please enter the correct password.", 'duplicator-pro');
|
||||
return $result;
|
||||
}
|
||||
$newPassword = ''; // reset password
|
||||
break;
|
||||
case License::VISIBILITY_NONE:
|
||||
case License::VISIBILITY_INFO:
|
||||
if ($oldVisibility == License::VISIBILITY_ALL) {
|
||||
$password_confirmation = SnapUtil::sanitizeInput(INPUT_POST, '_key_password_confirmation', '');
|
||||
|
||||
if (strlen($newPassword) === 0) {
|
||||
$result['license_message'] = __('Password cannot be empty.', 'duplicator-pro');
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($newPassword !== $password_confirmation) {
|
||||
$result['license_message'] = __("Passwords don't match.", 'duplicator-pro');
|
||||
return $result;
|
||||
}
|
||||
} else {
|
||||
if ($sglobal->lkp !== $newPassword) {
|
||||
$result['license_message'] = __("Wrong password entered. Please enter the correct password.", 'duplicator-pro');
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Exception(__('Invalid license visibility value.', 'duplicator-pro'));
|
||||
}
|
||||
|
||||
$global->license_key_visible = $newVisibility;
|
||||
$sglobal->lkp = $newPassword;
|
||||
|
||||
if ($global->save() && $sglobal->save()) {
|
||||
return array(
|
||||
'license_success' => true,
|
||||
'license_message' => __("License visibility changed", 'duplicator-pro'),
|
||||
);
|
||||
} else {
|
||||
return array(
|
||||
'license_success' => false,
|
||||
'license_message' => __("Couldn't change licnse vilisiblity.", 'duplicator-pro'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that clears the license key
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function clearLicenseKey()
|
||||
{
|
||||
$global = \DUP_PRO_Global_Entity::getInstance();
|
||||
$sglobal = \DUP_PRO_Secure_Global_Entity::getInstance();
|
||||
|
||||
LicenseData::getInstance()->setKey('');
|
||||
License::clearVersionCache();
|
||||
|
||||
$global->license_key_visible = License::VISIBILITY_ALL;
|
||||
$sglobal->lkp = '';
|
||||
|
||||
if ($global->save() && $sglobal->save()) {
|
||||
return array(
|
||||
'license_success' => true,
|
||||
'license_message' => __("License key cleared", 'duplicator-pro'),
|
||||
);
|
||||
} else {
|
||||
return array(
|
||||
'license_success' => false,
|
||||
'license_message' => __("Couldn't save changes", 'duplicator-pro'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that deactivates the license
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function deactivateLicense()
|
||||
{
|
||||
$result = array(
|
||||
'license_success' => true,
|
||||
'license_message' => __("License Deactivated", 'duplicator-pro'),
|
||||
);
|
||||
|
||||
try {
|
||||
$lData = LicenseData::getInstance();
|
||||
|
||||
$lData->clearCache();
|
||||
if ($lData->getStatus() !== LicenseData::STATUS_VALID) {
|
||||
$result = array(
|
||||
'license_success' => true,
|
||||
'license_message' => __('License already deactivated.', 'duplicator-pro'),
|
||||
);
|
||||
return $result;
|
||||
}
|
||||
|
||||
switch ($lData->deactivate()) {
|
||||
case LicenseData::ACTIVATION_RESPONSE_OK:
|
||||
break;
|
||||
case LicenseData::ACTIVATION_RESPONSE_INVALID:
|
||||
throw new Exception(__('Invalid license key.', 'duplicator-pro'));
|
||||
case LicenseData::ACTIVATION_REQUEST_ERROR:
|
||||
$result['license_request_error'] = $lData->getLastRequestError();
|
||||
throw new Exception(self::getRequestErrorMessage());
|
||||
default:
|
||||
throw new Exception(__('Error activating license.', 'duplicator-pro'));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$result['license_success'] = false;
|
||||
$result['license_message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, 'licensing/') === 0) {
|
||||
return ProBase::getAddonPath() . '/template/' . $slugTpl . '.php';
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action that activates the license
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function activateLicense()
|
||||
{
|
||||
$result = array(
|
||||
'license_success' => true,
|
||||
'license_message' => __("License Activated", 'duplicator-pro'),
|
||||
);
|
||||
|
||||
try {
|
||||
if (($licenseKey = SnapUtil::sanitizeDefaultInput(SnapUtil::INPUT_REQUEST, '_license_key')) === false) {
|
||||
throw new Exception(__('Please enter a valid key. Key should be 32 characters long.', 'duplicator-pro'));
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-f0-9]{32}$/i', $licenseKey)) {
|
||||
throw new Exception(__('Please enter a valid key. Key should be 32 characters long.', 'duplicator-pro'));
|
||||
}
|
||||
$lData = LicenseData::getInstance();
|
||||
// make sure reset old license key if exists
|
||||
self::clearLicenseKey();
|
||||
$lData->setKey($licenseKey);
|
||||
|
||||
switch ($lData->activate()) {
|
||||
case LicenseData::ACTIVATION_RESPONSE_OK:
|
||||
break;
|
||||
case LicenseData::ACTIVATION_RESPONSE_INVALID:
|
||||
throw new Exception(__('Invalid license key.', 'duplicator-pro'));
|
||||
case LicenseData::ACTIVATION_REQUEST_ERROR:
|
||||
$result['license_request_error'] = $lData->getLastRequestError();
|
||||
DUP_PRO_Log::traceObject('License request error', $result['license_request_error']);
|
||||
throw new Exception(self::getRequestErrorMessage());
|
||||
default:
|
||||
throw new Exception(__('Error activating license.', 'duplicator-pro'));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$result['license_success'] = false;
|
||||
$result['license_message'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render page content
|
||||
*
|
||||
* @param string[] $currentLevelSlugs current menu slugs
|
||||
* @param string $innerPage current inner page, empty if not set
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function renderLicenseContent($currentLevelSlugs, $innerPage)
|
||||
{
|
||||
switch ($currentLevelSlugs[1]) {
|
||||
case self::L2_SLUG_LICENSING:
|
||||
self::renderLicenseMessage();
|
||||
TplMng::getInstance()->render('licensing/main');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render activation/deactivation license message
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function renderLicenseMessage()
|
||||
{
|
||||
$tplData = TplMng::getInstance()->getGlobalData();
|
||||
if (empty($tplData['license_message'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$success = (isset($tplData['license_success']) && $tplData['license_success'] === true);
|
||||
AdminNotices::displayGeneralAdminNotice(
|
||||
TplMng::getInstance()->render('licensing/notices/activation_message', [], false),
|
||||
($success ? AdminNotices::GEN_SUCCESS_NOTICE : AdminNotices::GEN_ERROR_NOTICE),
|
||||
false,
|
||||
[],
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* License type viewer
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function displayLicenseInfo()
|
||||
{
|
||||
$license_type = License::getType();
|
||||
|
||||
if ($license_type === License::TYPE_UNLICENSED) {
|
||||
echo sprintf('<b>%s</b>', esc_html__("Unlicensed", 'duplicator-pro'));
|
||||
} else {
|
||||
echo '<b>' . esc_html(License::getLicenseToString()) . '</b> ';
|
||||
if (License::canBeUpgraded()) {
|
||||
LicenseNotices::getUpsellLinkHTML('[' . __('upgrade', 'duplicator-pro') . ']');
|
||||
}
|
||||
|
||||
$pt_class = License::can(License::CAPABILITY_POWER_TOOLS) ? 'far fa-check-circle' : 'far fa-circle';
|
||||
$mup_class = License::can(License::CAPABILITY_MULTISITE_PLUS) ? 'far fa-check-circle' : 'far fa-circle';
|
||||
|
||||
$txt_lic_hdr = __('Site Licenses', 'duplicator-pro');
|
||||
$txt_lic_msg = __(
|
||||
'Indicates the number of sites the plugin can be active on at any one time.
|
||||
At any point you may deactivate/uninstall the plugin to free up the license and use the plugin elsewhere if needed.',
|
||||
'duplicator-pro'
|
||||
);
|
||||
$txt_pt_hdr = __('Powertools', 'duplicator-pro');
|
||||
$txt_pt_msg = __(
|
||||
'Enhanced features that greatly improve the productivity of serious users. Include hourly schedules,
|
||||
installer branding, salt & key replacement, priority support and more.',
|
||||
'duplicator-pro'
|
||||
);
|
||||
$txt_mup_hdr = __('Multisite Plus+', 'duplicator-pro');
|
||||
$txt_mup_msg = __(
|
||||
'Adds the ability to install a subsite as a standalone site,
|
||||
insert a standalone site into a multisite, or insert a subsite from the same/different multisite into a multisite.',
|
||||
'duplicator-pro'
|
||||
);
|
||||
|
||||
$lic_limit = (License::isUnlimited() ? __('unlimited', 'duplicator-pro') : LicenseData::getInstance()->getLicenseLimit());
|
||||
$site_count = (LicenseData::getInstance()->getSiteCount() < 0 ? '?' : LicenseData::getInstance()->getSiteCount());
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div class="dup-license-type-info">
|
||||
<i class='far fa-check-circle'></i>
|
||||
<?php echo esc_html($txt_lic_hdr) . ': ' . esc_html($site_count) . ' of ' . esc_html($lic_limit); ?>
|
||||
<i
|
||||
class='fa fa-question-circle fa-sm'
|
||||
data-tooltip-title='<?php echo esc_attr($txt_lic_hdr) ?>'
|
||||
data-tooltip='<?php echo esc_attr($txt_lic_msg) ?>'
|
||||
>
|
||||
</i>
|
||||
<br/>
|
||||
<i class="<?php echo esc_attr($pt_class); ?>"></i>
|
||||
<?php echo esc_html($txt_pt_hdr) ?>
|
||||
<i
|
||||
class='fa fa-question-circle fa-sm'
|
||||
data-tooltip-title='<?php echo esc_attr($txt_pt_hdr) ?>'
|
||||
data-tooltip='<?php echo esc_attr($txt_pt_msg) ?>'
|
||||
>
|
||||
</i>
|
||||
<br/>
|
||||
<i class="<?php echo esc_attr($mup_class) ?>"></i>
|
||||
<?php echo esc_html($txt_mup_hdr) ?>
|
||||
<i
|
||||
class='fa fa-question-circle fa-sm'
|
||||
data-tooltip-title='<?php echo esc_attr($txt_mup_hdr) ?>'
|
||||
data-tooltip='<?php echo esc_attr($txt_mup_msg) ?>'
|
||||
>
|
||||
</i>
|
||||
<br/>
|
||||
</div>
|
||||
<?php
|
||||
ob_end_flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the communication error message
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function getRequestErrorMessage()
|
||||
{
|
||||
$result = sprintf(
|
||||
__('<b>License data request failed.</b> (URL: %1$s)', 'duplicator-pro'),
|
||||
License::EDD_DUPPRO_STORE_URL
|
||||
);
|
||||
$result .= '<br>';
|
||||
$result .= sprintf(
|
||||
_x(
|
||||
'Please see %1$sthis FAQ entry%2$s for possible causes and resolutions.',
|
||||
'%1$s and %2$s represents the opening and closing HTML tags for an anchor or link',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a href="' . DUPLICATOR_PRO_DUPLICATOR_DOCS_URL . 'how-to-resolve-license-activation-issues/" target="_blank">',
|
||||
'</a>'
|
||||
);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
* @package Duplicator
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\ProBase\Models;
|
||||
|
||||
use DateTime;
|
||||
use DUP_PRO_Global_Entity;
|
||||
use DUP_PRO_Log;
|
||||
use Duplicator\Addons\ProBase\License\License;
|
||||
use Duplicator\Core\Models\AbstractEntitySingleton;
|
||||
use Duplicator\Installer\Addons\ProBase\AbstractLicense;
|
||||
use Duplicator\Libs\Snap\SnapUtil;
|
||||
use Duplicator\Utils\Crypt\CryptBlowfish;
|
||||
use VendorDuplicator\Amk\JsonSerialize\JsonSerialize;
|
||||
use WP_Error;
|
||||
|
||||
class LicenseData extends AbstractEntitySingleton
|
||||
{
|
||||
/**
|
||||
* GENERAL SETTINGS
|
||||
*/
|
||||
const LICENSE_CACHE_TIME = 7 * DAY_IN_SECONDS;
|
||||
const LICENSE_OLD_KEY_OPTION_NAME = 'duplicator_pro_license_key';
|
||||
|
||||
/**
|
||||
* LICENSE STATUS
|
||||
*/
|
||||
const STATUS_UNKNOWN = -1;
|
||||
const STATUS_VALID = 0;
|
||||
const STATUS_INVALID = 1;
|
||||
const STATUS_INACTIVE = 2;
|
||||
const STATUS_DISABLED = 3;
|
||||
const STATUS_SITE_INACTIVE = 4;
|
||||
const STATUS_EXPIRED = 5;
|
||||
|
||||
/**
|
||||
* ACTIVATION REPONSE
|
||||
*/
|
||||
const ACTIVATION_RESPONSE_OK = 0;
|
||||
const ACTIVATION_REQUEST_ERROR = -1;
|
||||
const ACTIVATION_RESPONSE_INVALID = -2;
|
||||
|
||||
const DEFAULT_LICENSE_DATA = [
|
||||
'success' => false,
|
||||
'license' => 'invalid',
|
||||
'item_id' => false,
|
||||
'item_name' => '',
|
||||
'checksum' => '',
|
||||
'expires' => '',
|
||||
'payment_id' => -1,
|
||||
'customer_name' => '',
|
||||
'customer_email' => '',
|
||||
'license_limit' => -1,
|
||||
'site_count' => -1,
|
||||
'activations_left' => -1,
|
||||
'price_id' => AbstractLicense::TYPE_UNLICENSED,
|
||||
'activeSubscription' => false,
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
protected $licenseKey = '';
|
||||
/** @var int */
|
||||
protected $status = self::STATUS_INVALID;
|
||||
/** @var int */
|
||||
protected $type = AbstractLicense::TYPE_UNKNOWN;
|
||||
/** @var array<string,scalar> License remote data */
|
||||
protected $data = self::DEFAULT_LICENSE_DATA;
|
||||
/** @var string timestamp YYYY-MM-DD HH:MM:SS UTC */
|
||||
protected $lastRemoteUpdate = '';
|
||||
/**
|
||||
* Last error request
|
||||
*
|
||||
* @var array{code:int, message: string, details: string, requestDetails: string}
|
||||
*/
|
||||
protected $lastRequestError = [
|
||||
'code' => 0,
|
||||
'message' => '',
|
||||
'details' => '',
|
||||
'requestDetails' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*/
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Return entity type identifier
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getType()
|
||||
{
|
||||
return 'LicenseDataEntity';
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be called, automatically, when Serialize
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function __serialize() // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__serializeFound
|
||||
{
|
||||
$data = JsonSerialize::serializeToData($this, JsonSerialize::JSON_SKIP_MAGIC_METHODS | JsonSerialize::JSON_SKIP_CLASS_NAME);
|
||||
if (DUP_PRO_Global_Entity::getInstance()->crypt) {
|
||||
$data['licenseKey'] = CryptBlowfish::encrypt($data['licenseKey'], null, true);
|
||||
$data['status'] = CryptBlowfish::encrypt($data['status'], null, true);
|
||||
$data['type'] = CryptBlowfish::encrypt($data['type'], null, true);
|
||||
$data['data'] = CryptBlowfish::encrypt(JsonSerialize::serialize($this->data), null, true);
|
||||
}
|
||||
unset($data['lastRequestError']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize
|
||||
*
|
||||
* Wakeup method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
if (DUP_PRO_Global_Entity::getInstance()->crypt) {
|
||||
$this->licenseKey = CryptBlowfish::decrypt((string) $this->licenseKey, null, true);
|
||||
$this->status = (int) CryptBlowfish::decrypt((string) $this->status, null, true);
|
||||
$this->type = (int) CryptBlowfish::decrypt((string) $this->type, null, true);
|
||||
/** @var string PHP stan fix*/
|
||||
$dataString = $this->data;
|
||||
$this->data = JsonSerialize::unserialize(CryptBlowfish::decrypt($dataString, null, true));
|
||||
}
|
||||
|
||||
if (!is_array($this->data)) {
|
||||
$this->data = self::DEFAULT_LICENSE_DATA;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set license key
|
||||
*
|
||||
* @param string $licenseKey License key, if empty the license key will be removed
|
||||
*
|
||||
* @return bool return true if license key is valid and set
|
||||
*/
|
||||
public function setKey($licenseKey)
|
||||
{
|
||||
if ($this->licenseKey === $licenseKey) {
|
||||
return true;
|
||||
}
|
||||
if ($this->getStatus() === self::STATUS_VALID) {
|
||||
// Deactivate old license
|
||||
$this->deactivate();
|
||||
}
|
||||
if (preg_match('/^[a-f0-9]{32}$/i', $licenseKey) === 1) {
|
||||
$this->licenseKey = $licenseKey;
|
||||
} else {
|
||||
$this->licenseKey = '';
|
||||
}
|
||||
return $this->clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKey()
|
||||
{
|
||||
return $this->licenseKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset license data cache
|
||||
*
|
||||
* @param bool $save if true save the entity
|
||||
*
|
||||
* @return bool return true if license data cache is reset
|
||||
*/
|
||||
public function clearCache($save = true)
|
||||
{
|
||||
$this->data = self::DEFAULT_LICENSE_DATA;
|
||||
$this->status = self::STATUS_INVALID;
|
||||
$this->type = AbstractLicense::TYPE_UNKNOWN;
|
||||
$this->lastRemoteUpdate = '';
|
||||
return ($save ? $this->save() : true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license data.
|
||||
* This function manage the license data cache.
|
||||
*
|
||||
* @return false|array<string,scalar> License data
|
||||
*/
|
||||
public function getLicenseData() {
|
||||
// Override default license data to simulate a valid license.
|
||||
$this->data = [
|
||||
'success' => true,
|
||||
'license' => 'valid',
|
||||
'item_id' => 31,
|
||||
'item_name' => $this->plugin_name ?? '',
|
||||
'checksum' => '1415b451be1a13c283ba771ea52d38bb',
|
||||
'expires' => 'lifetime',
|
||||
// 'payment_id' => 31,
|
||||
'customer_name' => 'GPL',
|
||||
'customer_email' => 'noreply@gmail.com',
|
||||
'license_limit' => 1000,
|
||||
'site_count' => 1,
|
||||
'activations_left' => 999,
|
||||
'price_id' => '11',
|
||||
'activeSubscription' => true,
|
||||
];
|
||||
|
||||
$this->status = self::STATUS_VALID;
|
||||
$this->type = AbstractLicense::TYPE_ELITE;
|
||||
$this->lastRemoteUpdate = gmdate("Y-m-d H:i:s"); // Update with the current time.
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate license key
|
||||
*
|
||||
* @return int license status
|
||||
*/
|
||||
public function activate() {
|
||||
DUP_PRO_Log::trace("License considered activated.");
|
||||
return self::ACTIVATION_RESPONSE_OK;
|
||||
}
|
||||
|
||||
public function deactivate() {
|
||||
DUP_PRO_Log::trace("License considered deactivated.");
|
||||
return self::ACTIVATION_RESPONSE_OK;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get license status
|
||||
*
|
||||
* @return int ENUM self::STATUS_*
|
||||
*/
|
||||
public function getStatus()
|
||||
{
|
||||
if ($this->getLicenseData() === false) {
|
||||
return self::STATUS_INVALID;
|
||||
}
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license type
|
||||
*
|
||||
* @return int ENUM AbstractLicense::TYPE_*
|
||||
*/
|
||||
public function getLicenseType()
|
||||
{
|
||||
if ($this->getLicenseData() === false) {
|
||||
return AbstractLicense::TYPE_UNKNOWN;
|
||||
}
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license websites limit
|
||||
*
|
||||
* @return int<0, max>
|
||||
*/
|
||||
public function getLicenseLimit()
|
||||
{
|
||||
if ($this->getLicenseData() === false) {
|
||||
return 0;
|
||||
}
|
||||
return (int) max(0, (int) $this->data['license_limit']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get site count
|
||||
*
|
||||
* @return int<-1, max>
|
||||
*/
|
||||
public function getSiteCount()
|
||||
{
|
||||
if ($this->getLicenseData() === false) {
|
||||
return -1;
|
||||
}
|
||||
return (int) max(-1, (int) $this->data['site_count']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate license key
|
||||
*
|
||||
* @return int license status
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Get expiration date format
|
||||
*
|
||||
* @param string $format date format
|
||||
*
|
||||
* @return string return expirtation date formatted, Unknown if license data is not available or Lifetime if license is lifetime
|
||||
*/
|
||||
public function getExpirationDate($format = 'Y-m-d')
|
||||
{
|
||||
if ($this->getLicenseData() === false) {
|
||||
return 'Unknown';
|
||||
}
|
||||
if ($this->data['expires'] === 'lifetime') {
|
||||
return 'Lifetime';
|
||||
}
|
||||
if (empty($this->data['expires'])) {
|
||||
return 'Unknown';
|
||||
}
|
||||
$expirationDate = new DateTime($this->data['expires']);
|
||||
return $expirationDate->format($format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return expiration license days, if is expired a negative number is returned
|
||||
*
|
||||
* @return false|int reutrn false on fail or number of days to expire, PHP_INT_MAX is filetime
|
||||
*/
|
||||
public function getExpirationDays()
|
||||
{
|
||||
if ($this->getLicenseData() === false) {
|
||||
return false;
|
||||
}
|
||||
if ($this->data['expires'] === 'lifetime') {
|
||||
return PHP_INT_MAX;
|
||||
}
|
||||
if (empty($this->data['expires'])) {
|
||||
return false;
|
||||
}
|
||||
$expirationDate = new DateTime($this->data['expires']);
|
||||
return (-1 * intval($expirationDate->diff(new DateTime())->format('%r%a')));
|
||||
}
|
||||
|
||||
/**
|
||||
* check is have no activations left
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function haveNoActivationsLeft()
|
||||
{
|
||||
return ($this->getStatus() === self::STATUS_SITE_INACTIVE && $this->data['activations_left'] === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if have active subscription
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function haveActiveSubscription()
|
||||
{
|
||||
if ($this->getLicenseData() === false) {
|
||||
return false;
|
||||
}
|
||||
return $this->data['activeSubscription'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a license rquest
|
||||
*
|
||||
* @param mixed[] $params request params
|
||||
*
|
||||
* @return false|object
|
||||
*/
|
||||
public function request($params) {
|
||||
// Simulate a successful license check response.
|
||||
$response = (object)[
|
||||
'license' => 'valid',
|
||||
'item_name' => License::EDD_DUPPRO_ITEM_NAME,
|
||||
// Add other necessary fields here.
|
||||
];
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get last error request
|
||||
*
|
||||
* @return array{code:int, message: string, details: string}
|
||||
*/
|
||||
public function getLastRequestError()
|
||||
{
|
||||
return $this->lastRequestError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license status from status by string
|
||||
*
|
||||
* @param string $eddStatus license status string
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function getStatusFromEDDStatus($eddStatus)
|
||||
{
|
||||
switch ($eddStatus) {
|
||||
case 'valid':
|
||||
return self::STATUS_VALID;
|
||||
case 'invalid':
|
||||
return self::STATUS_INVALID;
|
||||
case 'expired':
|
||||
return self::STATUS_EXPIRED;
|
||||
case 'disabled':
|
||||
return self::STATUS_DISABLED;
|
||||
case 'site_inactive':
|
||||
return self::STATUS_SITE_INACTIVE;
|
||||
case 'inactive':
|
||||
return self::STATUS_INACTIVE;
|
||||
default:
|
||||
return self::STATUS_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return license statu string by status
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLicenseStatusString()
|
||||
{
|
||||
switch ($this->getStatus()) {
|
||||
case self::STATUS_VALID:
|
||||
return __('Valid', 'duplicator-pro');
|
||||
case self::STATUS_INVALID:
|
||||
return __('Invalid', 'duplicator-pro');
|
||||
case self::STATUS_EXPIRED:
|
||||
return __('Expired', 'duplicator-pro');
|
||||
case self::STATUS_DISABLED:
|
||||
return __('Disabled', 'duplicator-pro');
|
||||
case self::STATUS_SITE_INACTIVE:
|
||||
return __('Site Inactive', 'duplicator-pro');
|
||||
case self::STATUS_EXPIRED:
|
||||
return __('Expired', 'duplicator-pro');
|
||||
default:
|
||||
return __('Unknown', 'duplicator-pro');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\ProBase\License\License;
|
||||
use Duplicator\Addons\ProBase\LicensingController;
|
||||
use Duplicator\Addons\ProBase\Models\LicenseData;
|
||||
use Duplicator\Core\Controllers\ControllersManager;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
*/
|
||||
|
||||
$global = DUP_PRO_Global_Entity::getInstance();
|
||||
LicenseData::getInstance()->clearCache();
|
||||
|
||||
$license_status = LicenseData::getInstance()->getStatus();
|
||||
$license_type = License::getType();
|
||||
$license_text_disabled = false;
|
||||
$activate_button_text = __('Activate', 'duplicator-pro');
|
||||
$license_status_text_alt = false;
|
||||
|
||||
|
||||
switch ($license_status) {
|
||||
case LicenseData::STATUS_VALID:
|
||||
$license_status_style = 'color:#509B18';
|
||||
$activate_button_text = __('Deactivate', 'duplicator-pro');
|
||||
$license_text_disabled = true;
|
||||
|
||||
$license_key = License::getLicenseKey();
|
||||
|
||||
$license_status_text = '<b>' . __('Status: ', 'duplicator-pro') . '</b>' . __('Active', 'duplicator-pro');
|
||||
$license_status_text .= '<br/>';
|
||||
$license_status_text .= '<b>' . __('Expiration: ', 'duplicator-pro') . '</b>';
|
||||
$license_status_text .= LicenseData::getInstance()->getExpirationDate(get_option('date_format'));
|
||||
$expDays = LicenseData::getInstance()->getExpirationDays();
|
||||
|
||||
if ($expDays === false) {
|
||||
$expDays = __('no data', 'duplicator-pro');
|
||||
} elseif ($expDays <= 0) {
|
||||
$expDays = __('expired', 'duplicator-pro');
|
||||
} elseif ($expDays == PHP_INT_MAX) {
|
||||
$expDays = __('no expiration', 'duplicator-pro');
|
||||
} else {
|
||||
$expDays = sprintf(__('%d days left', 'duplicator-pro'), $expDays);
|
||||
}
|
||||
|
||||
$license_status_text .= ' (<b>' . $expDays . '</b>)';
|
||||
break;
|
||||
case LicenseData::STATUS_INACTIVE:
|
||||
$license_status_style = 'color:#dd3d36;';
|
||||
$license_status_text = __('Status: Inactive', 'duplicator-pro');
|
||||
break;
|
||||
case LicenseData::STATUS_SITE_INACTIVE:
|
||||
$license_status_style = 'color:#dd3d36;';
|
||||
$global = DUP_PRO_Global_Entity::getInstance();
|
||||
|
||||
if (LicenseData::getInstance()->haveNoActivationsLeft()) {
|
||||
$license_status_text = __('Status: Inactive (out of site licenses).', 'duplicator-pro') . '<br>' . License::getNoActivationLeftMessage();
|
||||
} else {
|
||||
$license_status_text = __('Status: Inactive', 'duplicator-pro');
|
||||
}
|
||||
break;
|
||||
case LicenseData::STATUS_EXPIRED:
|
||||
$renewal_url = DUPLICATOR_PRO_BLOG_URL . 'checkout?edd_license_key=' . License::getLicenseKey();
|
||||
$license_status_style = 'color:#dd3d36;';
|
||||
$license_status_text = sprintf(
|
||||
_x(
|
||||
'Your Duplicator Pro license key has expired so you aren\'t getting important updates! %1$sRenew your license now%2$s',
|
||||
'1: <a> tag, 2: </a> tag',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a target="_blank" href="' . $renewal_url . '">',
|
||||
'</a>'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// https://duplicator.com/knowledge-base/how-to-resolve-license-activation-issues/
|
||||
$license_status_style = 'color:#dd3d36;';
|
||||
$license_status_text = '<b>' . __('Status: ', 'duplicator-pro') . '</b>' .
|
||||
LicenseData::getInstance()->getLicenseStatusString() . '<br/>';
|
||||
$license_status_text_alt = true;
|
||||
break;
|
||||
}
|
||||
?>
|
||||
|
||||
|
||||
<form
|
||||
id="dup-license-activation-form"
|
||||
action="<?php echo esc_url(ControllersManager::getCurrentLink()); ?>"
|
||||
method="post"
|
||||
data-parsley-validate
|
||||
>
|
||||
<h3 class="title"><?php esc_html_e('Activation', 'duplicator-pro') ?> </h3>
|
||||
<hr size="1" />
|
||||
<table class="form-table">
|
||||
<?php
|
||||
if ($global->license_key_visible !== License::VISIBILITY_NONE) : ?>
|
||||
<tr valign="top" id="dup-tr-license-dashboard">
|
||||
<th scope="row"><?php esc_html_e('Dashboard', 'duplicator-pro') ?></th>
|
||||
<td>
|
||||
<i class="fa fa-th-large fa-sm"></i>
|
||||
<a target="_blank" href="<?php echo esc_url(DUPLICATOR_PRO_BLOG_URL . 'my-account'); ?>">
|
||||
<?php
|
||||
esc_html_e('Manage Account Online', 'duplicator-pro')
|
||||
?>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr valign="top" id="dup-tr-license-type">
|
||||
<th scope="row"><?php esc_html_e('License Type', 'duplicator-pro') ?></th>
|
||||
<td class="dup-license-type">
|
||||
<?php LicensingController::displayLicenseInfo(); ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if ($global->license_key_visible === License::VISIBILITY_ALL) : ?>
|
||||
<tr valign="top" id="dup-tr-license-key-and-description">
|
||||
<th scope="row">
|
||||
<label><?php esc_html_e('License Key', 'duplicator-pro'); ?></label>
|
||||
</th>
|
||||
<td class="dup-license-key-area">
|
||||
<input
|
||||
type="text"
|
||||
class="dup-license-key-input"
|
||||
name="_license_key"
|
||||
id="_license_key"
|
||||
value="<?php echo esc_attr(License::getLicenseKey()); ?>">
|
||||
<br>
|
||||
<p class="description">
|
||||
<span style="<?php echo esc_attr($license_status_style); ?>" >
|
||||
<?php
|
||||
echo wp_kses(
|
||||
$license_status_text,
|
||||
[
|
||||
'a' => [
|
||||
'href' => [],
|
||||
'target' => [],
|
||||
],
|
||||
'b' => [],
|
||||
'br' => [],
|
||||
]
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
<?php
|
||||
if ($license_status_text_alt) {
|
||||
esc_html_e('If license activation fails please wait a few minutes and retry.', 'duplicator-pro');
|
||||
?>
|
||||
<div class="dup-license-status-notes ">
|
||||
<?php
|
||||
printf(
|
||||
esc_html_x(
|
||||
'- Failure to activate after several attempts please review %1$sfaq activation steps%2$s.',
|
||||
'1 and 2 represent opening and closing anchor tags (<a> and </a>)',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a target="_blank" href="' . esc_url(DUPLICATOR_PRO_DUPLICATOR_DOCS_URL . 'how-to-resolve-license-activation-issues/') . '">',
|
||||
'</a>'
|
||||
);
|
||||
?>
|
||||
<br/>
|
||||
<?php
|
||||
printf(
|
||||
esc_html_x(
|
||||
'- To upgrade or renew your license visit %1$sduplicator.com%2$s.',
|
||||
'1 and 2 represent opening and closing anchor tags (<a> and </a>)',
|
||||
'duplicator-pro'
|
||||
),
|
||||
'<a target="_blank" href="' . esc_url(DUPLICATOR_PRO_BLOG_URL) . '">',
|
||||
'</a>'
|
||||
);
|
||||
?>
|
||||
<br/>
|
||||
<?php esc_html_e('- A valid key is needed for plugin updates but not for functionality.', 'duplicator-pro'); ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif;?>
|
||||
<tr>
|
||||
<th scope="row" class="dup-license-key-btns">
|
||||
<label><?php esc_html_e('License Action', 'duplicator-pro'); ?></label>
|
||||
</th>
|
||||
<td class="dup-license-key-btns">
|
||||
<?php $echostring = (($license_status != LicenseData::STATUS_VALID) ? 'true' : 'false'); ?>
|
||||
<div class="dup-license-key-btns">
|
||||
<?php if ($global->license_key_visible === License::VISIBILITY_ALL) : ?>
|
||||
<button
|
||||
id="dup-license-activation-btn"
|
||||
class="button"
|
||||
onclick="DupPro.Licensing.ChangeActivationStatus(<?php echo esc_js($echostring); ?>);return false;">
|
||||
<?php echo esc_html($activate_button_text); ?>
|
||||
</button>
|
||||
<?php endif;?>
|
||||
<button
|
||||
id="dup-license-clear-btn"
|
||||
class="button"
|
||||
onclick="DupPro.Licensing.ClearActivationStatus();return false;"
|
||||
>
|
||||
<?php esc_html_e('Clear Key', 'duplicator-pro') ?>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\ProBase\License\License;
|
||||
use Duplicator\Addons\ProBase\LicensingController;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
*/
|
||||
|
||||
$tplMng->render('licensing/activation');
|
||||
$tplMng->render('licensing/visibility');
|
||||
?>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
DupPro.Licensing = new Object();
|
||||
DupPro.Licensing.VISIBILITY_ALL = <?php echo (int) License::VISIBILITY_ALL;?>;
|
||||
DupPro.Licensing.VISIBILITY_INFO = <?php echo (int) License::VISIBILITY_INFO;?>;
|
||||
DupPro.Licensing.VISIBILITY_NONE = <?php echo (int) License::VISIBILITY_NONE;?>;
|
||||
|
||||
$("#_key_password, #_key_password_confirmation").keyup(function(event) {
|
||||
|
||||
if (event.keyCode == 13) {
|
||||
$("#show_hide").click();
|
||||
}
|
||||
});
|
||||
|
||||
DupPro.Licensing.ChangeActivationStatus = function(activate) {
|
||||
if (activate) {
|
||||
let licenseKey = $('.dup-license-key-input').val();
|
||||
window.location.href =
|
||||
<?php echo json_encode($tplData['actions'][LicensingController::ACTION_ACTIVATE_LICENSE]->getUrl()); ?> +
|
||||
'&_license_key=' + encodeURIComponent(licenseKey);
|
||||
} else {
|
||||
window.location.href = <?php echo json_encode($tplData['actions'][LicensingController::ACTION_DEACTIVATE_LICENSE]->getUrl()); ?>;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
DupPro.Licensing.ClearActivationStatus = function() {
|
||||
window.location.href = <?php echo json_encode($tplData['actions'][LicensingController::ACTION_CLEAR_KEY]->getUrl()); ?>;
|
||||
}
|
||||
|
||||
DupPro.Licensing.ChangeKeyVisibility = function(show) {
|
||||
$('#dup-license-visibility-form').submit();
|
||||
}
|
||||
|
||||
DupPro.Licensing.VisibilityTemporary = function(visibility) {
|
||||
switch (visibility) {
|
||||
case DupPro.Licensing.VISIBILITY_ALL:
|
||||
$("#dup-tr-license-dashboard").show();
|
||||
$("#dup-tr-license-type").show();
|
||||
$("#dup-tr-license-key-and-description").show();
|
||||
break;
|
||||
case DupPro.Licensing.VISIBILITY_INFO:
|
||||
$("#dup-tr-license-dashboard").show();
|
||||
$("#dup-tr-license-type").show();
|
||||
$("#dup-tr-license-key-and-description").hide();
|
||||
break;
|
||||
case DupPro.Licensing.VISIBILITY_NONE:
|
||||
$("#dup-tr-license-dashboard").hide();
|
||||
$("#dup-tr-license-type").hide();
|
||||
$("#dup-tr-license-key-and-description").hide();
|
||||
break;
|
||||
default:
|
||||
alert("Unexpected visibility value!");
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
*/
|
||||
|
||||
use Duplicator\Libs\Snap\SnapUtil;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
*/
|
||||
|
||||
if (empty($tplData['license_message'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$details = "";
|
||||
if (isset($tplData['license_request_error'])) {
|
||||
$details = 'Message: ' . $tplData['license_request_error']['message'] . "\n" .
|
||||
'Error code: ' . $tplData['license_request_error']['code'] . "\n" .
|
||||
"\n" . 'Request Details' . "\n" .
|
||||
$tplData['license_request_error']['requestDetails'] .
|
||||
"\n" . 'Response Details' . "\n" .
|
||||
$tplData['license_request_error']['details'];
|
||||
}
|
||||
|
||||
?>
|
||||
<p>
|
||||
<?php if (!$tplData['license_success']) { ?>
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<?php } ?>
|
||||
<?php echo esc_html($tplData['license_message']) ?>
|
||||
</p>
|
||||
<?php if (isset($tplData['license_request_error'])) {
|
||||
?>
|
||||
<textarea class="dup-error-message-textarea" disabled ><?php echo esc_textarea($details); ?></textarea>
|
||||
<button
|
||||
data-dup-copy-value="<?php echo esc_attr($details); ?>"
|
||||
data-dup-copy-title="<?php echo esc_attr("Copy Error Message to clipboard"); ?>"
|
||||
data-dup-copied-title="<?php echo esc_attr("Error Message copied to clipboard"); ?>"
|
||||
class="button dup-btn-copy-error-message">
|
||||
<?php esc_html_e('Copy error details', 'duplicator-pro'); ?>
|
||||
</button>
|
||||
<?php if (!SnapUtil::isCurlEnabled()) {
|
||||
$tplMng->render('licensing/notices/curl_message');
|
||||
} ?>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
wp_kses(
|
||||
__("If the error persists please open a ticket <a href=\"%s\">here</a> and attach the errors details.", 'duplicator-pro'),
|
||||
array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
),
|
||||
)
|
||||
),
|
||||
esc_url(DUPLICATOR_PRO_BLOG_URL . 'my-account/support/')
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php } ?>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
*/
|
||||
|
||||
use Duplicator\Views\ViewHelper;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
*/
|
||||
?>
|
||||
<p>
|
||||
<?php
|
||||
echo wp_kses(
|
||||
__(
|
||||
'<b>CURL isn\'t enabled.</b> This module is far more reliable for remote communication.',
|
||||
'duplicator-pro'
|
||||
),
|
||||
ViewHelper::GEN_KSES_TAGS
|
||||
);
|
||||
?>
|
||||
</br>
|
||||
<?php esc_html_e('A possible solution to the problem could be to activate it.', 'duplicator-pro'); ?>
|
||||
</br>
|
||||
<?php
|
||||
printf(
|
||||
wp_kses(
|
||||
_x(
|
||||
'For detailed steps on how to enable cURL please see <b>Solution 3, Issue A</b> in %1$sthis FAQ Entry%2$s.',
|
||||
'%1$s and %2$s represents the opening and closing HTML tags for an anchor or link',
|
||||
'duplicator-pro'
|
||||
),
|
||||
ViewHelper::GEN_KSES_TAGS
|
||||
),
|
||||
'<a href="' . esc_url(DUPLICATOR_PRO_DUPLICATOR_DOCS_URL . 'how-to-resolve-license-activation-issues/') . '" target="_blank">',
|
||||
'</a>'
|
||||
);
|
||||
?>
|
||||
<br/>
|
||||
</p>
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\ProBase\License\License;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
*/
|
||||
$daysLeft = $tplData['schedule_disalbe_days_left'];
|
||||
$activeSchedule = $tplData['active_schedule_present'];
|
||||
|
||||
if ($daysLeft === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($activeSchedule) {
|
||||
?>
|
||||
<u>
|
||||
<?php
|
||||
if (License::can(License::CAPABILITY_SCHEDULE)) {
|
||||
$message = sprintf(
|
||||
_n(
|
||||
'Scheduled Backups are going to be disabled <b><em>in %d day</em></b>.',
|
||||
'Scheduled Backups are going to be disabled <b><em>in %d days</em></b>.',
|
||||
$daysLeft,
|
||||
'duplicator-pro'
|
||||
),
|
||||
$daysLeft
|
||||
);
|
||||
$message .= __(' Please renew your license to assure your backups are not interrupted.', 'duplicator-pro');
|
||||
echo wp_kses(
|
||||
$message,
|
||||
array(
|
||||
'b' => array(),
|
||||
'em' => array(),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
esc_html_e(
|
||||
'All automatic backups have been disabeld. Please renew your license to re-enable them.',
|
||||
'duplicator-pro'
|
||||
);
|
||||
}
|
||||
?>
|
||||
</u>
|
||||
<?php
|
||||
} else {
|
||||
esc_html_e(
|
||||
'Scheduled Backups.',
|
||||
'duplicator-pro'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
*/
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
*/
|
||||
|
||||
$renewal_url = $tplData['renewal_url'];
|
||||
?>
|
||||
<span class='dashicons dashicons-warning'></span>
|
||||
<div class="dup-sub-content">
|
||||
<h3>
|
||||
<?php esc_html_e('Warning! Your Duplicator Pro license has expired...', 'duplicator-pro');?>
|
||||
</h3>
|
||||
<?php esc_html_e('You\'re currently missing:', 'duplicator-pro'); ?>
|
||||
<ul class="dup-pro-simple-style-disc" >
|
||||
<li><?php esc_html_e('Access to Advanced Features', 'duplicator-pro'); ?></li>
|
||||
<li><?php $tplMng->render('licensing/notices/drm_schedules_msg'); ?></li>
|
||||
<li><?php esc_html_e('Storages Management', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Templates Management', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('New Features', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Important Updates for Security Patches', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Bug Fixes', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Support Requests', 'duplicator-pro'); ?></li>
|
||||
</ul>
|
||||
<a class="button" target="_blank" href="<?php echo esc_url($renewal_url); ?>">
|
||||
<?php esc_html_e('Renew Now!', 'duplicator-pro'); ?>
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\ProBase\LicensingController;
|
||||
use Duplicator\Core\Controllers\ControllersManager;
|
||||
use Duplicator\Views\ViewHelper;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
*/
|
||||
|
||||
$img_url = plugins_url('duplicator-pro/assets/img/warning.png');
|
||||
$problem_text = $tplData['problem'];
|
||||
$licensing_tab_url = ControllersManager::getMenuLink(ControllersManager::SETTINGS_SUBMENU_SLUG, LicensingController::L2_SLUG_LICENSING);
|
||||
?>
|
||||
<span class='dashicons dashicons-warning'></span>
|
||||
<div class="dup-sub-content">
|
||||
<h3>
|
||||
<?php
|
||||
printf(
|
||||
esc_html_x('Your Duplicator Pro license key is %1$s ...', '%1$s represent the license status', 'duplicator-pro'),
|
||||
esc_html($tplData['problem'])
|
||||
);
|
||||
?>
|
||||
</h3>
|
||||
<?php esc_html_e('You\'re currently missing:', 'duplicator-pro'); ?>
|
||||
<ul class="dup-pro-simple-style-disc" >
|
||||
<li><?php esc_html_e('Access to Advanced Features', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Scheduled Backups', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Storages Management', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Templates Management', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('New Features', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Important Updates for Security Patches', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Bug Fixes', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Support Requests', 'duplicator-pro'); ?></li>
|
||||
</ul>
|
||||
<?php
|
||||
printf(
|
||||
wp_kses(
|
||||
_x(
|
||||
'<b>Please %1$sActivate Your License%2$s</b>. If you do not have a license key go to %3$sduplicator.com%4$s to get it.',
|
||||
'1 and 2 are opening and 3 and 4 are closing anchor tags (<a> and </a>)',
|
||||
'duplicator-pro'
|
||||
),
|
||||
ViewHelper::GEN_KSES_TAGS
|
||||
),
|
||||
'<a href="' . esc_url($licensing_tab_url) . '">',
|
||||
'</a>',
|
||||
'<a target="_blank" href="' . esc_url(DUPLICATOR_PRO_BLOG_URL . 'my-account') . '">',
|
||||
'</a>'
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\ProBase\LicensingController;
|
||||
use Duplicator\Core\Controllers\ControllersManager;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
*/
|
||||
|
||||
$licensing_tab_url = ControllersManager::getMenuLink(ControllersManager::SETTINGS_SUBMENU_SLUG, LicensingController::L2_SLUG_LICENSING);
|
||||
$dashboard_url = DUPLICATOR_PRO_BLOG_URL . 'my-account';
|
||||
$img_url = plugins_url('duplicator-pro/assets/img/warning.png');
|
||||
|
||||
?>
|
||||
<span class='dashicons dashicons-warning'></span>
|
||||
<div class="dup-sub-content">
|
||||
<h3>
|
||||
<?php esc_html_e('Duplicator Pro\'s license is deactivated because you\'re out of site activations.', 'duplicator-pro'); ?>
|
||||
</h3>
|
||||
<?php esc_html_e('You\'re currently missing:', 'duplicator-pro'); ?>
|
||||
<ul class="dup-pro-simple-style-disc" >
|
||||
<li><?php esc_html_e('Access to Advanced Features', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Scheduled Backups', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Storages Management', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Templates Management', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('New Features', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Important Updates for Security Patches', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Bug Fixes', 'duplicator-pro'); ?></li>
|
||||
<li><?php esc_html_e('Support Requests', 'duplicator-pro'); ?></li>
|
||||
</ul>
|
||||
<?php
|
||||
printf(
|
||||
wp_kses(
|
||||
_x(
|
||||
'Upgrade your license using the %1$sDuplicator Dashboard%2$s or deactivate plugin on old sites.<br/>
|
||||
After making necessary changes %3$srefresh the license status%4$s.',
|
||||
'1 and 2 are opening and 3 and 4 are closing anchor tags (<a> and </a>)',
|
||||
'duplicator-pro'
|
||||
),
|
||||
[
|
||||
'br' => [],
|
||||
]
|
||||
),
|
||||
'<a href="' . esc_url($dashboard_url) . '" target="_blank">',
|
||||
'</a>',
|
||||
'<a href="' . esc_url($licensing_tab_url) . '">',
|
||||
'</a>'
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Duplicator
|
||||
*/
|
||||
|
||||
use Duplicator\Addons\ProBase\License\License;
|
||||
use Duplicator\Addons\ProBase\LicensingController;
|
||||
use Duplicator\Core\Controllers\ControllersManager;
|
||||
|
||||
defined("ABSPATH") or die("");
|
||||
|
||||
/**
|
||||
* Variables
|
||||
*
|
||||
* @var Duplicator\Core\Controllers\ControllersManager $ctrlMng
|
||||
* @var Duplicator\Core\Views\TplMng $tplMng
|
||||
* @var array<string, mixed> $tplData
|
||||
*/
|
||||
|
||||
$global = DUP_PRO_Global_Entity::getInstance();
|
||||
?>
|
||||
<h3 class="title"><?php esc_html_e("Key Visibility", 'duplicator-pro') ?> </h3>
|
||||
<small>
|
||||
<?php
|
||||
esc_html_e(
|
||||
"This is an optional setting that prevents the 'License Key' from being copied.
|
||||
Select the desired visibility mode, enter a password and hit the 'Change Visibility' button.",
|
||||
'duplicator-pro'
|
||||
);
|
||||
echo '<br/>';
|
||||
esc_html_e("Note: the password can be anything, it does not have to be the same as the WordPress user password.", 'duplicator-pro');
|
||||
?>
|
||||
</small>
|
||||
<hr size="1" />
|
||||
<form
|
||||
id="dup-license-visibility-form"
|
||||
action="<?php echo esc_url(ControllersManager::getCurrentLink()); ?>"
|
||||
method="post"
|
||||
data-parsley-validate
|
||||
>
|
||||
<?php $tplData['actions'][LicensingController::ACTION_CHANGE_VISIBILITY]->getActionNonceFileds(); ?>
|
||||
<table class="form-table">
|
||||
<tr valign="top">
|
||||
<th scope="row"><label><?php esc_html_e("Visibility", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<label class="margin-right-1">
|
||||
<input
|
||||
type="radio"
|
||||
name="license_key_visible"
|
||||
value="<?php echo (int) License::VISIBILITY_ALL;?>"
|
||||
onclick="DupPro.Licensing.VisibilityTemporary(<?php echo (int) License::VISIBILITY_ALL;?>);"
|
||||
<?php checked($global->license_key_visible, License::VISIBILITY_ALL); ?>
|
||||
>
|
||||
<?php esc_html_e("License Visible", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<label class="margin-right-1">
|
||||
<input
|
||||
type="radio"
|
||||
name="license_key_visible"
|
||||
value="<?php echo (int) License::VISIBILITY_INFO;?>"
|
||||
onclick="DupPro.Licensing.VisibilityTemporary(<?php echo (int) License::VISIBILITY_INFO;?>);"
|
||||
<?php checked($global->license_key_visible, License::VISIBILITY_INFO); ?>
|
||||
>
|
||||
<?php esc_html_e("Info Only", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="license_key_visible"
|
||||
value="<?php echo (int) License::VISIBILITY_NONE;?>"
|
||||
onclick="DupPro.Licensing.VisibilityTemporary(<?php echo (int) License::VISIBILITY_NONE;?>);"
|
||||
<?php checked($global->license_key_visible, License::VISIBILITY_NONE); ?>
|
||||
>
|
||||
<?php esc_html_e("License Invisible", 'duplicator-pro'); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr valign="top">
|
||||
<th scope="row"><label><?php esc_html_e("Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input type="password" class="dup-wide-input" name="_key_password" id="_key_password" size="50" />
|
||||
</td>
|
||||
</tr>
|
||||
<?php if ($global->license_key_visible == License::VISIBILITY_ALL) { ?>
|
||||
<tr valign="top">
|
||||
<th scope="row"><label><?php esc_html_e("Retype Password", 'duplicator-pro'); ?></label></th>
|
||||
<td>
|
||||
<input
|
||||
type="password"
|
||||
class="dup-wide-input"
|
||||
name="_key_password_confirmation"
|
||||
id="_key_password_confirmation"
|
||||
data-parsley-equalto="#_key_password"
|
||||
size="50"
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
<tr valign="top">
|
||||
<th scope="row"></th>
|
||||
<td>
|
||||
<button
|
||||
class="button"
|
||||
id="show_hide"
|
||||
onclick="DupPro.Licensing.ChangeKeyVisibility(); return false;"
|
||||
>
|
||||
<?php esc_html_e('Change Visibility', 'duplicator-pro'); ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Test addon main class
|
||||
*
|
||||
* Version: 0.1
|
||||
* Addon URI: http://addon.test
|
||||
* Description: This is test addon
|
||||
* Author: Snap Creek
|
||||
* Author URI: http://snapcreek.com
|
||||
* Requires WP min version: 5.0
|
||||
* Requires PHP: 8.0.0
|
||||
* Requires Duplicator min version: 4.0.0
|
||||
* Requires addons:
|
||||
*
|
||||
* @package Duplicator
|
||||
* @subpackage Addons\Test
|
||||
* @copyright (c) 2022, Snap Creek LLC
|
||||
*/
|
||||
|
||||
namespace Duplicator\Addons\Test;
|
||||
|
||||
class Test extends \Duplicator\Core\Addons\AbstractAddonCore
|
||||
{
|
||||
/**
|
||||
* Undocumented function
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
add_action('duplicator_addons_loaded', array($this, 'addonsLoaded'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Undocumented function
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function canEnable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Undocumented function
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addonsLoaded()
|
||||
{
|
||||
echo '<pre>';
|
||||
var_dump($this->addonData);
|
||||
echo '</pre>';
|
||||
die;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return addon file path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getAddonFile()
|
||||
{
|
||||
return __FILE__;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user