first commit

This commit is contained in:
2026-04-20 13:11:14 +02:00
commit e0b63ca5f9
7793 changed files with 1844488 additions and 0 deletions

View File

@@ -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__;
}
}

View 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;
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>&nbsp;</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'); ?>

View File

@@ -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>