first commit
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\AesAdapter;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
/**
|
||||
* Abstract AES encryption class
|
||||
*/
|
||||
abstract class AbstractAdapter
|
||||
{
|
||||
/**
|
||||
* Trims or zero-pads a key / IV
|
||||
*
|
||||
* @param string $key The key or IV to treat
|
||||
* @param int $size The block size of the currently used algorithm
|
||||
*
|
||||
* @return null|string Null if $key is null, treated string of $size byte length otherwise
|
||||
*/
|
||||
public function resizeKey($key, $size)
|
||||
{
|
||||
if (empty($key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$keyLength = strlen($key);
|
||||
|
||||
if (function_exists('mb_strlen'))
|
||||
{
|
||||
$keyLength = mb_strlen($key, 'ASCII');
|
||||
}
|
||||
|
||||
if ($keyLength == $size)
|
||||
{
|
||||
return $key;
|
||||
}
|
||||
|
||||
if ($keyLength > $size)
|
||||
{
|
||||
if (function_exists('mb_substr'))
|
||||
{
|
||||
return mb_substr($key, 0, $size, 'ASCII');
|
||||
}
|
||||
|
||||
return substr($key, 0, $size);
|
||||
}
|
||||
|
||||
return $key . str_repeat("\0", ($size - $keyLength));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null bytes to append to the string so that it's zero padded to the specified block size
|
||||
*
|
||||
* @param string $string The binary string which will be zero padded
|
||||
* @param int $blockSize The block size
|
||||
*
|
||||
* @return string The zero bytes to append to the string to zero pad it to $blockSize
|
||||
*/
|
||||
protected function getZeroPadding($string, $blockSize)
|
||||
{
|
||||
$stringSize = strlen($string);
|
||||
|
||||
if (function_exists('mb_strlen'))
|
||||
{
|
||||
$stringSize = mb_strlen($string, 'ASCII');
|
||||
}
|
||||
|
||||
if ($stringSize == $blockSize)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($stringSize < $blockSize)
|
||||
{
|
||||
return str_repeat("\0", $blockSize - $stringSize);
|
||||
}
|
||||
|
||||
$paddingBytes = $stringSize % $blockSize;
|
||||
|
||||
return str_repeat("\0", $blockSize - $paddingBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\AesAdapter;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
/**
|
||||
* Interface for AES encryption adapters
|
||||
*/
|
||||
interface AdapterInterface
|
||||
{
|
||||
/**
|
||||
* Sets the AES encryption mode.
|
||||
*
|
||||
* WARNING: The strength is deprecated as it has a different effect in MCrypt and OpenSSL. MCrypt was abandoned in
|
||||
* 2003 before the Rijndael-128 algorithm was officially the Advanced Encryption Standard (AES). MCrypt also offered
|
||||
* Rijndael-192 and Rijndael-256 algorithms with different block sizes. These are NOT used in AES. OpenSSL, however,
|
||||
* implements AES correctly. It always uses a 128-bit (16 byte) block. The 192 and 256 bit strengths refer to the
|
||||
* key size, not the block size. Therefore using different strengths in MCrypt and OpenSSL will result in different
|
||||
* and incompatible ciphertexts.
|
||||
*
|
||||
* TL;DR: Always use $strength = 128!
|
||||
*
|
||||
* @param string $mode Choose between CBC (recommended) or ECB
|
||||
* @param int $strength Bit strength of the key (128, 192 or 256 bits). DEPRECATED. READ NOTES ABOVE.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function setEncryptionMode($mode = 'cbc', $strength = 128);
|
||||
|
||||
/**
|
||||
* Encrypts a string. Returns the raw binary ciphertext.
|
||||
*
|
||||
* WARNING: The plaintext is zero-padded to the algorithm's block size. You are advised to store the size of the
|
||||
* plaintext and trim the string to that length upon decryption.
|
||||
*
|
||||
* @param string $plainText The plaintext to encrypt
|
||||
* @param string $key The raw binary key (will be zero-padded or chopped if its size is different than the block size)
|
||||
* @param null|string $iv The initialization vector (for CBC mode algorithms)
|
||||
*
|
||||
* @return string The raw encrypted binary string.
|
||||
*/
|
||||
public function encrypt($plainText, $key, $iv = null);
|
||||
|
||||
/**
|
||||
* Decrypts a string. Returns the raw binary plaintext.
|
||||
*
|
||||
* $ciphertext MUST start with the IV followed by the ciphertext, even for EBC data (the first block of data is
|
||||
* dropped in EBC mode since there is no concept of IV in EBC).
|
||||
*
|
||||
* WARNING: The returned plaintext is zero-padded to the algorithm's block size during encryption. You are advised
|
||||
* to trim the string to the original plaintext's length upon decryption. While rtrim($decrypted, "\0") sounds
|
||||
* appealing it's NOT the correct approach for binary data (zero bytes may actually be part of your plaintext, not
|
||||
* just padding!).
|
||||
*
|
||||
* @param string $cipherText The ciphertext to encrypt
|
||||
* @param string $key The raw binary key (will be zero-padded or chopped if its size is different than the block size)
|
||||
*
|
||||
* @return string The raw unencrypted binary string.
|
||||
*/
|
||||
public function decrypt($cipherText, $key);
|
||||
|
||||
/**
|
||||
* Returns the encryption block size in bytes
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getBlockSize();
|
||||
|
||||
/**
|
||||
* Is this adapter supported?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSupported();
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\AesAdapter;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Util\RandomValue;
|
||||
|
||||
class Mcrypt extends AbstractAdapter implements AdapterInterface
|
||||
{
|
||||
protected $cipherType = MCRYPT_RIJNDAEL_128;
|
||||
|
||||
protected $cipherMode = MCRYPT_MODE_CBC;
|
||||
|
||||
public function setEncryptionMode($mode = 'cbc', $strength = 128)
|
||||
{
|
||||
switch ((int) $strength)
|
||||
{
|
||||
default:
|
||||
case '128':
|
||||
$this->cipherType = MCRYPT_RIJNDAEL_128;
|
||||
break;
|
||||
|
||||
case '192':
|
||||
$this->cipherType = MCRYPT_RIJNDAEL_192;
|
||||
break;
|
||||
|
||||
case '256':
|
||||
$this->cipherType = MCRYPT_RIJNDAEL_256;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (strtolower($mode))
|
||||
{
|
||||
case 'ecb':
|
||||
$this->cipherMode = MCRYPT_MODE_ECB;
|
||||
break;
|
||||
|
||||
default:
|
||||
case 'cbc':
|
||||
$this->cipherMode = MCRYPT_MODE_CBC;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function encrypt($plainText, $key, $iv = null)
|
||||
{
|
||||
$iv_size = $this->getBlockSize();
|
||||
$key = $this->resizeKey($key, $iv_size);
|
||||
$iv = $this->resizeKey($iv, $iv_size);
|
||||
|
||||
if (empty($iv))
|
||||
{
|
||||
$randVal = new RandomValue();
|
||||
$iv = $randVal->generate($iv_size);
|
||||
}
|
||||
|
||||
$cipherText = mcrypt_encrypt($this->cipherType, $key, $plainText, $this->cipherMode, $iv);
|
||||
$cipherText = $iv . $cipherText;
|
||||
|
||||
return $cipherText;
|
||||
}
|
||||
|
||||
public function decrypt($cipherText, $key)
|
||||
{
|
||||
$iv_size = $this->getBlockSize();
|
||||
$key = $this->resizeKey($key, $iv_size);
|
||||
$iv = substr($cipherText, 0, $iv_size);
|
||||
$cipherText = substr($cipherText, $iv_size);
|
||||
$plainText = mcrypt_decrypt($this->cipherType, $key, $cipherText, $this->cipherMode, $iv);
|
||||
|
||||
return $plainText;
|
||||
}
|
||||
|
||||
public function isSupported()
|
||||
{
|
||||
if (!function_exists('mcrypt_get_key_size'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('mcrypt_get_iv_size'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('mcrypt_create_iv'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('mcrypt_encrypt'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('mcrypt_decrypt'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('mcrypt_list_algorithms'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('hash'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('hash_algos'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$algorightms = mcrypt_list_algorithms();
|
||||
|
||||
if (!in_array('rijndael-128', $algorightms))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array('rijndael-192', $algorightms))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array('rijndael-256', $algorightms))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$algorightms = hash_algos();
|
||||
|
||||
if (!in_array('sha256', $algorightms))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getBlockSize()
|
||||
{
|
||||
return mcrypt_get_iv_size($this->cipherType, $this->cipherMode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\AesAdapter;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Util\RandomValue;
|
||||
|
||||
class OpenSSL extends AbstractAdapter implements AdapterInterface
|
||||
{
|
||||
/**
|
||||
* The OpenSSL options for encryption / decryption
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $openSSLOptions = 0;
|
||||
|
||||
/**
|
||||
* The encryption method to use
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $method = 'aes-128-cbc';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->openSSLOptions = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
|
||||
}
|
||||
|
||||
public function setEncryptionMode($mode = 'cbc', $strength = 128)
|
||||
{
|
||||
static $availableAlgorithms = null;
|
||||
static $defaultAlgo = 'aes-128-cbc';
|
||||
|
||||
if (!is_array($availableAlgorithms))
|
||||
{
|
||||
$availableAlgorithms = openssl_get_cipher_methods();
|
||||
|
||||
foreach ([
|
||||
'aes-256-cbc', 'aes-256-ecb', 'aes-192-cbc',
|
||||
'aes-192-ecb', 'aes-128-cbc', 'aes-128-ecb',
|
||||
] as $algo)
|
||||
{
|
||||
if (in_array($algo, $availableAlgorithms))
|
||||
{
|
||||
$defaultAlgo = $algo;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$strength = (int) $strength;
|
||||
$mode = strtolower($mode);
|
||||
|
||||
if (!in_array($strength, [128, 192, 256]))
|
||||
{
|
||||
$strength = 256;
|
||||
}
|
||||
|
||||
if (!in_array($mode, ['cbc', 'ebc']))
|
||||
{
|
||||
$mode = 'cbc';
|
||||
}
|
||||
|
||||
$algo = 'aes-' . $strength . '-' . $mode;
|
||||
|
||||
if (!in_array($algo, $availableAlgorithms))
|
||||
{
|
||||
$algo = $defaultAlgo;
|
||||
}
|
||||
|
||||
$this->method = $algo;
|
||||
}
|
||||
|
||||
public function encrypt($plainText, $key, $iv = null)
|
||||
{
|
||||
$iv_size = $this->getBlockSize();
|
||||
$key = $this->resizeKey($key, $iv_size);
|
||||
$iv = $this->resizeKey($iv, $iv_size);
|
||||
|
||||
if (empty($iv))
|
||||
{
|
||||
$randVal = new RandomValue();
|
||||
$iv = $randVal->generate($iv_size);
|
||||
}
|
||||
|
||||
$plainText .= $this->getZeroPadding($plainText, $iv_size);
|
||||
$cipherText = openssl_encrypt($plainText, $this->method, $key, $this->openSSLOptions, $iv);
|
||||
$cipherText = $iv . $cipherText;
|
||||
|
||||
return $cipherText;
|
||||
}
|
||||
|
||||
public function decrypt($cipherText, $key)
|
||||
{
|
||||
$iv_size = $this->getBlockSize();
|
||||
$key = $this->resizeKey($key, $iv_size);
|
||||
$iv = substr($cipherText, 0, $iv_size);
|
||||
$cipherText = substr($cipherText, $iv_size);
|
||||
$plainText = openssl_decrypt($cipherText, $this->method, $key, $this->openSSLOptions, $iv);
|
||||
|
||||
return $plainText;
|
||||
}
|
||||
|
||||
public function isSupported()
|
||||
{
|
||||
if (!function_exists('openssl_get_cipher_methods'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('openssl_random_pseudo_bytes'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('openssl_cipher_iv_length'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('openssl_encrypt'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('openssl_decrypt'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('hash'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!function_exists('hash_algos'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$algorightms = openssl_get_cipher_methods();
|
||||
|
||||
if (!in_array('aes-128-cbc', $algorightms))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$algorightms = hash_algos();
|
||||
|
||||
if (!in_array('sha256', $algorightms))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getBlockSize()
|
||||
{
|
||||
return openssl_cipher_iv_length($this->method);
|
||||
}
|
||||
}
|
||||
190
administrator/components/com_akeebabackup/engine/Util/Buffer.php
Normal file
190
administrator/components/com_akeebabackup/engine/Util/Buffer.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
/**
|
||||
* Generic Buffer stream handler
|
||||
*
|
||||
* This class provides a generic buffer stream. It can be used to store/retrieve/manipulate
|
||||
* string buffers with the standard PHP filesystem I/O methods.
|
||||
*/
|
||||
class Buffer
|
||||
{
|
||||
|
||||
/**
|
||||
* Stream position
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $position = 0;
|
||||
|
||||
/**
|
||||
* Buffer name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name = null;
|
||||
|
||||
/**
|
||||
* Buffer hash
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $_buffers = [];
|
||||
|
||||
/**
|
||||
* Function to open file or url
|
||||
*
|
||||
* @param string $path The URL that was passed
|
||||
* @param string $mode Mode used to open the file @see fopen
|
||||
* @param integer $options Flags used by the API, may be STREAM_USE_PATH and
|
||||
* STREAM_REPORT_ERRORS
|
||||
* @param string &$opened_path Full path of the resource. Used with STREAM_USE_PATH option
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @see streamWrapper::stream_open
|
||||
*/
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
$url = parse_url($path);
|
||||
$this->name = $url["host"];
|
||||
$this->_buffers[$this->name] = null;
|
||||
$this->position = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read stream
|
||||
*
|
||||
* @param integer $count How many bytes of data from the current position should be returned.
|
||||
*
|
||||
* @return mixed The data from the stream up to the specified number of bytes (all data if
|
||||
* the total number of bytes in the stream is less than $count. Null if
|
||||
* the stream is empty.
|
||||
*
|
||||
* @see streamWrapper::stream_read
|
||||
*/
|
||||
public function stream_read($count)
|
||||
{
|
||||
$ret = substr($this->_buffers[$this->name], $this->position, $count);
|
||||
$this->position += strlen($ret);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write stream
|
||||
*
|
||||
* @param string $data The data to write to the stream.
|
||||
*
|
||||
* @return integer
|
||||
*
|
||||
* @see streamWrapper::stream_write
|
||||
*/
|
||||
public function stream_write($data)
|
||||
{
|
||||
$left = substr($this->_buffers[$this->name], 0, $this->position);
|
||||
$right = substr($this->_buffers[$this->name], $this->position + strlen($data));
|
||||
$this->_buffers[$this->name] = $left . $data . $right;
|
||||
$this->position += strlen($data);
|
||||
|
||||
return strlen($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to get the current position of the stream
|
||||
*
|
||||
* @return integer
|
||||
*
|
||||
* @see streamWrapper::stream_tell
|
||||
*/
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to test for end of file pointer
|
||||
*
|
||||
* @return boolean True if the pointer is at the end of the stream
|
||||
*
|
||||
* @see streamWrapper::stream_eof
|
||||
*/
|
||||
public function stream_eof()
|
||||
{
|
||||
return $this->position >= strlen($this->_buffers[$this->name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The read write position updates in response to $offset and $whence
|
||||
*
|
||||
* @param integer $offset The offset in bytes
|
||||
* @param integer $whence Position the offset is added to
|
||||
* Options are SEEK_SET, SEEK_CUR, and SEEK_END
|
||||
*
|
||||
* @return boolean True if updated
|
||||
*
|
||||
* @see streamWrapper::stream_seek
|
||||
*/
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
switch ($whence)
|
||||
{
|
||||
case SEEK_SET:
|
||||
if ($offset < strlen($this->_buffers[$this->name]) && $offset >= 0)
|
||||
{
|
||||
$this->position = $offset;
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case SEEK_CUR:
|
||||
if ($offset >= 0)
|
||||
{
|
||||
$this->position += $offset;
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case SEEK_END:
|
||||
if (strlen($this->_buffers[$this->name]) + $offset >= 0)
|
||||
{
|
||||
$this->position = strlen($this->_buffers[$this->name]) + $offset;
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the stream
|
||||
stream_wrapper_register("buffer", Buffer::class);
|
||||
@@ -0,0 +1,390 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Platform;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* PHP port of http://github.com/danpalmer/jquery.complexify.js
|
||||
* Retrieved from https://github.com/mcrumley/php-complexify/blob/master/src/Complexify/Complexify.php
|
||||
* Error reporting is based on https://github.com/kislyuk/node-complexify
|
||||
*/
|
||||
class Complexify
|
||||
{
|
||||
private static $MIN_COMPLEXITY = 66;
|
||||
|
||||
private static $MAX_COMPLEXITY = 120; // 25 chars, all charsets
|
||||
|
||||
private static $CHARSETS = [
|
||||
// Commonly Used
|
||||
////////////////////
|
||||
[0x0020, 0x0020], // Space
|
||||
[0x0030, 0x0039], // Numbers
|
||||
[0x0041, 0x005A], // Uppercase
|
||||
[0x0061, 0x007A], // Lowercase
|
||||
[0x0021, 0x002F], // Punctuation
|
||||
[0x003A, 0x0040], // Punctuation
|
||||
[0x005B, 0x0060], // Punctuation
|
||||
[0x007B, 0x007E], // Punctuation
|
||||
// Everything Else
|
||||
////////////////////
|
||||
[0x0080, 0x00FF], // Latin-1 Supplement
|
||||
[0x0100, 0x017F], // Latin Extended-A
|
||||
[0x0180, 0x024F], // Latin Extended-B
|
||||
[0x0250, 0x02AF], // IPA Extensions
|
||||
[0x02B0, 0x02FF], // Spacing Modifier Letters
|
||||
[0x0300, 0x036F], // Combining Diacritical Marks
|
||||
[0x0370, 0x03FF], // Greek
|
||||
[0x0400, 0x04FF], // Cyrillic
|
||||
[0x0530, 0x058F], // Armenian
|
||||
[0x0590, 0x05FF], // Hebrew
|
||||
[0x0600, 0x06FF], // Arabic
|
||||
[0x0700, 0x074F], // Syriac
|
||||
[0x0780, 0x07BF], // Thaana
|
||||
[0x0900, 0x097F], // Devanagari
|
||||
[0x0980, 0x09FF], // Bengali
|
||||
[0x0A00, 0x0A7F], // Gurmukhi
|
||||
[0x0A80, 0x0AFF], // Gujarati
|
||||
[0x0B00, 0x0B7F], // Oriya
|
||||
[0x0B80, 0x0BFF], // Tamil
|
||||
[0x0C00, 0x0C7F], // Telugu
|
||||
[0x0C80, 0x0CFF], // Kannada
|
||||
[0x0D00, 0x0D7F], // Malayalam
|
||||
[0x0D80, 0x0DFF], // Sinhala
|
||||
[0x0E00, 0x0E7F], // Thai
|
||||
[0x0E80, 0x0EFF], // Lao
|
||||
[0x0F00, 0x0FFF], // Tibetan
|
||||
[0x1000, 0x109F], // Myanmar
|
||||
[0x10A0, 0x10FF], // Georgian
|
||||
[0x1100, 0x11FF], // Hangul Jamo
|
||||
[0x1200, 0x137F], // Ethiopic
|
||||
[0x13A0, 0x13FF], // Cherokee
|
||||
[0x1400, 0x167F], // Unified Canadian Aboriginal Syllabics
|
||||
[0x1680, 0x169F], // Ogham
|
||||
[0x16A0, 0x16FF], // Runic
|
||||
[0x1780, 0x17FF], // Khmer
|
||||
[0x1800, 0x18AF], // Mongolian
|
||||
[0x1E00, 0x1EFF], // Latin Extended Additional
|
||||
[0x1F00, 0x1FFF], // Greek Extended
|
||||
[0x2000, 0x206F], // General Punctuation
|
||||
[0x2070, 0x209F], // Superscripts and Subscripts
|
||||
[0x20A0, 0x20CF], // Currency Symbols
|
||||
[0x20D0, 0x20FF], // Combining Marks for Symbols
|
||||
[0x2100, 0x214F], // Letterlike Symbols
|
||||
[0x2150, 0x218F], // Number Forms
|
||||
[0x2190, 0x21FF], // Arrows
|
||||
[0x2200, 0x22FF], // Mathematical Operators
|
||||
[0x2300, 0x23FF], // Miscellaneous Technical
|
||||
[0x2400, 0x243F], // Control Pictures
|
||||
[0x2440, 0x245F], // Optical Character Recognition
|
||||
[0x2460, 0x24FF], // Enclosed Alphanumerics
|
||||
[0x2500, 0x257F], // Box Drawing
|
||||
[0x2580, 0x259F], // Block Elements
|
||||
[0x25A0, 0x25FF], // Geometric Shapes
|
||||
[0x2600, 0x26FF], // Miscellaneous Symbols
|
||||
[0x2700, 0x27BF], // Dingbats
|
||||
[0x2800, 0x28FF], // Braille Patterns
|
||||
[0x2E80, 0x2EFF], // CJK Radicals Supplement
|
||||
[0x2F00, 0x2FDF], // Kangxi Radicals
|
||||
[0x2FF0, 0x2FFF], // Ideographic Description Characters
|
||||
[0x3000, 0x303F], // CJK Symbols and Punctuation
|
||||
[0x3040, 0x309F], // Hiragana
|
||||
[0x30A0, 0x30FF], // Katakana
|
||||
[0x3100, 0x312F], // Bopomofo
|
||||
[0x3130, 0x318F], // Hangul Compatibility Jamo
|
||||
[0x3190, 0x319F], // Kanbun
|
||||
[0x31A0, 0x31BF], // Bopomofo Extended
|
||||
[0x3200, 0x32FF], // Enclosed CJK Letters and Months
|
||||
[0x3300, 0x33FF], // CJK Compatibility
|
||||
[0x3400, 0x4DB5], // CJK Unified Ideographs Extension A
|
||||
[0x4E00, 0x9FFF], // CJK Unified Ideographs
|
||||
[0xA000, 0xA48F], // Yi Syllables
|
||||
[0xA490, 0xA4CF], // Yi Radicals
|
||||
[0xAC00, 0xD7A3], // Hangul Syllables
|
||||
[0xD800, 0xDB7F], // High Surrogates
|
||||
[0xDB80, 0xDBFF], // High Private Use Surrogates
|
||||
[0xDC00, 0xDFFF], // Low Surrogates
|
||||
[0xE000, 0xF8FF], // Private Use
|
||||
[0xF900, 0xFAFF], // CJK Compatibility Ideographs
|
||||
[0xFB00, 0xFB4F], // Alphabetic Presentation Forms
|
||||
[0xFB50, 0xFDFF], // Arabic Presentation Forms-A
|
||||
[0xFE20, 0xFE2F], // Combining Half Marks
|
||||
[0xFE30, 0xFE4F], // CJK Compatibility Forms
|
||||
[0xFE50, 0xFE6F], // Small Form Variants
|
||||
[0xFE70, 0xFEFE], // Arabic Presentation Forms-B
|
||||
[0xFEFF, 0xFEFF], // Specials
|
||||
[0xFF00, 0xFFEF], // Halfwidth and Fullwidth Forms
|
||||
[0xFFF0, 0xFFFD] // Specials
|
||||
];
|
||||
|
||||
// Generated from 500 worst passwords and 370 Banned Twitter lists found at
|
||||
// @source http://www.skullsecurity.org/wiki/index.php/Passwords
|
||||
private static $BANLIST = [
|
||||
'0', '1111', '1212', '1234', '1313', '2000', '2112', '2222',
|
||||
'3333', '4128', '4321', '4444', '5150', '5555', '6666', '6969', '7777', 'aaaa',
|
||||
'alex', 'asdf', 'baby', 'bear', 'beer', 'bill', 'blue', 'cock', 'cool', 'cunt',
|
||||
'dave', 'dick', 'eric', 'fire', 'fish', 'ford', 'fred', 'fuck', 'girl', 'golf',
|
||||
'jack', 'jake', 'john', 'king', 'love', 'mark', 'matt', 'mike', 'mine', 'pass',
|
||||
'paul', 'porn', 'rock', 'sexy', 'shit', 'slut', 'star', 'test', 'time', 'tits',
|
||||
'wolf', 'xxxx', '11111', '12345', 'angel', 'apple', 'beach', 'billy', 'bitch',
|
||||
'black', 'boobs', 'booty', 'brian', 'bubba', 'buddy', 'chevy', 'chris', 'cream',
|
||||
'david', 'dirty', 'eagle', 'enjoy', 'enter', 'frank', 'girls', 'great', 'green',
|
||||
'happy', 'hello', 'horny', 'house', 'james', 'japan', 'jason', 'juice', 'kelly',
|
||||
'kevin', 'kitty', 'lover', 'lucky', 'magic', 'money', 'movie', 'music', 'naked',
|
||||
'ou812', 'paris', 'penis', 'peter', 'porno', 'power', 'pussy', 'qwert', 'sammy',
|
||||
'scott', 'smith', 'stars', 'steve', 'super', 'teens', 'tiger', 'video', 'viper',
|
||||
'white', 'women', 'xxxxx', 'young', '111111', '112233', '121212', '123123',
|
||||
'123456', '131313', '232323', '654321', '666666', '696969', '777777', '987654',
|
||||
'aaaaaa', 'abc123', 'abcdef', 'access', 'action', 'albert', 'alexis', 'amanda',
|
||||
'andrea', 'andrew', 'angela', 'angels', 'animal', 'apollo', 'apples', 'arthur',
|
||||
'asdfgh', 'ashley', 'august', 'austin', 'badboy', 'bailey', 'banana', 'barney',
|
||||
'batman', 'beaver', 'beavis', 'bigdog', 'birdie', 'biteme', 'blazer', 'blonde',
|
||||
'blowme', 'bonnie', 'booboo', 'booger', 'boomer', 'boston', 'brandy', 'braves',
|
||||
'brazil', 'bronco', 'buster', 'butter', 'calvin', 'camaro', 'canada', 'carlos',
|
||||
'carter', 'casper', 'cheese', 'coffee', 'compaq', 'cookie', 'cooper', 'cowboy',
|
||||
'dakota', 'dallas', 'daniel', 'debbie', 'dennis', 'diablo', 'doctor', 'doggie',
|
||||
'donald', 'dragon', 'dreams', 'driver', 'eagle1', 'eagles', 'edward', 'erotic',
|
||||
'falcon', 'fender', 'flower', 'flyers', 'freddy', 'fucked', 'fucker', 'fuckme',
|
||||
'gators', 'gemini', 'george', 'giants', 'ginger', 'golden', 'golfer', 'gordon',
|
||||
'guitar', 'gunner', 'hammer', 'hannah', 'harley', 'helpme', 'hentai', 'hockey',
|
||||
'horney', 'hotdog', 'hunter', 'iceman', 'iwantu', 'jackie', 'jaguar', 'jasper',
|
||||
'jeremy', 'johnny', 'jordan', 'joseph', 'joshua', 'junior', 'justin', 'killer',
|
||||
'knight', 'ladies', 'lakers', 'lauren', 'legend', 'little', 'london', 'lovers',
|
||||
'maddog', 'maggie', 'magnum', 'marine', 'martin', 'marvin', 'master', 'matrix',
|
||||
'member', 'merlin', 'mickey', 'miller', 'monica', 'monkey', 'morgan', 'mother',
|
||||
'muffin', 'murphy', 'nascar', 'nathan', 'nicole', 'nipple', 'oliver', 'orange',
|
||||
'parker', 'peanut', 'pepper', 'player', 'please', 'pookie', 'prince', 'purple',
|
||||
'qazwsx', 'qwerty', 'rabbit', 'rachel', 'racing', 'ranger', 'redsox', 'robert',
|
||||
'rocket', 'runner', 'russia', 'samson', 'sandra', 'saturn', 'scooby', 'secret',
|
||||
'sexsex', 'shadow', 'shaved', 'sierra', 'silver', 'skippy', 'slayer', 'smokey',
|
||||
'snoopy', 'soccer', 'sophie', 'spanky', 'sparky', 'spider', 'squirt', 'steven',
|
||||
'sticky', 'stupid', 'suckit', 'summer', 'surfer', 'sydney', 'taylor', 'tennis',
|
||||
'teresa', 'tester', 'theman', 'thomas', 'tigers', 'tigger', 'tomcat', 'topgun',
|
||||
'toyota', 'travis', 'tucker', 'turtle', 'united', 'vagina', 'victor', 'viking',
|
||||
'voodoo', 'walter', 'willie', 'wilson', 'winner', 'winter', 'wizard', 'xavier',
|
||||
'xxxxxx', 'yamaha', 'yankee', 'yellow', 'zxcvbn', 'zzzzzz', '1234567', '7777777',
|
||||
'8675309', 'abgrtyu', 'amateur', 'anthony', 'arsenal', 'asshole', 'bigcock',
|
||||
'bigdick', 'bigtits', 'bitches', 'blondes', 'blowjob', 'bond007', 'brandon',
|
||||
'broncos', 'bulldog', 'cameron', 'captain', 'charles', 'charlie', 'chelsea',
|
||||
'chester', 'chicago', 'chicken', 'college', 'cowboys', 'crystal', 'cumming',
|
||||
'cumshot', 'diamond', 'dolphin', 'extreme', 'ferrari', 'fishing', 'florida',
|
||||
'forever', 'freedom', 'fucking', 'fuckyou', 'gandalf', 'gateway', 'gregory',
|
||||
'heather', 'hooters', 'hunting', 'jackson', 'jasmine', 'jessica', 'johnson',
|
||||
'leather', 'letmein', 'madison', 'matthew', 'maxwell', 'melissa', 'michael',
|
||||
'monster', 'mustang', 'naughty', 'ncc1701', 'newyork', 'nipples', 'packers',
|
||||
'panther', 'panties', 'patrick', 'peaches', 'phantom', 'phoenix', 'porsche',
|
||||
'private', 'pussies', 'raiders', 'rainbow', 'rangers', 'rebecca', 'richard',
|
||||
'rosebud', 'scooter', 'scorpio', 'shannon', 'success', 'testing', 'thunder',
|
||||
'thx1138', 'tiffany', 'trouble', 'twitter', 'voyager', 'warrior', 'welcome',
|
||||
'william', 'winston', 'yankees', 'zxcvbnm', '11111111', '12345678', 'access14',
|
||||
'baseball', 'bigdaddy', 'butthead', 'cocacola', 'computer', 'corvette',
|
||||
'danielle', 'dolphins', 'einstein', 'firebird', 'football', 'hardcore',
|
||||
'iloveyou', 'internet', 'jennifer', 'marlboro', 'maverick', 'mercedes',
|
||||
'michelle', 'midnight', 'mistress', 'mountain', 'nicholas', 'password',
|
||||
'princess', 'qwertyui', 'redskins', 'redwings', 'rush2112', 'samantha',
|
||||
'scorpion', 'srinivas', 'startrek', 'starwars', 'steelers', 'sunshine',
|
||||
'superman', 'swimming', 'trustno1', 'victoria', 'whatever', 'xxxxxxxx',
|
||||
'password1', 'password12', 'password123',
|
||||
];
|
||||
|
||||
private $minimumChars = 8;
|
||||
|
||||
private $strengthScaleFactor = 1;
|
||||
|
||||
private $bannedPasswords = [];
|
||||
|
||||
private $banMode = 'strict'; // (strict|loose)
|
||||
|
||||
private $encoding = 'UTF-8';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $options Override default options using an associative array of options
|
||||
*
|
||||
* Options:
|
||||
* - minimumChars: Minimum password length (default: 8)
|
||||
* - strengthScaleFactor: Required password strength multiplier (default: 1)
|
||||
* - bannedPasswords: Custom list of banned passwords (default: long list of common passwords)
|
||||
* - banMode: Use strict or loose comparisons for banned passwords. "strict" = don't allow a substring of a banned
|
||||
* password, "loose" = only ban exact matches (default: strict)
|
||||
* - encoding: Character set encoding of the password (default: UTF-8)
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->bannedPasswords = self::$BANLIST;
|
||||
|
||||
foreach ($options as $opt => $val)
|
||||
{
|
||||
if ($opt === 'banmode')
|
||||
{
|
||||
trigger_error('The lowercase banmode option is deprecated. Use banMode instead.', E_USER_DEPRECATED);
|
||||
$opt = 'banMode';
|
||||
}
|
||||
|
||||
$this->{$opt} = $val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a password is strong enough for use on a live site. Used to check the front-end Secret Word.
|
||||
*
|
||||
* @param string $password The password to check
|
||||
* @param bool $throwExceptions Throw an exception if the password is not strong enough?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isStrongEnough($password, $throwExceptions = true)
|
||||
{
|
||||
$complexify = new self();
|
||||
|
||||
$res = (object) [
|
||||
'valid' => strlen($password) >= 32,
|
||||
'complexity' => 50,
|
||||
'errors' => (strlen($password) >= 32) ? [] : ['tooshort'],
|
||||
];
|
||||
|
||||
if (function_exists('mb_strlen') && function_exists('mb_convert_encoding') &&
|
||||
function_exists('mb_substr') && function_exists('mb_convert_case'))
|
||||
{
|
||||
$res = $complexify->evaluateSecurity($password);
|
||||
}
|
||||
|
||||
|
||||
if ($res->valid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$throwExceptions)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$error = count($res->errors) ? array_shift($res->errors) : 'toosimple';
|
||||
|
||||
$errorMessage = Platform::getInstance()->translate('COM_AKEEBA_CPANEL_ERR_FESECRETWORD_' . $error);
|
||||
|
||||
throw new RuntimeException($errorMessage, 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the complexity of a password
|
||||
*
|
||||
* @param string $password The password to check
|
||||
*
|
||||
* @return object StdClass object with properties "valid", "complexity", and "error"
|
||||
* - valid: TRUE if the password is complex enough, FALSE if it is not
|
||||
* - complexity: The complexity of the password as a percent
|
||||
* - errors: Array containing descriptions of what made the password fail. Possible values are: banned, toosimple,
|
||||
* tooshort
|
||||
*/
|
||||
public function evaluateSecurity($password)
|
||||
{
|
||||
$complexity = 0;
|
||||
$error = [];
|
||||
|
||||
// Reset complexity to 0 when banned password is found
|
||||
if (!$this->inBanlist($password))
|
||||
{
|
||||
// Add character complexity
|
||||
foreach (self::$CHARSETS as $charset)
|
||||
{
|
||||
$complexity += $this->additionalComplexityForCharset($password, $charset);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
array_push($error, 'banned');
|
||||
$complexity = 1;
|
||||
}
|
||||
|
||||
// Use natural log to produce linear scale
|
||||
$complexity = log($complexity ** mb_strlen($password, $this->encoding)) * (1 / $this->strengthScaleFactor);
|
||||
|
||||
if ($complexity <= self::$MIN_COMPLEXITY)
|
||||
{
|
||||
array_push($error, 'toosimple');
|
||||
}
|
||||
|
||||
if (mb_strlen($password, $this->encoding) < $this->minimumChars)
|
||||
{
|
||||
array_push($error, 'tooshort');
|
||||
}
|
||||
|
||||
// Scale to percentage, so it can be used for a progress bar
|
||||
$complexity = ($complexity / self::$MAX_COMPLEXITY) * 100;
|
||||
$complexity = ($complexity > 100) ? 100 : $complexity;
|
||||
|
||||
return (object) ['valid' => (is_array($error) || $error instanceof \Countable ? count($error) : 0) === 0, 'complexity' => $complexity, 'errors' => $error];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the complexity added from a character set if it is used in a string
|
||||
*
|
||||
* @param string $str String to check
|
||||
* @param int [2] $charset Array of unicode code points representing the lower and upper bound of the
|
||||
* character range
|
||||
*
|
||||
* @return int 0 if there are no characters from the character set, size of the character set if there are any
|
||||
* characters used in the string
|
||||
*/
|
||||
private function additionalComplexityForCharset($str, $charset)
|
||||
{
|
||||
$len = mb_strlen($str, $this->encoding);
|
||||
for ($i = 0; $i < $len; $i++)
|
||||
{
|
||||
$c =
|
||||
unpack('Nord', mb_convert_encoding(mb_substr($str, $i, 1, $this->encoding), 'UCS-4BE', $this->encoding));
|
||||
if ($charset[0] <= $c['ord'] && $c['ord'] <= $charset[1])
|
||||
{
|
||||
return $charset[1] - $charset[0] + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is in the banned password list
|
||||
*
|
||||
* @param string $str String to check
|
||||
*
|
||||
* @return bool TRUE if $str is a banned password, or if it is a substring of a banned password and
|
||||
* $this->banMode is 'strict'
|
||||
*/
|
||||
private function inBanlist($str)
|
||||
{
|
||||
if ($str == '')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$str = mb_convert_case($str, MB_CASE_LOWER, $this->encoding);
|
||||
|
||||
if ($this->banMode === 'strict')
|
||||
{
|
||||
for ($i = 0; $i < count($this->bannedPasswords); $i++)
|
||||
{
|
||||
if (mb_strpos($this->bannedPasswords[$i], $str, 0, $this->encoding) !== false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($str, $this->bannedPasswords);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,626 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
|
||||
/**
|
||||
* Quirk detection helper class
|
||||
*/
|
||||
class ConfigurationCheck
|
||||
{
|
||||
/**
|
||||
* The configuration checks to perform
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $configurationChecks = [
|
||||
['code' => '001', 'severity' => 'critical', 'callback' => [null, 'q001'],
|
||||
'description' => 'COM_AKEEBA_CPANEL_WARNING_Q001',
|
||||
],
|
||||
['code' => '003', 'severity' => 'critical', 'callback' => [null, 'q003'],
|
||||
'description' => 'COM_AKEEBA_CPANEL_WARNING_Q003',
|
||||
],
|
||||
['code' => '004', 'severity' => 'critical', 'callback' => [null, 'q004'],
|
||||
'description' => 'COM_AKEEBA_CPANEL_WARNING_Q004',
|
||||
],
|
||||
|
||||
['code' => '101', 'severity' => 'high', 'callback' => [null, 'q101'],
|
||||
'description' => 'COM_AKEEBA_CPANEL_WARNING_Q101',
|
||||
],
|
||||
['code' => '103', 'severity' => 'high', 'callback' => [null, 'q103'],
|
||||
'description' => 'COM_AKEEBA_CPANEL_WARNING_Q103',
|
||||
],
|
||||
['code' => '104', 'severity' => 'high', 'callback' => [null, 'q104'],
|
||||
'description' => 'COM_AKEEBA_CPANEL_WARNING_Q104',
|
||||
],
|
||||
['code' => '106', 'severity' => 'high', 'callback' => [null, 'q106'],
|
||||
'description' => 'COM_AKEEBA_CPANEL_WARNING_Q106',
|
||||
],
|
||||
|
||||
['code' => '201', 'severity' => 'medium', 'callback' => [null, 'q201'],
|
||||
'description' => 'COM_AKEEBA_CPANEL_WARNING_Q201',
|
||||
],
|
||||
['code' => '202', 'severity' => 'medium', 'callback' => [null, 'q202'],
|
||||
'description' => 'COM_AKEEBA_CPANEL_WARNING_Q202',
|
||||
],
|
||||
['code' => '204', 'severity' => 'medium', 'callback' => [null, 'q204'],
|
||||
'description' => 'COM_AKEEBA_CPANEL_WARNING_Q204',
|
||||
],
|
||||
|
||||
['code' => '203', 'severity' => 'medium', 'callback' => [null, 'q203'],
|
||||
'description' => 'COM_AKEEBA_CPANEL_WARNING_Q203',
|
||||
],
|
||||
// ['code' => '401', 'severity' => 'low', 'callback' => [null, 'q401'],
|
||||
// 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q401',
|
||||
// ],
|
||||
];
|
||||
|
||||
/**
|
||||
* The public constructor replaces the missing object reference in the configuration check callbacks
|
||||
*/
|
||||
function __construct()
|
||||
{
|
||||
$temp = [];
|
||||
|
||||
foreach ($this->configurationChecks as $check)
|
||||
{
|
||||
$check['callback'] = [$this, $check['callback'][1]];
|
||||
$temp[] = $check;
|
||||
}
|
||||
|
||||
$this->configurationChecks = $temp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the output & temporary folder writable status
|
||||
*
|
||||
* @return array A hash array with the writable status
|
||||
*/
|
||||
public function getFolderStatus()
|
||||
{
|
||||
static $status = null;
|
||||
|
||||
if (is_null($status))
|
||||
{
|
||||
$stock_dirs = Platform::getInstance()->get_stock_directories();
|
||||
|
||||
// Get output writable status
|
||||
$registry = Factory::getConfiguration();
|
||||
$outdir = $registry->get('akeeba.basic.output_directory');
|
||||
|
||||
foreach ($stock_dirs as $macro => $replacement)
|
||||
{
|
||||
$outdir = str_replace($macro, $replacement, $outdir);
|
||||
}
|
||||
|
||||
$status['output'] = @is_writable($outdir);
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the overall status. It's true when both the temporary and output directories are writable and there are
|
||||
* no critical configuration check failures.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function getShortStatus()
|
||||
{
|
||||
// Base the status on directory writeable status
|
||||
$status = $this->getFolderStatus();
|
||||
$ret = $status['output'];
|
||||
|
||||
// Scan for high severity configuration check errors
|
||||
$detailedStatus = $this->getDetailedStatus();
|
||||
|
||||
if (!empty($detailedStatus))
|
||||
{
|
||||
foreach ($detailedStatus as $configCheck)
|
||||
{
|
||||
if ($configCheck['severity'] == 'critical')
|
||||
{
|
||||
$ret = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return status
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a configuration check definition
|
||||
*
|
||||
* @param string $code The configuration check code (three digit number)
|
||||
* @param string $severity The severity (low, medium, high, critical)
|
||||
* @param string $description The description key for this configuration check
|
||||
* @param null $callback The callback used to determine the status of the configuration check
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addConfigurationCheckDefinition($code, $severity = 'low', $description = null, $callback = null)
|
||||
{
|
||||
if (!is_callable($callback))
|
||||
{
|
||||
$callback = [$this, 'q' . $code];
|
||||
}
|
||||
|
||||
if (empty($description))
|
||||
{
|
||||
$description = 'COM_AKEEBA_CPANEL_WARNING_Q' . $code;
|
||||
}
|
||||
|
||||
$newConfigurationCheck = [
|
||||
'code' => $code,
|
||||
'severity' => $severity,
|
||||
'description' => $description,
|
||||
'callback' => $callback,
|
||||
];
|
||||
|
||||
$this->configurationChecks[$code] = $newConfigurationCheck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a configuration check definition
|
||||
*
|
||||
* @param string $code The code of the configuration check to remove
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeConfigurationCheckDefinition($code)
|
||||
{
|
||||
if (isset($this->configurationChecks[$code]))
|
||||
{
|
||||
unset($this->configurationChecks[$code]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the configuration check definitions
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearConfigurationCheckDefinitions()
|
||||
{
|
||||
$this->configurationChecks = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the configuration check scripts. These are potential problems related to server
|
||||
* configuration, out of Akeeba's control. They are intended to give the user a
|
||||
* chance to fix them before they cause the backup to fail.
|
||||
*
|
||||
* Numbering scheme:
|
||||
* Q0xx No-go errors
|
||||
* Q1xx Critical system configuration errors
|
||||
* Q2xx Medium and low system configuration warnings
|
||||
* Q3xx Critical software configuration errors
|
||||
* Q4xx Medium and low component configuration warnings
|
||||
*
|
||||
* @param boolean $low_priority Should I include low priority quirks?
|
||||
* @param string $help_url_template The sprintf template from creating a help URL from a config check code
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDetailedStatus($low_priority = false, $help_url_template = 'https://www.akeeba.com/documentation/warnings/q%s.html')
|
||||
{
|
||||
static $detailedStatus = null;
|
||||
|
||||
if (is_null($detailedStatus) || $low_priority)
|
||||
{
|
||||
$detailedStatus = [];
|
||||
|
||||
foreach ($this->configurationChecks as $quirkDef)
|
||||
{
|
||||
if (!$low_priority && ($quirkDef['severity'] == 'low'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->checkConfiguration($detailedStatus, $quirkDef, $help_url_template);
|
||||
}
|
||||
}
|
||||
|
||||
return $detailedStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a path is restricted by open_basedirs
|
||||
*
|
||||
* @param string $check The path to check
|
||||
*
|
||||
* @return bool True if the path is restricted (which is bad)
|
||||
*/
|
||||
public function checkOpenBasedirs($check)
|
||||
{
|
||||
static $paths;
|
||||
|
||||
if (empty($paths))
|
||||
{
|
||||
$open_basedir = ini_get('open_basedir');
|
||||
|
||||
if (empty($open_basedir))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$delimiter = strpos($open_basedir, ';') !== false ? ';' : ':';
|
||||
$paths_temp = explode($delimiter, $open_basedir);
|
||||
|
||||
// Some open_basedirs are using environemtn variables
|
||||
$paths = [];
|
||||
|
||||
foreach ($paths_temp as $path)
|
||||
{
|
||||
if (array_key_exists($path, $_ENV))
|
||||
{
|
||||
$paths[] = $_ENV[$path];
|
||||
}
|
||||
else
|
||||
{
|
||||
$paths[] = $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($paths))
|
||||
{
|
||||
return false; // no restrictions
|
||||
}
|
||||
else
|
||||
{
|
||||
$newcheck = @realpath($check); // Resolve symlinks, like PHP does
|
||||
|
||||
if (!($newcheck === false))
|
||||
{
|
||||
$check = $newcheck;
|
||||
}
|
||||
|
||||
$included = false;
|
||||
|
||||
foreach ($paths as $path)
|
||||
{
|
||||
$newpath = @realpath($path);
|
||||
|
||||
if (!($newpath === false))
|
||||
{
|
||||
$path = $newpath;
|
||||
}
|
||||
|
||||
if (strlen($check) >= strlen($path))
|
||||
{
|
||||
// Only check if the path to check is longer than the inclusion path.
|
||||
// Otherwise, I guarantee it's not included!!
|
||||
// If the path to check begins with an inclusion path, it's permitted. Easy, huh?
|
||||
if (substr($check, 0, strlen($path)) == $path)
|
||||
{
|
||||
$included = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !$included;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a configuration check and adds it to the list if it raises a warning / error
|
||||
*
|
||||
* @param array $detailedStatus The configuration checks status array
|
||||
* @param array $quirkDef The configuration check definition
|
||||
* @param string $help_url_template The sprintf template from creating a help URL from a quirk code
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function checkConfiguration(&$detailedStatus, $quirkDef, $help_url_template)
|
||||
{
|
||||
if (call_user_func($quirkDef['callback']))
|
||||
{
|
||||
$description = Platform::getInstance()->translate($quirkDef['description']);
|
||||
|
||||
$detailedStatus[(string) $quirkDef['code']] = [
|
||||
'code' => $quirkDef['code'],
|
||||
'severity' => $quirkDef['severity'],
|
||||
'description' => $description,
|
||||
'help_url' => sprintf($help_url_template, $quirkDef['code']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Q001 - HIGH - Output directory unwriteable
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function q001()
|
||||
{
|
||||
$status = $this->getFolderStatus();
|
||||
|
||||
return !$status['output'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Q003 - HIGH - Backup output or temporary set to site's root
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function q003()
|
||||
{
|
||||
$stock_dirs = Platform::getInstance()->get_stock_directories();
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$outdir = $registry->get('akeeba.basic.output_directory');
|
||||
|
||||
foreach ($stock_dirs as $macro => $replacement)
|
||||
{
|
||||
$outdir = str_replace($macro, $replacement, $outdir);
|
||||
}
|
||||
|
||||
$outdir_real = @realpath($outdir);
|
||||
|
||||
if (!empty($outdir_real))
|
||||
{
|
||||
$outdir = $outdir_real;
|
||||
}
|
||||
|
||||
$siteroot = Platform::getInstance()->get_site_root();
|
||||
$siteroot_real = @realpath($siteroot);
|
||||
|
||||
if (!empty($siteroot_real))
|
||||
{
|
||||
$siteroot = $siteroot_real;
|
||||
}
|
||||
|
||||
return ($siteroot == $outdir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Q004 - HIGH - Free memory too low
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function q004()
|
||||
{
|
||||
// If we can't figure this out, don't report a problem. It doesn't
|
||||
// really matter, as the backup WILL crash eventually.
|
||||
if (!function_exists('ini_get'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$memLimit = ini_get("memory_limit");
|
||||
$memLimit = $this->_return_bytes($memLimit);
|
||||
|
||||
if ($memLimit <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// No limit?
|
||||
$availableRAM = $memLimit - memory_get_usage();
|
||||
|
||||
// We need at least 12Mb of free memory
|
||||
return ($availableRAM <= (12 * 1024 * 1024));
|
||||
}
|
||||
|
||||
/**
|
||||
* Q101 - HIGH - open_basedir on output directory
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function q101()
|
||||
{
|
||||
$stock_dirs = Platform::getInstance()->get_stock_directories();
|
||||
|
||||
// Get output writable status
|
||||
$registry = Factory::getConfiguration();
|
||||
$outdir = $registry->get('akeeba.basic.output_directory');
|
||||
|
||||
foreach ($stock_dirs as $macro => $replacement)
|
||||
{
|
||||
$outdir = str_replace($macro, $replacement, $outdir);
|
||||
}
|
||||
|
||||
return $this->checkOpenBasedirs($outdir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Q103 - HIGH - Less than 10" of max_execution_time with PHP Safe Mode enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function q103()
|
||||
{
|
||||
$exectime = ini_get('max_execution_time');
|
||||
$safemode = ini_get('safe_mode');
|
||||
|
||||
if (!$safemode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_numeric($exectime))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($exectime <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return $exectime < 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Q104 - HIGH - Temp directory is the same as the site's root
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function q104()
|
||||
{
|
||||
|
||||
$siteroot = Platform::getInstance()->get_site_root();
|
||||
$siteroot_real = @realpath($siteroot);
|
||||
|
||||
if (!empty($siteroot_real))
|
||||
{
|
||||
$siteroot = $siteroot_real;
|
||||
}
|
||||
|
||||
$stockDirs = Platform::getInstance()->get_stock_directories();
|
||||
$temp_directory = $stockDirs['[SITETMP]'];
|
||||
$temp_directory = @realpath($temp_directory);
|
||||
|
||||
if (empty($temp_directory))
|
||||
{
|
||||
$temp_directory = $siteroot;
|
||||
}
|
||||
|
||||
return ($siteroot == $temp_directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Q106 - HIGH - Table name prefix contains uppercase characters
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function q106()
|
||||
{
|
||||
$filters = Factory::getFilters();
|
||||
$databases = $filters->getInclusions('db');
|
||||
|
||||
foreach ($databases as $db)
|
||||
{
|
||||
if (!isset($db['prefix']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/[A-Z]/', $db['prefix']))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Q201 - MEDIUM - Outdated PHP version.
|
||||
*
|
||||
* We currently check for PHP lower than 7.4.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function q201()
|
||||
{
|
||||
return version_compare(PHP_VERSION, '7.4.0', 'lt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Q202 - MED - CRC problems with hash extension not present
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function q202()
|
||||
{
|
||||
$registry = Factory::getConfiguration();
|
||||
$archiver = $registry->get('akeeba.advanced.archiver_engine');
|
||||
|
||||
if ($archiver != 'zip')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !function_exists('hash_file');
|
||||
}
|
||||
|
||||
/**
|
||||
* Q203 - MED - Default output directory in use
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function q203()
|
||||
{
|
||||
$stock_dirs = Platform::getInstance()->get_stock_directories();
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$outdir = $registry->get('akeeba.basic.output_directory');
|
||||
|
||||
foreach ($stock_dirs as $macro => $replacement)
|
||||
{
|
||||
$outdir = str_replace($macro, $replacement, $outdir);
|
||||
}
|
||||
|
||||
$default = $stock_dirs['[DEFAULT_OUTPUT]'];
|
||||
|
||||
$outdir = Factory::getFilesystemTools()->TranslateWinPath($outdir);
|
||||
$default = Factory::getFilesystemTools()->TranslateWinPath($default);
|
||||
|
||||
return $outdir == $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Q204 - MED - Disabled functions may affect operation
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function q204()
|
||||
{
|
||||
$disabled = ini_get('disabled_functions');
|
||||
|
||||
return (!empty($disabled));
|
||||
}
|
||||
|
||||
/**
|
||||
* Q401 - LOW - ZIP format selected
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function q401()
|
||||
{
|
||||
$registry = Factory::getConfiguration();
|
||||
$archiver = $registry->get('akeeba.advanced.archiver_engine');
|
||||
|
||||
return $archiver == 'zip';
|
||||
}
|
||||
|
||||
private function _return_bytes($setting)
|
||||
{
|
||||
$val = trim($setting);
|
||||
$last = strtolower(substr($val, -1));
|
||||
$val = substr($val, 0, -1);
|
||||
|
||||
if (is_numeric($last))
|
||||
{
|
||||
return $setting;
|
||||
}
|
||||
|
||||
switch ($last)
|
||||
{
|
||||
case 't':
|
||||
$val *= 1024;
|
||||
case 'g':
|
||||
$val *= 1024;
|
||||
case 'm':
|
||||
$val *= 1024;
|
||||
case 'k':
|
||||
$val *= 1024;
|
||||
}
|
||||
|
||||
return (int) $val;
|
||||
}
|
||||
}
|
||||
1002
administrator/components/com_akeebabackup/engine/Util/Encrypt.php
Normal file
1002
administrator/components/com_akeebabackup/engine/Util/Encrypt.php
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Management class for temporary storage of the serialised engine state.
|
||||
*/
|
||||
class FactoryStorage
|
||||
{
|
||||
protected static $tempFileStoragePath;
|
||||
|
||||
/**
|
||||
* Returns the fully qualified path to the storage file
|
||||
*
|
||||
* @param string $tag Backup tag
|
||||
* @param string $extension File extension, default is php
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_storage_filename($tag = null, $extension = 'php')
|
||||
{
|
||||
if (is_null(self::$tempFileStoragePath))
|
||||
{
|
||||
$registry = Factory::getConfiguration();
|
||||
self::$tempFileStoragePath = $registry->get('akeeba.basic.output_directory', '');
|
||||
|
||||
if (empty(self::$tempFileStoragePath))
|
||||
{
|
||||
throw new InvalidArgumentException('You have not set a backup output directory.');
|
||||
}
|
||||
|
||||
self::$tempFileStoragePath = rtrim(self::$tempFileStoragePath, '/\\');
|
||||
|
||||
if (!is_writable(self::$tempFileStoragePath) || !is_readable(self::$tempFileStoragePath))
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('Backup output directory %s needs to be both readable and writeable to PHP for this backup software to function correctly.', self::$tempFileStoragePath));
|
||||
}
|
||||
}
|
||||
|
||||
$tag = empty($tag) ? '' : $tag;
|
||||
$filename = sprintf("akstorage%s%s.%s", empty($tag) ? '' : '_', $tag, $extension);
|
||||
|
||||
return self::$tempFileStoragePath . DIRECTORY_SEPARATOR . $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the storage. This method removes all stored values.
|
||||
*
|
||||
* @param null $tag
|
||||
*
|
||||
* @return bool True on success
|
||||
*/
|
||||
public function reset($tag = null)
|
||||
{
|
||||
$filename = $this->get_storage_filename($tag);
|
||||
|
||||
if (!is_file($filename) && !is_link($filename))
|
||||
{
|
||||
$filename = $this->get_storage_filename($tag, 'dat');
|
||||
}
|
||||
|
||||
if (!is_file($filename) && !is_link($filename))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return @unlink($this->get_storage_filename($tag));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a value to the storage
|
||||
*
|
||||
* @param string $value Serialised value to store
|
||||
* @param string|null $tag Backup tag
|
||||
* @param string $extension File extension to use, default is php
|
||||
*
|
||||
* @return bool True on success
|
||||
*/
|
||||
public function set($value, $tag = null, $extension = 'php')
|
||||
{
|
||||
$storage_filename = $this->get_storage_filename($tag, $extension);
|
||||
|
||||
if (file_exists($storage_filename))
|
||||
{
|
||||
@unlink($storage_filename);
|
||||
}
|
||||
|
||||
$isPHPFile = strtolower($extension) == 'php';
|
||||
|
||||
return @file_put_contents($storage_filename, $this->encode($value, $isPHPFile)) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a value from storage
|
||||
*
|
||||
* @param string|null $tag Backup tag. Used to determine the session state (memory) file name.
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public function &get($tag = null)
|
||||
{
|
||||
$ret = false;
|
||||
$storage_filename = $this->get_storage_filename($tag);
|
||||
$isPHPFile = true;
|
||||
$data = @file_get_contents($storage_filename);
|
||||
|
||||
/**
|
||||
* Some hosts, like WPEngine, do not allow us to use .php files for storing the factory state. In these case we
|
||||
* fall back to using the far less secure .dat extension. This if-block caters for that case.
|
||||
*/
|
||||
if ($data === false)
|
||||
{
|
||||
$storage_filename = $this->get_storage_filename($tag, 'dat');
|
||||
$isPHPFile = false;
|
||||
$data = @file_get_contents($storage_filename);
|
||||
}
|
||||
|
||||
if ($data === false)
|
||||
{
|
||||
return $ret;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$ret = $this->decode($data, $isPHPFile);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
$ret = false;
|
||||
}
|
||||
|
||||
unset($data);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the (serialized) data in a format suitable for storing in a deliberately web-inaccessible PHP file.
|
||||
*
|
||||
* IMPORTANT: On some hosts we HAVE to fall back to a .dat file. This is nowhere near as secure. This is not a
|
||||
* problem with Akeeba Backup but with the host, e.g. WPEngine. We WANT to do things securely but hosts' misguided
|
||||
* attempts at "security" force us to have a very insecure fallback. Please do not report this as a security issue
|
||||
* with us. report it to the host. We can't do something the host doesn't allow our code to do, obviously!
|
||||
*
|
||||
* @param string $data The data to encode
|
||||
* @param bool $isPHPFile Is this file extension .php?
|
||||
*
|
||||
* @return string The encoded data
|
||||
*/
|
||||
public function encode(&$data, $isPHPFile = true)
|
||||
{
|
||||
$encodingMethod = $this->getEncodingMethod();
|
||||
|
||||
switch ($encodingMethod)
|
||||
{
|
||||
case 'base64':
|
||||
$ret = base64_encode($data);
|
||||
break;
|
||||
|
||||
case 'uuencode':
|
||||
$ret = convert_uuencode($data);
|
||||
break;
|
||||
|
||||
case 'plain':
|
||||
default:
|
||||
$ret = $data;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($isPHPFile)
|
||||
{
|
||||
return '<' . '?' . 'php die(); ' . '>' . '?' . "\n" .
|
||||
$encodingMethod . "\n" . $ret;
|
||||
}
|
||||
|
||||
return $encodingMethod . "\n" . $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the data read from the deliberately web-inaccessible PHP file.
|
||||
*
|
||||
* @param string $data The data read from the file
|
||||
* @param bool $isPHPFile Does the memory file have a .php extension?
|
||||
*
|
||||
* @return false|string The decoded data. False if the decoding failed.
|
||||
*/
|
||||
public function decode(&$data, $isPHPFile = true)
|
||||
{
|
||||
// Parts: 0 = PHP die line; 1 = encoding mode; 2 = data
|
||||
$parts = explode("\n", $data, 3);
|
||||
|
||||
$expectedPartsCount = $isPHPFile ? 3 : 2;
|
||||
|
||||
if (count($parts) != $expectedPartsCount)
|
||||
{
|
||||
throw new RuntimeException("Invalid backup temporary data (memory file)");
|
||||
}
|
||||
|
||||
$encodingIndex = $isPHPFile ? 1 : 0;
|
||||
$dataIndex = $isPHPFile ? 2 : 1;
|
||||
|
||||
switch ($parts[$encodingIndex])
|
||||
{
|
||||
case 'base64';
|
||||
return base64_decode($parts[$dataIndex]);
|
||||
break;
|
||||
|
||||
case 'uuencode':
|
||||
return convert_uudecode($parts[$dataIndex]);
|
||||
break;
|
||||
|
||||
case 'plain':
|
||||
return $parts[$dataIndex];
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException(sprintf('Unsupported encoding method “%s”', $parts[$encodingIndex]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recommended method for encoding the temporary data
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getEncodingMethod()
|
||||
{
|
||||
// Preferred encoding: base sixty four, handled by PHP
|
||||
if (function_exists('base64_encode') && function_exists('base64_decode'))
|
||||
{
|
||||
return 'base64';
|
||||
}
|
||||
|
||||
// Fallback: UUencoding
|
||||
if (function_exists('convert_uuencode') && function_exists('convert_uudecode'))
|
||||
{
|
||||
return 'uuencode';
|
||||
}
|
||||
|
||||
// Final fallback (should NOT be necessary): plain text encoding
|
||||
return 'plain';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Throwable;
|
||||
|
||||
trait FileCloseAware
|
||||
{
|
||||
protected function conditionalFileClose($fp): bool
|
||||
{
|
||||
if (!is_resource($fp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return @fclose($fp);
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/* Windows system detection */
|
||||
if (!defined('_AKEEBA_IS_WINDOWS'))
|
||||
{
|
||||
$isWindows = DIRECTORY_SEPARATOR == '\\';
|
||||
|
||||
if (function_exists('php_uname'))
|
||||
{
|
||||
$isWindows = stristr(php_uname(), 'windows');
|
||||
}
|
||||
|
||||
define('_AKEEBA_IS_WINDOWS', $isWindows);
|
||||
}
|
||||
|
||||
/**
|
||||
* A filesystem scanner, for internal use
|
||||
*/
|
||||
class FileLister
|
||||
{
|
||||
public function &getFiles($folder, $fullpath = false)
|
||||
{
|
||||
// Initialize variables
|
||||
$arr = [];
|
||||
$false = false;
|
||||
|
||||
if (!is_dir($folder) && !is_dir($folder . '/'))
|
||||
{
|
||||
return $false;
|
||||
}
|
||||
|
||||
$handle = @opendir($folder);
|
||||
if ($handle === false)
|
||||
{
|
||||
$handle = @opendir($folder . '/');
|
||||
}
|
||||
// If directory is not accessible, just return FALSE
|
||||
if ($handle === false)
|
||||
{
|
||||
return $false;
|
||||
}
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$dereferencesymlinks = $registry->get('engine.archiver.common.dereference_symlinks');
|
||||
|
||||
while ((($file = @readdir($handle)) !== false))
|
||||
{
|
||||
if (($file != '.') && ($file != '..'))
|
||||
{
|
||||
// # Fix 2.4.b1: Do not add DS if we are on the site's root and it's an empty string
|
||||
// # Fix 2.4.b2: Do not add DS is the last character _is_ DS
|
||||
$ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR;
|
||||
$dir = "$folder/$file";
|
||||
$isDir = @is_dir($dir);
|
||||
$isLink = @is_link($dir);
|
||||
|
||||
//if (!$isDir || ($isDir && $isLink && !$dereferencesymlinks) ) {
|
||||
if (!$isDir)
|
||||
{
|
||||
if ($fullpath)
|
||||
{
|
||||
$data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($dir) : $dir;
|
||||
}
|
||||
else
|
||||
{
|
||||
$data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($file) : $file;
|
||||
}
|
||||
if ($data)
|
||||
{
|
||||
$arr[] = $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@closedir($handle);
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
public function &getFolders($folder, $fullpath = false)
|
||||
{
|
||||
// Initialize variables
|
||||
$arr = [];
|
||||
$false = false;
|
||||
|
||||
if (!is_dir($folder) && !is_dir($folder . '/'))
|
||||
{
|
||||
return $false;
|
||||
}
|
||||
|
||||
$handle = @opendir($folder);
|
||||
if ($handle === false)
|
||||
|
||||
{
|
||||
$handle = @opendir($folder . '/');
|
||||
}
|
||||
|
||||
// If directory is not accessible, just return FALSE
|
||||
if ($handle === false)
|
||||
{
|
||||
return $false;
|
||||
}
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$dereferencesymlinks = $registry->get('engine.archiver.common.dereference_symlinks');
|
||||
|
||||
while ((($file = @readdir($handle)) !== false))
|
||||
{
|
||||
if (($file != '.') && ($file != '..'))
|
||||
{
|
||||
$dir = "$folder/$file";
|
||||
$isDir = @is_dir($dir);
|
||||
$isLink = @is_link($dir);
|
||||
|
||||
if ($isDir)
|
||||
{
|
||||
//if(!$dereferencesymlinks && $isLink) continue;
|
||||
if ($fullpath)
|
||||
{
|
||||
$data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($dir) : $dir;
|
||||
}
|
||||
else
|
||||
{
|
||||
$data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($file) : $file;
|
||||
}
|
||||
|
||||
if ($data)
|
||||
{
|
||||
$arr[] = $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@closedir($handle);
|
||||
|
||||
return $arr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
|
||||
/**
|
||||
* Utility functions related to filesystem objects, e.g. path translation
|
||||
*/
|
||||
class FileSystem
|
||||
{
|
||||
/**
|
||||
* Are we running under Windows?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $isWindows = false;
|
||||
|
||||
/**
|
||||
* Local cache of the platform stock directories
|
||||
*
|
||||
* @var array|null
|
||||
* @since 7.0.3
|
||||
*/
|
||||
protected static $stockDirs = null;
|
||||
|
||||
/**
|
||||
* Initialise the object
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->isWindows = (DIRECTORY_SEPARATOR == '\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a Windows path more UNIX-like, by turning backslashes to forward slashes.
|
||||
* It takes into account UNC paths, e.g. \\myserver\some\folder becomes
|
||||
* \\myserver/some/folder.
|
||||
*
|
||||
* This function will also fix paths with multiple slashes, e.g. convert /var//www////html to /var/www/html
|
||||
*
|
||||
* @param string $p_path The path to transform
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function TranslateWinPath($p_path)
|
||||
{
|
||||
$is_unc = false;
|
||||
|
||||
if ($this->isWindows)
|
||||
{
|
||||
// Is this a UNC path?
|
||||
$is_unc = (substr($p_path, 0, 2) == '\\\\') || (substr($p_path, 0, 2) == '//');
|
||||
|
||||
// Change potential windows directory separator
|
||||
if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0, 1) == '\\'))
|
||||
{
|
||||
$p_path = strtr($p_path, '\\', '/');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove multiple slashes
|
||||
$p_path = str_replace('///', '/', $p_path);
|
||||
$p_path = str_replace('//', '/', $p_path);
|
||||
|
||||
// Fix UNC paths
|
||||
if ($is_unc)
|
||||
{
|
||||
$p_path = '//' . ltrim($p_path, '/');
|
||||
}
|
||||
|
||||
return $p_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes trailing slash or backslash from a pathname
|
||||
*
|
||||
* @param string $path The path to treat
|
||||
*
|
||||
* @return string The path without the trailing slash/backslash
|
||||
*/
|
||||
public function TrimTrailingSlash($path)
|
||||
{
|
||||
$newpath = $path;
|
||||
|
||||
if (substr($path, strlen($path) - 1, 1) == '\\')
|
||||
{
|
||||
$newpath = substr($path, 0, strlen($path) - 1);
|
||||
}
|
||||
|
||||
if (substr($path, strlen($path) - 1, 1) == '/')
|
||||
{
|
||||
$newpath = substr($path, 0, strlen($path) - 1);
|
||||
}
|
||||
|
||||
return $newpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with the archive name variables and their values. This is used to replace variables in archive
|
||||
* and directory names, etc.
|
||||
*
|
||||
* If there is a non-empty configuration value called volatile.core.archivenamevars with a serialised array it will
|
||||
* be unserialised and used. Otherwise the name variables will be calculated on-the-fly.
|
||||
*
|
||||
* IMPORTANT: These variables do NOT include paths such as [SITEROOT]
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_archive_name_variables()
|
||||
{
|
||||
$variables = [];
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$serialized = $registry->get('volatile.core.archivenamevars', null);
|
||||
|
||||
if (!empty($serialized))
|
||||
{
|
||||
$variables = @unserialize($serialized);
|
||||
}
|
||||
|
||||
if (empty($variables) || !is_array($variables))
|
||||
{
|
||||
$host = Platform::getInstance()->get_host();
|
||||
$version = defined('AKEEBA_VERSION') ? AKEEBA_VERSION : 'svn';
|
||||
$version = defined('AKEEBABACKUP_VERSION') ? AKEEBABACKUP_VERSION : $version;
|
||||
$platformVars = Platform::getInstance()->getPlatformVersion();
|
||||
|
||||
$siteName = $this->stringUrlUnicodeSlug(Platform::getInstance()->get_site_name());
|
||||
|
||||
if (strlen($siteName) > 50)
|
||||
{
|
||||
$siteName = substr($siteName, 0, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Time components. Expressed in whatever timezone the Platform decides to use.
|
||||
*/
|
||||
// Raw timezone, e.g. "EEST"
|
||||
$rawTz = Platform::getInstance()->get_local_timestamp("T");
|
||||
// Filename-safe timezone, e.g. "eest". Note the lowercase letters.
|
||||
$fsSafeTZ = strtolower(str_replace([' ', '/', ':'], ['_', '_', '_'], $rawTz));
|
||||
|
||||
$randVal = new RandomValue();
|
||||
|
||||
$variables = [
|
||||
'[DATE]' => Platform::getInstance()->get_local_timestamp("Ymd"),
|
||||
'[YEAR]' => Platform::getInstance()->get_local_timestamp("Y"),
|
||||
'[MONTH]' => Platform::getInstance()->get_local_timestamp("m"),
|
||||
'[DAY]' => Platform::getInstance()->get_local_timestamp("d"),
|
||||
'[TIME]' => Platform::getInstance()->get_local_timestamp("His"),
|
||||
'[TIME_TZ]' => Platform::getInstance()->get_local_timestamp("His") . $fsSafeTZ,
|
||||
'[WEEK]' => Platform::getInstance()->get_local_timestamp("W"),
|
||||
'[WEEKDAY]' => Platform::getInstance()->get_local_timestamp("l"),
|
||||
'[TZ]' => $fsSafeTZ,
|
||||
'[TZ_RAW]' => $rawTz,
|
||||
'[GMT_OFFSET]' => Platform::getInstance()->get_local_timestamp("O"),
|
||||
'[HOST]' => empty($host) ? 'unknown_host' : $host,
|
||||
'[VERSION]' => $version,
|
||||
'[PLATFORM_NAME]' => $platformVars['name'],
|
||||
'[PLATFORM_VERSION]' => $platformVars['version'],
|
||||
'[SITENAME]' => $siteName,
|
||||
'[RANDOM]' => $randVal->generateString(16, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789'),
|
||||
];
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the archive name variables in $source. For example "[DATE]-foobar" would be expanded to something
|
||||
* like "141101-foobar". IMPORTANT: These variables do NOT include paths.
|
||||
*
|
||||
* @param string $source The input string, possibly containing variables in the form of [VARIABLE]
|
||||
*
|
||||
* @return string The expanded string
|
||||
*/
|
||||
public function replace_archive_name_variables($source)
|
||||
{
|
||||
$tagReplacements = $this->get_archive_name_variables();
|
||||
|
||||
return str_replace(array_keys($tagReplacements), array_values($tagReplacements), $source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the platform-specific stock directories variables in the input string. For example "[SITEROOT]/foobar"
|
||||
* would be expanded to something like "/var/www/html/mysite/foobar"
|
||||
*
|
||||
* @param string $folder The input string to expand
|
||||
* @param bool $translate_win_dirs Should I translate Windows path separators to UNIX path separators? (default: false)
|
||||
* @param bool $trim_trailing_slash Should I remove the trailing slash (default: false)
|
||||
*
|
||||
* @return string The expanded string
|
||||
*/
|
||||
public function translateStockDirs($folder, $translate_win_dirs = false, $trim_trailing_slash = false)
|
||||
{
|
||||
if (is_null(self::$stockDirs))
|
||||
{
|
||||
self::$stockDirs = Platform::getInstance()->get_stock_directories();
|
||||
}
|
||||
|
||||
$temp = $folder;
|
||||
|
||||
foreach (self::$stockDirs as $find => $replace)
|
||||
{
|
||||
$temp = str_replace($find, $replace, $temp);
|
||||
}
|
||||
|
||||
if ($translate_win_dirs)
|
||||
{
|
||||
$temp = $this->TranslateWinPath($temp);
|
||||
}
|
||||
|
||||
if ($trim_trailing_slash)
|
||||
{
|
||||
$temp = $this->TrimTrailingSlash($temp);
|
||||
}
|
||||
|
||||
return $temp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebase a path to the platform filesystem variables (most to least specific).
|
||||
*
|
||||
* This is the inverse procedure of translateStockDirs().
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return string
|
||||
* @since 7.3.0
|
||||
*/
|
||||
public function rebaseFolderToStockDirs(string $path): string
|
||||
{
|
||||
// Normalize the path
|
||||
$path = $this->TrimTrailingSlash($path);
|
||||
$path = $this->TranslateWinPath($path);
|
||||
|
||||
// Get the stock directories, normalize them and sort them by longest to shortest
|
||||
$stock_directories = Platform::getInstance()->get_stock_directories();
|
||||
|
||||
$stock_directories = array_map(function ($path) {
|
||||
$path = $this->TrimTrailingSlash($path);
|
||||
|
||||
return $this->TranslateWinPath($path);
|
||||
}, $stock_directories);
|
||||
|
||||
uasort($stock_directories, function ($a, $b) {
|
||||
return -($a <=> $b);
|
||||
});
|
||||
|
||||
// Start replacing paths with variables
|
||||
foreach ($stock_directories as $var => $stockPath)
|
||||
{
|
||||
if (empty($stockPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strpos($path, $stockPath) !== 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $var . substr($path, strlen($stockPath));
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a set of files which prevent direct web access or at least web listing of the folder contents.
|
||||
*
|
||||
* This method generates a .htaccess for Apache, Lighttpd and Litespeed; a web.config file for IIS 7 or later; an
|
||||
* index.php, index.html and index.htm file for all other browsers.
|
||||
*
|
||||
* Despite this security precaution it is STRONGLY advised to keep your backup archives in a directory outside the
|
||||
* site's web root as explained in the Security Information chapter of the documentation. This method is designed
|
||||
* to only provide a defence of last resort.
|
||||
*
|
||||
* @param string $dir The output directory to secure against web access
|
||||
* @param bool $force Forcibly overwrite existing files
|
||||
*
|
||||
* @return void
|
||||
* @since 7.0.3
|
||||
*/
|
||||
public function ensureNoAccess($dir, $force = false)
|
||||
{
|
||||
// Create a .htaccess file to prevent all web access (Apache 1.3+, Lightspeed, Lighttpd, ...)
|
||||
if (!is_file($dir . '/.htaccess') || $force)
|
||||
{
|
||||
$htaccess = <<< APACHE
|
||||
## This file was generated automatically by the Akeeba Backup Engine
|
||||
##
|
||||
## DO NOT REMOVE THIS FILE
|
||||
##
|
||||
## This file makes sure that your backup output directory is not directly accessible from the web if you are using
|
||||
## the Apache, Lighttpd and Litespeed web server. This prevents unauthorized access to your backup archive files and
|
||||
## backup log files. Removing this file could have security implications for your site.
|
||||
##
|
||||
## You are strongly advised to never delete or modify any of the files automatically created in this folder by the
|
||||
## Akeeba Backup Engine, namely:
|
||||
##
|
||||
## * .htaccess
|
||||
## * web.config
|
||||
## * index.html
|
||||
## * index.htm
|
||||
## * index.php
|
||||
##
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
<IfModule mod_authz_core.c>
|
||||
<RequireAll>
|
||||
Require all denied
|
||||
</RequireAll>
|
||||
</IfModule>
|
||||
APACHE;
|
||||
|
||||
@file_put_contents($dir . '/.htaccess', $htaccess);
|
||||
}
|
||||
|
||||
// Create a web.config to prevent all web access (IIS 7+)
|
||||
if (!is_file($dir . '/web.config') || $force)
|
||||
{
|
||||
$webConfig = <<< XML
|
||||
<?xml version="1.0"?>
|
||||
<!--
|
||||
This file was generated automatically by the Akeeba Backup Engine
|
||||
|
||||
DO NOT REMOVE THIS FILE
|
||||
|
||||
This file makes sure that your backup output directory is not directly accessible from the web if you are using the
|
||||
Microsoft Internet Information Services (IIS) web server, version 7 or later. This prevents unauthorized access to your
|
||||
backup archive files and backup log files. Removing this file could have security implications for your site.
|
||||
|
||||
As noted above, this only works on IIS 7 or later.
|
||||
See https://www.iis.net/configreference/system.webserver/security/requestfiltering/fileextensions
|
||||
|
||||
You are strongly advised to never delete or modify any of the files automatically created in this folder by the
|
||||
Akeeba Backup Engine, namely:
|
||||
|
||||
* .htaccess
|
||||
* web.config
|
||||
* index.html
|
||||
* index.htm
|
||||
* index.php
|
||||
|
||||
-->
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<security>
|
||||
<requestFiltering>
|
||||
<fileExtensions allowUnlisted="false" >
|
||||
<clear />
|
||||
<add fileExtension=".html" allowed="true"/>
|
||||
</fileExtensions>
|
||||
</requestFiltering>
|
||||
</security>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
XML;
|
||||
@file_put_contents($dir . '/web.config', $webConfig);
|
||||
}
|
||||
|
||||
// Create a blank index.html or index.htm to prevent directory listings (all servers)
|
||||
$blankHtml = <<< HTML
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<title>Access Denied</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Access Denied</h1>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
|
||||
if (!is_file($dir . '/index.html') || $force)
|
||||
{
|
||||
@file_put_contents($dir . '/index.html', $blankHtml);
|
||||
}
|
||||
|
||||
if (!is_file($dir . '/index.htm') || $force)
|
||||
{
|
||||
@file_put_contents($dir . '/index.htm', $blankHtml);
|
||||
}
|
||||
|
||||
// Create a default index.php to prevent directory listings with an error (all servers)
|
||||
if (!is_file($dir . '/index.php') || $force)
|
||||
{
|
||||
$deadPHP = '<' . '?' . 'php header(\'HTTP/1.1 403 Forbidden\'); return;' . '?' . ">\n";
|
||||
$deadPHP .= <<< TEXT
|
||||
This file was generated automatically by the Akeeba Backup Engine
|
||||
|
||||
DO NOT REMOVE THIS FILE
|
||||
|
||||
This file tells your web server to not list the contents of this directory, instead returning an HTTP 403 Forbidden
|
||||
error. This makes it implausible for a malicious third party to successfully guess the filenames of your backup
|
||||
archives. Therefore, even if this folder is directly web accessible – despite the .htaccess and web.config file already
|
||||
put in place by the Akeeba Backup Engine – it will still be reasonably protected against malicious users trying to
|
||||
download your backup archives.
|
||||
|
||||
Please do not remove this file as it could have security implications for your site.
|
||||
|
||||
You are strongly advised to never delete or modify any of the files automatically created in this folder by the
|
||||
Akeeba Backup Engine, namely:
|
||||
|
||||
* .htaccess
|
||||
* web.config
|
||||
* index.html
|
||||
* index.htm
|
||||
* index.php
|
||||
|
||||
TEXT;
|
||||
|
||||
@file_put_contents($dir . '/index.php', $deadPHP);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to a (Unicode) slug
|
||||
*
|
||||
* @param string $string String to process
|
||||
*
|
||||
* @return string Processed string
|
||||
*
|
||||
* @since 7.5.0
|
||||
*/
|
||||
public function stringUrlUnicodeSlug(string $string): string
|
||||
{
|
||||
// Replace double byte whitespaces by single byte (East Asian languages)
|
||||
$str = preg_replace('/\xE3\x80\x80/', ' ', $string);
|
||||
|
||||
// Remove any '-' from the string as they will be used as concatenator.
|
||||
$str = str_replace('-', ' ', $str);
|
||||
|
||||
// Replace forbidden characters by whitespaces
|
||||
$str = preg_replace('#[:\?\#\*"@+=;!><&\.%()\]\/\'\\\\|\[]#', "\x20", $str);
|
||||
|
||||
// Delete all '?'
|
||||
$str = str_replace('?', '', $str);
|
||||
|
||||
// Trim white spaces at beginning and end of alias and make lowercase
|
||||
$str = trim(strtolower($str));
|
||||
|
||||
// Remove any duplicate whitespace and replace whitespaces by hyphens
|
||||
$str = preg_replace('#\x20+#', '-', $str);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
/**
|
||||
* Parses directory listings of the standard UNIX or MS-DOS style, i.e. what is most commonly returned by FTP and SFTP
|
||||
* servers running on *NIX and Windows machines.
|
||||
*
|
||||
* This class is intended to be used with the result RemoteResourceInterface::getRawList, parsing the raw folder listing
|
||||
* returned by an (S)FTP server -meant to be read by a human- into something you can programmatically work with. Using
|
||||
* RemoteResourceInterface::getWrapperStringFor with DirectoryIterator is generally preferable, if only much slower due
|
||||
* to the synchronous nature of remote stat() requests on each iterated element.
|
||||
*/
|
||||
class ListingParser
|
||||
{
|
||||
/**
|
||||
* Parse a UNIX- or MS-DOS-style directory listing.
|
||||
*
|
||||
* You get a hash array with entries. Each entry has the following keys:
|
||||
* name: the file / folder name.
|
||||
* type: file, dir or link.
|
||||
* target: link target (when type == link).
|
||||
* user: owner user, numeric or text. IIS FTP fakes this with the literal string "owner".
|
||||
* group: owner group, numeric or text. IIS FTP fakes this with the literal string "group".
|
||||
* size: size in bytes; note that some Linux servers report non-zero sizes for directories.
|
||||
* date: file creation date, most likely blatantly wrong; see below
|
||||
* perms: permissions in decimal format. Cast with dec2oct to get the 4 digit permissions string, e.g. 1755
|
||||
*
|
||||
* Important Notes
|
||||
*
|
||||
* Some UNIX systems report a size for directories. Do not assume that something is a directory if it's size is 0,
|
||||
* you will be surprised. Look at the 'type' element instead.
|
||||
*
|
||||
* Dates can be off. UNIX-style directory listings state either the year or the time, not both. If the file was
|
||||
* modified during this year you will get a date with a resolution of 1 minute. If the file was modified on a
|
||||
* different year you'll get a date with a resolution of 1 day. MS-DOS listings always contain the time. Again, the
|
||||
* resolution is 1 minute.
|
||||
*
|
||||
* Most MS-DOS style listings don't list junctions and symlinks. As a result they will be reported as regular
|
||||
* directories / files. This is the case for the IIS FTP server. Other servers may return the raw "dir" command
|
||||
* results (as CMD.EXE would parse it) in which case links and link targets do get reported.
|
||||
*
|
||||
* @param string $list The raw listing
|
||||
* @param bool $quick True to only include name, type, size and link target for each file.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function parseListing($list, $quick = false)
|
||||
{
|
||||
$res = $this->parseUnixListing($list, $quick);
|
||||
|
||||
if (empty($res))
|
||||
{
|
||||
$res = $this->parseMSDOSListing($list, $quick);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a UNIX-style directory listing. This is the format produced by ls -la on *NIX systems.
|
||||
*
|
||||
* You get a hash array with entries. Each entry has the following keys:
|
||||
* name: the file / folder name.
|
||||
* type: file, dir or link.
|
||||
* target: link target (when type == link).
|
||||
* user: owner user, numeric or text. IIS FTP fakes this with the literal string "owner".
|
||||
* group: owner group, numeric or text. IIS FTP fakes this with the literal string "group".
|
||||
* size: size in bytes; note that some Linux servers report non-zero sizes for directories.
|
||||
* date: file creation date, most likely blatantly wrong; see below
|
||||
* perms: permissions in decimal format. Cast with dec2oct to get the 4 digit permissions string, e.g. 1755
|
||||
*
|
||||
* @param string $list The raw listing
|
||||
* @param bool $quick True to only include name, type, size and link target for each file.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function parseUnixListing($list, $quick = false)
|
||||
{
|
||||
$ret = [];
|
||||
|
||||
$list = str_replace(["\r\n", "\r", "\n\n"], ["\n", "\n", "\n"], $list);
|
||||
$list = explode("\n", $list);
|
||||
$list = array_map('rtrim', $list);
|
||||
|
||||
foreach ($list as $v)
|
||||
{
|
||||
$vInfo = preg_split("/[\s]+/", $v, 9);
|
||||
|
||||
if ((is_array($vInfo) || $vInfo instanceof \Countable ? count($vInfo) : 0) != 9)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = [
|
||||
'name' => '',
|
||||
'type' => 'file',
|
||||
'target' => '',
|
||||
'user' => '0',
|
||||
'group' => '0',
|
||||
'size' => '0',
|
||||
'date' => '0',
|
||||
'perms' => '0',
|
||||
];
|
||||
|
||||
if ($quick)
|
||||
{
|
||||
$entry = [
|
||||
'name' => '',
|
||||
'type' => 'file',
|
||||
'size' => '0',
|
||||
'target' => '',
|
||||
];
|
||||
}
|
||||
|
||||
// ===== Parse permissions =====
|
||||
$permString = $vInfo[0];
|
||||
$permStringLen = strlen($permString);
|
||||
$typeBit = '-';
|
||||
$userPerms = 'r--';
|
||||
$groupPerms = 'r--';
|
||||
$otherPerms = 'r--';
|
||||
|
||||
if ($permStringLen)
|
||||
{
|
||||
$typeBit = substr($permString, 0, 1);
|
||||
}
|
||||
|
||||
switch ($typeBit)
|
||||
{
|
||||
case "d":
|
||||
$entry['type'] = 'dir';
|
||||
break;
|
||||
|
||||
case "l":
|
||||
$entry['type'] = 'link';
|
||||
break;
|
||||
}
|
||||
|
||||
// ===== Parse size =====
|
||||
$entry['size'] = $vInfo[4];
|
||||
|
||||
if (!$quick)
|
||||
{
|
||||
if ($permStringLen >= 4)
|
||||
{
|
||||
$userPerms = substr($permString, 1, 3);
|
||||
}
|
||||
|
||||
if ($permStringLen >= 7)
|
||||
{
|
||||
$groupPerms = substr($permString, 4, 3);
|
||||
}
|
||||
|
||||
if ($permStringLen >= 10)
|
||||
{
|
||||
$otherPerms = substr($permString, 7, 3);
|
||||
}
|
||||
|
||||
$bitPart = 0;
|
||||
$permsPart = '';
|
||||
|
||||
[$thisPerms, $thisBit] = $this->textPermsDecode($userPerms);
|
||||
$bitPart += 4 * $thisBit; // SetUID
|
||||
$permsPart .= $thisPerms;
|
||||
|
||||
[$thisPerms, $thisBit] = $this->textPermsDecode($groupPerms);
|
||||
$bitPart += 2 * $thisBit; // SetGID
|
||||
$permsPart .= $thisPerms;
|
||||
|
||||
[$thisPerms, $thisBit] = $this->textPermsDecode($otherPerms);
|
||||
$bitPart += $thisBit; // Sticky (restricted deletion)
|
||||
$permsPart .= $thisPerms;
|
||||
|
||||
$entry['perms'] = octdec($bitPart . $permsPart);
|
||||
|
||||
// ===== Parse ownership =====
|
||||
$entry['user'] = $vInfo[2];
|
||||
$entry['group'] = $vInfo[3];
|
||||
|
||||
// ===== Parse date =====
|
||||
$dateString = $vInfo[6] . ' ' . $vInfo[5] . ' ' . $vInfo[7];
|
||||
$x = date_create($dateString);
|
||||
$entry['date'] = ($x === false) ? 0 : $x->getTimestamp();
|
||||
}
|
||||
|
||||
// ===== Parse name =====
|
||||
$name = $vInfo[8];
|
||||
|
||||
// Ubuntu (possibly others?) tacks a start when either suid/sgid bits is set
|
||||
if (substr($name, -1) == '*')
|
||||
{
|
||||
$name = substr($name, 0, -1);
|
||||
}
|
||||
|
||||
// Link target parsing
|
||||
if (strpos($name, '->') !== false)
|
||||
{
|
||||
[$name, $target] = explode('->', $name);
|
||||
|
||||
$entry['target'] = trim($target);
|
||||
}
|
||||
|
||||
$entry['name'] = trim($name);
|
||||
|
||||
// ===== Return the entry =====
|
||||
$ret[] = $entry;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse am MS-DOS-style directory listing. This is the format produced by dir on MS-DOS and Windows systems.
|
||||
*
|
||||
* You get a hash array with entries. Each entry has the following keys:
|
||||
* name: the file / folder name.
|
||||
* type: file, dir or link.
|
||||
* target: link target (when type == link).
|
||||
* user: owner user, numeric or text. IIS FTP fakes this with the literal string "owner".
|
||||
* group: owner group, numeric or text. IIS FTP fakes this with the literal string "group".
|
||||
* size: size in bytes; note that some Linux servers report non-zero sizes for directories.
|
||||
* date: file creation date, most likely blatantly wrong; see below
|
||||
* perms: permissions in decimal format. Cast with dec2oct to get the 4 digit permissions string, e.g. 1755
|
||||
*
|
||||
* @param string $list The raw listing
|
||||
* @param bool $quick True to only include name, type, size and link target for each file.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function parseMSDOSListing($list, $quick = false)
|
||||
{
|
||||
$ret = [];
|
||||
|
||||
$list = str_replace(["\r\n", "\r", "\n\n"], ["\n", "\n", "\n"], $list);
|
||||
$list = explode("\n", $list);
|
||||
$list = array_map('rtrim', $list);
|
||||
|
||||
foreach ($list as $v)
|
||||
{
|
||||
$vInfo = preg_split("/[\s]+/", $v, 5);
|
||||
|
||||
if ((is_array($vInfo) || $vInfo instanceof \Countable ? count($vInfo) : 0) < 4)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = [
|
||||
'name' => '',
|
||||
'type' => 'file',
|
||||
'target' => '',
|
||||
'user' => '0',
|
||||
'group' => '0',
|
||||
'size' => '0',
|
||||
'date' => '0',
|
||||
'perms' => '0',
|
||||
];
|
||||
|
||||
if ($quick)
|
||||
{
|
||||
$entry = [
|
||||
'name' => '',
|
||||
'type' => 'file',
|
||||
'size' => '0',
|
||||
'target' => '',
|
||||
];
|
||||
}
|
||||
|
||||
// The first two fields are date and time
|
||||
$dateString = $vInfo[0] . ' ' . $vInfo[1];
|
||||
|
||||
// If position 2 is AM/PM append it and remove it from the list
|
||||
if (in_array(strtoupper($vInfo[2]), ['AM', 'PM']))
|
||||
{
|
||||
$dateString .= ' ' . $vInfo[2];
|
||||
|
||||
// This trick is required to remove the element and fix the indices for the rest of the parsing to work.
|
||||
unset ($vInfo[2]);
|
||||
$vInfo = array_merge($vInfo);
|
||||
}
|
||||
|
||||
if (!$quick)
|
||||
{
|
||||
$x = date_create($dateString);
|
||||
$entry['date'] = ($x === false) ? 0 : $x->getTimestamp();
|
||||
}
|
||||
|
||||
// The third field is either a special type indicator or the file size
|
||||
switch (strtoupper($vInfo[2]))
|
||||
{
|
||||
// Regular directory
|
||||
case '<DIR>':
|
||||
$entry['type'] = 'dir';
|
||||
break;
|
||||
|
||||
// Junction (like a directory symlink, pre-Win7)
|
||||
case '<JUNCTION>':
|
||||
// File symlink
|
||||
case '<SYMLINK>':
|
||||
// Directory symlink
|
||||
case '<SYMLINKD>':
|
||||
$entry['type'] = 'link';
|
||||
break;
|
||||
|
||||
default:
|
||||
$entry['size'] = (int) $vInfo[2];
|
||||
break;
|
||||
}
|
||||
|
||||
// And finally the file name. If it's a link it's in the format 'name [target]'
|
||||
preg_match('/(.*)[\s]+\[(.*)\]/', $vInfo[3], $matches);
|
||||
|
||||
if (empty($matches))
|
||||
{
|
||||
$entry['name'] = $vInfo[3];
|
||||
}
|
||||
else
|
||||
{
|
||||
$entry['type'] = 'link';
|
||||
$entry['name'] = $matches[1];
|
||||
$entry['target'] = $matches[2];
|
||||
}
|
||||
|
||||
// ===== Return the entry =====
|
||||
$ret[] = $entry;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a textual permissions representation for a user, group or others to a pair of octal digits (permissions
|
||||
* and flags). For example "r--" is converted to [4, 0], "r-x" to [5, 0], "r-t" to [5, 1]
|
||||
*
|
||||
* @param string $perms The textual permissions representation for a user, group or others
|
||||
*
|
||||
* @return array Two octal digits for permissions and flags (suid/sgid/sticky bit)
|
||||
*/
|
||||
private function textPermsDecode($perms)
|
||||
{
|
||||
$permBit = 0;
|
||||
$flagBits = 0;
|
||||
|
||||
if (strpos($perms, 'r'))
|
||||
{
|
||||
$permBit += 4;
|
||||
}
|
||||
|
||||
if (strpos($perms, 'w'))
|
||||
{
|
||||
$permBit += 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Both s and t denote flag set and imply the execute permissions is also granted. For user/groups it's
|
||||
* SetUID/SetGID respectively, for others it's the "sticky" bit (restricted deletion). Since only one of x, s
|
||||
* and t can be present at one time we use an if/elseif block. I don't use a switch because a. I am not 100%
|
||||
* sure that all servers will report the text permissions in rwx order and b. I am not sure that switch and
|
||||
* substr are faster than strpos (and too lazy to benchmark; sorry).
|
||||
*/
|
||||
if (strpos($perms, 'x'))
|
||||
{
|
||||
$permBit += 1;
|
||||
}
|
||||
elseif (strpos($perms, 't'))
|
||||
{
|
||||
$flagBits += 1;
|
||||
}
|
||||
elseif (strpos($perms, 's'))
|
||||
{
|
||||
$flagBits += 1;
|
||||
}
|
||||
|
||||
return [$permBit, $flagBits];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\Log;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
/**
|
||||
* The interface for Akeeba Engine logger objects
|
||||
*/
|
||||
interface LogInterface
|
||||
{
|
||||
/**
|
||||
* Open a new log instance with the specified tag. If another log is already open it is closed before switching to
|
||||
* the new log tag. If the tag is null use the default log defined in the logging system.
|
||||
*
|
||||
* @param string|null $tag The log to open
|
||||
* @param string $extension The log file extension (default: .php, use empty string for .log files)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function open($tag = null, $extension = '.php');
|
||||
|
||||
/**
|
||||
* Close the currently active log and set the current tag to null.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close();
|
||||
|
||||
/**
|
||||
* Reset (remove entries) of the log with the specified tag.
|
||||
*
|
||||
* @param string|null $tag The log to reset
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reset($tag = null);
|
||||
|
||||
/**
|
||||
* Add a message to the log
|
||||
*
|
||||
* @param string $level One of the Akeeba\Engine\Psr\Log\LogLevel constants
|
||||
* @param string $message The message to log
|
||||
* @param array $context Currently not used. Left here for PSR-3 compatibility.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function log($level, $message, array $context = []);
|
||||
|
||||
/**
|
||||
* Temporarily pause log output. The log() method MUST respect this.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function pause();
|
||||
|
||||
/**
|
||||
* Resume the previously paused log output. The log() method MUST respect this.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unpause();
|
||||
|
||||
/**
|
||||
* Returns the timestamp (in UNIX time long integer format) of the last log message written to the log with the
|
||||
* specific tag. The timestamp MUST be read from the log itself, not from the logger object. It is used by the
|
||||
* engine to find out the age of stalled backups which may have crashed.
|
||||
*
|
||||
* @param string|null $tag The log tag for which the last timestamp is returned
|
||||
*
|
||||
* @return int|null The timestamp of the last log message, in UNIX time. NULL if we can't get the timestamp.
|
||||
*/
|
||||
public function getLastTimestamp($tag = null);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\Log;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
trait WarningsLoggerAware
|
||||
{
|
||||
/**
|
||||
* The warnings in the current queue
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $warningsQueue = [];
|
||||
|
||||
/**
|
||||
* The maximum length of the warnings queue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $warningsQueueSize = 0;
|
||||
|
||||
/**
|
||||
* A combination of getWarnings() and resetWarnings(). Returns the warnings and immediately resets the warnings
|
||||
* queue.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
final public function getAndResetWarnings()
|
||||
{
|
||||
$ret = $this->getWarnings();
|
||||
|
||||
$this->resetWarnings();
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all warnings logged since the last time warnings were reset. The maximum number of warnings
|
||||
* returned is controlled by setWarningsQueueSize().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
final public function getWarnings()
|
||||
{
|
||||
return $this->warningsQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the warnings queue.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
final public function resetWarnings()
|
||||
{
|
||||
$this->warningsQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the warnings queue size.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
final public function getWarningsQueueSize()
|
||||
{
|
||||
return $this->warningsQueueSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the warnings queue size. A size of 0 means "no limit".
|
||||
*
|
||||
* @param int $queueSize The size of the warnings queue (in number of warnings items)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
final public function setWarningsQueueSize($queueSize = 0)
|
||||
{
|
||||
if (!is_numeric($queueSize) || empty($queueSize) || ($queueSize < 0))
|
||||
{
|
||||
$queueSize = 0;
|
||||
}
|
||||
|
||||
$this->warningsQueueSize = $queueSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a warning to the warnings queue.
|
||||
*
|
||||
* @param string $warning
|
||||
*/
|
||||
final protected function enqueueWarning($warning)
|
||||
{
|
||||
$this->warningsQueue[] = $warning;
|
||||
|
||||
// If there is no queue size limit there's nothing else to be done.
|
||||
if ($this->warningsQueueSize <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the queue size is exceeded remove as many of the earliest elements as required
|
||||
if (count($this->warningsQueue) > $this->warningsQueueSize)
|
||||
{
|
||||
$this->warningsQueueSize = array_slice($this->warningsQueue, -$this->warningsQueueSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\Log;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
interface WarningsLoggerInterface
|
||||
{
|
||||
/**
|
||||
* Returns an array with all warnings logged since the last time warnings were reset. The maximum number of warnings
|
||||
* returned is controlled by setWarningsQueueSize().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getWarnings();
|
||||
|
||||
/**
|
||||
* Resets the warnings queue.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function resetWarnings();
|
||||
|
||||
/**
|
||||
* A combination of getWarnings() and resetWarnings(). Returns the warnings and immediately resets the warnings
|
||||
* queue.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAndResetWarnings();
|
||||
|
||||
/**
|
||||
* Set the warnings queue size. A size of 0 means "no limit".
|
||||
*
|
||||
* @param int $queueSize The size of the warnings queue (in number of warnings items)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setWarningsQueueSize($queueSize = 0);
|
||||
|
||||
/**
|
||||
* Returns the warnings queue size.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getWarningsQueueSize();
|
||||
}
|
||||
594
administrator/components/com_akeebabackup/engine/Util/Logger.php
Normal file
594
administrator/components/com_akeebabackup/engine/Util/Logger.php
Normal file
@@ -0,0 +1,594 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Akeeba\Engine\Util\Log\LogInterface;
|
||||
use Akeeba\Engine\Util\Log\WarningsLoggerAware;
|
||||
use Akeeba\Engine\Util\Log\WarningsLoggerInterface;
|
||||
use Akeeba\Engine\Psr\Log\InvalidArgumentException;
|
||||
use Akeeba\Engine\Psr\Log\LoggerInterface;
|
||||
use Akeeba\Engine\Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Writes messages to the backup log file
|
||||
*/
|
||||
class Logger implements LoggerInterface, LogInterface, WarningsLoggerInterface
|
||||
{
|
||||
use WarningsLoggerAware;
|
||||
|
||||
/** @var string Full path to log file */
|
||||
protected $logName = null;
|
||||
|
||||
/** @var string The current log tag */
|
||||
protected $currentTag = null;
|
||||
|
||||
/** @var resource The file pointer to the current log file */
|
||||
protected $fp = null;
|
||||
|
||||
/** @var bool Is the logging currently paused? */
|
||||
protected $paused = false;
|
||||
|
||||
/** @var int The minimum log level */
|
||||
protected $configuredLoglevel;
|
||||
|
||||
/** @var string The untranslated path to the site's root */
|
||||
protected $site_root_untranslated;
|
||||
|
||||
/** @var string The translated path to the site's root */
|
||||
protected $site_root;
|
||||
|
||||
/**
|
||||
* Public constructor. Initialises the properties with the parameters from the backup profile and platform.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->initialiseWithProfileParameters();
|
||||
}
|
||||
|
||||
/**
|
||||
* When shutting down this class always close any open log files.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the logfile
|
||||
*
|
||||
* @param string $tag Backup origin
|
||||
*/
|
||||
public function reset($tag = null)
|
||||
{
|
||||
// Pause logging
|
||||
$this->pause();
|
||||
|
||||
// Get the file names for the default log and the tagged log
|
||||
$currentLogName = $this->logName;
|
||||
$this->logName = $this->getLogFilename($tag);
|
||||
|
||||
// Close the file if it's open
|
||||
if ($currentLogName == $this->logName)
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
// Remove the log file if it exists
|
||||
@unlink($this->logName);
|
||||
|
||||
// Reset the log file
|
||||
$fp = @fopen($this->logName, 'w');
|
||||
$hasWritten = false;
|
||||
|
||||
if ($fp !== false)
|
||||
{
|
||||
$hasWritten = fwrite($fp, '<' . '?' . 'php die(); ' . '?' . '>' . "\n") !== false;
|
||||
@fclose($fp);
|
||||
}
|
||||
|
||||
// If I could not write to a .log.php file try using a .log file instead.
|
||||
if (!$hasWritten)
|
||||
{
|
||||
$this->logName = $this->getLogFilename($tag, '');
|
||||
$fp = @fopen($this->logName, 'w');
|
||||
$hasWritten = false;
|
||||
|
||||
if ($fp !== false)
|
||||
{
|
||||
$hasWritten = fwrite($fp, "\n") !== false;
|
||||
@fclose($fp);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the default log file(s) if they exists
|
||||
$defaultLog = $this->getLogFilename(null);
|
||||
|
||||
if (!empty($tag) && @file_exists($defaultLog))
|
||||
{
|
||||
@unlink($defaultLog);
|
||||
}
|
||||
|
||||
$defaultLog = $this->getLogFilename(null, '');
|
||||
|
||||
if (!empty($tag) && @file_exists($defaultLog))
|
||||
{
|
||||
@unlink($defaultLog);
|
||||
}
|
||||
|
||||
// Set the current log tag
|
||||
$this->currentTag = $tag;
|
||||
|
||||
// Unpause logging
|
||||
$this->unpause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a line to the log, if the log level is high enough
|
||||
*
|
||||
* @param string $level The log level
|
||||
* @param string $message The message to write to the log
|
||||
* @param array $context The logging context. For PSR-3 compatibility but not used in text file logs.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function log($level, $message = '', array $context = [])
|
||||
{
|
||||
// Warnings are enqueued no matter what is the minimum log level to report in the log file
|
||||
if (in_array($level, [LogLevel::WARNING, LogLevel::NOTICE]))
|
||||
{
|
||||
$this->enqueueWarning($message);
|
||||
}
|
||||
|
||||
// If we are told to not log anything we can't continue
|
||||
if ($this->configuredLoglevel == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the log if it's closed
|
||||
if (is_null($this->fp))
|
||||
{
|
||||
$this->open($this->currentTag);
|
||||
}
|
||||
|
||||
// If the log could not be opened we can't continue
|
||||
if (is_null($this->fp))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the logging is paused we can't continue
|
||||
if ($this->paused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the log level as an integer (compatibility with our minimum log level configuration parameter)
|
||||
switch ($level)
|
||||
{
|
||||
case LogLevel::EMERGENCY:
|
||||
case LogLevel::ALERT:
|
||||
case LogLevel::CRITICAL:
|
||||
case LogLevel::ERROR:
|
||||
$intLevel = 1;
|
||||
break;
|
||||
|
||||
case LogLevel::WARNING:
|
||||
case LogLevel::NOTICE:
|
||||
$intLevel = 2;
|
||||
break;
|
||||
|
||||
case LogLevel::INFO:
|
||||
$intLevel = 3;
|
||||
break;
|
||||
|
||||
case LogLevel::DEBUG:
|
||||
$intLevel = 4;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException("Unknown log level $level", 500);
|
||||
break;
|
||||
}
|
||||
|
||||
// If the minimum log level is lower than what we're trying to log we cannot continue
|
||||
if ($this->configuredLoglevel < $intLevel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$translateRoot = true;
|
||||
|
||||
if (array_key_exists('root_translate', $context))
|
||||
{
|
||||
$translateRoot = ($context['root_translate'] === 1) || ($context['root_translate'] === '1') || ($context['root_translate'] === true);
|
||||
}
|
||||
|
||||
// Replace the site's root with <root> in the log file
|
||||
if ($translateRoot && !defined('AKEEBADEBUG'))
|
||||
{
|
||||
$message = str_replace($this->site_root_untranslated, "<root>", $message);
|
||||
$message = str_replace($this->site_root, "<root>", $message);
|
||||
}
|
||||
|
||||
// Replace new lines
|
||||
$message = str_replace("\r\n", "\n", $message);
|
||||
$message = str_replace("\r", "\n", $message);
|
||||
$message = str_replace("\n", ' \n ', $message);
|
||||
|
||||
switch ($level)
|
||||
{
|
||||
case LogLevel::EMERGENCY:
|
||||
case LogLevel::ALERT:
|
||||
case LogLevel::CRITICAL:
|
||||
case LogLevel::ERROR:
|
||||
$string = "ERROR |";
|
||||
break;
|
||||
|
||||
case LogLevel::WARNING:
|
||||
case LogLevel::NOTICE:
|
||||
$string = "WARNING |";
|
||||
break;
|
||||
|
||||
case LogLevel::INFO:
|
||||
$string = "INFO |";
|
||||
break;
|
||||
|
||||
default:
|
||||
$string = "DEBUG |";
|
||||
break;
|
||||
}
|
||||
|
||||
$string .= gmdate('Ymd H:i:s') . "|$message\r\n";
|
||||
|
||||
@fwrite($this->fp, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the absolute path to the log file
|
||||
*
|
||||
* @param string $tag The backup run's tag
|
||||
*
|
||||
* @return string The absolute path to the log file
|
||||
*/
|
||||
public function getLogFilename($tag = null, $extension = '.php')
|
||||
{
|
||||
if (empty($tag))
|
||||
{
|
||||
$fileName = 'akeeba.log' . $extension;
|
||||
}
|
||||
else
|
||||
{
|
||||
$fileName = "akeeba.$tag.log" . $extension;
|
||||
}
|
||||
|
||||
// Get output directory
|
||||
$registry = Factory::getConfiguration();
|
||||
$outputDirectory = $registry->get('akeeba.basic.output_directory');
|
||||
|
||||
// Get the log file name
|
||||
$absoluteLogFilename = Factory::getFilesystemTools()->TranslateWinPath($outputDirectory . DIRECTORY_SEPARATOR . $fileName);
|
||||
|
||||
return $absoluteLogFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the currently active log and set the current tag to null.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
// The log file changed. Close the old log.
|
||||
if (is_resource($this->fp))
|
||||
{
|
||||
@fclose($this->fp);
|
||||
}
|
||||
|
||||
$this->fp = null;
|
||||
$this->currentTag = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new log instance with the specified tag. If another log is already open it is closed before switching to
|
||||
* the new log tag. If the tag is null use the default log defined in the logging system.
|
||||
*
|
||||
* @param string|null $tag The log to open
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function open($tag = null, $extension = '.php')
|
||||
{
|
||||
// If the log is already open do nothing
|
||||
if (is_resource($this->fp) && ($tag == $this->currentTag))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If another log is open, close it
|
||||
if (is_resource($this->fp))
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
// Re-initialise site root and minimum log level since the active profile might have changed in the meantime
|
||||
$this->initialiseWithProfileParameters();
|
||||
|
||||
// Set the current tag
|
||||
$this->currentTag = $tag;
|
||||
|
||||
// Get the log filename
|
||||
$this->logName = $this->getLogFilename($tag, $extension);
|
||||
|
||||
// Touch the file
|
||||
@touch($this->logName);
|
||||
|
||||
// Open the log file. DO NOT USE APPEND ('ab') MODE. I NEED TO SEEK INTO THE FILE. SEE FURTHER BELOW!
|
||||
$this->fp = @fopen($this->logName, 'c');
|
||||
|
||||
// If we couldn't open the file set the file pointer to null
|
||||
if ($this->fp === false)
|
||||
{
|
||||
$this->fp = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Go to the end of the file, emulating append mode. DO NOT REPLACE THE fopen() FILE MODE!
|
||||
if (@fseek($this->fp, 0, SEEK_END) === -1)
|
||||
{
|
||||
@fclose($this->fp);
|
||||
@unlink($this->logName);
|
||||
|
||||
$this->fp = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The following sounds pretty stupid but there is a reason for that convoluted code.
|
||||
*
|
||||
* Some hosts, like WP Engine, will now allow you to write to a log file with a .php extension. The code below
|
||||
* tries to anticipate that when the log extension is .php. It will try to write to the *.log.php file and the
|
||||
* text is actually resembling PHP code. Hosts like WP Engine will fail the fwrite() which will cause this
|
||||
* method to terminate early and return a null pointer. Our code will catch this case and try to use a .log
|
||||
* extension as a safe fallback.
|
||||
*/
|
||||
if ($extension !== '.php')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to write something into the file
|
||||
$written = @fwrite($this->fp, '<?php die("test"); ?>' . "\n");
|
||||
|
||||
if ($written === false)
|
||||
{
|
||||
@fclose($this->fp);
|
||||
@unlink($this->logName);
|
||||
|
||||
$this->fp = null;
|
||||
|
||||
$this->open($tag, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Store truncate offset, we will have to rewind the internal pointer to it
|
||||
$truncate_point = ftell($this->fp) - $written;
|
||||
|
||||
if (ftruncate($this->fp, $truncate_point) === false)
|
||||
{
|
||||
@fclose($this->fp);
|
||||
@unlink($this->logName);
|
||||
|
||||
$this->fp = null;
|
||||
|
||||
$this->open($tag, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Finally, move the file pointer at the truncation point. Otherwise PHP will append NULL bytes to the string
|
||||
// to "pad" the file length to the internal file pointer. No need to check if the operation was successful,
|
||||
// worst case scenario we will have some extra NULL bytes, there's no need to kill the log operation
|
||||
@fseek($this->fp, $truncate_point);
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily pause log output. The log() method MUST respect this.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function pause()
|
||||
{
|
||||
$this->paused = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the previously paused log output. The log() method MUST respect this.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unpause()
|
||||
{
|
||||
$this->paused = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamp (in UNIX time long integer format) of the last log message written to the log with the
|
||||
* specific tag. The timestamp MUST be read from the log itself, not from the logger object. It is used by the
|
||||
* engine to find out the age of stalled backups which may have crashed.
|
||||
*
|
||||
* @param string|null $tag The log tag for which the last timestamp is returned
|
||||
*
|
||||
* @return int|null The timestamp of the last log message, in UNIX time. NULL if we can't get the timestamp.
|
||||
*/
|
||||
public function getLastTimestamp($tag = null)
|
||||
{
|
||||
$fileName = $this->getLogFilename($tag);
|
||||
|
||||
/**
|
||||
* The log file akeeba.tag.log.php may not exist but the akeeba.tag.log does. This would be the case in some bad
|
||||
* hosts, like WPEngine, which do not allow us to create .php files EVEN THOUGH that's the only way to ensure
|
||||
* the privileged information in the log file is not readable over the web. You can't fix bad hosts, you can
|
||||
* only work around them.
|
||||
*/
|
||||
if (!@file_exists($fileName) && @file_exists(substr($fileName, 0, -4)))
|
||||
{
|
||||
$fileName = substr($fileName, 0, -4);
|
||||
}
|
||||
|
||||
$timestamp = @filemtime($fileName);
|
||||
|
||||
if ($timestamp === false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* System is unusable.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function emergency($message, array $context = [])
|
||||
{
|
||||
$this->log(LogLevel::EMERGENCY, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action must be taken immediately.
|
||||
*
|
||||
* Example: Entire website down, database unavailable, etc. This should
|
||||
* trigger the SMS alerts and wake you up.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function alert($message, array $context = [])
|
||||
{
|
||||
$this->log(LogLevel::ALERT, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Critical conditions.
|
||||
*
|
||||
* Example: Application component unavailable, unexpected exception.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function critical($message, array $context = [])
|
||||
{
|
||||
$this->log(LogLevel::CRITICAL, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime errors that do not require immediate action but should typically
|
||||
* be logged and monitored.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function error($message, array $context = [])
|
||||
{
|
||||
$this->log(LogLevel::ERROR, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* \Exceptional occurrences that are not errors.
|
||||
*
|
||||
* Example: Use of deprecated APIs, poor use of an API, undesirable things
|
||||
* that are not necessarily wrong.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function warning($message, array $context = [])
|
||||
{
|
||||
$this->log(LogLevel::WARNING, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normal but significant events.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function notice($message, array $context = [])
|
||||
{
|
||||
$this->log(LogLevel::NOTICE, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interesting events.
|
||||
*
|
||||
* Example: User logs in, SQL logs.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function info($message, array $context = [])
|
||||
{
|
||||
$this->log(LogLevel::INFO, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed debug information.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function debug($message, array $context = [])
|
||||
{
|
||||
$this->log(LogLevel::DEBUG, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the logger properties with parameters from the backup profile and the platform
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function initialiseWithProfileParameters()
|
||||
{
|
||||
// Get the site's translated and untranslated root
|
||||
$this->site_root_untranslated = Platform::getInstance()->get_site_root();
|
||||
$this->site_root = Factory::getFilesystemTools()->TranslateWinPath($this->site_root_untranslated);
|
||||
|
||||
// Load the registry and fetch log level
|
||||
$registry = Factory::getConfiguration();
|
||||
$this->configuredLoglevel = $registry->get('akeeba.basic.log_level');
|
||||
$this->configuredLoglevel = $this->configuredLoglevel * 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
/**
|
||||
* A utility class to parse INI files.
|
||||
*
|
||||
* This is marked deprecated since Akeeba Engine 6.4.1. The configuration of the engine is no longer stored as INI data.
|
||||
* Moreover, we will be migrating away from the current INI files used for defining engine and GUI configuration
|
||||
* parameters.
|
||||
*
|
||||
* @package Akeeba\Engine\Util
|
||||
*
|
||||
* @deprecated 6.4.1
|
||||
*/
|
||||
abstract class ParseIni
|
||||
{
|
||||
/**
|
||||
* Parse an INI file and return an associative array. This monstrosity is required because some so-called hosts
|
||||
* have disabled PHP's parse_ini_file() function for "security reasons". Apparently their blatant ignorance doesn't
|
||||
* allow them to discern between the innocuous parse_ini_file and the potentially dangerous ini_set, leading them to
|
||||
* disable the former and let the latter enabled.
|
||||
*
|
||||
* @param string $file The file name or raw INI data to process
|
||||
* @param bool $process_sections True to also process INI sections
|
||||
* @param bool $rawdata Is this raw INI data? False when $file is a filepath.
|
||||
* @param bool $forcePHP Should I force the use of the pure-PHP INI file parser?
|
||||
*
|
||||
* @return array An associative array of sections, keys and values
|
||||
*/
|
||||
public static function parse_ini_file($file, $process_sections = false, $rawdata = false, $forcePHP = false)
|
||||
{
|
||||
/**
|
||||
* WARNING: DO NOT USE INI_SCANNER_RAW IN THE parse_ini_string / parse_ini_file FUNCTION CALLS WITHOUT POST-
|
||||
* PROCESSING!
|
||||
*
|
||||
* Sometimes we need to save data which is either multiline or has double quotes in the Engine's
|
||||
* configuration. For this reason we have to manually escape \r, \n, \t and \" in
|
||||
* Akeeba\Engine\Configuration::dumpObject(). If we don't we end up with multiline INI values which
|
||||
* won't work. However, if we are using INI_SCANNER_RAW these characters are not escaped back to their
|
||||
* original form. As a result we end up with broken data which cause various problems, the most visible
|
||||
* of which is that Google Storage integration is broken since the JSON data included in the config is
|
||||
* now unparseable.
|
||||
*
|
||||
* However, not using raw mode introduces other problems. For example, the sequence \$ is converted to $ because
|
||||
* it's assumed to be an escaped dollar sign. Things like $foo are addressed as variable interpolation, i.e.
|
||||
* "This is ${foo} wrong" results in "This is wrong" because $foo is considered as an interpolated variable.
|
||||
*
|
||||
* The solution to that is to use raw mode to parse the INI files and THEN unescape the variables. However, we
|
||||
* cannot simply use stripslashes/stripcslashes because we could end up replacing more than we should (unlike
|
||||
* addcslashes we cannot specify a list of escaped characters to consider). We have to do a slower string
|
||||
* replace instead.
|
||||
*
|
||||
* The next problem to consider is that when $process_sections is true some of the values generated are arrays
|
||||
* or even nested arrays. If you try to string replace on them hilarity ensues. Therefore we have the recursive
|
||||
* unescape method which takes care of that. To make things faster and maintain the array keys we use array_map
|
||||
* to apply recursiveUnescape to the array.
|
||||
*/
|
||||
|
||||
if ($rawdata)
|
||||
{
|
||||
if (!function_exists('parse_ini_string'))
|
||||
{
|
||||
return self::parse_ini_file_php($file, $process_sections, $rawdata);
|
||||
}
|
||||
|
||||
// !!! VERY IMPORTANT !!! Read the warning above before touching this line
|
||||
return array_map([
|
||||
__CLASS__, 'recursiveUnescape',
|
||||
], parse_ini_string($file, $process_sections, INI_SCANNER_RAW));
|
||||
}
|
||||
|
||||
if (!function_exists('parse_ini_file'))
|
||||
{
|
||||
return self::parse_ini_file_php($file, $process_sections);
|
||||
}
|
||||
|
||||
// !!! VERY IMPORTANT !!! Read the warning above before touching this line
|
||||
return array_map([__CLASS__, 'recursiveUnescape'], parse_ini_file($file, $process_sections, INI_SCANNER_RAW));
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively unescape values which have been escaped by Akeeba\Engine\Configuration::dumpObject().
|
||||
*
|
||||
* @param string|array $value
|
||||
*
|
||||
* @return string|array Unescaped result
|
||||
*/
|
||||
static function recursiveUnescape($value)
|
||||
{
|
||||
if (is_array($value))
|
||||
{
|
||||
return array_map([__CLASS__, 'recursiveUnescape'], $value);
|
||||
}
|
||||
|
||||
return str_replace(['\r', '\n', '\t', '\"'], ["\r", "\n", "\t", '"'], $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A PHP based INI file parser.
|
||||
*
|
||||
* Thanks to asohn ~at~ aircanopy ~dot~ net for posting this handy function on
|
||||
* the parse_ini_file page on http://gr.php.net/parse_ini_file
|
||||
*
|
||||
* @param string $file Filename to process
|
||||
* @param bool $process_sections True to also process INI sections
|
||||
* @param bool $rawdata If true, the $file contains raw INI data, not a filename
|
||||
*
|
||||
* @return array An associative array of sections, keys and values
|
||||
*/
|
||||
static function parse_ini_file_php($file, $process_sections = false, $rawdata = false)
|
||||
{
|
||||
$process_sections = ($process_sections !== true) ? false : true;
|
||||
|
||||
if (!$rawdata)
|
||||
{
|
||||
$ini = file($file);
|
||||
}
|
||||
else
|
||||
{
|
||||
$file = str_replace("\r", "", $file);
|
||||
$ini = explode("\n", $file);
|
||||
}
|
||||
|
||||
if (!is_array($ini))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count($ini) == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$sections = [];
|
||||
$values = [];
|
||||
$result = [];
|
||||
$globals = [];
|
||||
$i = 0;
|
||||
foreach ($ini as $line)
|
||||
{
|
||||
$line = trim($line);
|
||||
$line = str_replace("\t", " ", $line);
|
||||
|
||||
// Comments
|
||||
if (!preg_match('/^[a-zA-Z0-9[]/', $line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sections
|
||||
if ($line[0] == '[')
|
||||
{
|
||||
$tmp = explode(']', $line);
|
||||
$sections[] = trim(substr($tmp[0], 1));
|
||||
$i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key-value pair
|
||||
$lineParts = explode('=', $line, 2);
|
||||
if (count($lineParts) != 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$key = trim($lineParts[0]);
|
||||
$value = trim($lineParts[1]);
|
||||
unset($lineParts);
|
||||
|
||||
if (strstr($value, ";"))
|
||||
{
|
||||
$tmp = explode(';', $value);
|
||||
if (count($tmp) == 2)
|
||||
{
|
||||
if ((($value[0] != '"') && ($value[0] != "'")) ||
|
||||
preg_match('/^".*"\s*;/', $value) || preg_match('/^".*;[^"]*$/', $value) ||
|
||||
preg_match("/^'.*'\s*;/", $value) || preg_match("/^'.*;[^']*$/", $value)
|
||||
)
|
||||
{
|
||||
$value = $tmp[0];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($value[0] == '"')
|
||||
{
|
||||
$value = preg_replace('/^"(.*)".*/', '$1', $value);
|
||||
}
|
||||
elseif ($value[0] == "'")
|
||||
{
|
||||
$value = preg_replace("/^'(.*)'.*/", '$1', $value);
|
||||
}
|
||||
else
|
||||
{
|
||||
$value = $tmp[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
$value = trim($value);
|
||||
$value = trim($value, "'\"");
|
||||
|
||||
if ($i == 0)
|
||||
{
|
||||
if (substr($line, -1, 2) == '[]')
|
||||
{
|
||||
$globals[$key][] = $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
$globals[$key] = $value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (substr($line, -1, 2) == '[]')
|
||||
{
|
||||
$values[$i - 1][$key][] = $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
$values[$i - 1][$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ($j = 0; $j < $i; $j++)
|
||||
{
|
||||
if ($process_sections === true)
|
||||
{
|
||||
if (isset($sections[$j]) && isset($values[$j]))
|
||||
{
|
||||
$result[$sections[$j]] = $values[$j];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isset($values[$j]))
|
||||
{
|
||||
$result[] = $values[$j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result + $globals;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* This helper class is used to migrate Akeeba Backup profiles to the new storage format implemented since version
|
||||
* 6.4.1
|
||||
*
|
||||
* @since 6.4.1
|
||||
*/
|
||||
abstract class ProfileMigration
|
||||
{
|
||||
/**
|
||||
* Tries to migrate a backup profile to the new JSON-based storage format used since version 6.4.1.
|
||||
*
|
||||
* @param int $profileID The ID of the profile to migrate
|
||||
*
|
||||
* @return bool Whether we converted the profile
|
||||
*
|
||||
* @since 6.4.1
|
||||
*/
|
||||
public static function migrateProfile($profileID)
|
||||
{
|
||||
$platform = Platform::getInstance();
|
||||
$db = Factory::getDatabase($platform->get_platform_database_options());
|
||||
|
||||
// Is the database connected?
|
||||
if (!$db->connected())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load the raw data from the database
|
||||
try
|
||||
{
|
||||
$sql = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->qn($platform->tableNameProfiles))
|
||||
->where($db->qn('id') . ' = ' . $db->q($profileID));
|
||||
|
||||
$rawData = $db->setQuery($sql)->loadAssoc();
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decrypt the configuration data if required
|
||||
$rawData['configuration'] = self::decryptConfiguration($rawData['configuration']);
|
||||
$migrated = false;
|
||||
|
||||
// Migrate the configuration from INI to JSON format
|
||||
if (self::looksLikeIni($rawData['configuration']))
|
||||
{
|
||||
$rawData['configuration'] = self::convertINItoJSON($rawData['configuration']);
|
||||
|
||||
$migrated = true;
|
||||
}
|
||||
|
||||
// Migrate the filters from INI to JSON format
|
||||
if (self::looksLikeSerialized($rawData['filters']))
|
||||
{
|
||||
$rawData['filters'] = self::convertSerializedToJSON($rawData['filters']);
|
||||
|
||||
$migrated = true;
|
||||
}
|
||||
|
||||
if (!$migrated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$rawData['configuration'] = self::encryptConfiguration($rawData['configuration']);
|
||||
|
||||
$sql = $db->getQuery(true)
|
||||
->update($db->qn($platform->tableNameProfiles))
|
||||
->set($db->qn('configuration') . ' = ' . $db->q($rawData['configuration']))
|
||||
->set($db->qn('filters') . ' = ' . $db->q($rawData['filters']))
|
||||
->where($db->qn('id') . ' = ' . $db->q($profileID));
|
||||
|
||||
$db->setQuery($sql);
|
||||
|
||||
try
|
||||
{
|
||||
$result = $db->query();
|
||||
}
|
||||
catch (Exception $exc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($result == true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the configuration data if necessary. Returns the decrypted data.
|
||||
*
|
||||
* @param string $configData The possibly encrypted data.
|
||||
*
|
||||
* @return string The decrypted data
|
||||
*
|
||||
* @since 6.4.1
|
||||
*/
|
||||
public static function decryptConfiguration($configData)
|
||||
{
|
||||
$noData = empty($configData);
|
||||
$signature = ($noData || (strlen($configData) < 12)) ? '' : substr($configData, 0, 12);
|
||||
|
||||
if (in_array($signature, ['###AES128###', '###CTR128###']))
|
||||
{
|
||||
return Factory::getSecureSettings()->decryptSettings($configData);
|
||||
}
|
||||
|
||||
return $configData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the configuration data if necessary.
|
||||
*
|
||||
* @param string $configData The raw configuration data
|
||||
*
|
||||
* @return string The possibly encrypted configuration data
|
||||
*
|
||||
* @since 6.4.1
|
||||
*/
|
||||
public static function encryptConfiguration($configData)
|
||||
{
|
||||
$secureSettings = Factory::getSecureSettings();
|
||||
|
||||
return $secureSettings->encryptSettings($configData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the provided configuration data look like it's INI encoded?
|
||||
*
|
||||
* @param string $configData The unencrypted configuration data we read from the database.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 6.4.1
|
||||
*/
|
||||
public static function looksLikeIni($configData)
|
||||
{
|
||||
if (empty($configData))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($configData) < 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((substr($configData, 0, 8) == '[global]') || substr($configData, 0, 8) == '[akeeba]')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the INI-encoded data to JSON-encoded data
|
||||
*
|
||||
* @param string $configData The INI-encoded data
|
||||
*
|
||||
* @return string The JSON-encoded data
|
||||
*
|
||||
* @since 6.4.1
|
||||
*/
|
||||
public static function convertINItoJSON($configData)
|
||||
{
|
||||
$dataArray = ParseIni::parse_ini_file($configData, true, true);
|
||||
|
||||
return json_encode($dataArray, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_FORCE_OBJECT | JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the raw filters string provided seems to use serialized data? We actually check if it looks like JSON.
|
||||
* If it's not, we assume it's serialized data.
|
||||
*
|
||||
* @param string $rawFilters The raw filters string
|
||||
*
|
||||
* @return bool Does it look like a serialized string?
|
||||
*
|
||||
* @since 6.4.1
|
||||
*/
|
||||
public static function looksLikeSerialized($rawFilters)
|
||||
{
|
||||
if (empty($rawFilters))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (substr($rawFilters, 0, 1) == '{')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the serialized array in $rawFilters to JSON representation
|
||||
*
|
||||
* @param string $rawFilters Raw serialized string
|
||||
*
|
||||
* @return string JSON-encoded string
|
||||
*
|
||||
* @since 6.4.1
|
||||
*/
|
||||
public static function convertSerializedToJSON($rawFilters)
|
||||
{
|
||||
$filters = unserialize($rawFilters);
|
||||
|
||||
if (empty($filters))
|
||||
{
|
||||
$filters = [];
|
||||
}
|
||||
|
||||
return json_encode($filters, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_FORCE_OBJECT | JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Akeeba\Engine\Util\Pushbullet\Connector;
|
||||
use Exception;
|
||||
|
||||
class PushMessages
|
||||
{
|
||||
/**
|
||||
* The PushBullet connector
|
||||
*
|
||||
* @var Connector[]
|
||||
*/
|
||||
private $connectors = [];
|
||||
|
||||
/**
|
||||
* Should we send push messages?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $enabled = true;
|
||||
|
||||
/**
|
||||
* Creates the push messaging object
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$pushPreference = Platform::getInstance()->get_platform_configuration_option('push_preference', '0');
|
||||
$apiKey = Platform::getInstance()->get_platform_configuration_option('push_apikey', '');
|
||||
|
||||
// No API key? No push messages are enabled, so no point continuing really...
|
||||
if (empty($apiKey))
|
||||
{
|
||||
$pushPreference = 0;
|
||||
}
|
||||
|
||||
// We use a switch in case we add support for more push APIs in the future. The push_preference platform
|
||||
// option will tell us which service to use. In that case we'll have to refactor this class, but the public
|
||||
// API will remain the same.
|
||||
switch ($pushPreference)
|
||||
{
|
||||
default:
|
||||
case 0:
|
||||
$this->enabled = false;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
$keys = explode(',', $apiKey);
|
||||
$keys = array_map('trim', $keys);
|
||||
|
||||
foreach ($keys as $key)
|
||||
{
|
||||
try
|
||||
{
|
||||
$connector = new Connector($key);
|
||||
$connector->getDevices();
|
||||
$this->connectors[] = $connector;
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
Factory::getLog()->warning("Push messages cannot be sent with API key $key. Error received when trying to establish PushBullet connection: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->connectors))
|
||||
{
|
||||
Factory::getLog()->warning('No push messages can be sent: none of the provided API keys is usable. Push messages have been deactivated.');
|
||||
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a push message to all connected devices. The intent is to provide the user with an information message,
|
||||
* e.g. notify them about the progress of the backup.
|
||||
*
|
||||
* @param string $subject The subject of the message, shown in the lock screen. Keep it short.
|
||||
* @param string $details Long(er) description of what the message is about. Plain text (no HTML).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function message($subject, $details = null)
|
||||
{
|
||||
if (!$this->enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->connectors as $connector)
|
||||
{
|
||||
try
|
||||
{
|
||||
$connector->pushNote('', $subject, $details);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
Factory::getLog()->warning('Push messages suspended. Error received when trying to send push message:' . $e->getMessage());
|
||||
$this->enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a push message, containing a URL/URI, to all connected devices. The URL will be rendered as something
|
||||
* clickable on most devices.
|
||||
*
|
||||
* @param string $url The URL/URI
|
||||
* @param string $subject The subject of the message, shown in the lock screen. Keep it short.
|
||||
* @param string $details Long(er) description of what the message is about. Plain text (no HTML).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function link($url, $subject, $details = null)
|
||||
{
|
||||
if (!$this->enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->connectors as $connector)
|
||||
{
|
||||
try
|
||||
{
|
||||
$connector->pushLink('', $subject, $url, $details);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
Factory::getLog()->warning('Push messages suspended. Error received when trying to send push message with a link:' . $e->getMessage());
|
||||
$this->enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\Pushbullet;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Exception;
|
||||
|
||||
class ApiException extends Exception
|
||||
{
|
||||
// Exception thrown by Pushbullet
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\Pushbullet;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Postproc\ProxyAware;
|
||||
use CURLFile;
|
||||
|
||||
/**
|
||||
* Based on Pushbullet-for-PHP 2.10.1 – https://github.com/ivkos/Pushbullet-for-PHP/tree/v2
|
||||
*
|
||||
* The license for the original class is as follows:
|
||||
* ----------
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014 Ivaylo Stoyanov
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
* ----------
|
||||
*
|
||||
* The following class is a derivative work, NOT the original work.
|
||||
*/
|
||||
class Connector
|
||||
{
|
||||
use ProxyAware;
|
||||
|
||||
public const URL_PUSHES = 'https://api.pushbullet.com/v2/pushes';
|
||||
public const URL_DEVICES = 'https://api.pushbullet.com/v2/devices';
|
||||
public const URL_CONTACTS = 'https://api.pushbullet.com/v2/contacts';
|
||||
public const URL_UPLOAD_REQUEST = 'https://api.pushbullet.com/v2/upload-request';
|
||||
public const URL_USERS = 'https://api.pushbullet.com/v2/users';
|
||||
public const URL_SUBSCRIPTIONS = 'https://api.pushbullet.com/v2/subscriptions';
|
||||
public const URL_CHANNEL_INFO = 'https://api.pushbullet.com/v2/channel-info';
|
||||
public const URL_EPHEMERALS = 'https://api.pushbullet.com/v2/ephemerals';
|
||||
public const URL_PHONEBOOK = 'https://api.pushbullet.com/v2/permanents/phonebook';
|
||||
private $_apiKey;
|
||||
private $_curlCallback;
|
||||
|
||||
/**
|
||||
* Pushbullet constructor.
|
||||
*
|
||||
* @param string $apiKey API key.
|
||||
*
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function __construct($apiKey)
|
||||
{
|
||||
$this->_apiKey = $apiKey;
|
||||
|
||||
if (!function_exists('curl_init'))
|
||||
{
|
||||
throw new ApiException('cURL library is not loaded.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse recipient.
|
||||
*
|
||||
* @param string $recipient Recipient string.
|
||||
* @param array $data Data array to populate with the correct recipient parameter.
|
||||
*/
|
||||
private static function _parseRecipient($recipient, array &$data)
|
||||
{
|
||||
if (!empty($recipient))
|
||||
{
|
||||
if (filter_var($recipient, FILTER_VALIDATE_EMAIL) !== false)
|
||||
{
|
||||
$data['email'] = $recipient;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (substr($recipient, 0, 1) == "#")
|
||||
{
|
||||
$data['channel_tag'] = substr($recipient, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
$data['device_iden'] = $recipient;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a note.
|
||||
*
|
||||
* @param string $recipient Recipient. Can be device_iden, email or channel #tagname.
|
||||
* @param string $title The note's title.
|
||||
* @param string $body The note's message.
|
||||
*
|
||||
* @return object Response.
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function pushNote($recipient, $title, $body = null)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
Connector::_parseRecipient($recipient, $data);
|
||||
$data['type'] = 'note';
|
||||
$data['title'] = $title;
|
||||
$data['body'] = $body;
|
||||
|
||||
return $this->_curlRequest(self::URL_PUSHES, 'POST', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a link.
|
||||
*
|
||||
* @param string $recipient Recipient. Can be device_iden, email or channel #tagname.
|
||||
* @param string $title The link's title.
|
||||
* @param string $url The URL to open.
|
||||
* @param string $body A message associated with the link.
|
||||
*
|
||||
* @return object Response.
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function pushLink($recipient, $title, $url, $body = null)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
Connector::_parseRecipient($recipient, $data);
|
||||
$data['type'] = 'link';
|
||||
$data['title'] = $title;
|
||||
$data['url'] = $url;
|
||||
$data['body'] = $body;
|
||||
|
||||
return $this->_curlRequest(self::URL_PUSHES, 'POST', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a checklist.
|
||||
*
|
||||
* @param string $recipient Recipient. Can be device_iden, email or channel #tagname.
|
||||
* @param string $title The list's title.
|
||||
* @param string[] $items The list items.
|
||||
*
|
||||
* @return object Response.
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function pushList($recipient, $title, array $items)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
Connector::_parseRecipient($recipient, $data);
|
||||
$data['type'] = 'list';
|
||||
$data['title'] = $title;
|
||||
$data['items'] = $items;
|
||||
|
||||
return $this->_curlRequest(self::URL_PUSHES, 'POST', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a file.
|
||||
*
|
||||
* @param string $recipient Recipient. Can be device_iden, email or channel #tagname.
|
||||
* @param string $filePath The path of the file to push.
|
||||
* @param string $mimeType The MIME type of the file. If null, we'll try to guess it.
|
||||
* @param string $title The title of the push notification.
|
||||
* @param string $body The body of the push notification.
|
||||
* @param string $altFileName Alternative file name to use instead of the original one.
|
||||
* For example, you might want to push 'someFile.tmp' as 'image.jpg'.
|
||||
*
|
||||
* @return object Response.
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function pushFile($recipient, $filePath, $mimeType = null, $title = null, $body = null, $altFileName = null)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
$fullFilePath = realpath($filePath);
|
||||
|
||||
if (!is_readable($fullFilePath))
|
||||
{
|
||||
throw new ApiException('File: File does not exist or is unreadable.');
|
||||
}
|
||||
|
||||
if (filesize($fullFilePath) > 25 * 1024 * 1024)
|
||||
{
|
||||
throw new ApiException('File: File size exceeds 25 MB.');
|
||||
}
|
||||
|
||||
$data['file_name'] = $altFileName ?? basename($fullFilePath);
|
||||
|
||||
// Try to guess the MIME type if the argument is NULL
|
||||
$data['file_type'] = $mimeType ?? mime_content_type($fullFilePath);
|
||||
|
||||
// Request authorization to upload the file
|
||||
$response = $this->_curlRequest(self::URL_UPLOAD_REQUEST, 'GET', $data);
|
||||
$data['file_url'] = $response->file_url;
|
||||
|
||||
$response->data->file = new CURLFile($fullFilePath);
|
||||
|
||||
// Upload the file
|
||||
$this->_curlRequest($response->upload_url, 'POST', $response->data, false, false);
|
||||
|
||||
Connector::_parseRecipient($recipient, $data);
|
||||
$data['type'] = 'file';
|
||||
$data['title'] = $title;
|
||||
$data['body'] = $body;
|
||||
|
||||
return $this->_curlRequest(self::URL_PUSHES, 'POST', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get push history.
|
||||
*
|
||||
* @param int $modifiedAfter Request pushes modified after this UNIX timestamp.
|
||||
* @param string $cursor Request the next page via its cursor from a previous response. See the API
|
||||
* documentation (https://docs.pushbullet.com/http/) for a detailed description.
|
||||
* @param int $limit Maximum number of objects on each page.
|
||||
*
|
||||
* @return object Response.
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function getPushHistory($modifiedAfter = 0, $cursor = null, $limit = null)
|
||||
{
|
||||
$data = [];
|
||||
$data['modified_after'] = $modifiedAfter;
|
||||
|
||||
if ($cursor !== null)
|
||||
{
|
||||
$data['cursor'] = $cursor;
|
||||
}
|
||||
|
||||
if ($limit !== null)
|
||||
{
|
||||
$data['limit'] = $limit;
|
||||
}
|
||||
|
||||
return $this->_curlRequest(self::URL_PUSHES, 'GET', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss a push.
|
||||
*
|
||||
* @param string $pushIden push_iden of the push notification.
|
||||
*
|
||||
* @return object Response.
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function dismissPush($pushIden)
|
||||
{
|
||||
return $this->_curlRequest(self::URL_PUSHES . '/' . $pushIden, 'POST', ['dismissed' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a push.
|
||||
*
|
||||
* @param string $pushIden push_iden of the push notification.
|
||||
*
|
||||
* @return object Response.
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function deletePush($pushIden)
|
||||
{
|
||||
return $this->_curlRequest(self::URL_PUSHES . '/' . $pushIden, 'DELETE');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of available devices.
|
||||
*
|
||||
* @param int $modifiedAfter Request devices modified after this UNIX timestamp.
|
||||
* @param string $cursor Request the next page via its cursor from a previous response. See the API
|
||||
* documentation (https://docs.pushbullet.com/http/) for a detailed description.
|
||||
* @param int $limit Maximum number of objects on each page.
|
||||
*
|
||||
* @return object Response.
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function getDevices($modifiedAfter = 0, $cursor = null, $limit = null)
|
||||
{
|
||||
$data = [];
|
||||
$data['modified_after'] = $modifiedAfter;
|
||||
|
||||
if ($cursor !== null)
|
||||
{
|
||||
$data['cursor'] = $cursor;
|
||||
}
|
||||
|
||||
if ($limit !== null)
|
||||
{
|
||||
$data['limit'] = $limit;
|
||||
}
|
||||
|
||||
return $this->_curlRequest(self::URL_DEVICES, 'GET', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the current user.
|
||||
*
|
||||
* @return object Response.
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function getUserInformation()
|
||||
{
|
||||
return $this->_curlRequest(self::URL_USERS . '/me', 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update preferences for the current user.
|
||||
*
|
||||
* @param array $preferences Preferences.
|
||||
*
|
||||
* @return object Response.
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function updateUserPreferences($preferences)
|
||||
{
|
||||
return $this->_curlRequest(self::URL_USERS . '/me', 'POST', ['preferences' => $preferences]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a callback function that will be invoked right before executing each cURL request.
|
||||
*
|
||||
* @param callable $callback The callback function.
|
||||
*/
|
||||
public function addCurlCallback(callable $callback)
|
||||
{
|
||||
$this->_curlCallback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to a remote server using cURL.
|
||||
*
|
||||
* @param string $url URL to send the request to.
|
||||
* @param string $method HTTP method.
|
||||
* @param array $data Query data.
|
||||
* @param bool $sendAsJSON Send the request as JSON.
|
||||
* @param bool $auth Use the API key to authenticate
|
||||
*
|
||||
* @return object Response.
|
||||
* @throws ApiException
|
||||
*/
|
||||
private function _curlRequest($url, $method, $data = null, $sendAsJSON = true, $auth = true)
|
||||
{
|
||||
$curl = curl_init();
|
||||
|
||||
$this->applyProxySettingsToCurl($curl);
|
||||
|
||||
if ($method == 'GET' && $data !== null)
|
||||
{
|
||||
$url .= '?' . http_build_query($data);
|
||||
}
|
||||
|
||||
curl_setopt($curl, CURLOPT_URL, $url);
|
||||
|
||||
if ($auth)
|
||||
{
|
||||
curl_setopt($curl, CURLOPT_USERPWD, $this->_apiKey);
|
||||
}
|
||||
|
||||
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
|
||||
|
||||
if ($method == 'POST' && $data !== null)
|
||||
{
|
||||
if ($sendAsJSON)
|
||||
{
|
||||
$data = json_encode($data);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($data),
|
||||
]);
|
||||
}
|
||||
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
|
||||
}
|
||||
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_HEADER, false);
|
||||
|
||||
@curl_setopt($curl, CURLOPT_CAINFO, AKEEBA_CACERT_PEM);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
|
||||
|
||||
if ($this->_curlCallback !== null)
|
||||
{
|
||||
$curlCallback = $this->_curlCallback;
|
||||
$curlCallback($curl);
|
||||
}
|
||||
|
||||
$response = curl_exec($curl);
|
||||
|
||||
if ($response === false)
|
||||
{
|
||||
$curlError = curl_error($curl);
|
||||
curl_close($curl);
|
||||
throw new ApiException('cURL Error: ' . $curlError);
|
||||
}
|
||||
|
||||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
|
||||
if ($httpCode >= 400)
|
||||
{
|
||||
curl_close($curl);
|
||||
$responseParsed = json_decode($response);
|
||||
throw new ApiException('HTTP Error ' . $httpCode .
|
||||
' (' . $responseParsed->error->type . '): ' . $responseParsed->error->message);
|
||||
}
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
return json_decode($response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
/**
|
||||
* Crypto-safe random value generator. Based on the Randval class of the Aura for PHP's Session package.
|
||||
* The following is the license file accompanying the original file.
|
||||
*
|
||||
* ********************************************************************************
|
||||
* Copyright (c) 2011-2016, Aura for PHP
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* - Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
* ********************************************************************************
|
||||
*
|
||||
* Please note that this is a MODIFIED copy of the Randval class, mainly to allow it to be used on hosts
|
||||
* which lack both mbcrypt and OpenSSL PHP modules.
|
||||
*/
|
||||
class RandomValue
|
||||
{
|
||||
/**
|
||||
*
|
||||
* Returns a cryptographically secure random value.
|
||||
*
|
||||
* @param integer $bytes How many bytes to return
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generate($bytes = 32)
|
||||
{
|
||||
return random_bytes($bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random string with the specified length. WARNING: You get to specify the number of
|
||||
* random characters in the string, not the number of random bytes. The character pool is 64 characters
|
||||
* (6 bits) long. The entropy of your string is 6 * $characters bits. This means that a random string
|
||||
* of 32 characters has an entropy of 192 bits whereas a random sequence of 32 bytes returned by generate()
|
||||
* has an entropy of 8 * 32 = 256 bits.
|
||||
*
|
||||
* @param int $characters Number of characters
|
||||
* @param string $characterSet Characters to pick from
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateString($characters = 32, $characterSet = 'abcdefghijklmnopqrstuvwxyz-ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789')
|
||||
{
|
||||
$sourceString = str_split('abcdefghijklmnopqrstuvwxyz-ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789', 1);
|
||||
$ret = '';
|
||||
|
||||
$bytes = ceil($characters / 4) * 3;
|
||||
$randBytes = $this->generate($bytes);
|
||||
|
||||
for ($i = 0; $i < $bytes; $i += 3)
|
||||
{
|
||||
$subBytes = substr($randBytes, $i, 3);
|
||||
$subBytes = str_split($subBytes, 1);
|
||||
$subBytes = ord($subBytes[0]) * 65536 + ord($subBytes[1]) * 256 + ord($subBytes[2]);
|
||||
$subBytes = $subBytes & bindec('00000000111111111111111111111111');
|
||||
|
||||
$b = [];
|
||||
$b[0] = $subBytes >> 18;
|
||||
$b[1] = ($subBytes >> 12) & bindec('111111');
|
||||
$b[2] = ($subBytes >> 6) & bindec('111111');
|
||||
$b[3] = $subBytes & bindec('111111');
|
||||
|
||||
$ret .= $sourceString[$b[0]] . $sourceString[$b[1]] . $sourceString[$b[2]] . $sourceString[$b[3]];
|
||||
}
|
||||
|
||||
return substr($ret, 0, $characters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
|
||||
/**
|
||||
* Implements encrypted settings handling features
|
||||
*
|
||||
* @author nicholas
|
||||
*/
|
||||
class SecureSettings
|
||||
{
|
||||
/**
|
||||
* The filename for the settings encryption key
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $keyFilename = 'serverkey.php';
|
||||
|
||||
protected $key = null;
|
||||
|
||||
/**
|
||||
* Set the key filename e.g. 'serverkey.php';
|
||||
*
|
||||
* @param string $filename The new filename to use
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setKeyFilename($filename)
|
||||
{
|
||||
$this->keyFilename = $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the server key, overriding an already loaded key.
|
||||
*
|
||||
* @param $key
|
||||
*/
|
||||
public function setKey($key)
|
||||
{
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configured server key, automatically loading the server key storage file
|
||||
* if required.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKey()
|
||||
{
|
||||
if (is_null($this->key))
|
||||
{
|
||||
$this->key = '';
|
||||
|
||||
if (!defined('AKEEBA_SERVERKEY'))
|
||||
{
|
||||
$filename = dirname(__FILE__) . '/../' . $this->keyFilename;
|
||||
|
||||
if (file_exists($filename))
|
||||
{
|
||||
include_once $filename;
|
||||
}
|
||||
}
|
||||
|
||||
if (defined('AKEEBA_SERVERKEY'))
|
||||
{
|
||||
$this->key = base64_decode(AKEEBA_SERVERKEY);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the server options allow us to use settings encryption?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function supportsEncryption()
|
||||
{
|
||||
// Do we have the encrypt.php plugin?
|
||||
if (!class_exists('\\Akeeba\\Engine\\Util\\Encrypt', true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Did the user intentionally disable settings encryption?
|
||||
$useEncryption = Platform::getInstance()->get_platform_configuration_option('useencryption', -1);
|
||||
|
||||
if ($useEncryption == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do we have base64_encode/_decode required for encryption?
|
||||
if (!function_exists('base64_encode') || !function_exists('base64_decode'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pre-requisites met. We can encrypt and decrypt!
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the preferred encryption mode. Currently, if mcrypt is installed and activated we will
|
||||
* use AES128.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function preferredEncryption()
|
||||
{
|
||||
$aes = new Encrypt();
|
||||
$adapter = $aes->getAdapter();
|
||||
|
||||
if (!$adapter->isSupported())
|
||||
{
|
||||
return 'CTR128';
|
||||
}
|
||||
|
||||
return 'AES128';
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the settings using the automatically detected preferred algorithm
|
||||
*
|
||||
* @param $rawSettings string The raw settings string
|
||||
* @param $key string The encryption key. Set to NULL to automatically find the key.
|
||||
*
|
||||
* @return string The encrypted data to store in the database
|
||||
*/
|
||||
public function encryptSettings($rawSettings, $key = null)
|
||||
{
|
||||
// Do we really support encryption?
|
||||
if (!$this->supportsEncryption())
|
||||
{
|
||||
return $rawSettings;
|
||||
}
|
||||
|
||||
// Does any of the preferred encryption engines exist?
|
||||
$encryption = $this->preferredEncryption();
|
||||
|
||||
if (empty($encryption))
|
||||
{
|
||||
return $rawSettings;
|
||||
}
|
||||
|
||||
// Do we have a non-empty key to begin with?
|
||||
if (empty($key))
|
||||
{
|
||||
$key = $this->getKey();
|
||||
}
|
||||
|
||||
if (empty($key))
|
||||
{
|
||||
return $rawSettings;
|
||||
}
|
||||
|
||||
if ($encryption == 'AES128')
|
||||
{
|
||||
$encrypted = Factory::getEncryption()->AESEncryptCBC($rawSettings, $key);
|
||||
|
||||
if (empty($encrypted))
|
||||
{
|
||||
$encryption = 'CTR128';
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note: CBC returns the encrypted data as a binary string and requires Base 64 encoding
|
||||
$rawSettings = '###AES128###' . base64_encode($encrypted);
|
||||
}
|
||||
}
|
||||
|
||||
if ($encryption == 'CTR128')
|
||||
{
|
||||
$encrypted = Factory::getEncryption()->AESEncryptCtr($rawSettings, $key, 128);
|
||||
|
||||
if (!empty($encrypted))
|
||||
{
|
||||
// Note: CTR returns the encrypted data readily encoded in Base 64
|
||||
$rawSettings = '###CTR128###' . $encrypted;
|
||||
}
|
||||
}
|
||||
|
||||
return $rawSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the encrypted settings and returns the plaintext INI string
|
||||
*
|
||||
* @param string $encrypted The encrypted data
|
||||
*
|
||||
* @return string The decrypted data
|
||||
*/
|
||||
public function decryptSettings($encrypted, $key = null)
|
||||
{
|
||||
if (substr($encrypted, 0, 12) == '###AES128###')
|
||||
{
|
||||
$mode = 'AES128';
|
||||
}
|
||||
elseif (substr($encrypted, 0, 12) == '###CTR128###')
|
||||
{
|
||||
$mode = 'CTR128';
|
||||
}
|
||||
else
|
||||
{
|
||||
return $encrypted;
|
||||
}
|
||||
|
||||
if (empty($key))
|
||||
{
|
||||
$key = $this->getKey();
|
||||
}
|
||||
|
||||
if (empty($key))
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
$encrypted = substr($encrypted, 12);
|
||||
|
||||
switch ($mode)
|
||||
{
|
||||
default:
|
||||
case 'AES128':
|
||||
$encrypted = base64_decode($encrypted);
|
||||
$decrypted = rtrim(Factory::getEncryption()->AESDecryptCBC($encrypted, $key), "\0");
|
||||
break;
|
||||
|
||||
case 'CTR128':
|
||||
$decrypted = Factory::getEncryption()->AESDecryptCtr($encrypted, $key, 128);
|
||||
break;
|
||||
}
|
||||
|
||||
if (empty($decrypted))
|
||||
{
|
||||
$decrypted = '';
|
||||
}
|
||||
|
||||
return $decrypted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Exception;
|
||||
|
||||
class Statistics
|
||||
{
|
||||
/** @var bool used to block multipart updating initializing the backup */
|
||||
private $multipart_lock = true;
|
||||
|
||||
/** @var int The statistics record number of the current backup attempt */
|
||||
private $statistics_id = null;
|
||||
|
||||
/** @var array Local cache of the stat record data */
|
||||
private $cached_data = [];
|
||||
|
||||
/**
|
||||
* Returns all the filenames of the backup archives for the specified stat record,
|
||||
* or null if the backup type is wrong or the file doesn't exist. It takes into
|
||||
* account the multipart nature of Split Backup Archives.
|
||||
*
|
||||
* @param array $stat The backup statistics record
|
||||
* @param bool $skipNonComplete Skips over backups with no files produced
|
||||
*
|
||||
* @return array|null The filenames or null if it's not applicable
|
||||
*/
|
||||
public static function get_all_filenames($stat, $skipNonComplete = true)
|
||||
{
|
||||
// Shortcut for database entries marked as having no files
|
||||
if ($stat['filesexist'] == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// Initialize
|
||||
$base_directory = @dirname($stat['absolute_path']);
|
||||
$base_filename = $stat['archivename'];
|
||||
$filenames = [$base_filename];
|
||||
|
||||
if (empty($base_filename))
|
||||
{
|
||||
// This is a backup with a writer which doesn't store files on the server
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate all the filenames for this backup
|
||||
if ($stat['multipart'] > 1)
|
||||
{
|
||||
// Find the base filename and extension
|
||||
$dotpos = strrpos($base_filename, '.');
|
||||
$extension = substr($base_filename, $dotpos);
|
||||
$basefile = substr($base_filename, 0, $dotpos);
|
||||
|
||||
// Calculate the multiple names
|
||||
$multipart = $stat['multipart'];
|
||||
|
||||
for ($i = 1; $i < $multipart; $i++)
|
||||
{
|
||||
// Note: For $multipart = 10, it will produce i.e. .z01 through .z10
|
||||
// This is intentional. If the backup aborts and multipart=1, we
|
||||
// might be stuck with a .z01 file instead of a .zip. So do not
|
||||
// change the less than or equal with a straight less than.
|
||||
$filenames[] = $basefile . substr($extension, 0, 2) . sprintf('%02d', $i);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the files exist, otherwise attempt to provide relocated filename
|
||||
$ret = [];
|
||||
|
||||
$ds = DIRECTORY_SEPARATOR;
|
||||
// $test_file is the first file which must have been created
|
||||
$test_file = count($filenames) == 1 ? $filenames[0] : $filenames[1];
|
||||
|
||||
if (
|
||||
(!@file_exists($base_directory . $ds . $test_file)) ||
|
||||
(!is_dir($base_directory))
|
||||
)
|
||||
{
|
||||
// The test file wasn't detected. Use the configured output directory.
|
||||
$registry = Factory::getConfiguration();
|
||||
$base_directory = $registry->get('akeeba.basic.output_directory');
|
||||
}
|
||||
|
||||
foreach ($filenames as $filename)
|
||||
{
|
||||
// Turn relative path to absolute
|
||||
$filename = $base_directory . $ds . $filename;
|
||||
|
||||
// Return the new filename IF IT EXISTS!
|
||||
if (!@file_exists($filename))
|
||||
{
|
||||
$filename = '';
|
||||
}
|
||||
|
||||
// Do not return filename for invalid backups
|
||||
if (!empty($filename))
|
||||
{
|
||||
$ret[] = $filename;
|
||||
}
|
||||
}
|
||||
|
||||
// Edge case: still running backups, we have to brute force the scan
|
||||
// of existing files (multipart may be lying)
|
||||
if ($stat['status'] == 'run')
|
||||
{
|
||||
$base_filename = $stat['archivename'];
|
||||
$dotpos = strrpos($base_filename, '.');
|
||||
$extension = substr($base_filename, $dotpos);
|
||||
$basefile = substr($base_filename, 0, $dotpos);
|
||||
|
||||
$registry = Factory::getConfiguration();
|
||||
$dirs = [
|
||||
@dirname($stat['absolute_path']),
|
||||
$registry->get('akeeba.basic.output_directory'),
|
||||
];
|
||||
|
||||
// Look for base file
|
||||
foreach ($dirs as $dir)
|
||||
{
|
||||
if (@file_exists($dir . $ds . $base_filename))
|
||||
{
|
||||
$ret[] = $dir . $ds . $base_filename;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Look for added files
|
||||
$found = true;
|
||||
$i = 0;
|
||||
|
||||
while ($found)
|
||||
{
|
||||
$i++;
|
||||
$found = false;
|
||||
$part_file_name = $basefile . substr($extension, 0, 2) . sprintf('%02d', $i);
|
||||
|
||||
foreach ($dirs as $dir)
|
||||
{
|
||||
if (@file_exists($dir . $ds . $part_file_name))
|
||||
{
|
||||
$ret[] = $dir . $ds . $part_file_name;
|
||||
$found = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((count($ret) == 0) && $skipNonComplete)
|
||||
{
|
||||
$ret = null;
|
||||
}
|
||||
|
||||
if (!empty($ret) && is_array($ret))
|
||||
{
|
||||
$ret = array_unique($ret);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the initial multipart lock
|
||||
*/
|
||||
public function release_multipart_lock()
|
||||
{
|
||||
$this->multipart_lock = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the multipart status of the current backup attempt's statistics record
|
||||
*
|
||||
* @param int $multipart The new multipart status
|
||||
*/
|
||||
public function updateMultipart($multipart)
|
||||
{
|
||||
if ($this->multipart_lock)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Factory::getLog()->debug('Updating multipart status to ' . $multipart);
|
||||
|
||||
// Cache this change and commit to db only after the backup is done, or failed
|
||||
$registry = Factory::getConfiguration();
|
||||
$registry->set('volatile.statistics.multipart', $multipart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or updates the statistics record of the current backup attempt
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setStatistics($data)
|
||||
{
|
||||
$ret = Platform::getInstance()->set_or_update_statistics($this->statistics_id, $data);
|
||||
|
||||
if ($ret === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_null($ret))
|
||||
{
|
||||
$this->statistics_id = $ret;
|
||||
}
|
||||
|
||||
$this->cached_data = array_merge($this->cached_data, $data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the statistics record ID (used in DB backup classes)
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->statistics_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the cached data
|
||||
* @return array
|
||||
*/
|
||||
public function getRecord()
|
||||
{
|
||||
return $this->cached_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the "in step" flag of the current backup record.
|
||||
*
|
||||
* @param false $inStep Am I currently executing a backup step? False if just finished.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function updateInStep($inStep = false)
|
||||
{
|
||||
if (!$this->getId())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $this->getRecord();
|
||||
|
||||
/**
|
||||
* We will only update the instep of running backups for two reasons:
|
||||
*
|
||||
* 1. The very last Kettenrad entry is after the backup process is complete. The record is marked 'complete'. I
|
||||
* must not touch it in this case.
|
||||
*
|
||||
* 2. When a record is marked 'fail' its instep is also set to 0. This happens in Factory::resetState().
|
||||
*/
|
||||
//
|
||||
if ($data['status'] == 'complete')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$data['instep'] = $inStep ? 1 : 0;
|
||||
$data['backupend'] = Platform::getInstance()->get_timestamp_database();
|
||||
|
||||
try
|
||||
{
|
||||
return $this->setStatistics($data);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Temporary files management class. Handles creation, tracking and cleanup.
|
||||
*/
|
||||
class TemporaryFiles
|
||||
{
|
||||
|
||||
/**
|
||||
* Creates a randomly-named temporary file, registers it with the temporary
|
||||
* files management and returns its absolute path
|
||||
*
|
||||
* @return string The temporary file name
|
||||
*/
|
||||
public function createRegisterTempFile()
|
||||
{
|
||||
// Create a randomly named file in the temp directory
|
||||
$registry = Factory::getConfiguration();
|
||||
$tempFile = tempnam($registry->get('akeeba.basic.output_directory'), 'ak');
|
||||
|
||||
// Register it and return its absolute path
|
||||
$tempName = basename($tempFile);
|
||||
|
||||
return Factory::getTempFiles()->registerTempFile($tempName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a temporary file with the Akeeba Engine, storing the list of temporary files
|
||||
* in another temporary flat database file.
|
||||
*
|
||||
* @param string $fileName The path of the file, relative to the temporary directory
|
||||
*
|
||||
* @return string The absolute path to the temporary file, for use in file operations
|
||||
*/
|
||||
public function registerTempFile($fileName)
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$tempFiles = $configuration->get('volatile.tempfiles', false);
|
||||
if ($tempFiles === false)
|
||||
{
|
||||
$tempFiles = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
$tempFiles = @unserialize($tempFiles);
|
||||
|
||||
if ($tempFiles === false)
|
||||
{
|
||||
$tempFiles = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!in_array($fileName, $tempFiles))
|
||||
{
|
||||
$tempFiles[] = $fileName;
|
||||
$configuration->set('volatile.tempfiles', serialize($tempFiles));
|
||||
}
|
||||
|
||||
return Factory::getFilesystemTools()->TranslateWinPath($configuration->get('akeeba.basic.output_directory') . '/' . $fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister and delete a temporary file
|
||||
*
|
||||
* @param string $fileName The filename to unregister and delete
|
||||
* @param bool $removePrefix The prefix to remove
|
||||
*
|
||||
* @return bool True on success
|
||||
*/
|
||||
public function unregisterAndDeleteTempFile($fileName, $removePrefix = false)
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
if ($removePrefix)
|
||||
{
|
||||
$fileName = str_replace(Factory::getFilesystemTools()->TranslateWinPath($configuration->get('akeeba.basic.output_directory')), '', $fileName);
|
||||
|
||||
if ((substr($fileName, 0, 1) == '/') || (substr($fileName, 0, 1) == '\\'))
|
||||
{
|
||||
$fileName = substr($fileName, 1);
|
||||
}
|
||||
|
||||
if ((substr($fileName, -1) == '/') || (substr($fileName, -1) == '\\'))
|
||||
{
|
||||
$fileName = substr($fileName, 0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure this file is registered
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
$serialised = $configuration->get('volatile.tempfiles', false);
|
||||
$tempFiles = [];
|
||||
|
||||
if ($serialised !== false)
|
||||
{
|
||||
$tempFiles = @unserialize($serialised);
|
||||
}
|
||||
|
||||
if (!is_array($tempFiles))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($fileName, $tempFiles))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$file = $configuration->get('akeeba.basic.output_directory') . '/' . $fileName;
|
||||
Factory::getLog()->debug("-- Removing temporary file $fileName");
|
||||
$platform = strtoupper(PHP_OS);
|
||||
|
||||
// Chown normally doesn't work on Windows but many years ago I found it necessary to delete temp files. No idea.
|
||||
if ((substr($platform, 0, 6) == 'CYGWIN') || (substr($platform, 0, 3) == 'WIN'))
|
||||
{
|
||||
// On Windows we have to chown() the file first to make it owned by Nobody
|
||||
Factory::getLog()->debug("-- Windows hack: chowning $fileName");
|
||||
@chown($file, 600);
|
||||
}
|
||||
|
||||
$result = @$this->nullifyAndDelete($file);
|
||||
|
||||
// Make sure the file is removed before unregistering it
|
||||
if (!@file_exists($file))
|
||||
{
|
||||
$aPos = array_search($fileName, $tempFiles);
|
||||
|
||||
if ($aPos !== false)
|
||||
{
|
||||
unset($tempFiles[$aPos]);
|
||||
|
||||
$configuration->set('volatile.tempfiles', serialize($tempFiles));
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes all temporary files
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteTempFiles()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
$serialised = $configuration->get('volatile.tempfiles', false);
|
||||
$tempFiles = [];
|
||||
|
||||
if ($serialised !== false)
|
||||
{
|
||||
$tempFiles = @unserialize($serialised);
|
||||
}
|
||||
|
||||
if (!is_array($tempFiles))
|
||||
{
|
||||
$tempFiles = [];
|
||||
}
|
||||
|
||||
$fileName = null;
|
||||
|
||||
if (!empty($tempFiles))
|
||||
{
|
||||
foreach ($tempFiles as $fileName)
|
||||
{
|
||||
Factory::getLog()->debug("-- Removing temporary file $fileName");
|
||||
$file = $configuration->get('akeeba.basic.output_directory') . '/' . $fileName;
|
||||
$platform = strtoupper(PHP_OS);
|
||||
|
||||
// Chown normally doesn't work on Windows but many years ago I found it necessary to delete temp files. No idea.
|
||||
if ((substr($platform, 0, 6) == 'CYGWIN') || (substr($platform, 0, 3) == 'WIN'))
|
||||
{
|
||||
// On Windows we have to chwon() the file first to make it owned by Nobody
|
||||
@chown($file, 600);
|
||||
}
|
||||
|
||||
$ret = @$this->nullifyAndDelete($file);
|
||||
}
|
||||
}
|
||||
|
||||
$tempFiles = [];
|
||||
$configuration->set('volatile.tempfiles', serialize($tempFiles));
|
||||
}
|
||||
|
||||
/**
|
||||
* Nullify the contents of the file and try to delete it as well
|
||||
*
|
||||
* @param string $filename The absolute path to the file to delete
|
||||
*
|
||||
* @return bool True of the deletion is successful
|
||||
*/
|
||||
public function nullifyAndDelete($filename)
|
||||
{
|
||||
// Try to nullify (method #1)
|
||||
$fp = @fopen($filename, 'w');
|
||||
|
||||
if (is_resource($fp))
|
||||
{
|
||||
@fclose($fp);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try to nullify (method #2)
|
||||
@file_put_contents($filename, '');
|
||||
}
|
||||
|
||||
// Unlink
|
||||
return @unlink($filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,740 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\Transfer;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* FTP transfer object, using PHP as the transport backend
|
||||
*/
|
||||
class Ftp implements TransferInterface, RemoteResourceInterface
|
||||
{
|
||||
/**
|
||||
* FTP server's hostname or IP address
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $host = 'localhost';
|
||||
|
||||
/**
|
||||
* FTP server's port, default: 21
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
protected $port = 21;
|
||||
|
||||
/**
|
||||
* Username used to authenticate to the FTP server
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $username = '';
|
||||
|
||||
/**
|
||||
* Password used to authenticate to the FTP server
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $password = '';
|
||||
|
||||
/**
|
||||
* FTP initial directory
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $directory = '/';
|
||||
|
||||
/**
|
||||
* Should I use SSL to connect to the server (FTP over explicit SSL, a.k.a. FTPS)?
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $ssl = false;
|
||||
|
||||
/**
|
||||
* Should I use FTP passive mode?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $passive = true;
|
||||
|
||||
/**
|
||||
* Timeout for connecting to the FTP server, default: 10
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
protected $timeout = 10;
|
||||
|
||||
/**
|
||||
* The FTP connection handle
|
||||
*
|
||||
* @var resource|null
|
||||
*/
|
||||
private $connection = null;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param array $options Configuration options
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
if (isset($options['host']))
|
||||
{
|
||||
$this->host = $options['host'];
|
||||
}
|
||||
|
||||
if (isset($options['port']))
|
||||
{
|
||||
$this->port = (int) $options['port'];
|
||||
}
|
||||
|
||||
if (isset($options['username']))
|
||||
{
|
||||
$this->username = $options['username'];
|
||||
}
|
||||
|
||||
if (isset($options['password']))
|
||||
{
|
||||
$this->password = $options['password'];
|
||||
}
|
||||
|
||||
if (isset($options['directory']))
|
||||
{
|
||||
$this->directory = '/' . ltrim(trim($options['directory']), '/');
|
||||
}
|
||||
|
||||
if (isset($options['ssl']))
|
||||
{
|
||||
$this->ssl = $options['ssl'];
|
||||
}
|
||||
|
||||
if (isset($options['passive']))
|
||||
{
|
||||
$this->passive = $options['passive'];
|
||||
}
|
||||
|
||||
if (isset($options['timeout']))
|
||||
{
|
||||
$this->timeout = max(1, (int) $options['timeout']);
|
||||
}
|
||||
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this transfer method blocked by a server firewall?
|
||||
*
|
||||
* @param array $params Any additional parameters you might need to pass
|
||||
*
|
||||
* @return boolean True if the firewall blocks connections to a known host
|
||||
*/
|
||||
public static function isFirewalled(array $params = [])
|
||||
{
|
||||
try
|
||||
{
|
||||
$connector = new static([
|
||||
'host' => 'test.rebex.net',
|
||||
'port' => 21,
|
||||
'username' => 'demo',
|
||||
'password' => 'password',
|
||||
'directory' => '',
|
||||
'ssl' => $params['ssl'] ?? false,
|
||||
'passive' => true,
|
||||
'timeout' => 5,
|
||||
]);
|
||||
|
||||
$data = $connector->read('readme.txt');
|
||||
|
||||
if (empty($data))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all parameters on serialization except the connection resource
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return ['host', 'port', 'username', 'password', 'directory', 'ssl', 'passive', 'timeout'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect to the server on unserialize
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the FTP server
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
// Try to connect to the server
|
||||
if ($this->ssl)
|
||||
{
|
||||
if (function_exists('ftp_ssl_connect'))
|
||||
{
|
||||
$this->connection = @ftp_ssl_connect($this->host, $this->port);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->connection = false;
|
||||
|
||||
throw new RuntimeException('ftp_ssl_connect not available on this server', 500);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->connection = @ftp_connect($this->host, $this->port, $this->timeout);
|
||||
}
|
||||
|
||||
if ($this->connection === false)
|
||||
{
|
||||
throw new RuntimeException(sprintf('Cannot connect to FTP server [host:port] = %s:%s', $this->host, $this->port), 500);
|
||||
}
|
||||
|
||||
// Attempt to authenticate
|
||||
if (!@ftp_login($this->connection, $this->username, $this->password))
|
||||
{
|
||||
@ftp_close($this->connection);
|
||||
$this->connection = null;
|
||||
|
||||
throw new RuntimeException(sprintf('Cannot log in to FTP server [username:password] = %s:%s', $this->username, $this->password), 500);
|
||||
}
|
||||
|
||||
// Attempt to change to the initial directory
|
||||
$defaultDir = @ftp_pwd($this->connection) ?: '/';
|
||||
$directories = [
|
||||
$this->directory,
|
||||
rtrim($this->directory, '/'),
|
||||
trim($this->directory, '/'),
|
||||
$defaultDir,
|
||||
];
|
||||
|
||||
foreach ($directories as $dir)
|
||||
{
|
||||
$changedDir = @ftp_chdir($this->connection, $dir);
|
||||
|
||||
if ($changedDir)
|
||||
{
|
||||
$this->directory = $dir;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$changedDir)
|
||||
{
|
||||
@ftp_close($this->connection);
|
||||
$this->connection = null;
|
||||
|
||||
throw new RuntimeException(sprintf('Cannot change to initial FTP directory "%s" – make sure the folder exists and that you have adequate permissions to it. Pro tip: the default directory of your FTP connection is reported to be %s', $this->directory, $defaultDir), 500);
|
||||
}
|
||||
|
||||
// Apply the passive mode preference
|
||||
@ftp_pasv($this->connection, $this->passive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public destructor, closes any open FTP connections
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (!is_null($this->connection))
|
||||
{
|
||||
@ftp_close($this->connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the contents into the file
|
||||
*
|
||||
* @param string $fileName The full path to the file
|
||||
* @param string $contents The contents to write to the file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function write($fileName, $contents)
|
||||
{
|
||||
// Make sure the buffer:// wrapper is loaded
|
||||
class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);
|
||||
|
||||
$handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+');
|
||||
fwrite($handle, $contents);
|
||||
rewind($handle);
|
||||
|
||||
$cwd = $this->cwd();
|
||||
$remoteFilename = '/' . ltrim($fileName, '/');
|
||||
$remotePath = dirname($remoteFilename);
|
||||
$remoteName = basename($remoteFilename);
|
||||
|
||||
if (!$this->isDir($remotePath))
|
||||
{
|
||||
$this->mkdir($remotePath);
|
||||
}
|
||||
|
||||
$changedDir = @ftp_chdir($this->connection, $remotePath);
|
||||
|
||||
$ret = $changedDir && @ftp_fput($this->connection, $remoteName, $handle, FTP_BINARY);
|
||||
|
||||
if ($changedDir)
|
||||
{
|
||||
@ftp_chdir($this->connection, $cwd);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a local file to the remote storage
|
||||
*
|
||||
* @param string $localFilename The full path to the local file
|
||||
* @param string $remoteFilename The full path to the remote file
|
||||
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function upload($localFilename, $remoteFilename, $useExceptions = true)
|
||||
{
|
||||
$handle = @fopen($localFilename, 'r');
|
||||
|
||||
if ($handle === false)
|
||||
{
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw new RuntimeException("Unreadable local file $localFilename");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$cwd = $this->cwd();
|
||||
$remoteFilename = '/' . ltrim($remoteFilename, '/');
|
||||
$remotePath = dirname($remoteFilename);
|
||||
$remoteName = basename($remoteFilename);
|
||||
|
||||
if (!$this->isDir($remotePath))
|
||||
{
|
||||
$this->mkdir($remotePath);
|
||||
}
|
||||
|
||||
$changedDir = @ftp_chdir($this->connection, $remotePath);
|
||||
$ret = $changedDir && @ftp_fput($this->connection, $remoteName, $handle, FTP_BINARY);
|
||||
|
||||
if ($changedDir)
|
||||
{
|
||||
@ftp_chdir($this->connection, $cwd);
|
||||
}
|
||||
|
||||
@fclose($handle);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the contents of a remote file into a string
|
||||
*
|
||||
* @param string $fileName The full path to the remote file
|
||||
*
|
||||
* @return string The contents of the remote file
|
||||
*/
|
||||
public function read($fileName)
|
||||
{
|
||||
// Make sure the buffer:// wrapper is loaded
|
||||
class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);
|
||||
|
||||
$handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+');
|
||||
|
||||
$cwd = $this->cwd();
|
||||
$remoteFilename = '/' . ltrim($fileName, '/');
|
||||
$remotePath = dirname($remoteFilename);
|
||||
$remoteName = basename($remoteFilename);
|
||||
$changedDir = @ftp_chdir($this->connection, $remotePath);
|
||||
$result = $changedDir && @ftp_fget($this->connection, $handle, $remoteName, FTP_BINARY);
|
||||
|
||||
if ($changedDir)
|
||||
{
|
||||
@ftp_chdir($this->connection, $cwd);
|
||||
}
|
||||
|
||||
if ($result === false)
|
||||
{
|
||||
fclose($handle);
|
||||
throw new RuntimeException("Can not download remote file $fileName");
|
||||
}
|
||||
|
||||
rewind($handle);
|
||||
|
||||
$ret = '';
|
||||
|
||||
while (!feof($handle))
|
||||
{
|
||||
$ret .= fread($handle, 131072);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a remote file into a local file
|
||||
*
|
||||
* @param string $remoteFilename The remote file path to download from
|
||||
* @param string $localFilename The local file path to download to
|
||||
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function download($remoteFilename, $localFilename, $useExceptions = true)
|
||||
{
|
||||
$cwd = $this->cwd();
|
||||
$remoteFilename = '/' . ltrim($remoteFilename, '/');
|
||||
$remotePath = dirname($remoteFilename);
|
||||
$remoteName = basename($remoteFilename);
|
||||
$changedDir = @ftp_chdir($this->connection, $remotePath);
|
||||
|
||||
$ret = $changedDir && @ftp_get($this->connection, $localFilename, $remoteName, FTP_BINARY);
|
||||
|
||||
if ($changedDir)
|
||||
{
|
||||
@ftp_chdir($this->connection, $cwd);
|
||||
}
|
||||
|
||||
if (!$ret && $useExceptions)
|
||||
{
|
||||
throw new RuntimeException("Cannot download remote file $remoteFilename through FTP.");
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file (remove it from the disk)
|
||||
*
|
||||
* @param string $fileName The full path to the file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function delete($fileName)
|
||||
{
|
||||
$cwd = $this->cwd();
|
||||
$remoteFilename = '/' . ltrim($fileName, '/');
|
||||
$remotePath = dirname($remoteFilename);
|
||||
$remoteName = basename($remoteFilename);
|
||||
|
||||
if (!$this->isDir($remotePath))
|
||||
{
|
||||
$this->mkdir($remotePath);
|
||||
}
|
||||
|
||||
$changedDir = @ftp_chdir($this->connection, $remotePath);
|
||||
$ret = $changedDir && @ftp_delete($this->connection, $remoteName);;
|
||||
|
||||
if ($changedDir)
|
||||
{
|
||||
ftp_chdir($this->connection, $cwd);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of the file. Actually, we have to read it in memory and upload it again.
|
||||
*
|
||||
* @param string $from The full path of the file to copy from
|
||||
* @param string $to The full path of the file that will hold the copy
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function copy($from, $to)
|
||||
{
|
||||
// Make sure the buffer:// wrapper is loaded
|
||||
class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);
|
||||
|
||||
$handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+');
|
||||
|
||||
$cwd = $this->cwd();
|
||||
$remoteFilename = '/' . ltrim($from, '/');
|
||||
$remotePath = dirname($remoteFilename);
|
||||
$remoteName = basename($remoteFilename);
|
||||
$changedDir = @ftp_chdir($this->connection, $remotePath);
|
||||
|
||||
$ret = $changedDir && @ftp_fget($this->connection, $handle, $remoteName, FTP_BINARY);
|
||||
|
||||
if ($ret !== false)
|
||||
{
|
||||
rewind($handle);
|
||||
|
||||
$remoteFilename = '/' . ltrim($to, '/');
|
||||
$remotePath = dirname($remoteFilename);
|
||||
$remoteName = basename($remoteFilename);
|
||||
|
||||
if (!$this->isDir($remotePath))
|
||||
{
|
||||
$this->mkdir($remotePath);
|
||||
}
|
||||
|
||||
$changedDir = @ftp_chdir($this->connection, $remotePath);
|
||||
$ret = $changedDir && @ftp_fput($this->connection, $remoteName, $handle, FTP_BINARY);
|
||||
}
|
||||
|
||||
if ($changedDir)
|
||||
{
|
||||
@ftp_chdir($this->connection, $cwd);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move or rename a file
|
||||
*
|
||||
* @param string $from The full path of the file to move
|
||||
* @param string $to The full path of the target file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function move($from, $to)
|
||||
{
|
||||
return @ftp_rename($this->connection, $from, $to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the permissions of a file
|
||||
*
|
||||
* @param string $fileName The full path of the file whose permissions will change
|
||||
* @param integer $permissions The new permissions, e.g. 0644 (remember the leading zero in octal numbers!)
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function chmod($fileName, $permissions)
|
||||
{
|
||||
if (@ftp_chmod($this->connection, $permissions, $fileName) !== false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$permissionsOctal = decoct((int) $permissions);
|
||||
|
||||
if (@ftp_site($this->connection, "CHMOD $permissionsOctal $fileName") !== false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all
|
||||
* intermediate directories if they do not already exist.
|
||||
*
|
||||
* @param string $dirName The full path of the directory to create
|
||||
* @param integer $permissions The permissions of the created directory
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function mkdir($dirName, $permissions = 0755)
|
||||
{
|
||||
$targetDir = rtrim($dirName, '/');
|
||||
|
||||
$directories = explode('/', $targetDir);
|
||||
|
||||
$remoteDir = '';
|
||||
|
||||
foreach ($directories as $dir)
|
||||
{
|
||||
if (!$dir)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$remoteDir .= '/' . $dir;
|
||||
|
||||
// Continue if the folder already exists. Otherwise I'll get a an error even if everything is fine
|
||||
if ($this->isDir($remoteDir))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$ret = @ftp_mkdir($this->connection, $remoteDir);
|
||||
|
||||
if ($ret === false)
|
||||
{
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
$this->chmod($dirName, $permissions);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given directory exists
|
||||
*
|
||||
* @param string $path The full path of the remote directory to check
|
||||
*
|
||||
* @return boolean True if the directory exists
|
||||
*/
|
||||
public function isDir($path)
|
||||
{
|
||||
$cur_dir = ftp_pwd($this->connection);
|
||||
|
||||
if (@ftp_chdir($this->connection, $path))
|
||||
{
|
||||
// If it is a directory, then change the directory back to the original directory
|
||||
ftp_chdir($this->connection, $cur_dir);
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current working directory
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function cwd()
|
||||
{
|
||||
return ftp_pwd($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute remote path from a path relative to the initial directory configured when creating the
|
||||
* transfer object.
|
||||
*
|
||||
* @param string $fileName The relative path of a file or directory
|
||||
*
|
||||
* @return string The absolute path for use by the transfer object
|
||||
*/
|
||||
public function getPath($fileName)
|
||||
{
|
||||
$fileName = str_replace('\\', '/', $fileName);
|
||||
|
||||
if (strpos($fileName, $this->directory) === 0)
|
||||
{
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
$fileName = trim($fileName, '/');
|
||||
$fileName = rtrim($this->directory, '/') . '/' . $fileName;
|
||||
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the subdirectories inside an FTP directory
|
||||
*
|
||||
* @param null|string $dir The directory to scan. Skip to use the current directory.
|
||||
*
|
||||
* @return array|bool A list of folders, or false if we could not get a listing
|
||||
*
|
||||
* @throws RuntimeException When the server is incompatible with our FTP folder scanner
|
||||
*/
|
||||
public function listFolders($dir = null)
|
||||
{
|
||||
if (!@ftp_chdir($this->connection, $dir))
|
||||
{
|
||||
throw new RuntimeException(sprintf('Cannot change to FTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $dir), 500);
|
||||
}
|
||||
|
||||
$list = @ftp_rawlist($this->connection, '.');
|
||||
|
||||
if ($list === false)
|
||||
{
|
||||
throw new RuntimeException("Sorry, your FTP server doesn't support our FTP directory browser.");
|
||||
}
|
||||
|
||||
$folders = [];
|
||||
|
||||
foreach ($list as $v)
|
||||
{
|
||||
$vInfo = preg_split("/[\s]+/", $v, 9);
|
||||
|
||||
if ($vInfo[0] !== "total")
|
||||
{
|
||||
$perms = $vInfo[0];
|
||||
|
||||
if (substr($perms, 0, 1) == 'd')
|
||||
{
|
||||
$folders[] = $vInfo[8];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
asort($folders);
|
||||
|
||||
return $folders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string with the appropriate stream wrapper protocol for $path. You can use the result with all PHP
|
||||
* functions / classes which accept file paths such as DirectoryIterator, file_get_contents, file_put_contents,
|
||||
* fopen etc.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWrapperStringFor($path)
|
||||
{
|
||||
$usernameEncoded = urlencode($this->username);
|
||||
$passwordEncoded = urlencode($this->password);
|
||||
$hostname = $this->host . ($this->port ? ":{$this->port}" : '');
|
||||
$protocol = $this->ssl ? "ftps" : "ftp";
|
||||
|
||||
return "{$protocol}://{$usernameEncoded}:{$passwordEncoded}@{$hostname}{$path}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the raw server listing for the requested folder.
|
||||
*
|
||||
* @param string $folder The path name to list
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRawList($folder)
|
||||
{
|
||||
return ftp_rawlist($this->connection, $folder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,831 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\Transfer;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Postproc\ProxyAware;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* FTP transfer object, using cURL as the transport backend
|
||||
*/
|
||||
class FtpCurl extends Ftp implements TransferInterface
|
||||
{
|
||||
use ProxyAware;
|
||||
|
||||
/**
|
||||
* Timeout for transferring data to the FTP server, default: 10 minutes
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
protected $timeout = 600;
|
||||
|
||||
/**
|
||||
* Should I ignore the IP returned by the server during Passive mode transfers?
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @see http://www.elitehosts.com/blog/php-ftp-passive-ftp-server-behind-nat-nightmare/
|
||||
*/
|
||||
private $skipPassiveIP = true;
|
||||
|
||||
/**
|
||||
* Should we enable verbose output to STDOUT? Useful for debugging.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $verbose = false;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param array $options Configuration options
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
parent::__construct($options);
|
||||
|
||||
if (isset($options['passive_fix']))
|
||||
{
|
||||
$this->skipPassiveIP = $options['passive_fix'] ? true : false;
|
||||
}
|
||||
|
||||
if (isset($options['verbose']))
|
||||
{
|
||||
$this->verbose = $options['verbose'] ? true : false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all parameters on serialization except the connection resource
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return [
|
||||
'host',
|
||||
'port',
|
||||
'username',
|
||||
'password',
|
||||
'directory',
|
||||
'ssl',
|
||||
'passive',
|
||||
'timeout',
|
||||
'skipPassiveIP',
|
||||
'verbose',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the connection to the FTP server and whether the initial directory is correct. This is done by attempting to
|
||||
* list the contents of the initial directory. The listing is not parsed (we don't really care!) and we do NOT check
|
||||
* if we can upload files to that remote folder.
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
$ch = $this->getCurlHandle($this->directory . '/');
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
curl_setopt($ch, CURLOPT_NOBODY, 1);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
curl_exec($ch);
|
||||
|
||||
$errNo = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo)
|
||||
{
|
||||
throw new RuntimeException("cURL Error $errNo connecting to remote FTP server: $error", 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the contents into the file
|
||||
*
|
||||
* @param string $fileName The full path to the file
|
||||
* @param string $contents The contents to write to the file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function write($fileName, $contents)
|
||||
{
|
||||
// Make sure the buffer:// wrapper is loaded
|
||||
class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);
|
||||
|
||||
$handle = fopen('buffer://akeeba_engine_transfer_ftp_curl', 'r+');
|
||||
fwrite($handle, $contents);
|
||||
|
||||
// Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle
|
||||
try
|
||||
{
|
||||
$this->uploadFromHandle($fileName, $handle);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a local file to the remote storage
|
||||
*
|
||||
* @param string $localFilename The full path to the local file
|
||||
* @param string $remoteFilename The full path to the remote file
|
||||
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function upload($localFilename, $remoteFilename, $useExceptions = true)
|
||||
{
|
||||
$fp = @fopen($localFilename, 'r');
|
||||
|
||||
if ($fp === false)
|
||||
{
|
||||
throw new RuntimeException("Unreadable local file $localFilename");
|
||||
}
|
||||
|
||||
// Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle
|
||||
try
|
||||
{
|
||||
$this->uploadFromHandle($remoteFilename, $fp);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the contents of a remote file into a string
|
||||
*
|
||||
* @param string $fileName The full path to the remote file
|
||||
*
|
||||
* @return string The contents of the remote file
|
||||
*/
|
||||
public function read($fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return $this->downloadToString($fileName);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
throw new RuntimeException("Can not download remote file $fileName", 500, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a remote file into a local file
|
||||
*
|
||||
* @param string $remoteFilename The remote file path to download from
|
||||
* @param string $localFilename The local file path to download to
|
||||
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function download($remoteFilename, $localFilename, $useExceptions = true)
|
||||
{
|
||||
$fp = @fopen($localFilename, 'w');
|
||||
|
||||
if ($fp === false)
|
||||
{
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw new RuntimeException(sprintf('Download from FTP failed. Can not open local file %s for writing.', $localFilename));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Note: don't manually close the file pointer, it's closed automatically by downloadToHandle
|
||||
try
|
||||
{
|
||||
$this->downloadToHandle($remoteFilename, $fp);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file (remove it from the disk)
|
||||
*
|
||||
* @param string $fileName The full path to the file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function delete($fileName)
|
||||
{
|
||||
$commands = [
|
||||
'DELE ' . $this->getPath($fileName),
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$this->executeServerCommands($commands);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of the file. Actually, we have to read it in memory and upload it again.
|
||||
*
|
||||
* @param string $from The full path of the file to copy from
|
||||
* @param string $to The full path of the file that will hold the copy
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function copy($from, $to)
|
||||
{
|
||||
// Make sure the buffer:// wrapper is loaded
|
||||
class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);
|
||||
|
||||
$handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+');
|
||||
|
||||
try
|
||||
{
|
||||
$this->downloadToHandle($from, $handle, false);
|
||||
$this->uploadFromHandle($to, $handle);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move or rename a file
|
||||
*
|
||||
* @param string $from The full path of the file to move
|
||||
* @param string $to The full path of the target file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function move($from, $to)
|
||||
{
|
||||
$from = $this->getPath($from);
|
||||
$to = $this->getPath($to);
|
||||
|
||||
$commands = [
|
||||
'RNFR /' . $from,
|
||||
'RNTO /' . $to,
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$this->executeServerCommands($commands);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the permissions of a file
|
||||
*
|
||||
* @param string $fileName The full path of the file whose permissions will change
|
||||
* @param integer $permissions The new permissions, e.g. 0644 (remember the leading zero in octal numbers!)
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function chmod($fileName, $permissions)
|
||||
{
|
||||
// Make sure permissions are in an octal string representation
|
||||
if (!is_string($permissions))
|
||||
{
|
||||
$permissions = decoct($permissions);
|
||||
}
|
||||
|
||||
$commands = [
|
||||
'SITE CHMOD ' . $permissions . ' /' . $this->getPath($fileName),
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$this->executeServerCommands($commands);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all
|
||||
* intermediate directories if they do not already exist.
|
||||
*
|
||||
* @param string $dirName The full path of the directory to create
|
||||
* @param integer $permissions The permissions of the created directory
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function mkdir($dirName, $permissions = 0755)
|
||||
{
|
||||
$targetDir = rtrim($dirName, '/');
|
||||
|
||||
$directories = explode('/', $targetDir);
|
||||
|
||||
$remoteDir = '';
|
||||
|
||||
foreach ($directories as $dir)
|
||||
{
|
||||
if (!$dir)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$remoteDir .= '/' . $dir;
|
||||
|
||||
// Continue if the folder already exists. Otherwise I'll get a an error even if everything is fine
|
||||
if ($this->isDir($remoteDir))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$commands = [
|
||||
'MKD ' . $remoteDir,
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$this->executeServerCommands($commands);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->chmod($dirName, $permissions);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given directory exists
|
||||
*
|
||||
* @param string $path The full path of the remote directory to check
|
||||
*
|
||||
* @return boolean True if the directory exists
|
||||
*/
|
||||
public function isDir($path)
|
||||
{
|
||||
$ch = $this->getCurlHandle($path . '/');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
curl_exec($ch);
|
||||
|
||||
$errNo = curl_errno($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current working directory. NOT IMPLEMENTED.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function cwd()
|
||||
{
|
||||
$commands = [
|
||||
'PWD',
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$result = $this->executeServerCommands($commands, '');
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return '/';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the subdirectories inside an FTP directory
|
||||
*
|
||||
* @param null|string $dir The directory to scan. Skip to use the current directory.
|
||||
*
|
||||
* @return array|bool A list of folders, or false if we could not get a listing
|
||||
*
|
||||
* @throws RuntimeException When the server is incompatible with our FTP folder scanner
|
||||
*/
|
||||
public function listFolders($dir = null)
|
||||
{
|
||||
if (empty($dir))
|
||||
{
|
||||
$dir = $this->directory;
|
||||
}
|
||||
|
||||
$dir = rtrim($dir, '/');
|
||||
|
||||
$ch = $this->getCurlHandle($dir . '/');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
$list = curl_exec($ch);
|
||||
|
||||
$errNo = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo)
|
||||
{
|
||||
throw new RuntimeException(sprintf("cURL Error $errNo ($error) while listing contents of directory \"%s\" – make sure the folder exists and that you have adequate permissions to it", $dir), 500);
|
||||
}
|
||||
|
||||
if (empty($list))
|
||||
{
|
||||
throw new RuntimeException("Sorry, your FTP server doesn't support our FTP directory browser.");
|
||||
}
|
||||
|
||||
$folders = [];
|
||||
|
||||
// Convert the directory listing into an array of lines without *NIX/Windows/Mac line ending characters
|
||||
$list = explode("\n", $list);
|
||||
$list = array_map('rtrim', $list);
|
||||
|
||||
foreach ($list as $v)
|
||||
{
|
||||
$vInfo = preg_split("/[\s]+/", $v, 9);
|
||||
|
||||
if ($vInfo[0] !== "total")
|
||||
{
|
||||
$perms = $vInfo[0];
|
||||
|
||||
if (substr($perms, 0, 1) == 'd')
|
||||
{
|
||||
$folders[] = $vInfo[8];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
asort($folders);
|
||||
|
||||
return $folders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the verbose debug option set?
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isVerbose()
|
||||
{
|
||||
return $this->verbose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the verbose debug option
|
||||
*
|
||||
* @param boolean $verbose
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setVerbose($verbose)
|
||||
{
|
||||
$this->verbose = $verbose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute remote path from a path relative to the initial directory configured when creating the
|
||||
* transfer object.
|
||||
*
|
||||
* @param string $fileName The relative path of a file or directory
|
||||
*
|
||||
* @return string The absolute path for use by the transfer object
|
||||
*/
|
||||
public function getPath($fileName)
|
||||
{
|
||||
$fileName = ltrim(str_replace('\\', '/', $fileName), '/');
|
||||
|
||||
$isInitialDirectory = $fileName === $this->directory;
|
||||
$startsWithInitialDirectory = (strpos($fileName, rtrim($this->directory, '/') . '/') === 0)
|
||||
|| (strpos($fileName, trim($this->directory, '/') . '/') === 0);
|
||||
|
||||
// Relative file? Add the initial directory
|
||||
if (!$isInitialDirectory && !$startsWithInitialDirectory)
|
||||
{
|
||||
$fileName = '/' . trim($this->directory, '/') .
|
||||
(empty($fileName) ? '' : '/') . $fileName;
|
||||
}
|
||||
|
||||
return '/' . ltrim($fileName, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cURL resource handler for the remote FTP server
|
||||
*
|
||||
* @param string $remoteFile Optional. The remote file / folder on the FTP server you'll be manipulating with cURL.
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
protected function getCurlHandle($remoteFile = '')
|
||||
{
|
||||
/**
|
||||
* Get the FTP URI
|
||||
*
|
||||
* VERY IMPORTANT! WE NEED THE DOUBLE SLASH AFTER THE HOST NAME since we are giving an absolute path.
|
||||
* @see https://technicalsanctuary.wordpress.com/2012/11/01/curl-curl-9-server-denied-you-to-change-to-the-given-directory/
|
||||
*/
|
||||
$ftpUri = 'ftp://' . $this->host . '//';
|
||||
|
||||
$isInitialDirectory = $remoteFile === $this->directory;
|
||||
$startsWithInitialDirectory = (strpos($remoteFile, rtrim($this->directory, '/') . '/') === 0)
|
||||
|| (strpos($remoteFile, trim($this->directory, '/') . '/') === 0);
|
||||
|
||||
// Relative file? Add the initial directory
|
||||
if (!$isInitialDirectory && !$startsWithInitialDirectory)
|
||||
{
|
||||
$ftpUri .= '/' . trim($this->directory, '/');
|
||||
}
|
||||
|
||||
if (!empty($remoteFile) && substr($ftpUri, -2) !== '//')
|
||||
{
|
||||
$ftpUri .= '/';
|
||||
}
|
||||
|
||||
// Add a remote file if necessary. The filename must be URL encoded since we're creating a URI.
|
||||
if (!empty($remoteFile))
|
||||
{
|
||||
$suffix = '';
|
||||
|
||||
if (substr($remoteFile, -7, 6) == ';type=')
|
||||
{
|
||||
$suffix = substr($remoteFile, -7);
|
||||
$remoteFile = substr($remoteFile, 0, -7);
|
||||
}
|
||||
|
||||
$dirname = dirname($remoteFile);
|
||||
|
||||
// Windows messing up dirname('/'). KILL ME.
|
||||
if ($dirname == '\\')
|
||||
{
|
||||
$dirname = '';
|
||||
}
|
||||
|
||||
$dirname = trim($dirname, '/');
|
||||
$basename = basename($remoteFile);
|
||||
|
||||
if ((substr($remoteFile, -1) == '/') && !empty($basename))
|
||||
{
|
||||
$suffix = '/' . $suffix;
|
||||
}
|
||||
|
||||
$ftpUri .= '/' . $dirname . (empty($dirname) ? '' : '/') . urlencode($basename) . $suffix;
|
||||
}
|
||||
|
||||
// Colons in usernames must be URL escaped
|
||||
$username = str_replace(':', '%3A', $this->username);
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
$this->applyProxySettingsToCurl($ch);
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $ftpUri);
|
||||
curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $this->password);
|
||||
curl_setopt($ch, CURLOPT_PORT, $this->port);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
|
||||
|
||||
// Should I enable Implict SSL?
|
||||
if ($this->ssl)
|
||||
{
|
||||
curl_setopt($ch, CURLOPT_FTP_SSL, CURLFTPSSL_ALL);
|
||||
curl_setopt($ch, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_DEFAULT);
|
||||
|
||||
// Most FTPS servers use self-signed certificates. That's the only way to connect to them :(
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
}
|
||||
|
||||
// Should I ignore the server-supplied passive mode IP address?
|
||||
if ($this->passive && $this->skipPassiveIP)
|
||||
{
|
||||
curl_setopt($ch, CURLOPT_FTP_SKIP_PASV_IP, 1);
|
||||
}
|
||||
|
||||
// Should I enable active mode?
|
||||
if (!$this->passive)
|
||||
{
|
||||
/**
|
||||
* cURL always uses passive mode for FTP transfers. Setting the CURLOPT_FTPPORT flag enables the FTP PORT
|
||||
* command which makes the connection active. Setting it to '-' lets the library use your system's default
|
||||
* IP address.
|
||||
*
|
||||
* @see https://curl.haxx.se/libcurl/c/CURLOPT_FTPPORT.html
|
||||
*/
|
||||
curl_setopt($ch, CURLOPT_FTPPORT, '-');
|
||||
}
|
||||
|
||||
// Should I enable verbose output? Useful for debugging.
|
||||
if ($this->verbose)
|
||||
{
|
||||
curl_setopt($ch, CURLOPT_VERBOSE, 1);
|
||||
}
|
||||
|
||||
// Automatically create missing directories
|
||||
curl_setopt($ch, CURLOPT_FTP_CREATE_MISSING_DIRS, 1);
|
||||
|
||||
return $ch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file using file contents provided through a file handle
|
||||
*
|
||||
* @param string $remoteFilename Remote file to write contents to
|
||||
* @param resource $fp File or stream handler of the source data to upload
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function uploadFromHandle($remoteFilename, $fp)
|
||||
{
|
||||
// We need the file size. We can do that by getting the file position at EOF
|
||||
fseek($fp, 0, SEEK_END);
|
||||
$filesize = ftell($fp);
|
||||
rewind($fp);
|
||||
|
||||
/**
|
||||
* The ;type=i suffix forces Binary file transfer mode
|
||||
*
|
||||
* @see https://curl.haxx.se/mail/archive-2008-05/0089.html
|
||||
*/
|
||||
$ch = $this->getCurlHandle($remoteFilename . ';type=i');
|
||||
curl_setopt($ch, CURLOPT_UPLOAD, 1);
|
||||
curl_setopt($ch, CURLOPT_INFILE, $fp);
|
||||
curl_setopt($ch, CURLOPT_INFILESIZE, $filesize);
|
||||
|
||||
curl_exec($ch);
|
||||
|
||||
$error_no = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
fclose($fp);
|
||||
|
||||
if ($error_no)
|
||||
{
|
||||
throw new RuntimeException($error, $error_no);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a remote file to the provided file handle
|
||||
*
|
||||
* @param string $remoteFilename Filename on the remote server
|
||||
* @param resource $fp File handle where the downloaded content will be written to
|
||||
* @param bool $close Optional. Should I close the file handle when I'm done? (Default: true)
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function downloadToHandle($remoteFilename, $fp, $close = true)
|
||||
{
|
||||
/**
|
||||
* The ;type=i suffix forces Binary file transfer mode
|
||||
*
|
||||
* @see https://curl.haxx.se/mail/archive-2008-05/0089.html
|
||||
*/
|
||||
$ch = $this->getCurlHandle($remoteFilename . ';type=i');
|
||||
|
||||
curl_setopt($ch, CURLOPT_FILE, $fp);
|
||||
|
||||
curl_exec($ch);
|
||||
|
||||
$error_no = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($close)
|
||||
{
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
if ($error_no)
|
||||
{
|
||||
throw new RuntimeException($error, $error_no);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a remote file and returns it as a string
|
||||
*
|
||||
* @param string $remoteFilename Filename on the remote server
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function downloadToString($remoteFilename)
|
||||
{
|
||||
/**
|
||||
* The ;type=i suffix forces Binary file transfer mode
|
||||
*
|
||||
* @see https://curl.haxx.se/mail/archive-2008-05/0089.html
|
||||
*/
|
||||
$ch = $this->getCurlHandle($remoteFilename . ';type=i');
|
||||
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
|
||||
$ret = curl_exec($ch);
|
||||
|
||||
$error_no = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($error_no)
|
||||
{
|
||||
throw new RuntimeException($error, $error_no);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes arbitrary FTP commands
|
||||
*
|
||||
* @param string[] $commands An array with the FTP commands to be executed
|
||||
* @param string|null $remoteFile The remote file / folder to use in the cURL URI when executing the commands
|
||||
*
|
||||
* @return string The output of the executed commands
|
||||
*
|
||||
*/
|
||||
protected function executeServerCommands(array $commands, ?string $remoteFile = null): string
|
||||
{
|
||||
$remoteFile = $remoteFile ?? ($this->directory . '/');
|
||||
|
||||
$ch = $this->getCurlHandle($remoteFile);
|
||||
|
||||
curl_setopt($ch, CURLOPT_QUOTE, $commands);
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
curl_setopt($ch, CURLOPT_NOBODY, 1);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
$listing = curl_exec($ch);
|
||||
$errNo = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo)
|
||||
{
|
||||
throw new RuntimeException($error, $errNo);
|
||||
}
|
||||
|
||||
return $listing;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\Transfer;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
/**
|
||||
* An interface for Transfer adapters which support remote resources, allowing us to efficient read from / write to
|
||||
* remote locations as if they were local files.
|
||||
*/
|
||||
interface RemoteResourceInterface
|
||||
{
|
||||
/**
|
||||
* Return a string with the appropriate stream wrapper protocol for $path. You can use the result with all PHP
|
||||
* functions / classes which accept file paths such as DirectoryIterator, file_get_contents, file_put_contents,
|
||||
* fopen etc.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWrapperStringFor($path);
|
||||
|
||||
/**
|
||||
* Return the raw server listing for the requested folder.
|
||||
*
|
||||
* @param string $folder The path name to list
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRawList($folder);
|
||||
}
|
||||
@@ -0,0 +1,667 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\Transfer;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use DirectoryIterator;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* SFTP transfer object
|
||||
*/
|
||||
class Sftp implements TransferInterface, RemoteResourceInterface
|
||||
{
|
||||
/**
|
||||
* SFTP server's hostname or IP address
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $host = 'localhost';
|
||||
|
||||
/**
|
||||
* SFTP server's port, default: 21
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $port = 22;
|
||||
|
||||
/**
|
||||
* Username used to authenticate to the SFTP server
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $username = '';
|
||||
|
||||
/**
|
||||
* Password used to authenticate to the SFTP server
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $password = '';
|
||||
|
||||
/**
|
||||
* SFTP initial directory
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $directory = '/';
|
||||
|
||||
/**
|
||||
* The absolute filesystem path to a private key file used for authentication instead of a password.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $privateKey = '';
|
||||
|
||||
/**
|
||||
* The absolute filesystem path to a public key file used for authentication instead of a password.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $publicKey = '';
|
||||
|
||||
/**
|
||||
* The SSH2 connection handle
|
||||
*
|
||||
* @var resource|null
|
||||
*/
|
||||
private $connection = null;
|
||||
|
||||
/**
|
||||
* The SFTP connection handle
|
||||
*
|
||||
* @var resource|null
|
||||
*/
|
||||
private $sftpHandle = null;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param array $options Configuration options for the filesystem abstraction object
|
||||
*
|
||||
* @return Sftp
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
if (isset($options['host']))
|
||||
{
|
||||
$this->host = $options['host'];
|
||||
}
|
||||
|
||||
if (isset($options['port']))
|
||||
{
|
||||
$this->port = (int) $options['port'];
|
||||
}
|
||||
|
||||
if (isset($options['username']))
|
||||
{
|
||||
$this->username = $options['username'];
|
||||
}
|
||||
|
||||
if (isset($options['password']))
|
||||
{
|
||||
$this->password = $options['password'];
|
||||
}
|
||||
|
||||
if (isset($options['directory']))
|
||||
{
|
||||
$this->directory = '/' . ltrim(trim($options['directory']), '/');
|
||||
}
|
||||
|
||||
if (isset($options['privateKey']))
|
||||
{
|
||||
$this->privateKey = $options['privateKey'];
|
||||
}
|
||||
|
||||
if (isset($options['publicKey']))
|
||||
{
|
||||
$this->publicKey = $options['publicKey'];
|
||||
}
|
||||
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this transfer method blocked by a server firewall?
|
||||
*
|
||||
* @param array $params Any additional parameters you might need to pass
|
||||
*
|
||||
* @return boolean True if the firewall blocks connections to a known host
|
||||
*/
|
||||
public static function isFirewalled(array $params = [])
|
||||
{
|
||||
try
|
||||
{
|
||||
$connector = new static([
|
||||
'host' => 'test.rebex.net',
|
||||
'port' => 22,
|
||||
'username' => 'demo',
|
||||
'password' => 'password',
|
||||
'directory' => '',
|
||||
]);
|
||||
|
||||
$data = $connector->read('readme.txt');
|
||||
|
||||
if (empty($data))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all parameters on serialization except the connection resource
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return ['host', 'port', 'username', 'password', 'directory', 'privateKey', 'publicKey'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect to the server on unserialize
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if (is_resource($this->connection))
|
||||
{
|
||||
@ssh2_exec($this->connection, 'exit;');
|
||||
$this->connection = null;
|
||||
$this->sftpHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the FTP server
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
// Try to connect to the SSH server
|
||||
if (!function_exists('ssh2_connect'))
|
||||
{
|
||||
throw new RuntimeException('Your web server does not have the SSH2 PHP module, therefore can not connect to SFTP servers.', 500);
|
||||
}
|
||||
|
||||
$this->connection = ssh2_connect($this->host, $this->port);
|
||||
|
||||
if ($this->connection === false)
|
||||
{
|
||||
$this->connection = null;
|
||||
|
||||
throw new RuntimeException(sprintf('Cannot connect to SFTP server [host:port] = %s:%s', $this->host, $this->port), 500);
|
||||
}
|
||||
|
||||
// Attempt to authenticate
|
||||
if (!empty($this->publicKey) && !empty($this->privateKey))
|
||||
{
|
||||
if (!@ssh2_auth_pubkey_file($this->connection, $this->username, $this->publicKey, $this->privateKey, $this->password))
|
||||
{
|
||||
$this->connection = null;
|
||||
|
||||
throw new RuntimeException(sprintf('Cannot log in to SFTP server using key files [username:private_key_file:public_key_file:password] = %s:%s:%s:%s', $this->username, $this->privateKey, $this->publicKey, $this->password), 500);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!@ssh2_auth_password($this->connection, $this->username, $this->password))
|
||||
{
|
||||
$this->connection = null;
|
||||
|
||||
throw new RuntimeException(sprintf('Cannot log in to SFTP server [username:password] = %s:%s', $this->username, $this->password), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Get an SFTP handle
|
||||
$this->sftpHandle = ssh2_sftp($this->connection);
|
||||
|
||||
if ($this->sftpHandle === false)
|
||||
{
|
||||
throw new RuntimeException('Cannot start an SFTP session with the server', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the contents into the file
|
||||
*
|
||||
* @param string $fileName The full path to the file
|
||||
* @param string $contents The contents to write to the file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function write($fileName, $contents)
|
||||
{
|
||||
$fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$fileName", 'w');
|
||||
|
||||
if ($fp === false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$ret = @fwrite($fp, $contents);
|
||||
|
||||
@fclose($fp);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a local file to the remote storage
|
||||
*
|
||||
* @param string $localFilename The full path to the local file
|
||||
* @param string $remoteFilename The full path to the remote file
|
||||
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function upload($localFilename, $remoteFilename, $useExceptions = true)
|
||||
{
|
||||
$fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$remoteFilename", 'w');
|
||||
|
||||
if ($fp === false)
|
||||
{
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw new RuntimeException("Could not open remote SFTP file $remoteFilename for writing");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$localFp = @fopen($localFilename, 'r');
|
||||
|
||||
if ($localFp === false)
|
||||
{
|
||||
fclose($fp);
|
||||
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw new RuntimeException("Could not open local file $localFilename for reading");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
while (!feof($localFp))
|
||||
{
|
||||
$data = fread($localFp, 131072);
|
||||
$ret = @fwrite($fp, $data);
|
||||
|
||||
if ($ret < strlen($data))
|
||||
{
|
||||
fclose($fp);
|
||||
fclose($localFp);
|
||||
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw new RuntimeException("An error occurred while copying file $localFilename to $remoteFilename");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@fclose($fp);
|
||||
@fclose($localFp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the contents of a remote file into a string
|
||||
*
|
||||
* @param string $fileName The full path to the remote file
|
||||
*
|
||||
* @return string The contents of the remote file
|
||||
*/
|
||||
public function read($fileName)
|
||||
{
|
||||
$fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$fileName", 'r');
|
||||
|
||||
if ($fp === false)
|
||||
{
|
||||
throw new RuntimeException("Can not download remote file $fileName");
|
||||
}
|
||||
|
||||
$ret = '';
|
||||
|
||||
while (!feof($fp))
|
||||
{
|
||||
$ret .= fread($fp, 131072);
|
||||
}
|
||||
|
||||
@fclose($fp);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a remote file into a local file
|
||||
*
|
||||
* @param string $remoteFilename The remote file path to download from
|
||||
* @param string $localFilename The local file path to download to
|
||||
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function download($remoteFilename, $localFilename, $useExceptions = true)
|
||||
{
|
||||
$fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$remoteFilename", 'r');
|
||||
|
||||
if ($fp === false)
|
||||
{
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw new RuntimeException("Could not open remote SFTP file $remoteFilename for reading");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$localFp = @fopen($localFilename, 'w');
|
||||
|
||||
if ($localFp === false)
|
||||
{
|
||||
fclose($fp);
|
||||
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw new RuntimeException("Could not open local file $localFilename for writing");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
while (!feof($fp))
|
||||
{
|
||||
$chunk = fread($fp, 131072);
|
||||
|
||||
if ($chunk === false)
|
||||
{
|
||||
fclose($fp);
|
||||
fclose($localFp);
|
||||
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw new RuntimeException("An error occurred while copying file $remoteFilename to $localFilename");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fwrite($localFp, $chunk);
|
||||
}
|
||||
|
||||
@fclose($fp);
|
||||
@fclose($localFp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file (remove it from the disk)
|
||||
*
|
||||
* @param string $fileName The full path to the file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function delete($fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
$ret = @ssh2_sftp_unlink($this->sftpHandle, $fileName);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
$ret = false;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of the file. Actually, we have to read it in memory and upload it again.
|
||||
*
|
||||
* @param string $from The full path of the file to copy from
|
||||
* @param string $to The full path of the file that will hold the copy
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function copy($from, $to)
|
||||
{
|
||||
$contents = @file_get_contents($from);
|
||||
|
||||
return $this->write($to, $contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move or rename a file. Actually, we have to read it, upload it again and then delete the original.
|
||||
*
|
||||
* @param string $from The full path of the file to move
|
||||
* @param string $to The full path of the target file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function move($from, $to)
|
||||
{
|
||||
$ret = $this->copy($from, $to);
|
||||
|
||||
if ($ret)
|
||||
{
|
||||
$ret = $this->delete($from);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the permissions of a file
|
||||
*
|
||||
* @param string $fileName The full path of the file whose permissions will change
|
||||
* @param integer $permissions The new permissions, e.g. 0644 (remember the leading zero in octal numbers!)
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function chmod($fileName, $permissions)
|
||||
{
|
||||
// Prefer the SFTP way, if available
|
||||
if (function_exists('ssh2_sftp_chmod'))
|
||||
{
|
||||
return @ssh2_sftp_chmod($this->sftpHandle, $fileName, $permissions);
|
||||
}
|
||||
// Otherwise fall back to the (likely to fail) raw command mode
|
||||
else
|
||||
{
|
||||
$cmd = 'chmod ' . decoct($permissions) . ' ' . escapeshellarg($fileName);
|
||||
|
||||
return @ssh2_exec($this->connection, $cmd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all
|
||||
* intermediate directories if they do not already exist.
|
||||
*
|
||||
* @param string $dirName The full path of the directory to create
|
||||
* @param integer $permissions The permissions of the created directory
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function mkdir($dirName, $permissions = 0755)
|
||||
{
|
||||
$targetDir = rtrim($dirName, '/');
|
||||
|
||||
$ret = @ssh2_sftp_mkdir($this->sftpHandle, $targetDir, $permissions, true);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given directory exists
|
||||
*
|
||||
* @param string $path The full path of the remote directory to check
|
||||
*
|
||||
* @return boolean True if the directory exists
|
||||
*/
|
||||
public function isDir($path)
|
||||
{
|
||||
return @ssh2_sftp_stat($this->sftpHandle, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current working directory
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function cwd()
|
||||
{
|
||||
return ssh2_sftp_realpath($this->sftpHandle, ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute remote path from a path relative to the initial directory configured when creating the
|
||||
* transfer object.
|
||||
*
|
||||
* @param string $fileName The relative path of a file or directory
|
||||
*
|
||||
* @return string The absolute path for use by the transfer object
|
||||
*/
|
||||
public function getPath($fileName)
|
||||
{
|
||||
$fileName = str_replace('\\', '/', $fileName);
|
||||
$fileName = rtrim($this->directory, '/') . '/' . $fileName;
|
||||
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the subdirectories inside an SFTP directory
|
||||
*
|
||||
* @param null|string $dir The directory to scan. Skip to use the current directory.
|
||||
*
|
||||
* @return array|bool A list of folders, or false if we could not get a listing
|
||||
*
|
||||
* @throws RuntimeException When the server is incompatible with our SFTP folder scanner
|
||||
*/
|
||||
public function listFolders($dir = null)
|
||||
{
|
||||
if (empty($dir))
|
||||
{
|
||||
$dir = $this->directory;
|
||||
}
|
||||
|
||||
// Get a raw directory listing (hoping it's a UNIX server!)
|
||||
$list = [];
|
||||
$dir = ltrim($dir, '/');
|
||||
|
||||
try
|
||||
{
|
||||
$di = new DirectoryIterator("ssh2.sftp://" . $this->sftpHandle . "/$dir");
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
throw new RuntimeException(sprintf('Cannot change to SFTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $dir), 500);
|
||||
}
|
||||
|
||||
if (!$di->valid())
|
||||
{
|
||||
throw new RuntimeException(sprintf('Cannot change to SFTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $dir), 500);
|
||||
}
|
||||
|
||||
/** @var DirectoryIterator $entry */
|
||||
foreach ($di as $entry)
|
||||
{
|
||||
if ($entry->isDot())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$entry->isDir())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[] = $entry->getFilename();
|
||||
}
|
||||
|
||||
unset($di);
|
||||
|
||||
if (!empty($list))
|
||||
{
|
||||
asort($list);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string with the appropriate stream wrapper protocol for $path. You can use the result with all PHP
|
||||
* functions / classes which accept file paths such as DirectoryIterator, file_get_contents, file_put_contents,
|
||||
* fopen etc.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getWrapperStringFor($path)
|
||||
{
|
||||
return "ssh2.sftp://{$this->sftpHandle}{$path}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the raw server listing for the requested folder.
|
||||
*
|
||||
* @param string $folder The path name to list
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRawList($folder)
|
||||
{
|
||||
// First try the command for Linxu servers
|
||||
$res = $this->ssh2cmd('ls -l ' . escapeshellarg($folder));
|
||||
|
||||
// If an error occurred let's try the command for Windows servers
|
||||
if (empty($res))
|
||||
{
|
||||
$res = $this->ssh2cmd('CMD /C ' . escapeshellarg($folder));
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
private function ssh2cmd($command)
|
||||
{
|
||||
$stream = ssh2_exec($this->connection, $command);
|
||||
stream_set_blocking($stream, true);
|
||||
$res = @stream_get_contents($stream);
|
||||
@fclose($stream);
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,876 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\Transfer;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use Akeeba\Engine\Postproc\ProxyAware;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* SFTP transfer object, using cURL as the transport backend
|
||||
*/
|
||||
class SftpCurl extends Sftp implements TransferInterface
|
||||
{
|
||||
use ProxyAware;
|
||||
|
||||
/**
|
||||
* SFTP server's hostname or IP address
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $host = 'localhost';
|
||||
|
||||
/**
|
||||
* SFTP server's port, default: 21
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $port = 22;
|
||||
|
||||
/**
|
||||
* Username used to authenticate to the SFTP server
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $username = '';
|
||||
|
||||
/**
|
||||
* Password used to authenticate to the SFTP server
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $password = '';
|
||||
|
||||
/**
|
||||
* SFTP initial directory
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $directory = '/';
|
||||
|
||||
/**
|
||||
* The absolute filesystem path to a private key file used for authentication instead of a password.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $privateKey = '';
|
||||
|
||||
/**
|
||||
* The absolute filesystem path to a public key file used for authentication instead of a password.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $publicKey = '';
|
||||
|
||||
/**
|
||||
* Timeout for connecting to the SFTP server, default: 10 minutes
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $timeout = 600;
|
||||
|
||||
/**
|
||||
* Should we enable verbose output to STDOUT? Useful for debugging.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $verbose = false;
|
||||
|
||||
/**
|
||||
* Should I enabled the passive IP workaround for cURL?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $skipPassiveIP = false;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param array $options Configuration options
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
if (isset($options['host']))
|
||||
{
|
||||
$this->host = $options['host'];
|
||||
}
|
||||
|
||||
if (isset($options['port']))
|
||||
{
|
||||
$this->port = (int) $options['port'];
|
||||
}
|
||||
|
||||
if (isset($options['username']))
|
||||
{
|
||||
$this->username = $options['username'];
|
||||
}
|
||||
|
||||
if (isset($options['password']))
|
||||
{
|
||||
$this->password = $options['password'];
|
||||
}
|
||||
|
||||
if (isset($options['directory']))
|
||||
{
|
||||
$this->directory = '/' . ltrim(trim($options['directory']), '/');
|
||||
}
|
||||
|
||||
if (isset($options['privateKey']))
|
||||
{
|
||||
$this->privateKey = $options['privateKey'];
|
||||
}
|
||||
|
||||
if (isset($options['publicKey']))
|
||||
{
|
||||
$this->publicKey = $options['publicKey'];
|
||||
}
|
||||
|
||||
if (isset($options['timeout']))
|
||||
{
|
||||
$this->timeout = max(1, (int) $options['timeout']);
|
||||
}
|
||||
|
||||
if (isset($options['passive_fix']))
|
||||
{
|
||||
$this->skipPassiveIP = $options['passive_fix'] ? true : false;
|
||||
}
|
||||
|
||||
if (isset($options['verbose']))
|
||||
{
|
||||
$this->verbose = $options['verbose'] ? true : false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all parameters on serialization except the connection resource
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
return [
|
||||
'host',
|
||||
'port',
|
||||
'username',
|
||||
'password',
|
||||
'directory',
|
||||
'privateKey',
|
||||
'publicKey',
|
||||
'timeout',
|
||||
'skipPassiveIP',
|
||||
'verbose',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the connection to the SFTP server and whether the initial directory is correct. This is done by attempting to
|
||||
* list the contents of the initial directory. The listing is not parsed (we don't really care!) and we do NOT check
|
||||
* if we can upload files to that remote folder.
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
$ch = $this->getCurlHandle($this->directory . '/');
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
curl_setopt($ch, CURLOPT_NOBODY, 1);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
$listing = curl_exec($ch);
|
||||
$errNo = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo)
|
||||
{
|
||||
throw new RuntimeException("cURL Error $errNo connecting to remote SFTP server: $error", 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the contents into the file
|
||||
*
|
||||
* @param string $fileName The full path to the file
|
||||
* @param string $contents The contents to write to the file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function write($fileName, $contents)
|
||||
{
|
||||
// Make sure the buffer:// wrapper is loaded
|
||||
class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);
|
||||
|
||||
$handle = fopen('buffer://akeeba_engine_transfer_ftp_curl', 'r+');
|
||||
fwrite($handle, $contents);
|
||||
|
||||
// Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle
|
||||
try
|
||||
{
|
||||
$this->uploadFromHandle($fileName, $handle);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a local file to the remote storage
|
||||
*
|
||||
* @param string $localFilename The full path to the local file
|
||||
* @param string $remoteFilename The full path to the remote file
|
||||
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function upload($localFilename, $remoteFilename, $useExceptions = true)
|
||||
{
|
||||
$fp = @fopen($localFilename, 'r');
|
||||
|
||||
if ($fp === false)
|
||||
{
|
||||
throw new RuntimeException("Unreadable local file $localFilename");
|
||||
}
|
||||
|
||||
// Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle
|
||||
try
|
||||
{
|
||||
$this->uploadFromHandle($remoteFilename, $fp);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the contents of a remote file into a string
|
||||
*
|
||||
* @param string $fileName The full path to the remote file
|
||||
*
|
||||
* @return string The contents of the remote file
|
||||
*/
|
||||
public function read($fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return $this->downloadToString($fileName);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
throw new RuntimeException("Can not download remote file $fileName", 500, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a remote file into a local file
|
||||
*
|
||||
* @param string $remoteFilename The remote file path to download from
|
||||
* @param string $localFilename The local file path to download to
|
||||
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function download($remoteFilename, $localFilename, $useExceptions = true)
|
||||
{
|
||||
$fp = @fopen($localFilename, 'w');
|
||||
|
||||
if ($fp === false)
|
||||
{
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw new RuntimeException(sprintf('Download from FTP failed. Can not open local file %s for writing.', $localFilename));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Note: don't manually close the file pointer, it's closed automatically by downloadToHandle
|
||||
try
|
||||
{
|
||||
$this->downloadToHandle($remoteFilename, $fp);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
if ($useExceptions)
|
||||
{
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file (remove it from the disk)
|
||||
*
|
||||
* @param string $fileName The full path to the file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function delete($fileName)
|
||||
{
|
||||
$commands = [
|
||||
'rm ' . $this->getPath($fileName),
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$this->executeServerCommands($commands);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of the file. Actually, we have to read it in memory and upload it again.
|
||||
*
|
||||
* @param string $from The full path of the file to copy from
|
||||
* @param string $to The full path of the file that will hold the copy
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function copy($from, $to)
|
||||
{
|
||||
// Make sure the buffer:// wrapper is loaded
|
||||
class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);
|
||||
|
||||
$handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+');
|
||||
|
||||
try
|
||||
{
|
||||
$this->downloadToHandle($from, $handle, false);
|
||||
$this->uploadFromHandle($to, $handle);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move or rename a file
|
||||
*
|
||||
* @param string $from The full path of the file to move
|
||||
* @param string $to The full path of the target file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function move($from, $to)
|
||||
{
|
||||
$from = $this->getPath($from);
|
||||
$to = $this->getPath($to);
|
||||
|
||||
$commands = [
|
||||
'rename ' . $from . ' ' . $to,
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$this->executeServerCommands($commands);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the permissions of a file
|
||||
*
|
||||
* @param string $fileName The full path of the file whose permissions will change
|
||||
* @param integer $permissions The new permissions, e.g. 0644 (remember the leading zero in octal numbers!)
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function chmod($fileName, $permissions)
|
||||
{
|
||||
// Make sure permissions are in an octal string representation
|
||||
if (!is_string($permissions))
|
||||
{
|
||||
$permissions = decoct($permissions);
|
||||
}
|
||||
|
||||
$commands = [
|
||||
'chmod ' . $permissions . ' ' . $this->getPath($fileName),
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$this->executeServerCommands($commands);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all
|
||||
* intermediate directories if they do not already exist.
|
||||
*
|
||||
* @param string $dirName The full path of the directory to create
|
||||
* @param integer $permissions The permissions of the created directory
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function mkdir($dirName, $permissions = 0755)
|
||||
{
|
||||
$targetDir = rtrim($dirName, '/');
|
||||
|
||||
$directories = explode('/', $targetDir);
|
||||
|
||||
$remoteDir = '';
|
||||
|
||||
foreach ($directories as $dir)
|
||||
{
|
||||
if (!$dir)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$remoteDir .= '/' . $dir;
|
||||
|
||||
// Continue if the folder already exists. Otherwise I'll get a an error even if everything is fine
|
||||
if ($this->isDir($remoteDir))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$commands = [
|
||||
'mkdir ' . $remoteDir,
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$this->executeServerCommands($commands);
|
||||
}
|
||||
catch (RuntimeException $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->chmod($dirName, $permissions);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given directory exists
|
||||
*
|
||||
* @param string $path The full path of the remote directory to check
|
||||
*
|
||||
* @return boolean True if the directory exists
|
||||
*/
|
||||
public function isDir($path)
|
||||
{
|
||||
$ch = $this->getCurlHandle($path . '/');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
$list = curl_exec($ch);
|
||||
|
||||
$errNo = curl_errno($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current working directory. NOT IMPLEMENTED.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function cwd()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute remote path from a path relative to the initial directory configured when creating the
|
||||
* transfer object.
|
||||
*
|
||||
* @param string $fileName The relative path of a file or directory
|
||||
*
|
||||
* @return string The absolute path for use by the transfer object
|
||||
*/
|
||||
public function getPath($fileName)
|
||||
{
|
||||
$fileName = str_replace('\\', '/', $fileName);
|
||||
|
||||
if (strpos($fileName, $this->directory) === 0)
|
||||
{
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
$fileName = trim($fileName, '/');
|
||||
$fileName = rtrim($this->directory, '/') . '/' . $fileName;
|
||||
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the subdirectories inside an SFTP directory
|
||||
*
|
||||
* @param null|string $dir The directory to scan. Skip to use the current directory.
|
||||
*
|
||||
* @return array|bool A list of folders, or false if we could not get a listing
|
||||
*
|
||||
* @throws RuntimeException When the server is incompatible with our SFTP folder scanner
|
||||
*/
|
||||
public function listFolders($dir = null)
|
||||
{
|
||||
if (empty($dir))
|
||||
{
|
||||
$dir = $this->directory;
|
||||
}
|
||||
|
||||
$dir = rtrim($dir, '/');
|
||||
|
||||
$ch = $this->getCurlHandle($dir . '/');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
$list = curl_exec($ch);
|
||||
|
||||
$errNo = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo)
|
||||
{
|
||||
throw new RuntimeException(sprintf("cURL Error $errNo ($error) while listing contents of directory \"%s\" – make sure the folder exists and that you have adequate permissions to it", $dir), 500);
|
||||
}
|
||||
|
||||
if (empty($list))
|
||||
{
|
||||
throw new RuntimeException("Sorry, your SFTP server doesn't support our SFTP directory browser.");
|
||||
}
|
||||
|
||||
$folders = [];
|
||||
|
||||
// Convert the directory listing into an array of lines without *NIX/Windows/Mac line ending characters
|
||||
$list = explode("\n", $list);
|
||||
$list = array_map('rtrim', $list);
|
||||
|
||||
foreach ($list as $v)
|
||||
{
|
||||
$vInfo = preg_split("/[\s]+/", $v, 9);
|
||||
|
||||
if ($vInfo[0] !== "total")
|
||||
{
|
||||
$perms = $vInfo[0];
|
||||
|
||||
if (substr($perms, 0, 1) == 'd')
|
||||
{
|
||||
$folders[] = $vInfo[8];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
asort($folders);
|
||||
|
||||
return $folders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the verbose debug option set?
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isVerbose()
|
||||
{
|
||||
return $this->verbose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the verbose debug option
|
||||
*
|
||||
* @param boolean $verbose
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setVerbose($verbose)
|
||||
{
|
||||
$this->verbose = $verbose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cURL resource handler for the remote SFTP server
|
||||
*
|
||||
* @param string $remoteFile Optional. The remote file / folder on the SFTP server you'll be manipulating with cURL.
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
protected function getCurlHandle($remoteFile = '')
|
||||
{
|
||||
// Remember, the username has to be URL encoded as it's part of a URI!
|
||||
$authentication = urlencode($this->username);
|
||||
|
||||
// We will only use username and password authentication if there are no certificates configured.
|
||||
if (empty($this->publicKey))
|
||||
{
|
||||
// Remember, both the username and password have to be URL encoded as they're part of a URI!
|
||||
$password = urlencode($this->password);
|
||||
$authentication .= ':' . $password;
|
||||
}
|
||||
|
||||
$ftpUri = 'sftp://' . $authentication . '@' . $this->host;
|
||||
|
||||
if (!empty($this->port))
|
||||
{
|
||||
$ftpUri .= ':' . (int) $this->port;
|
||||
}
|
||||
|
||||
// Relative path? Append the initial directory.
|
||||
if (substr($remoteFile, 0, 1) != '/')
|
||||
{
|
||||
$ftpUri .= $this->directory;
|
||||
}
|
||||
|
||||
// Add a remote file if necessary. The filename must be URL encoded since we're creating a URI.
|
||||
if (!empty($remoteFile))
|
||||
{
|
||||
$suffix = '';
|
||||
|
||||
$dirname = dirname($remoteFile);
|
||||
|
||||
// Windows messing up dirname('/'). KILL ME.
|
||||
if ($dirname == '\\')
|
||||
{
|
||||
$dirname = '';
|
||||
}
|
||||
|
||||
$dirname = trim($dirname, '/');
|
||||
$basename = basename($remoteFile);
|
||||
|
||||
if ((substr($remoteFile, -1) == '/') && !empty($basename))
|
||||
{
|
||||
$suffix = '/' . $suffix;
|
||||
}
|
||||
|
||||
$ftpUri .= '/' . $dirname . (empty($dirname) ? '' : '/') . urlencode($basename) . $suffix;
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
$this->applyProxySettingsToCurl($ch);
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $ftpUri);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
|
||||
|
||||
// Do I have to use certificate authentication?
|
||||
if (!empty($this->publicKey))
|
||||
{
|
||||
// We always need to provide a public key file
|
||||
curl_setopt($ch, CURLOPT_SSH_PUBLIC_KEYFILE, $this->publicKey);
|
||||
|
||||
// Since SSH certificates are self-signed we cannot have cURL verify their signatures against a CA.
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYSTATUS, 0);
|
||||
|
||||
/**
|
||||
* This is optional because newer versions of cURL can extract the private key file from a combined
|
||||
* certificate file.
|
||||
*/
|
||||
if (!empty($this->privateKey))
|
||||
{
|
||||
curl_setopt($ch, CURLOPT_SSH_PRIVATE_KEYFILE, $this->privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of encrypted (a.k.a. password protected) private key files you need to also specify the
|
||||
* certificate decryption key in the password field. However, if libcurl is compiled against the GnuTLS
|
||||
* library (instead of OpenSSL) this will NOT work because of bugs / missing features in GnuTLS. It's the
|
||||
* same problem you get when libssh is compiled against GnuTLS. The solution to that is having an
|
||||
* unencrypted private key file.
|
||||
*/
|
||||
if (!empty($this->password))
|
||||
{
|
||||
curl_setopt($ch, CURLOPT_KEYPASSWD, $this->password);
|
||||
}
|
||||
}
|
||||
|
||||
// Should I enable verbose output? Useful for debugging.
|
||||
if ($this->verbose)
|
||||
{
|
||||
curl_setopt($ch, CURLOPT_VERBOSE, 1);
|
||||
}
|
||||
|
||||
// Automatically create missing directories
|
||||
curl_setopt($ch, CURLOPT_FTP_CREATE_MISSING_DIRS, 1);
|
||||
|
||||
return $ch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file using file contents provided through a file handle
|
||||
*
|
||||
* @param string $remoteFilename
|
||||
* @param resource $fp
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function uploadFromHandle($remoteFilename, $fp)
|
||||
{
|
||||
// We need the file size. We can do that by getting the file position at EOF
|
||||
fseek($fp, 0, SEEK_END);
|
||||
$filesize = ftell($fp);
|
||||
rewind($fp);
|
||||
|
||||
$ch = $this->getCurlHandle($remoteFilename);
|
||||
curl_setopt($ch, CURLOPT_UPLOAD, 1);
|
||||
curl_setopt($ch, CURLOPT_INFILE, $fp);
|
||||
curl_setopt($ch, CURLOPT_INFILESIZE, $filesize);
|
||||
|
||||
curl_exec($ch);
|
||||
|
||||
$error_no = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
fclose($fp);
|
||||
|
||||
if ($error_no)
|
||||
{
|
||||
throw new RuntimeException($error, $error_no);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a remote file to the provided file handle
|
||||
*
|
||||
* @param string $remoteFilename Filename on the remote server
|
||||
* @param resource $fp File handle where the downloaded content will be written to
|
||||
* @param bool $close Optional. Should I close the file handle when I'm done? (Default: true)
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function downloadToHandle($remoteFilename, $fp, $close = true)
|
||||
{
|
||||
$ch = $this->getCurlHandle($remoteFilename);
|
||||
|
||||
curl_setopt($ch, CURLOPT_FILE, $fp);
|
||||
|
||||
curl_exec($ch);
|
||||
|
||||
$error_no = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($close)
|
||||
{
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
if ($error_no)
|
||||
{
|
||||
throw new RuntimeException($error, $error_no);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a remote file and returns it as a string
|
||||
*
|
||||
* @param string $remoteFilename Filename on the remote server
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function downloadToString($remoteFilename)
|
||||
{
|
||||
$ch = $this->getCurlHandle($remoteFilename);
|
||||
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
|
||||
$ret = curl_exec($ch);
|
||||
|
||||
$error_no = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($error_no)
|
||||
{
|
||||
throw new RuntimeException($error, $error_no);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes arbitrary SFTP commands
|
||||
*
|
||||
* @param array $commands An array with the SFTP commands to be executed
|
||||
*
|
||||
* @return string The output of the executed commands
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function executeServerCommands($commands)
|
||||
{
|
||||
$ch = $this->getCurlHandle($this->directory . '/');
|
||||
|
||||
curl_setopt($ch, CURLOPT_QUOTE, $commands);
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
curl_setopt($ch, CURLOPT_NOBODY, 1);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
$listing = curl_exec($ch);
|
||||
$errNo = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($errNo)
|
||||
{
|
||||
throw new RuntimeException($error, $errNo);
|
||||
}
|
||||
|
||||
return $listing;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util\Transfer;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* An interface for Transfer adapters, used to transfer files to remote servers over FTP, FTPS, SFTP and possibly other
|
||||
* file transfer methods we might implement.
|
||||
*
|
||||
* @package Akeeba\Engine\Util\Transfer
|
||||
*/
|
||||
interface TransferInterface
|
||||
{
|
||||
/**
|
||||
* Creates the uploader
|
||||
*
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config);
|
||||
|
||||
/**
|
||||
* Is this transfer method blocked by a server firewall?
|
||||
*
|
||||
* @param array $params Any additional parameters you might need to pass
|
||||
*
|
||||
* @return boolean True if the firewall blocks connections to a known host
|
||||
*/
|
||||
public static function isFirewalled(array $params = []);
|
||||
|
||||
/**
|
||||
* Write the contents into the file
|
||||
*
|
||||
* @param string $fileName The full path to the remote file
|
||||
* @param string $contents The contents to write to the file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function write($fileName, $contents);
|
||||
|
||||
/**
|
||||
* Uploads a local file to the remote storage
|
||||
*
|
||||
* @param string $localFilename The full path to the local file
|
||||
* @param string $remoteFilename The full path to the remote file
|
||||
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function upload($localFilename, $remoteFilename, $useExceptions = true);
|
||||
|
||||
/**
|
||||
* Read the contents of a remote file into a string
|
||||
*
|
||||
* @param string $fileName The full path to the remote file
|
||||
*
|
||||
* @return string The contents of the remote file
|
||||
*/
|
||||
public function read($fileName);
|
||||
|
||||
/**
|
||||
* Download a remote file into a local file
|
||||
*
|
||||
* @param string $remoteFilename The remote file path to download from
|
||||
* @param string $localFilename The local file path to download to
|
||||
* @param bool $useExceptions Throw an exception instead of returning "false" on connection error.
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function download($remoteFilename, $localFilename, $useExceptions = true);
|
||||
|
||||
/**
|
||||
* Delete a remote file
|
||||
*
|
||||
* @param string $fileName The full path to the remote file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function delete($fileName);
|
||||
|
||||
/**
|
||||
* Create a copy of the remote file
|
||||
*
|
||||
* @param string $from The full path of the remote file to copy from
|
||||
* @param string $to The full path of the remote file that will hold the copy
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function copy($from, $to);
|
||||
|
||||
/**
|
||||
* Move or rename a file
|
||||
*
|
||||
* @param string $from The full remote path of the file to move
|
||||
* @param string $to The full remote path of the target file
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function move($from, $to);
|
||||
|
||||
/**
|
||||
* Change the permissions of a file
|
||||
*
|
||||
* @param string $fileName The full path of the remote file whose permissions will change
|
||||
* @param integer $permissions The new permissions, e.g. 0644 (remember the leading zero in octal numbers!)
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function chmod($fileName, $permissions);
|
||||
|
||||
/**
|
||||
* Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all
|
||||
* intermediate directories if they do not already exist.
|
||||
*
|
||||
* @param string $dirName The full path of the remote directory to create
|
||||
* @param integer $permissions The permissions of the created directory
|
||||
*
|
||||
* @return boolean True on success
|
||||
*/
|
||||
public function mkdir($dirName, $permissions = 0755);
|
||||
|
||||
/**
|
||||
* Checks if the given directory exists
|
||||
*
|
||||
* @param string $path The full path of the remote directory to check
|
||||
*
|
||||
* @return boolean True if the directory exists
|
||||
*/
|
||||
public function isDir($path);
|
||||
|
||||
/**
|
||||
* Get the current working directory
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function cwd();
|
||||
|
||||
/**
|
||||
* Returns the absolute remote path from a path relative to the initial directory configured when creating the
|
||||
* transfer object.
|
||||
*
|
||||
* @param string $fileName The relative path of a file or directory
|
||||
*
|
||||
* @return string The absolute path for use by the transfer object
|
||||
*/
|
||||
public function getPath($fileName);
|
||||
|
||||
/**
|
||||
* Lists the subdirectories inside a directory
|
||||
*
|
||||
* @param null|string $dir The directory to scan. Skip to use the current directory.
|
||||
*
|
||||
* @return array|bool A list of folders, or false if we could not get a listing
|
||||
*
|
||||
* @throws RuntimeException When the server is incompatible with our folder scanner
|
||||
*/
|
||||
public function listFolders($dir = null);
|
||||
}
|
||||
116
administrator/components/com_akeebabackup/engine/Util/Utf8.php
Normal file
116
administrator/components/com_akeebabackup/engine/Util/Utf8.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Util;
|
||||
|
||||
defined('AKEEBAENGINE') || die();
|
||||
|
||||
/**
|
||||
* Replacement for the utf8_encode and utf8_decode functions on PHP 8.2 and later.
|
||||
*
|
||||
* @see https://wiki.php.net/rfc/remove_utf8_decode_and_utf8_encode
|
||||
*/
|
||||
class Utf8
|
||||
{
|
||||
public static function utf8_encode($s)
|
||||
{
|
||||
if (version_compare(PHP_VERSION, '8.1.999', 'le'))
|
||||
{
|
||||
return utf8_encode($s);
|
||||
}
|
||||
|
||||
if (function_exists('mb_convert_encoding'))
|
||||
{
|
||||
return mb_convert_encoding($s, 'UTF-8', 'ISO-8859-1');
|
||||
}
|
||||
|
||||
if (class_exists('UConverter'))
|
||||
{
|
||||
return UConverter::transcode($s, 'UTF8', 'ISO-8859-1');
|
||||
}
|
||||
|
||||
if (function_exists('iconv'))
|
||||
{
|
||||
return iconv('ISO-8859-1', 'UTF-8', $s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback to the pure PHP implementation from Symfony Polyfill for PHP 7.2
|
||||
*
|
||||
* @see https://github.com/symfony/polyfill-php72/blob/v1.26.0/Php72.php
|
||||
*/
|
||||
$s .= $s;
|
||||
$len = \strlen($s);
|
||||
|
||||
for ($i = $len >> 1, $j = 0; $i < $len; ++$i, ++$j) {
|
||||
switch (true) {
|
||||
case $s[$i] < "\x80": $s[$j] = $s[$i]; break;
|
||||
case $s[$i] < "\xC0": $s[$j] = "\xC2"; $s[++$j] = $s[$i]; break;
|
||||
default: $s[$j] = "\xC3"; $s[++$j] = \chr(\ord($s[$i]) - 64); break;
|
||||
}
|
||||
}
|
||||
|
||||
return substr($s, 0, $j);
|
||||
}
|
||||
|
||||
public static function utf8_decode($s)
|
||||
{
|
||||
if (version_compare(PHP_VERSION, '8.1.999', 'le'))
|
||||
{
|
||||
return utf8_decode($s);
|
||||
}
|
||||
|
||||
if (function_exists('mb_convert_encoding'))
|
||||
{
|
||||
return mb_convert_encoding($s, 'ISO-8859-1', 'UTF-8');
|
||||
}
|
||||
|
||||
if (class_exists('UConverter'))
|
||||
{
|
||||
return UConverter::transcode($s, 'ISO-8859-1', 'UTF8');
|
||||
}
|
||||
|
||||
if (function_exists('iconv'))
|
||||
{
|
||||
return iconv('UTF-8', 'ISO-8859-1', $s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback to the pure PHP implementation from Symfony Polyfill for PHP 7.2
|
||||
*
|
||||
* @see https://github.com/symfony/polyfill-php72/blob/v1.26.0/Php72.php
|
||||
*/
|
||||
$s = (string) $s;
|
||||
$len = \strlen($s);
|
||||
|
||||
for ($i = 0, $j = 0; $i < $len; ++$i, ++$j) {
|
||||
switch ($s[$i] & "\xF0") {
|
||||
case "\xC0":
|
||||
case "\xD0":
|
||||
$c = (\ord($s[$i] & "\x1F") << 6) | \ord($s[++$i] & "\x3F");
|
||||
$s[$j] = $c < 256 ? \chr($c) : '?';
|
||||
break;
|
||||
|
||||
case "\xF0":
|
||||
++$i;
|
||||
// no break
|
||||
|
||||
case "\xE0":
|
||||
$s[$j] = '?';
|
||||
$i += 2;
|
||||
break;
|
||||
|
||||
default:
|
||||
$s[$j] = $s[$i];
|
||||
}
|
||||
}
|
||||
|
||||
return substr($s, 0, $j);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user