Files
carpseeds.pl/wp-content/plugins/akeebabackupwp/app/Solo/Model/Transfers.php
2024-07-15 11:28:08 +02:00

1351 lines
36 KiB
PHP

<?php
/**
* @package solo
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or later
*/
namespace Solo\Model;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Util\RandomValue;
use Akeeba\Engine\Util\Transfer;
use Awf\Download\Download;
use Awf\Mvc\Model;
use Awf\Session\Segment;
use Awf\Text\Text;
use Awf\Uri\Uri;
use Solo\Model\Exception\TransferFatalError;
use Solo\Model\Exception\TransferIgnorableError;
class Transfers extends Model
{
/**
* Get the information for the latest backup
*
* @return array|null An array of backup record information or null if there is no usable backup for site transfer
*/
public function getLatestBackupInformation()
{
// Initialise
$ret = null;
$db = Factory::getDatabase();
/** @var Manage $model */
$model = Model::getTmpInstance($this->container->application_name, 'Manage', $this->container);
$model->savestate(0);
$model->setState('limitstart', 0);
$model->setState('limit', 1);
$backups = $model->getStatisticsListWithMeta(false, null, $db->qn('id') . ' DESC');
// No valid backups? No joy.
if (empty($backups))
{
return $ret;
}
// Get the latest backup
$backup = array_shift($backups);
// If it's not stored on the server (e.g. remote backup), no joy.
if ($backup['meta'] != 'ok')
{
return $ret;
}
// If it's not a full site backup, no joy.
if ($backup['type'] != 'full')
{
return $ret;
}
return $backup;
}
/**
* Returns the amount of space required on the target server. The two array keys are
* size In bytes
* string Pretty formatted, user-friendly string
*
* @return array
*/
public function getApproximateSpaceRequired()
{
$backup = $this->getLatestBackupInformation();
if (is_null($backup))
{
return [
'size' => 0,
'string' => '0.00 KB'
];
}
$approximateSize = 2.5 * (float) $backup['size'];
$unit = array('b', 'KB', 'MB', 'GB', 'TB', 'PB');
if (version_compare(PHP_VERSION, '5.6.0', 'lt'))
{
return [
'size' => $approximateSize,
'string' => @round($approximateSize / pow(1024, ($i = floor(log($approximateSize, 1024)))), 2) . ' ' . $unit[$i]
];
}
return [
'size' => $approximateSize,
'string' => @round($approximateSize / (1024 ** ($i = floor(log($approximateSize, 1024)))), 2) . ' ' . $unit[$i]
];
}
/**
* Cleans up a URL and makes sure it is a valid-looking URL
*
* @param string $url The URL to check
*
* @return array status [ok, invalid, same, notexists] (check status); url (the cleaned URL)
*/
public function checkAndCleanUrl($url)
{
// Initialise
$result = array(
'status' => 'ok',
'url' => $url
);
// Am I missing the protocol?
if (strpos($url, '://') === false)
{
$url = 'http://' . $url;
}
$result['url'] = $url;
// Verify that it is an HTTP or HTTPS URL.
$uri = Uri::getInstance($url);
$protocol = $uri->getScheme();
if (!in_array($protocol, array('http', 'https')))
{
$result['status'] = 'invalid';
return $result;
}
// Verify we are not restoring to the same site we are backing up from
$path = $this->simplifyPath($uri->getPath());
$uri->setPath('/' . $path);
$siteUri = Uri::getInstance();
if ($siteUri->getHost() == $uri->getHost())
{
$sitePath = $this->simplifyPath($siteUri->getPath());
if ($sitePath == $path)
{
$result['status'] = 'same';
return $result;
}
}
$result['url'] = $uri->toString(array('scheme', 'user', 'pass', 'host', 'port', 'path'));
// Verify we can reach the domain. Since it can be an IP we check both name to IP and IP to name.
$host = $uri->getHost();
if (function_exists('idn_to_ascii'))
{
$host = idn_to_ascii($host);
}
$isValid = ($siteUri->getHost() == $uri->getHost()) || ($host == 'localhost') || ($host == '127.0.0.1') || (($host !== false) && checkdnsrr($host, 'A'));
// Sometimes we have a domain name without a DNS record which *can* be accessed locally, e.g. through the hosts
// file. We have to cater for that, just in case...
if (!$isValid)
{
$download = new Download($this->container);
$dummy = $download->getFromURL($uri->toString());
$isValid = $dummy !== false;
}
if (!$isValid)
{
$result['status'] = 'notexists';
return $result;
}
// All checks pass
return $result;
}
/**
* Tries to simplify a server path to get the site's root. It can handle most forms on non-SEF and non-rewrite SEF
* URLs (as in index.php?foo=bar, something.php/this/is?completely=nuts#ok). It can't fix stupid but it tries really
* bloody hard to.
*
* @param string $path The path to simplify. We *expect* this to contain nonsense.
*
* @return string The scrubbed clean URL, hopefully leading to the site's root.
*/
private function simplifyPath($path)
{
$path = ltrim($path, '/');
if (empty($path))
{
return $path;
}
// Trim out anything after a .php file (including the .php file itself)
if (substr($path, -1) != '/')
{
$parts = explode('/', $path);
$newParts = array();
foreach ($parts as $part)
{
if (substr($part, -4) == '.php')
{
break;
}
$newParts[] = $part;
}
$path = implode('/', $newParts);
}
if (substr($path, -13) == 'administrator')
{
$path = substr($path, 0, -13);
}
return $path;
}
/**
* Determines the status of FTP, FTPS and SFTP support. The returned array has two keys 'supported' and 'firewalled'
* each one being an array. You want the protocol to has its 'supported' value set to true and its 'firewalled'
* value set to false. This would mean that the server supports this protocol AND does not block outbound
* connections over this protocol.
*
* @return array
*/
public function getFTPSupport()
{
// Initialise
$result = array(
'supported' => array(
'ftpcurl' => false,
'ftpscurl' => false,
'sftpcurl' => false,
'ftp' => false,
'ftps' => false,
'sftp' => false,
),
'firewalled' => array(
'ftpcurl' => false,
'ftpscurl' => false,
'sftpcurl' => false,
'ftp' => false,
'ftps' => false,
'sftp' => false
)
);
// Necessary functions for each connection method
$supportChecks = array(
'ftpcurl' => array('curl_init', 'curl_exec', 'curl_setopt', 'curl_errno', 'curl_error'),
'ftpscurl' => array('curl_init', 'curl_exec', 'curl_setopt', 'curl_errno', 'curl_error'),
'sftpcurl' => array('curl_init', 'curl_exec', 'curl_setopt', 'curl_errno', 'curl_error'),
'ftp' => array('ftp_connect', 'ftp_login', 'ftp_close', 'ftp_chdir', 'ftp_mkdir', 'ftp_pasv', 'ftp_put', 'ftp_delete'),
'ftps' => array('ftp_ssl_connect', 'ftp_login', 'ftp_close', 'ftp_chdir', 'ftp_mkdir', 'ftp_pasv', 'ftp_put', 'ftp_delete'),
'sftp' => array('ssh2_connect', 'ssh2_auth_password', 'ssh2_auth_pubkey_file', 'ssh2_sftp', 'ssh2_exec', 'ssh2_sftp_unlink', 'ssh2_sftp_stat', 'ssh2_sftp_mkdir')
);
// Determine which connection methods are supported
$supported = array();
foreach ($supportChecks as $protocol => $functions)
{
$supported[$protocol] = true;
foreach ($functions as $function)
{
if (!function_exists($function))
{
$supported[$protocol] = false;
break;
}
}
}
$result['supported'] = $supported;
// Check firewall settings -- Disabled because the 3PD test server got clogged :(
/**
$result['firewalled'] = array(
'ftp' => !$result['supported']['ftp'] ? false : Transfer\Ftp::isFirewalled(),
'ftpcurl' => !$result['supported']['ftp'] ? false : Transfer\FtpCurl::isFirewalled(),
'ftps' => !$result['supported']['ftps'] ? false : Transfer\Ftp::isFirewalled(array('ssl' => true)),
'ftpscurl' => !$result['supported']['ftps'] ? false : Transfer\FtpCurl::isFirewalled(array('ssl' => true)),
'sftp' => !$result['supported']['sftp'] ? false : Transfer\Sftp::isFirewalled(),
'sftpcurl' => !$result['supported']['sftp'] ? false : Transfer\SftpCurl::isFirewalled(),
);
/**/
return $result;
}
/**
* Checks the FTP connection parameters
*
* @param array $config FTP/SFTP connection details
*
* @throws \RuntimeException
*/
public function testConnection(array $config)
{
/** @var Transfer\TransferInterface $connector */
$connector = $this->getConnector($config);
// Is it the same site we are restoring from? It is if the configuration.php exists and has the same contents as
// the one I read from our server.
$this->checkIfSameSite($connector);
// Only perform those checks if I'm not forcing the transfer
if (!$config['force'])
{
// Check if there's a special file in this directory, e.g. .htaccess, php.ini, .user.ini or web.config.
$this->checkIfHasSpecialFile($connector);
// Check if there's another site present in this directory
$this->checkIfExistingSite($connector);
}
// Does it match the URL to the site?
$this->checkIfMatchesUrl($connector);
}
/**
* Upload Kickstart, our extra script and check that the target server fullfills our criteria
*
* @param array $config FTP/SFTP connection details
*
* @throws \RuntimeException
* @throws \Exception
*/
public function initialiseUpload(array $config)
{
/** @var Transfer\TransferInterface $connector */
$connector = $this->getConnector($config);
// Can I upload Kickstart and my extra script?
$files = array(
APATH_ROOT . '/Solo/assets/installers/kickstart.txt' => 'kickstart.php',
APATH_ROOT . '/Solo/assets/installers/kickstart.transfer.php' => 'kickstart.transfer.php'
);
$createdFiles = array();
$transferredSize = 0;
$transferTime = 0;
try
{
foreach ($files as $localFile => $remoteFile)
{
$start = microtime(true);
$connector->upload($localFile, $connector->getPath($remoteFile));
$end = microtime(true);
$createdFiles[] = $remoteFile;
$transferredSize += filesize($localFile);
$transferTime += $end - $start;
}
}
catch (\Exception $e)
{
// An upload failed. Remove existing files.
$this->removeRemoteFiles($connector, $createdFiles, true);
throw new \RuntimeException(Text::_('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADKICKSTART'));
}
// Get the transfer speed between the two servers in bytes / second
$transferSpeed = $transferredSize / $transferTime;
try
{
$connector->mkdir($connector->getPath('kicktemp'), 0777);
}
catch (\Exception $e)
{
// Don't sweat if we can't create our temporary directory.
}
// Can I run Kickstart and my extra script?
try
{
$this->checkRemoteServerEnvironment($config['force']);
}
catch (\Exception $e)
{
$this->removeRemoteFiles($connector, $createdFiles, true);
throw $e;
}
// Get the lowest maximum execution time between our local and remote server
$remoteTimeout = $this->container->segment->get('transfer.remoteTimeLimit', 5);
$localTimeout = 5;
if (function_exists('ini_get'))
{
$localTimeout = ini_get("max_execution_time");
}
$timeout = min($localTimeout, $remoteTimeout);
if ($localTimeout == 0)
{
$timeout = $remoteTimeout;
}
elseif ($remoteTimeout == 0)
{
$timeout = $localTimeout;
}
if ($timeout == 0)
{
$timeout = 5;
}
// Get the maximimum transfer size, rounded down to 512K
$maxTransferSize = $transferSpeed * $timeout;
$maxTransferSize = floor($maxTransferSize / 524288) * 524288;
if ($maxTransferSize == 0)
{
$maxTransferSize = 524288;
}
/**
* We never go above a maximum transfer size that depends on the server memory setting and the maximum remote
* upload size (minus 10Kb for overhead data)
*/
// Maximum chunk size determined by local server's memory constraints
$chunkSizeLimit = $this->getMaxChunkSize();
// Chunk size selected by the user
$maxUploadLimit = $this->container->segment->get('transfer.uploadLimit', 5242880) - 10240;
// Maximum chunk size determined by the remote server
$userChunkSize = $this->container->segment->get('transfer.chunkSize', 5242880) - 10240;
// Calculated optimum chunk size (maxTransferSize is calculated by server-to-server speed limits)
$maxTransferSize = min($maxUploadLimit, $userChunkSize, $maxTransferSize, $chunkSizeLimit);
/**
* A little explanation for "$maxUploadLimit / 4" below. We are uploading binary data which gets encoded as
* form data. The integer part is a rough estimation of the size discrepancy between raw and encoded data.
*/
if ($config['chunkMode'] == 'post')
{
$maxTransferSize = min(floor($maxUploadLimit / 4), $maxTransferSize, $chunkSizeLimit);
}
// Save the optimal transfer size in the session
$this->container->segment->set('transfer.fragSize', $maxTransferSize);
}
/**
* Upload the next fragment
*
* @param array $config FTP/SFTP connection details
*
* @return array
*
* @throws \RuntimeException
*/
public function uploadChunk(array $config)
{
$ret = array(
'result' => true,
'done' => false,
'message' => '',
'totalSize' => 0,
'doneSize' => 0
);
// Get information from the session
$session = $this->container->segment;
$fragSize = $session->get('transfer.fragSize', 5242880);
$backup = $session->get('transfer.lastBackup', array());
$totalSize = $session->get('transfer.totalSize', 0);
$doneSize = $session->get('transfer.doneSize', 0);
$part = $session->get('transfer.part', -1);
$frag = $session->get('transfer.frag', -1);
// Do I need to update the total size?
if (!$totalSize)
{
$totalSize = $backup['total_size'];
$session->set('transfer.totalSize', $totalSize);
}
$ret['totalSize'] = $totalSize;
// First fragment of a new part
if ($frag == -1)
{
$frag = 0;
$part++;
}
/**
* If the backup is single part then $backup['multipart'] is 0. This means that the next if-block will report
* that the transfer is done. In these cases we have to convert $backup['multipart'] to 1 to let the upload
* actually run at all.
*/
if ($backup['multipart'] == 0)
{
$backup['multipart'] = 1;
}
// If I'm past the last part I'm done
if ($part >= $backup['multipart'])
{
// We are done
$ret['done'] = true;
return $ret;
}
// Get the information for this part
$fileName = $this->getPartFilename($backup['absolute_path'], $part);
$fileSize = filesize($fileName);
$intendedSeekPosition = $fragSize * $frag;
// I am trying to seek past EOF. Oops. Upload the next part.
if ($intendedSeekPosition >= $fileSize)
{
$session->set('transfer.frag', -1);
return $this->uploadChunk($config);
}
// Open the part
$fp = @fopen($fileName, 'rb');
if ($fp === false)
{
$ret['result'] = false;
$ret['message'] = Text::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTREADLOCALFILE', $fileName);
return $ret;
}
// Seek to position
if (fseek($fp, $intendedSeekPosition) == -1)
{
@fclose($fp);
$ret['result'] = false;
$ret['message'] = Text::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTREADLOCALFILE', $fileName);
return $ret;
}
// Read the data
$data = fread($fp, $fragSize);
$doneSize += strlen($data);
$ret['doneSize'] = $doneSize;
$session->set('transfer.doneSize', $doneSize);
// Upload the data
$session->set('transfer.frag', $frag);
try
{
switch ($config['chunkMode'])
{
case 'post':
$dataLength = $this->uploadUsingPost($fileName, $data, $session);
break;
case 'chunked':
default:
$dataLength = $this->uploadUsingChunked($fileName, $data, $session, $config);
break;
}
}
// A finally{} block is what we really need but it's not supported until PHP 5.5 and I'm stuck supporting 5.3 :(
catch (\RuntimeException $e)
{
// Close the part
fclose($fp);
// Rethrow the exception
throw $e;
}
// Close the part
fclose($fp);
// Update the session data
$session->set('transfer.fragSize', $fragSize);
$session->set('transfer.totalSize', $totalSize);
$session->set('transfer.doneSize', $doneSize);
$session->set('transfer.part', $part);
$session->set('transfer.frag', ++$frag);
// Did I go past EOF? Then on to the next part
$intendedSeekPosition += $dataLength;
if ($intendedSeekPosition >= $fileSize)
{
$session->set('transfer.frag', -1);
$session->set('transfer.part', ++$part);
}
// Did I reach the last part? Then I'm done
if ($part >= $backup['multipart'])
{
// We are done
$ret['done'] = true;
}
return $ret;
}
/**
* Reset the upload information. Required to start over.
*
* @return void
*/
public function resetUpload()
{
$session = $this->container->segment;
$session->set('transfer.totalSize', 0);
$session->set('transfer.doneSize', 0);
$session->set('transfer.part', -1);
$session->set('transfer.frag', -1);
}
/**
* Gets the TransferInterface connector object based on the $config configuration parameters array
*
* @param array $config The configuration array with the FTP/SFTP connection information
*
* @return Transfer\TransferInterface
*
* @throws \RuntimeException
*/
private function getConnector(array $config)
{
switch ($config['method'])
{
case 'sftp':
$connector = new Transfer\Sftp($config);
break;
case 'sftpcurl':
$connector = new Transfer\SftpCurl($config);
break;
case 'ftpcurl':
case 'ftpscurl':
$connector = new Transfer\FtpCurl($config);
break;
default:
$connector = new Transfer\Ftp($config);
break;
}
return $connector;
}
/**
* Checks if the remote site is the same as the site we are running the wizard from.
*
* @param Transfer\TransferInterface $connector
*/
private function checkIfSameSite(Transfer\TransferInterface $connector)
{
// TODO Is really possible to check if the site is the same?
$myConfiguration = @file_get_contents(APATH_ROOT . '/version.php');
if ($myConfiguration === false)
{
return;
}
try
{
$otherConfiguration = $connector->read($connector->getPath('version.php'));
}
catch (\Exception $e)
{
// File not found. No harm done.
return;
}
if ($otherConfiguration == $myConfiguration)
{
throw new \RuntimeException(Text::_('COM_AKEEBA_TRANSFER_ERR_SAMESITE'));
}
}
/**
* Check if there's a special file which might prevent site transfer from taking place.
*
* @param Transfer\TransferInterface $connector
*/
private function checkIfHasSpecialFile(Transfer\TransferInterface $connector)
{
$possibleFiles = array('.htaccess', 'web.config', 'php.ini', '.user.ini');
foreach ($possibleFiles as $file)
{
try
{
$fileContents = $connector->read($connector->getPath($file));
}
catch (\Exception $e)
{
// File not found. No harm done.
continue;
}
if (empty($fileContents))
{
continue;
}
throw new TransferIgnorableError(Text::sprintf('COM_AKEEBA_TRANSFER_ERR_HTACCESS', $file));
}
}
/**
* Check if there's an existing site
*
* @param Transfer\TransferInterface $connector
*/
private function checkIfExistingSite(Transfer\TransferInterface $connector)
{
/**
* I run into a PHP bug. When we try to read 'wordpress/index.php' over FTP to determine if it exists we end up
* with the folder "wordpress" being created. I have only been able to reproduce with with VSFTPd. The VSFTPd
* log claims there is only an unsuccessful read operation. Why the folder is create is a mystery, but I have to
* remove it anyway. I know, right?
*/
// $possibleFiles = array('index.php', 'wordpress/index.php');
$possibleFiles = array('index.php');
foreach ($possibleFiles as $file)
{
try
{
$fileContents = $connector->read($connector->getPath($file));
}
catch (\Exception $e)
{
// File not found. No harm done.
continue;
}
if (empty($fileContents))
{
continue;
}
throw new TransferIgnorableError(Text::_('COM_AKEEBA_TRANSFER_ERR_EXISTINGSITE'));
}
}
/**
* Check if the connection matches the site's stated URL
*
* @param Transfer\TransferInterface $connector
*/
private function checkIfMatchesUrl(Transfer\TransferInterface $connector)
{
$sourceFile = APATH_SITE . '/media/logo/' . $this->container->iconBaseName . '-16.png';
// Try to upload the file
try
{
$connector->upload($sourceFile, $connector->getPath(basename($sourceFile)));
}
catch (\Exception $e)
{
$errorMessage = Text::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADTESTFILE', basename($sourceFile));
$errorMessage .= " &mdash; [ " . $e->getMessage() . ' ]';
throw new \RuntimeException($errorMessage);
}
// Try to fetch the file over HTTP
$session = $this->container->segment;
$url = $session->get('transfer.url', '');
$url = rtrim($url, '/');
$downloader = new Download($this->container);
$wrongSSL = false;
$data = $downloader->getFromURL($url . '/' . basename($sourceFile));
/**
* The download of the test file failed. This can mean that the (S)FTP directory does not match the site URL we
* were given, DNS resolution does not work or we have an SSL issue. We are going to determine which one is it.
*/
if ($data === false)
{
$uri = new Uri($url);
$hostname = $uri->getHost();
$results = dns_get_record($hostname, DNS_A);
// If there are no IPv4 records let's try to get IPv6 records
if (count($results) == 0)
{
$results = dns_get_record($hostname, DNS_AAAA);
}
// No DNS records. So, that's why fetching data failed!
if (count($results) == 0)
{
// Delete the temporary file
$connector->delete($connector->getPath(basename($sourceFile)));
// And now throw the error
throw new TransferFatalError(Text::sprintf('COM_AKEEBA_TRANSFER_ERR_WRONGSSL', $hostname));
}
/**
* The DNS resolution worked. The next theory we have to test is that the SSL certificate is invalid or
* self-signed. The best way to do that without having to go through the OpenSSL extensions (which might not
* be installed or activated) is to do no SSL checking and retry the download. If that works we definitely
* have an SSL issue.
*/
$options = [
CURLOPT_SSL_VERIFYPEER => 0,
CURLOPT_SSL_VERIFYHOST => 0,
];
if ($downloader->getAdapterName() == 'fopen')
{
$options = [
'ssl' => [
'verify_peer' => false,
],
];
}
$downloader->setAdapterOptions($options);
$wrongSSL = true;
$data = $downloader->getFromURL($url . '/' . basename($sourceFile));
}
// Delete the temporary file
$connector->delete($connector->getPath(basename($sourceFile)));
// Could we get it over HTTP?
$originalData = file_get_contents($sourceFile);
// Downloaded data is verified but the SSL certificate was bad: tell the user to fix the SSL certificate.
if ($wrongSSL && ($originalData == $data))
{
throw new TransferFatalError(Text::_('COM_AKEEBA_TRANSFER_ERR_WRONGSSL'));
}
if ($originalData != $data)
{
throw new TransferFatalError(Text::_('COM_AKEEBA_TRANSFER_ERR_CANNOTACCESSTESTFILE'));
}
}
/**
* Gets the FTP configuration from the session
*
* @return array
*/
public function getFtpConfig()
{
$session = $this->container->segment;
$transferOption = $session->get('transfer.transferOption', '');
return array(
'method' => $transferOption,
'force' => $session->get('transfer.force', 0),
'host' => $session->get('transfer.ftpHost', ''),
'port' => $session->get('transfer.ftpPort', ''),
'username' => $session->get('transfer.ftpUsername', ''),
'password' => $session->get('transfer.ftpPassword', ''),
'directory' => $session->get('transfer.ftpDirectory', ''),
'ssl' => $transferOption == 'ftps',
'passive' => $session->get('transfer.ftpPassive', 1),
'passive_fix' => $session->get('transfer.ftpPassiveFix', 1),
'privateKey' => $session->get('transfer.ftpPrivateKey', ''),
'publicKey' => $session->get('transfer.ftpPubKey', ''),
'chunkMode' => $session->get('transfer.chunkMode', 'chunked'),
'chunkSize' => $session->get('transfer.chunkSize', '5242880'),
);
}
/**
* Removes files stored remotely
*
* @param Transfer\TransferInterface $connector The transfer object
* @param array $files The list of remote files to delete (relative paths)
* @param bool|true $ignoreExceptions Should I ignore exceptions thrown?
*
* @return void
*
* @throws \Exception
*/
private function removeRemoteFiles(Transfer\TransferInterface $connector, array $files, $ignoreExceptions = true)
{
if (empty($files))
{
return;
}
foreach ($files as $file)
{
$remoteFile = $connector->getPath($file);
try
{
$connector->delete($remoteFile);
}
catch (\Exception $e)
{
// Only let the exception bubble up if we are told not to ignore exceptions
if (!$ignoreExceptions)
{
throw $e;
}
}
}
}
/**
* Check if the remote server environment matches our expectations.
*
* @param bool $forced Are we forcing the transfer? If so some checks are ignored
*
* @throws \Exception
*/
private function checkRemoteServerEnvironment($forced)
{
$session = $this->container->segment;
$baseUrl = $session->get('transfer.url', '');
$baseUrl = rtrim($baseUrl, '/');
$downloader = new Download($this->container);
$rawData = $downloader->getFromURL($baseUrl . '/kickstart.php?task=serverinfo');
if ($rawData == false)
{
// Cannot access Kickstart on the remote server
throw new \RuntimeException(Text::_('COM_AKEEBA_TRANSFER_ERR_CANNOTRUNKICKSTART'));
}
// Try to get the raw JSON data
$pos = strpos($rawData, '###');
if ($pos === false)
{
// Invalid AJAX data, no leading ###
throw new \RuntimeException(Text::_('COM_AKEEBA_TRANSFER_ERR_CANNOTRUNKICKSTART'));
}
// Remove the leading ###
$rawData = substr($rawData, $pos + 3);
$pos = strpos($rawData, '###');
if ($pos === false)
{
// Invalid AJAX data, no trailing ###
throw new \RuntimeException(Text::_('COM_AKEEBA_TRANSFER_ERR_CANNOTRUNKICKSTART'));
}
// Remove the trailing ###
$rawData = substr($rawData, 0, $pos);
// Get the JSON response
$data = @json_decode($rawData, true);
if (empty($data))
{
// Invalid AJAX data, can't decode this stuff
throw new \RuntimeException(Text::_('COM_AKEEBA_TRANSFER_ERR_CANNOTRUNKICKSTART'));
}
// Disk space check could be ignored since some hosts return the wrong value for the available disk space
if (!$forced)
{
// Does the server have enough disk space?
$freeSpace = $data['freeSpace'];
$requiredSize = $this->getApproximateSpaceRequired();
if ($requiredSize['size'] > $freeSpace)
{
$unit = array('b', 'KB', 'MB', 'GB', 'TB', 'PB');
$freeSpaceString = @round($freeSpace / pow(1024, ($i = floor(log($freeSpace, 1024)))), 2) . ' ' . $unit[$i];
throw new TransferIgnorableError(Text::sprintf('COM_AKEEBA_TRANSFER_ERR_NOTENOUGHSPACE', $requiredSize['string'], $freeSpaceString));
}
}
// Can I write to remote files?
$canWrite = $data['canWrite'];
$canWriteTemp = $data['canWriteTemp'];
if (!$canWrite && !$canWriteTemp)
{
throw new \RuntimeException(Text::_('COM_AKEEBA_TRANSFER_ERR_CANNOTWRITEREMOTEFILES'));
}
if ($canWrite)
{
$session->set('transfer.targetPath', '');
}
else
{
$session->set('transfer.targetPath', 'kicktemp');
}
$session->set('transfer.remoteTimeLimit', $data['maxExecTime']);
// What is my upload limit?
$uploadLimit = min($data['maxPost'], $data['maxUpload']);
if (empty($data['maxPost']))
{
$uploadLimit = $data['maxUpload'];
}
elseif (empty($data['maxUpload']))
{
$uploadLimit = $data['maxPost'];
}
if (empty($uploadLimit))
{
$uploadLimit = 1048576;
}
$session->set('transfer.uploadLimit', $uploadLimit, 'akeeba');
}
/**
* Get the filename for a backup part file, given the base file and the part number
*
* @param string $baseFile Full path to the base file (.jpa, .jps, .zip)
* @param int $part Part number
*
* @return string
*/
private function getPartFilename($baseFile, $part = 0)
{
if ($part == 0)
{
return $baseFile;
}
$dirname = dirname($baseFile);
$basename = basename($baseFile);
$pos = strrpos($basename, '.');
$extension = substr($basename, $pos + 1);
$newExtension = substr($baseFile, 0, 1) . sprintf('%02u', $part);
return $dirname . '/' . basename($basename, '.' . $extension) . '.' .$newExtension;
}
/**
* Returns the PHP memory limit. If ini_get is not available it will assume 8Mb.
*
* @return int
*/
private function getServerMemoryLimit()
{
// Default reported memory limit: 8Mb
$memLimit = 8388608;
// If we can't find out how much PHP memory we have available use 8Mb by default
if (!function_exists('ini_get'))
{
return $memLimit;
}
$iniMemLimit = ini_get("memory_limit");
$iniMemLimit = $this->convertMemoryLimitToBytes($iniMemLimit);
$memLimit = ($iniMemLimit > 0) ? $iniMemLimit : $memLimit;
return (int) $memLimit;
}
/**
* Gets the maximum chunk size the server can handle safely. It does so by finding the PHP memory limit, removing
* the current memory usage (or at least 2Mb) and rounding down to the closest 512Kb. It can never be lower than
* 512Kb.
*/
private function getMaxChunkSize()
{
$memoryLimit = $this->getServerMemoryLimit();
$usedMemory = max(memory_get_usage(), memory_get_peak_usage(), 2048);
$maxChunkSize = max(($memoryLimit - $usedMemory) / 2, 524288);
return floor($maxChunkSize / 524288) * 524288;
}
/**
* Convert the textual representation of PHP memory limit to an integer, e.g. convert 8M to 8388608
*
* @param string $setting The PHP memory limit
*
* @return int PHP memory limit as an integer
*/
private function convertMemoryLimitToBytes($setting)
{
$val = trim($setting);
$last = strtolower(substr($val, -1));
if (is_numeric($last))
{
return $setting;
}
$val = substr($val, 0, -1);
switch ($last)
{
case 't':
$val *= 1024;
case 'g':
$val *= 1024;
case 'm':
$val *= 1024;
case 'k':
$val *= 1024;
}
return (int) $val;
}
/**
* Uploads a chunk of a backup part file using a direct POST to Kickstart.
*
* This is the method supported by the Site Transfer Wizard since its inception. However, it may not work with hosts
* which have a sensitive server protection, e.g. the very tight mod_security2 rules on SiteGround servers. In those
* cases the remote server will respond with a 500 Internal Server Error, a 403 Forbidden or another server error.
*
* @param string $fileName The filename to upload
* @param string $data The data to upload
* @param Segment $session The current session fragment
*
* @return int The length of the data we managed to upload
*
* @since 3.1.0
*/
private function uploadUsingPost($fileName, $data, $session)
{
$frag = $session->get('transfer.frag', -1);
$fragSize = $session->get('transfer.fragSize', 5242880);
$url = $session->get('transfer.url', '');
$directory = $session->get('transfer.targetPath', '');
$url = rtrim($url, '/') . '/kickstart.php';
$uri = Uri::getInstance($url);
$uri->setVar('task', 'uploadFile');
$uri->setVar('file', basename($fileName));
$uri->setVar('directory', $directory);
$uri->setVar('frag', $frag);
$uri->setVar('fragSize', $fragSize);
$downloader = new Download($this->container);
$downloader->setAdapterOptions(array(
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => array(
'data' => $data
)
));
$dataLength = strlen($data);
unset($data);
$rawData = $downloader->getFromURL($uri->toString());
// Try to get the raw JSON data
$pos = strpos($rawData, '###');
if ($pos === false)
{
// Invalid AJAX data, no leading ###
throw new \RuntimeException(Text::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADARCHIVE', basename($fileName)));
}
// Remove the leading ###
$rawData = substr($rawData, $pos + 3);
$pos = strpos($rawData, '###');
if ($pos === false)
{
// Invalid AJAX data, no trailing ###
throw new \RuntimeException(Text::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADARCHIVE', basename($fileName)));
}
// Remove the trailing ###
$rawData = substr($rawData, 0, $pos);
// Get the JSON response
$data = @json_decode($rawData, true);
if (empty($data))
{
// Invalid AJAX data, can't decode this stuff
throw new \RuntimeException(Text::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADARCHIVE', basename($fileName)));
}
if (!$data['status'])
{
throw new \RuntimeException(Text::sprintf('COM_AKEEBA_TRANSFER_ERR_ERRORFROMREMOTE', $data['message']));
}
return $dataLength;
}
/**
* Uploads a chunk of a backup part file via FTP and then uses Kickstart to piece the file together.
*
* This is a new upload method which works better on servers with tighter security. The only downside is that we
* have to open many FTP/SFTP upload sessions which may result in the remote server eventually blocking our uploads.
*
* @param string $fileName The filename to upload
* @param string $data The data to upload
* @param Segment $session The current session fragment
* @param array $config The FTP/SFTP configuration
*
* @return int The length of the data we managed to upload
*
* @since 3.1.0
*/
private function uploadUsingChunked($fileName, $data, $session, $config)
{
// ==== Initialize
$frag = $session->get('transfer.frag', -1);
$fragSize = $session->get('transfer.fragSize', 5242880);
$url = $session->get('transfer.url', '');
$directory = $session->get('transfer.targetPath', '');
// ==== Upload the data to the same folder as Kickstart, under a temporary name
// Even though the connector has the write() method, it's not very good for over 1M files. So we create a temp file instead.
$engineConfig = Factory::getConfiguration();
$localTempFile = tempnam($this->container->temporaryPath, 'stw');
$localTempFile = ($localTempFile === false) ? tempnam(sys_get_temp_dir(), 'stw') : $localTempFile;
$localTempFile = ($localTempFile === false) ? tempnam($engineConfig->get('akeeba.basic.output_directory', '[DEFAULT_OUTPUT]'), 'stw') : $localTempFile;
if ($localTempFile === false)
{
throw new \RuntimeException(Text::_('COM_AKEEBA_TRANSFER_ERR_CANTCREATETEMPCHUNK'));
}
if (!$this->container->fileSystem->write($localTempFile, $data))
{
throw new \RuntimeException(Text::_('COM_AKEEBA_TRANSFER_ERR_CANTCREATETEMPCHUNK'));
}
$random = new RandomValue();
$tempFile = strtolower($random->generateString(8)) . '.dat';
$connector = $this->getConnector($config);
try
{
$remoteDirectory = $config['directory'] . (empty($directory) ? '' : ('/' . $directory));
$remoteFile = $remoteDirectory . '/' . $tempFile;
$connector->upload($localTempFile, $remoteFile, true);
}
catch (\RuntimeException $e)
{
$this->container->fileSystem->delete($localTempFile);
throw $e;
}
// ==== Call Kickstart to piece together the file
$url = rtrim($url, '/') . '/kickstart.php';
$uri = Uri::getInstance($url);
$uri->setVar('task', 'uploadFile');
$uri->setVar('file', basename($fileName));
$uri->setVar('directory', $directory);
$uri->setVar('frag', $frag);
$uri->setVar('fragSize', $fragSize);
$uri->setVar('dataFile', $tempFile);
$downloader = new Download($this->container);
$dataLength = strlen($data);
unset($data);
$rawData = $downloader->getFromURL($uri->toString());
// ==== Delete the temporary files
$this->container->fileSystem->delete($localTempFile);
$connector->delete($remoteFile);
// ==== Parse Kickstart's response
// Try to get the raw JSON data
$pos = strpos($rawData, '###');
if ($pos === false)
{
// Invalid AJAX data, no leading ###
throw new \RuntimeException(Text::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADARCHIVE', basename($fileName)));
}
// Remove the leading ###
$rawData = substr($rawData, $pos + 3);
$pos = strpos($rawData, '###');
if ($pos === false)
{
// Invalid AJAX data, no trailing ###
throw new \RuntimeException(Text::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADARCHIVE', basename($fileName)));
}
// Remove the trailing ###
$rawData = substr($rawData, 0, $pos);
// Get the JSON response
$data = @json_decode($rawData, true);
if (empty($data))
{
// Invalid AJAX data, can't decode this stuff
throw new \RuntimeException(Text::sprintf('COM_AKEEBA_TRANSFER_ERR_CANNOTUPLOADARCHIVE', basename($fileName)));
}
if (!$data['status'])
{
throw new \RuntimeException(Text::sprintf('COM_AKEEBA_TRANSFER_ERR_ERRORFROMREMOTE', $data['message']));
}
return $dataLength;
}
}