first commit

This commit is contained in:
2024-07-15 11:28:08 +02:00
commit f52d538ea5
21891 changed files with 6161164 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,118 @@
<?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;
/**
* A handy class to abstract the calculation of CRC32 of files under various
* server conditions and versions of PHP.
*/
class CRC32
{
/**
* Returns the CRC32 of a file, selecting the more appropriate algorithm.
*
* @param string $filename Absolute path to the file being processed
* @param integer $AkeebaPackerZIP_CHUNK_SIZE Obsoleted
*
* @return integer The CRC32 in numerical form
*/
public function crc32_file($filename, $AkeebaPackerZIP_CHUNK_SIZE)
{
static $configuration;
if (!$configuration)
{
$configuration = Factory::getConfiguration();
}
$res = false;
if (function_exists("hash_file"))
{
$res = $this->crc32UsingHashExtension($filename);
Factory::getLog()->debug("File $filename - CRC32 = " . dechex($res) . " [HASH_FILE]");
}
else if (function_exists("file_get_contents") && (@filesize($filename) <= $AkeebaPackerZIP_CHUNK_SIZE))
{
$res = $this->crc32Legacy($filename);
Factory::getLog()->debug("File $filename - CRC32 = " . dechex($res) . " [FILE_GET_CONTENTS]");
}
else
{
$res = 0;
Factory::getLog()->debug("File $filename - CRC32 = " . dechex($res) . " [FAKE - CANNOT CALCULATE]");
}
if ($res === false)
{
$res = 0;
Factory::getLog()->warning("File $filename - NOT READABLE: CRC32 IS WRONG!");
}
return $res;
}
/**
* Very efficient CRC32 calculation using the PHP 'hash' extension.
*
* @param string $filename Absolute filepath
*
* @return integer The CRC32
*/
protected function crc32UsingHashExtension($filename)
{
// Detection of buggy PHP hosts
static $mustInvert = null;
if (is_null($mustInvert))
{
$test_crc = @hash('crc32b', 'test', false);
$mustInvert = (strtolower($test_crc) == '0c7e7fd8'); // Normally, it's D87F7E0C :)
if ($mustInvert)
{
Factory::getLog()->warning('Your server has a buggy PHP version which produces inverted CRC32 values. Attempting a workaround. ZIP files may appear as corrupt.');
}
}
$res = @hash_file('crc32b', $filename, false);
if ($mustInvert)
{
// Workaround for buggy PHP versions (I think before 5.1.8) which produce inverted CRC32 sums
$res2 = substr($res, 6, 2) . substr($res, 4, 2) . substr($res, 2, 2) . substr($res, 0, 2);
$res = $res2;
}
$res = hexdec($res);
return $res;
}
/**
* A compatible CRC32 calculation using file_get_contents, utilizing immense amounts of RAM
*
* @param string $filename
*
* @return integer
*/
protected function crc32Legacy($filename)
{
return crc32(@file_get_contents($filename));
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,958 @@
<?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 DirectoryIterator;
use LogicException;
/**
* Unified engine parameters helper class. Deals with scripting, GUI configuration elements and information on engine
* parts (filters, dump engines, scan engines, archivers, installers).
*/
class EngineParameters
{
/**
* Holds the parsed scripting.json contents
*
* @var array
*/
public $scripting = null;
/**
* Holds the known paths holding JSON definitions of engines, installers and configuration gui elements
*
* @var array
*/
protected $enginePartPaths = [];
/**
* Cache of the engines known to this object
*
* @var array
*/
protected $engine_list = [];
/**
* Cache of the GUI configuration elements known to this object
*
* @var array
*/
protected $gui_list = [];
/**
* Cache of the installers known to this object
*
* @var array
*/
protected $installer_list = [];
/**
* The currently active scripting type
*
* @var string
*/
protected $activeType = null;
/**
* Loads the scripting.json and returns an array with the domains, the scripts and the raw data
*
* @return array The parsed scripting.json. Array keys: domains, scripts, data
*/
public function loadScripting($jsonPath = '')
{
if (!empty($this->scripting))
{
return $this->scripting;
}
$this->scripting = [];
$jsonPath = Factory::getAkeebaRoot() . '/Core/scripting.json';
if (!@file_exists($jsonPath))
{
return $this->scripting;
}
$rawData = file_get_contents($jsonPath);
$rawScriptingData = empty($rawData) ? [] : json_decode($rawData, true);
$domain_keys = explode('|', $rawScriptingData['volatile.akeebaengine.domains']);
$domains = [];
foreach ($domain_keys as $key)
{
$record = [
'domain' => $rawScriptingData['volatile.domain.' . $key . '.domain'],
'class' => $rawScriptingData['volatile.domain.' . $key . '.class'],
'text' => $rawScriptingData['volatile.domain.' . $key . '.text'],
];
$domains[$key] = $record;
}
$script_keys = explode('|', $rawScriptingData['volatile.akeebaengine.scripts']);
$scripts = [];
foreach ($script_keys as $key)
{
$record = [
'chain' => explode('|', $rawScriptingData['volatile.scripting.' . $key . '.chain']),
'text' => $rawScriptingData['volatile.scripting.' . $key . '.text'],
];
$scripts[$key] = $record;
}
$this->scripting = [
'domains' => $domains,
'scripts' => $scripts,
'data' => $rawScriptingData,
];
return $this->scripting;
}
/**
* Imports the volatile scripting parameters to the registry
*
* @return void
*/
public function importScriptingToRegistry()
{
$scripting = $this->loadScripting();
$configuration = Factory::getConfiguration();
$configuration->mergeArray($scripting['data'], false);
}
/**
* Returns a volatile scripting parameter for the active backup type
*
* @param string $key The relative key, e.g. core.createarchive
* @param mixed $default Default value
*
* @return mixed The scripting parameter's value
*/
public function getScriptingParameter($key, $default = null)
{
$configuration = Factory::getConfiguration();
if (is_null($this->activeType))
{
$this->activeType = $configuration->get('akeeba.basic.backup_type', 'full');
}
return $configuration->get('volatile.scripting.' . $this->activeType . '.' . $key, $default);
}
/**
* Returns an array with domain keys and domain class names for the current
* backup type. The idea is that shifting this array walks through the backup
* process. When the array is empty, the backup is done.
*
* Each element of the array is an array with two keys: domain and class.
*
* @return array
*/
public function getDomainChain()
{
$configuration = Factory::getConfiguration();
$script = $configuration->get('akeeba.basic.backup_type', 'full');
$scripting = $this->loadScripting();
$domains = $scripting['domains'];
$keys = $scripting['scripts'][$script]['chain'];
$result = [];
foreach ($keys as $domain_key)
{
$result[] = [
'domain' => $domains[$domain_key]['domain'],
'class' => $domains[$domain_key]['class'],
];
}
return $result;
}
/**
* Append a path to the end of the paths list for a specific section
*
* @param string $path Absolute filesystem path to add
* @param string $section The section to add it to (gui, engine, installer, filters)
*
* @return void
*/
public function addPath($path, $section = 'gui')
{
$path = Factory::getFilesystemTools()->TranslateWinPath($path);
// If the array is empty, populate with the defaults
if (!array_key_exists($section, $this->enginePartPaths))
{
$this->getEnginePartPaths($section);
}
// If the path doesn't already exist, add it
if (!in_array($path, $this->enginePartPaths[$section]))
{
$this->enginePartPaths[$section][] = $path;
}
}
/**
* Add a path to the beginning of the paths list for a specific section
*
* @param string $path Absolute filesystem path to add
* @param string $section The section to add it to (gui, engine, installer, filters)
*
* @return void
*/
public function prependPath($path, $section = 'gui')
{
$path = Factory::getFilesystemTools()->TranslateWinPath($path);
// If the array is empty, populate with the defaults
if (!array_key_exists($section, $this->enginePartPaths))
{
$this->getEnginePartPaths($section);
}
// If the path doesn't already exist, add it
if (!in_array($path, $this->enginePartPaths[$section]))
{
array_unshift($this->enginePartPaths[$section], $path);
}
}
/**
* Get the paths for a specific section
*
* @param string $section The section to get the path list for (engine, installer, gui, filter)
*
* @return array
*/
public function getEnginePartPaths($section = 'gui')
{
// Create the key if it's not already present
if (!array_key_exists($section, $this->enginePartPaths))
{
$this->enginePartPaths[$section] = [];
}
if (!empty($this->enginePartPaths[$section]))
{
return $this->enginePartPaths[$section];
}
// Add the defaults if the list is empty
switch ($section)
{
case 'engine':
$this->enginePartPaths[$section] = [
Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot()),
];
break;
case 'installer':
$this->enginePartPaths[$section] = [
Factory::getFilesystemTools()->TranslateWinPath(Platform::getInstance()->get_installer_images_path()),
];
break;
case 'gui':
// Add core GUI definitions
$this->enginePartPaths[$section] = [
Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Core'),
];
// Add platform GUI definition files
$platform_paths = Platform::getInstance()->getPlatformDirectories();
foreach ($platform_paths as $p)
{
$this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Config');
$pro = defined('AKEEBA_PRO') && AKEEBA_PRO;
$pro = defined('AKEEBABACKUP_PRO') ? (AKEEBABACKUP_PRO ? true : false) : $pro;
if ($pro)
{
$this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Config/Pro');
}
}
break;
case 'filter':
$this->enginePartPaths[$section] = [
Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Platform/Filter/Stack'),
Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Filter/Stack'),
];
$platform_paths = Platform::getInstance()->getPlatformDirectories();
foreach ($platform_paths as $p)
{
$this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Filter/Stack');
}
break;
default:
throw new LogicException(sprintf('Can not get paths for engine section %s. No section by this name is known to Akeeba Engine.', $section));
}
return $this->enginePartPaths[$section];
}
/**
* Returns a hash list of Akeeba engines and their data. Each entry has the engine name as key and contains two
* arrays, under the 'information' and 'parameters' keys.
*
* @param string $engine_type The engine type to return information for
*
* @return array
*/
public function getEnginesList($engine_type)
{
$engine_type = ucfirst($engine_type);
// Try to serve cached data first
if (isset($this->engine_list[$engine_type]))
{
return $this->engine_list[$engine_type];
}
// Find absolute path to normal and plugins directories
$temp = $this->getEnginePartPaths('engine');
$path_list = [];
foreach ($temp as $path)
{
$path_list[] = $path . '/' . $engine_type;
}
// Initialize the array where we store our data
$this->engine_list[$engine_type] = [];
// Loop for the paths where engines can be found
foreach ($path_list as $path)
{
if (!@is_dir($path))
{
continue;
}
if (!@is_readable($path))
{
continue;
}
$di = new DirectoryIterator($path);
/** @var DirectoryIterator $file */
foreach ($di as $file)
{
if (!$file->isFile())
{
continue;
}
if ($file->getExtension() !== 'json')
{
continue;
}
$bare_name = ucfirst($file->getBasename('.json'));
// Some hosts copy .json and .php files, renaming them (ie foobar.1.php)
// We need to exclude them, otherwise we'll get a fatal error for declaring the same class twice
if (preg_match('/[^A-Za-z0-9]/', $bare_name))
{
continue;
}
$information = [];
$parameters = [];
$this->parseEngineJSON($file->getRealPath(), $information, $parameters);
$this->engine_list[$engine_type][lcfirst($bare_name)] = [
'information' => $information,
'parameters' => $parameters,
];
}
}
return $this->engine_list[$engine_type];
}
/**
* Parses the GUI JSON files and returns an array of groups and their data
*
* @return array
*/
public function getGUIGroups()
{
// Try to serve cached data first
if (!empty($this->gui_list) && is_array($this->gui_list))
{
if (count($this->gui_list) > 0)
{
return $this->gui_list;
}
}
// Find absolute path to normal and plugins directories
$path_list = $this->getEnginePartPaths('gui');
// Initialize the array where we store our data
$this->gui_list = [];
// Loop for the paths where engines can be found
foreach ($path_list as $path)
{
if (!@is_dir($path))
{
continue;
}
if (!@is_readable($path))
{
continue;
}
$allJSONFiles = [];
$di = new DirectoryIterator($path);
/** @var DirectoryIterator $file */
foreach ($di as $file)
{
if (!$file->isFile())
{
continue;
}
// PHP 5.3.5 and earlier do not support getExtension
if ($file->getExtension() !== 'json')
{
continue;
}
$allJSONFiles[] = $file->getRealPath();
}
if (empty($allJSONFiles))
{
continue;
}
// Sort GUI files alphabetically
asort($allJSONFiles);
// Include each GUI def file
foreach ($allJSONFiles as $filename)
{
$information = [];
$parameters = [];
$this->parseInterfaceJSON($filename, $information, $parameters);
// This effectively skips non-GUI JSONs (e.g. the scripting JSON)
if (!empty($information['description']))
{
if (!isset($information['merge']))
{
$information['merge'] = 0;
}
$group_name = substr(basename($filename), 0, -5);
$def = [
'information' => $information,
'parameters' => $parameters,
];
if (!$information['merge'] || !isset($this->gui_list[$group_name]))
{
$this->gui_list[$group_name] = $def;
}
else
{
$this->gui_list[$group_name]['information'] = array_merge($this->gui_list[$group_name]['information'], $def['information']);
$this->gui_list[$group_name]['parameters'] = array_merge($this->gui_list[$group_name]['parameters'], $def['parameters']);
}
}
}
}
ksort($this->gui_list);
// Push stack filter settings to the 03.filters section
$path_list = $this->getEnginePartPaths('filter');
// Loop for the paths where optional filters can be found
foreach ($path_list as $path)
{
if (!@is_dir($path))
{
continue;
}
if (!@is_readable($path))
{
continue;
}
// Store JSON names in temp array because we'll sort based on filename (GUI order IS IMPORTANT!!)
$allJSONFiles = [];
$di = new DirectoryIterator($path);
/** @var DirectoryIterator $file */
foreach ($di as $file)
{
if (!$file->isFile())
{
continue;
}
// PHP 5.3.5 and earlier do not support getExtension
if ($file->getExtension() !== 'json')
{
continue;
}
$allJSONFiles[] = $file->getRealPath();
}
if (empty($allJSONFiles))
{
continue;
}
// Sort filter files alphabetically
asort($allJSONFiles);
// Include each filter def file
foreach ($allJSONFiles as $filename)
{
$information = [];
$parameters = [];
$this->parseInterfaceJSON($filename, $information, $parameters);
if (!array_key_exists('03.filters', $this->gui_list))
{
$this->gui_list['03.filters'] = ['parameters' => []];
}
if (!array_key_exists('parameters', $this->gui_list['03.filters']))
{
$this->gui_list['03.filters']['parameters'] = [];
}
if (!is_array($parameters))
{
$parameters = [];
}
$this->gui_list['03.filters']['parameters'] = array_merge($this->gui_list['03.filters']['parameters'], $parameters);
}
}
return $this->gui_list;
}
/**
* Parses the installer JSON files and returns an array of installers and their data
*
* @param boolean $forDisplay If true only returns the information relevant for displaying the GUI
*
* @return array
*/
public function getInstallerList($forDisplay = false)
{
// Try to serve cached data first
if (!empty($this->installer_list) && is_array($this->installer_list))
{
if (count($this->installer_list) > 0)
{
return $this->installer_list;
}
}
// Find absolute path to normal and plugins directories
$path_list = [
Platform::getInstance()->get_installer_images_path(),
];
// Initialize the array where we store our data
$this->installer_list = [];
// Loop for the paths where engines can be found
foreach ($path_list as $path)
{
if (!@is_dir($path))
{
continue;
}
if (!@is_readable($path))
{
continue;
}
$di = new DirectoryIterator($path);
/** @var DirectoryIterator $file */
foreach ($di as $file)
{
if (!$file->isFile())
{
continue;
}
// PHP 5.3.5 and earlier do not support getExtension
if ($file->getExtension() !== 'json')
{
continue;
}
$rawData = file_get_contents($file->getRealPath());
$data = empty($rawData) ? [] : json_decode($rawData, true);
if ($forDisplay)
{
$innerData = reset($data);
if (array_key_exists('listinoptions', $innerData))
{
if ($innerData['listinoptions'] == 0)
{
continue;
}
}
}
foreach ($data as $key => $values)
{
$this->installer_list[$key] = [];
foreach ($values as $key2 => $value)
{
$this->installer_list[$key][$key2] = $value;
}
}
}
}
return $this->installer_list;
}
/**
* Returns the JSON representation of the GUI definition and the associated values
*
* @return string
*/
public function getJsonGuiDefinition()
{
// Initialize the array which will be converted to JSON representation
$json_array = [
'engines' => [],
'installers' => [],
'gui' => [],
];
// Get a reference to the configuration
$configuration = Factory::getConfiguration();
// Get data for all engines
$engine_types = [
'archiver',
'dump',
'scan',
'writer',
'postproc',
];
foreach ($engine_types as $type)
{
$engines = $this->getEnginesList($type);
$tempArray = [];
$engineTitles = [];
foreach ($engines as $engine_name => $engine_data)
{
// Translate information
foreach ($engine_data['information'] as $key => $value)
{
switch ($key)
{
case 'title':
case 'description':
$value = Platform::getInstance()->translate($value);
break;
}
$tempArray[$engine_name]['information'][$key] = $value;
if ($key == 'title')
{
$engineTitles[$engine_name] = $value;
}
}
// Process parameters
$parameters = [];
foreach ($engine_data['parameters'] as $param_key => $param)
{
$param['default'] = $configuration->get($param_key, $param['default'], false);
foreach ($param as $option_key => $option_value)
{
// Translate title, description, enumkeys
switch ($option_key)
{
case 'title':
case 'description':
case 'labelempty':
case 'labelnotempty':
$param[$option_key] = Platform::getInstance()->translate($option_value);
break;
case 'enumkeys':
$enumkeys = explode('|', $option_value);
$new_keys = [];
foreach ($enumkeys as $old_key)
{
$new_keys[] = Platform::getInstance()->translate($old_key);
}
$param[$option_key] = implode('|', $new_keys);
break;
default:
}
}
$parameters[$param_key] = $param;
}
// Add processed parameters
$tempArray[$engine_name]['parameters'] = $parameters;
}
asort($engineTitles);
foreach ($engineTitles as $engineName => $title)
{
$json_array['engines'][$type][$engineName] = $tempArray[$engineName];
}
}
// Get data for GUI elements
$json_array['gui'] = [];
$groupdefs = $this->getGUIGroups();
foreach ($groupdefs as $groupKey => $definition)
{
$group_name = '';
if (isset($definition['information']) && isset($definition['information']['description']))
{
$group_name = Platform::getInstance()->translate($definition['information']['description']);
}
// Skip no-name groups
if (empty($group_name))
{
continue;
}
$parameters = [];
foreach ($definition['parameters'] as $param_key => $param)
{
$param['default'] = $configuration->get($param_key, $param['default'], false);
foreach ($param as $option_key => $option_value)
{
// Translate title, description, enumkeys
switch ($option_key)
{
case 'title':
case 'description':
$param[$option_key] = Platform::getInstance()->translate($option_value);
break;
case 'enumkeys':
$enumkeys = explode('|', $option_value);
$new_keys = [];
foreach ($enumkeys as $old_key)
{
$new_keys[] = Platform::getInstance()->translate($old_key);
}
$param[$option_key] = implode('|', $new_keys);
break;
default:
}
}
$parameters[$param_key] = $param;
}
$json_array['gui'][$group_name] = $parameters;
}
// Get data for the installers
$json_array['installers'] = $this->getInstallerList(true);
uasort($json_array['installers'], function ($a, $b) {
if ($a['name'] == $b['name'])
{
return 0;
}
return ($a['name'] < $b['name']) ? -1 : 1;
});
$json = json_encode($json_array);
return $json;
}
/**
* Parses an engine JSON file returning two arrays, one with the general information
* of that engine and one with its configuration variables' definitions
*
* @param string $jsonPath Absolute path to engine JSON file
* @param array $information [out] The engine information hash array
* @param array $parameters [out] The parameters hash array
*
* @return bool True if the file was loaded
*/
public function parseEngineJSON($jsonPath, &$information, &$parameters)
{
if (!file_exists($jsonPath))
{
return false;
}
$information = [
'title' => '',
'description' => '',
];
$parameters = [];
$rawData = file_get_contents($jsonPath);
$jsonData = empty($rawData) ? [] : json_decode($rawData, true);
foreach ($jsonData ?? [] as $section => $data)
{
if (is_array($data))
{
if ($section == '_information')
{
// Parse information
foreach ($data as $key => $value)
{
$information[$key] = $value;
}
}
elseif (substr($section, 0, 1) != '_')
{
// Parse parameters
$newparam = [
'title' => '',
'description' => '',
'type' => 'string',
'default' => '',
];
foreach ($data as $key => $value)
{
$newparam[$key] = $value;
}
$parameters[$section] = $newparam;
}
}
}
return true;
}
/**
* Parses a graphical interface JSON file returning two arrays, one with the general
* information of that configuration section and one with its configuration variables'
* definitions.
*
* @param string $jsonPath Absolute path to engine JSON file
* @param array $information [out] The GUI information hash array
* @param array $parameters [out] The parameters hash array
*
* @return bool True if the file was loaded
*/
public function parseInterfaceJSON($jsonPath, &$information, &$parameters)
{
if (!file_exists($jsonPath))
{
return false;
}
$information = [
'description' => '',
];
$parameters = [];
$rawData = file_get_contents($jsonPath);
$jsonData = empty($rawData) ? [] : json_decode($rawData, true);
foreach ($jsonData as $section => $data)
{
if (is_array($data))
{
if ($section == '_group')
{
// Parse information
foreach ($data as $key => $value)
{
$information[$key] = $value;
}
continue;
}
if (substr($section, 0, 1) != '_')
{
// Parse parameters
$newparam = [
'title' => '',
'description' => '',
'type' => 'string',
'default' => '',
'protected' => 0,
];
foreach ($data as $key => $value)
{
$newparam[$key] = $value;
}
$parameters[$section] = $newparam;
}
}
}
return true;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 Psr\Log\InvalidArgumentException;
use Psr\Log\LoggerInterface;
use 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,625 @@
<?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
if (!@ftp_chdir($this->connection, $this->directory))
{
@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', $this->directory), 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);
$ret = @ftp_fput($this->connection, $fileName, $handle, FTP_BINARY);
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;
}
$ret = @ftp_fput($this->connection, $remoteFilename, $handle, FTP_BINARY);
@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+');
$result = @ftp_fget($this->connection, $handle, $fileName, FTP_BINARY);
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)
{
$ret = @ftp_get($this->connection, $localFilename, $remoteFilename, FTP_BINARY);
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)
{
return @ftp_delete($this->connection, $fileName);
}
/**
* 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+');
$ret = @ftp_fget($this->connection, $handle, $from, FTP_BINARY);
if ($ret !== false)
{
rewind($handle);
$ret = @ftp_fput($this->connection, $to, $handle, FTP_BINARY);
}
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)
{
$passwordEncoded = urlencode($this->password);
$hostname = $this->host . ($this->port ? ":{$this->port}" : '');
$protocol = $this->ssl ? "ftps" : "ftp";
return "{$protocol}://{$this->username}:{$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);
}
}

View File

@@ -0,0 +1,782 @@
<?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()
{
return '';
}
/**
* 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 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 . '/';
// 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 = '';
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 array $commands An array with the FTP 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;
}
}

View File

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

View File

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

View File

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

View File

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