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,823 @@
<?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\Archiver;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Base\Exceptions\WarningException;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Util\FileCloseAware;
use Akeeba\Engine\Util\FileSystem;
use Exception;
use RuntimeException;
/**
* Abstract parent class of all archiver engines
*/
abstract class Base
{
use FileCloseAware;
/** @var string The archive's comment. It's currently used ONLY in the ZIP file format */
protected $_comment;
/** @var Filesystem Filesystem utilities object */
protected $fsUtils = null;
/** @var resource JPA transformation source handle */
private $_xform_fp;
/** @var int The total size of the source JPA file */
private $totalSourceJPASize = 0;
/**
* Public constructor
*
* @codeCoverageIgnore
*
* @return void
*/
public function __construct()
{
$this->__bootstrap_code();
}
/**
* Wakeup (unserialization) function
*
* @codeCoverageIgnore
*
* @return void
*/
public function __wakeup()
{
$this->__bootstrap_code();
}
/**
* Adds a single file in the archive
*
* @param string $file The absolute path to the file to add
* @param string $removePath Path to remove from $file
* @param string $addPath Path to prepend to $file
*
* @return void
*
* @throws Exception
*/
public final function addFile($file, $removePath = '', $addPath = '')
{
$storedName = $this->addRemovePaths($file, $removePath, $addPath);
$this->addFileRenamed($file, $storedName);
}
/**
* Adds a list of files into the archive, removing $removePath from the
* file names and adding $addPath to them.
*
* @param array $fileList A simple string array of filepaths to include
* @param string $removePath Paths to remove from the filepaths
* @param string $addPath Paths to add in front of the filepaths
*
* @return void
*
* @throws Exception
*/
public final function addFileList(&$fileList, $removePath = '', $addPath = '')
{
if (!is_array($fileList))
{
Factory::getLog()->warning('addFileList called without a file list array');
return;
}
foreach ($fileList as $file)
{
$this->addFile($file, $removePath, $addPath);
}
}
/**
* Adds a file to the archive, with a name that's different from the source
* filename
*
* @param string $sourceFile Absolute path to the source file
* @param string $targetFile Relative filename to store in archive
*
* @return void
*
* @throws Exception
*/
public function addFileRenamed($sourceFile, $targetFile)
{
$mb_encoding = '8bit';
if (function_exists('mb_internal_encoding'))
{
$mb_encoding = mb_internal_encoding();
mb_internal_encoding('ISO-8859-1');
}
try
{
$this->_addFile(false, $sourceFile, $targetFile);
}
catch (WarningException $e)
{
Factory::getLog()->warning($e->getMessage());
}
finally
{
if (function_exists('mb_internal_encoding'))
{
mb_internal_encoding($mb_encoding);
}
}
}
/**
* Adds a file to the archive, given the stored name and its contents
*
* @param string $fileName The base file name
* @param string $addPath The relative path to prepend to file name
* @param string $virtualContent The contents of the file to be archived
*
* @return void
*/
public final function addFileVirtual($fileName, $addPath, &$virtualContent)
{
$storedName = $this->addRemovePaths($fileName, '', $addPath);
$mb_encoding = '8bit';
if (function_exists('mb_internal_encoding'))
{
$mb_encoding = mb_internal_encoding();
mb_internal_encoding('ISO-8859-1');
}
try
{
$this->_addFile(true, $virtualContent, $storedName);
}
catch (WarningException $e)
{
Factory::getLog()->warning($e->getMessage());
}
finally
{
if (function_exists('mb_internal_encoding'))
{
mb_internal_encoding($mb_encoding);
}
}
}
/**
* Adds a file to the archive, given the stored name and its contents
*
* @param string $fileName The base file name
* @param string $addPath The relative path to prepend to file name
* @param string $virtualContent The contents of the file to be archived
*
* @return void
*
* @deprecated 7.0.0
*/
public final function addVirtualFile($fileName, $addPath, &$virtualContent)
{
Factory::getLog()->debug('DEPRECATED: addVirtualFile() has been renamed to addFileVirtual().');
$this->addFileVirtual($fileName, $addPath, $virtualContent);
}
/**
* Makes whatever finalization is needed for the archive to be considered
* complete and useful (or, generally, clean up)
*
* @return void
*/
abstract public function finalize();
/**
* Returns a string with the extension (including the dot) of the files produced
* by this class.
*
* @return string
*/
abstract public function getExtension();
/**
* Initialises the archiver class, creating the archive from an existent
* installer's JPA archive. MUST BE OVERRIDEN BY CHILDREN CLASSES.
*
* @param string $targetArchivePath Absolute path to the generated archive
* @param array $options A named key array of options (optional)
*
* @return void
*/
abstract public function initialize($targetArchivePath, $options = []);
/**
* Notifies the engine on the backup comment and converts it to plain text for
* inclusion in the archive file, if applicable.
*
* @param string $comment The archive's comment
*
* @return void
*/
public function setComment($comment)
{
// First, sanitize the comment in a text-only format
$comment = str_replace("\n", " ", $comment); // Replace newlines with spaces
$comment = str_replace("<br>", "\n", $comment); // Replace HTML4 <br> with single newlines
$comment = str_replace("<br/>", "\n", $comment); // Replace HTML4 <br> with single newlines
$comment = str_replace("<br />", "\n", $comment); // Replace HTML <br /> with single newlines
$comment = str_replace("</p>", "\n\n", $comment); // Replace paragraph endings with double newlines
$comment = str_replace("<b>", "*", $comment); // Replace bold with star notation
$comment = str_replace("</b>", "*", $comment); // Replace bold with star notation
$comment = str_replace("<i>", "_", $comment); // Replace italics with underline notation
$comment = str_replace("</i>", "_", $comment); // Replace italics with underline notation
$this->_comment = strip_tags($comment, '');
}
/**
* Transforms a JPA archive (containing an installer) to the native archive format
* of the class. It actually extracts the source JPA in memory and instructs the
* class to include each extracted file.
*
* @codeCoverageIgnore
*
* @param integer $index The index in the source JPA archive's list currently in use
* @param integer $offset The source JPA archive's offset to use
*
* @return array|bool False if an error occurred, return array otherwise
*/
public function transformJPA($index, $offset)
{
$xform_source = null;
// Do we have to open the file?
if (!$this->_xform_fp)
{
// Get the source path
$registry = Factory::getConfiguration();
$embedded_installer = $registry->get('akeeba.advanced.embedded_installer');
// Fetch the name of the installer image
$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
$xform_source = Platform::getInstance()->get_installer_images_path() .
'/foobar.jpa'; // We need this as a "safe fallback"
// Try to find a sane default if we are not given a valid embedded installer
if (!array_key_exists($embedded_installer, $installerDescriptors))
{
$embedded_installer = 'angie';
if (!array_key_exists($embedded_installer, $installerDescriptors))
{
$allInstallers = array_keys($installerDescriptors);
foreach ($allInstallers as $anInstaller)
{
if ($anInstaller == 'none')
{
continue;
}
$embedded_installer = $anInstaller;
break;
}
}
}
if (array_key_exists($embedded_installer, $installerDescriptors))
{
$packages = $installerDescriptors[$embedded_installer]['package'] ?? '';
$langPacks = $installerDescriptors[$embedded_installer]['language'] ?? '';
if (empty($packages))
{
// No installer package specified. Pretend we are done!
$retArray = [
"filename" => '', // File name extracted
"data" => '', // File data
"index" => 0, // How many source JPA files I have
"offset" => 0, // Offset in JPA file
"skip" => false, // Skip this?
"done" => true, // Are we done yet?
"filesize" => 0,
];
return $retArray;
}
$packages = explode(',', $packages);
$langPacks = explode(',', $langPacks);
$this->totalSourceJPASize = 0;
$pathPrefix = Platform::getInstance()->get_installer_images_path() . '/';
foreach ($packages as $package)
{
$filePath = $pathPrefix . $package;
$this->totalSourceJPASize += (int) @filesize($filePath);
}
foreach ($langPacks as $langPack)
{
$filePath = $pathPrefix . $langPack;
if (!is_file($filePath))
{
continue;
}
$packages[] = $langPack;
$this->totalSourceJPASize += (int) @filesize($filePath);
}
if (count($packages) < $index)
{
throw new RuntimeException(__CLASS__ . ":: Installer package index $index not found for embedded installer $embedded_installer");
}
$package = $packages[$index];
// A package is specified, use it!
$xform_source = $pathPrefix . $package;
}
// 2.3: Try to use sane default if the indicated installer doesn't exist
if (!is_null($xform_source) && !file_exists($xform_source) && (basename($xform_source) != 'angie.jpa'))
{
throw new RuntimeException(__CLASS__ . ":: Installer package $xform_source of embedded installer $embedded_installer not found. Please go to the configuration page, select an Embedded Installer, save the configuration and try backing up again.");
}
// Try opening the file
if (!is_null($xform_source) && file_exists($xform_source))
{
$this->_xform_fp = @fopen($xform_source, 'r');
if ($this->_xform_fp === false)
{
throw new RuntimeException(__CLASS__ . ":: Can't seed archive with installer package " . $xform_source);
}
}
else
{
throw new RuntimeException(__CLASS__ . ":: Installer package " . $xform_source . " does not exist!");
}
}
$headerDataLength = 0;
if (!$offset)
{
// First run detected!
Factory::getLog()->debug('Initializing with JPA package ' . $xform_source);
// Skip over the header and check no problem exists
$offset = $this->_xformReadHeader();
if ($offset === false)
{
throw new RuntimeException('JPA package file was not read');
}
$headerDataLength = $offset;
}
$ret = $this->_xformExtract($offset);
$ret['index'] = $index;
if (is_array($ret))
{
$ret['chunkProcessed'] = $headerDataLength + $ret['offset'] - $offset;
$offset = $ret['offset'];
if (!$ret['skip'] && !$ret['done'])
{
Factory::getLog()->debug(' Adding ' . $ret['filename'] . '; Next offset:' . $offset);
$this->addFileVirtual($ret['filename'], '', $ret['data']);
}
elseif ($ret['done'])
{
$registry = Factory::getConfiguration();
$embedded_installer = $registry->get('akeeba.advanced.embedded_installer');
$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
$packages = $installerDescriptors[$embedded_installer]['package'];
$packages = explode(',', $packages);
$pathPrefix = Platform::getInstance()->get_installer_images_path() . '/';
$langPacks = $installerDescriptors[$embedded_installer]['language'];
$langPacks = explode(',', $langPacks);
foreach ($langPacks as $langPack)
{
$filePath = $pathPrefix . $langPack;
if (!is_file($filePath))
{
continue;
}
$packages[] = $langPack;
}
Factory::getLog()->debug(' Done with package ' . $packages[$index]);
if (count($packages) > ($index + 1))
{
$ret['done'] = false;
$ret['index'] = $index + 1;
$ret['offset'] = 0;
$this->_xform_fp = null;
}
else
{
Factory::getLog()->debug(' Done with installer seeding.');
}
}
else
{
$reason = ' Skipping ' . $ret['filename'];
Factory::getLog()->debug($reason);
}
}
else
{
throw new RuntimeException('JPA extraction returned FALSE. The installer image is corrupt.');
}
if ($ret['done'])
{
// We are finished! Close the file
$this->conditionalFileClose($this->_xform_fp);
Factory::getLog()->debug('Initializing with JPA package has finished');
}
$ret['filesize'] = $this->totalSourceJPASize;
return $ret;
}
/**
* Common code which gets called on instance creation or wake-up (unserialization)
*
* @codeCoverageIgnore
*
* @return void
*/
protected function __bootstrap_code()
{
$this->fsUtils = Factory::getFilesystemTools();
}
/**
* The most basic file transaction: add a single entry (file or directory) to
* the archive.
*
* @param boolean $isVirtual If true, the next parameter contains file data instead of a file name
* @param string $sourceNameOrData Absolute file name to read data from or the file data itself is $isVirtual
* is true
* @param string $targetName The (relative) file name under which to store the file in the archive
*
* @return boolean True on success, false otherwise. DEPRECATED: Use exceptions instead.
*
* @throws WarningException When there's a warning (the backup integrity is NOT compromised)
* @throws ErrorException When there's an error (the backup integrity is compromised backup dead)
*/
abstract protected function _addFile($isVirtual, &$sourceNameOrData, $targetName);
/**
* This function indicates if the path $p_path is under the $p_dir tree. Or,
* said in an other way, if the file or sub-dir $p_path is inside the dir
* $p_dir.
* The function indicates also if the path is exactly the same as the dir.
* This function supports path with duplicated '/' like '//', but does not
* support '.' or '..' statements.
*
* Copied verbatim from pclZip library
*
* @codeCoverageIgnore
*
* @param string $p_dir Source tree
* @param string $p_path Check if this is part of $p_dir
*
* @return integer 0 if $p_path is not inside directory $p_dir,
* 1 if $p_path is inside directory $p_dir
* 2 if $p_path is exactly the same as $p_dir
*/
private function _PathInclusion($p_dir, $p_path)
{
$v_result = 1;
// ----- Explode dir and path by directory separator
$v_list_dir = explode("/", $p_dir);
$v_list_dir_size = count($v_list_dir);
$v_list_path = explode("/", $p_path);
$v_list_path_size = count($v_list_path);
// ----- Study directories paths
$i = 0;
$j = 0;
while (($i < $v_list_dir_size) && ($j < $v_list_path_size) && ($v_result))
{
// ----- Look for empty dir (path reduction)
if ($v_list_dir[$i] == '')
{
$i++;
continue;
}
if ($v_list_path[$j] == '')
{
$j++;
continue;
}
// ----- Compare the items
if (($v_list_dir[$i] != $v_list_path[$j]) && ($v_list_dir[$i] != '') && ($v_list_path[$j] != ''))
{
$v_result = 0;
}
// ----- Next items
$i++;
$j++;
}
// ----- Look if everything seems to be the same
if ($v_result)
{
// ----- Skip all the empty items
while (($j < $v_list_path_size) && ($v_list_path[$j] == ''))
{
$j++;
}
while (($i < $v_list_dir_size) && ($v_list_dir[$i] == ''))
{
$i++;
}
if (($i >= $v_list_dir_size) && ($j >= $v_list_path_size))
{
// ----- There are exactly the same
$v_result = 2;
}
else if ($i < $v_list_dir_size)
{
// ----- The path is shorter than the dir
$v_result = 0;
}
}
// ----- Return
return $v_result;
}
/**
* Extracts a file from the JPA archive and returns an in-memory array containing it
* and its file data. The data returned is an array, consisting of the following keys:
* "filename" => relative file path stored in the archive
* "data" => file data
* "offset" => next offset to use
* "skip" => if this is not a file, just skip it...
* "done" => No more files left in archive
*
* @codeCoverageIgnore
*
* @param integer $offset The absolute data offset from archive's header
*
* @return array|bool See description for more information
*/
private function &_xformExtract($offset)
{
$false = false; // Used to return false values in case an error occurs
// Generate a return array
$retArray = [
"filename" => '', // File name extracted
"data" => '', // File data
"offset" => 0, // Offset in ZIP file
"skip" => false, // Skip this?
"done" => false // Are we done yet?
];
// If we can't open the file, return an error condition
if ($this->_xform_fp === false)
{
return $false;
}
// Go to the offset specified
if (!fseek($this->_xform_fp, $offset) == 0)
{
return $false;
}
// Get and decode Entity Description Block
$signature = fread($this->_xform_fp, 3);
// Check signature
if ($signature == 'JPF')
{
// This a JPA Entity Block. Process the header.
// Read length of EDB and of the Entity Path Data
$length_array = unpack('vblocksize/vpathsize', fread($this->_xform_fp, 4));
// Read the path data
$file = fread($this->_xform_fp, $length_array['pathsize']);
// Read and parse the known data portion
$bin_data = fread($this->_xform_fp, 14);
$header_data = unpack('Ctype/Ccompression/Vcompsize/Vuncompsize/Vperms', $bin_data);
// Read any unknwon data
$restBytes = $length_array['blocksize'] - (21 + $length_array['pathsize']);
if ($restBytes > 0)
{
$junk = fread($this->_xform_fp, $restBytes);
}
$compressionType = $header_data['compression'];
// Populate the return array
$retArray['filename'] = $file;
$retArray['skip'] = ($header_data['compsize'] == 0); // Skip over directories
switch ($header_data['type'])
{
case 0:
// directory
break;
case 1:
// file
switch ($compressionType)
{
case 0: // No compression
if ($header_data['compsize'] > 0) // 0 byte files do not have data to be read
{
$retArray['data'] = fread($this->_xform_fp, $header_data['compsize']);
}
break;
case 1: // GZip compression
$zipData = fread($this->_xform_fp, $header_data['compsize']);
$retArray['data'] = gzinflate($zipData);
break;
case 2: // BZip2 compression
$zipData = fread($this->_xform_fp, $header_data['compsize']);
$retArray['data'] = bzdecompress($zipData);
break;
}
break;
}
}
else
{
// This is not a file header. This means we are done.
$retArray['done'] = true;
}
$retArray['offset'] = ftell($this->_xform_fp);
return $retArray;
}
/**
* Skips over the JPA header entry and returns the offset file data starts from
*
* @codeCoverageIgnore
*
* @return boolean|integer False on failure, offset otherwise
*/
private function _xformReadHeader()
{
// Fail for unreadable files
if ($this->_xform_fp === false)
{
return false;
}
// Go to the beggining of the file
rewind($this->_xform_fp);
// Read the signature
$sig = fread($this->_xform_fp, 3);
// Not a JPA Archive?
if ($sig != 'JPA')
{
return false;
}
// Read and parse header length
$header_length_array = unpack('v', fread($this->_xform_fp, 2));
$header_length = $header_length_array[1];
// Read and parse the known portion of header data (14 bytes)
$bin_data = fread($this->_xform_fp, 14);
$header_data = unpack('Cmajor/Cminor/Vcount/Vuncsize/Vcsize', $bin_data);
// Load any remaining header data (forward compatibility)
$rest_length = $header_length - 19;
if ($rest_length > 0)
{
$junk = fread($this->_xform_fp, $rest_length);
}
return ftell($this->_xform_fp);
}
/**
* Removes the $p_remove_dir from $p_filename, while prepending it with $p_add_dir.
* Largely based on code from the pclZip library.
*
* @param string $p_filename The absolute file name to treat
* @param string $p_remove_dir The path to remove
* @param string $p_add_dir The path to prefix the treated file name with
*
* @return string The treated file name
*/
private function addRemovePaths($p_filename, $p_remove_dir, $p_add_dir)
{
$p_filename = $this->fsUtils->TranslateWinPath($p_filename);
$p_remove_dir = ($p_remove_dir == '') ? '' :
$this->fsUtils->TranslateWinPath($p_remove_dir); //should fix corrupt backups, fix by nicholas
$v_stored_filename = $p_filename;
if (!($p_remove_dir == ""))
{
if (substr($p_remove_dir, -1) != '/')
{
$p_remove_dir .= "/";
}
if ((substr($p_filename, 0, 2) == "./") || (substr($p_remove_dir, 0, 2) == "./"))
{
if ((substr($p_filename, 0, 2) == "./") && (substr($p_remove_dir, 0, 2) != "./"))
{
$p_remove_dir = "./" . $p_remove_dir;
}
if ((substr($p_filename, 0, 2) != "./") && (substr($p_remove_dir, 0, 2) == "./"))
{
$p_remove_dir = substr($p_remove_dir, 2);
}
}
$v_compare = $this->_PathInclusion($p_remove_dir, $p_filename);
if ($v_compare > 0)
{
if ($v_compare == 2)
{
$v_stored_filename = "";
}
else
{
$v_stored_filename =
substr($p_filename, (function_exists('mb_strlen') ? mb_strlen($p_remove_dir, '8bit') :
strlen($p_remove_dir)));
}
}
}
else
{
$v_stored_filename = $p_filename;
}
if (!($p_add_dir == ""))
{
if (substr($p_add_dir, -1) == "/")
{
$v_stored_filename = $p_add_dir . $v_stored_filename;
}
else
{
$v_stored_filename = $p_add_dir . "/" . $v_stored_filename;
}
}
return $v_stored_filename;
}
}

View File

@@ -0,0 +1,721 @@
<?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\Archiver;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Base\Exceptions\WarningException;
use Akeeba\Engine\Factory;
if (!defined('AKEEBA_CHUNK'))
{
$configuration = Factory::getConfiguration();
$chunksize = $configuration->get('engine.archiver.common.chunk_size', 1048576);
define('AKEEBA_CHUNK', $chunksize);
}
if (!function_exists('aksubstr'))
{
/**
* Attempt to use mbstring for getting parts of strings
*
* @param string $string
* @param int $start
* @param int|null $length
*
* @return string
*/
function aksubstr($string, $start, $length = null)
{
return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') :
substr($string, $start, $length);
}
}
/**
* Abstract class for custom archiver implementations
*/
abstract class BaseArchiver extends BaseFileManagement
{
/** @var array The last part which has been finalized and waits to be post-processed */
public $finishedPart = [];
/** @var resource File pointer to the archive being currently written to */
protected $fp = null;
/** @var resource File pointer to the archive's central directory file (for ZIP) */
protected $cdfp = null;
/** @var string The name of the file holding the archive's data, which becomes the final archive */
protected $_dataFileName;
/** @var string Archive full path without extension */
protected $dataFileNameWithoutExtension = '';
/** @var bool Should I store symlinks as such (no dereferencing?) */
protected $storeSymlinkTarget = false;
/** @var int Part size for split archives, in bytes */
protected $partSize = 0;
/** @var bool Should I use Split ZIP? */
protected $useSplitArchive = false;
/** @var int Permissions for the backup archive part files */
protected $permissions = null;
/**
* Release file pointers when the object is being serialized
*
* @codeCoverageIgnore
*
* @return void
*/
public function _onSerialize()
{
$this->_closeAllFiles();
$this->fp = null;
$this->cdfp = null;
}
/**
* Release file pointers when the object is being destroyed
*
* @codeCoverageIgnore
*
* @return void
*/
public function __destruct()
{
$this->_closeAllFiles();
$this->fp = null;
$this->cdfp = null;
}
/**
* Create a new archive part file (but does NOT open it for writing)
*
* @param bool $finalPart True if this is the final part
*
* @return bool False if creating a new part fails
*/
abstract protected function createNewPartFile($finalPart = false);
/**
* Create a new part file and open it for writing
*
* @param bool $finalPart Is this the final part?
*
* @return void
*/
protected function createAndOpenNewPart($finalPart = false)
{
@$this->fclose($this->fp);
$this->fp = null;
// Not enough space on current part, create new part
if (!$this->createNewPartFile($finalPart))
{
$extension = $this->getExtension();
$extension = ltrim(strtoupper($extension), '.');
throw new ErrorException("Could not create new $extension part file " . basename($this->_dataFileName));
}
$this->openArchiveForOutput(true);
}
/**
* Create a new backup archive
*
* @return void
*
* @throws ErrorException
*/
protected function createNewBackupArchive()
{
Factory::getLog()->debug(__CLASS__ . " :: Killing old archive");
$this->fp = $this->fopen($this->_dataFileName, "w");
if ($this->fp === false)
{
if (file_exists($this->_dataFileName))
{
@unlink($this->_dataFileName);
}
@touch($this->_dataFileName);
@chmod($this->_dataFileName, 0666);
$this->fp = $this->fopen($this->_dataFileName, "w");
if ($this->fp !== false)
{
throw new ErrorException("Could not open archive file '{$this->_dataFileName}' for append!");
}
}
@ftruncate($this->fp, 0);
}
/**
* Opens the backup archive file for output. Returns false if the archive file cannot be opened in binary append
* mode.
*
* @param bool $force Should I forcibly reopen the file? If false, I'll only open the file if the current
* file pointer is null.
*
* @return void
*/
protected function openArchiveForOutput($force = false)
{
if (is_null($this->fp) || $force)
{
$this->fp = $this->fopen($this->_dataFileName, "a");
}
if ($this->fp === false)
{
$this->fp = null;
throw new ErrorException("Could not open archive file '{$this->_dataFileName}' for append!");
}
}
/**
* Converts a human formatted size to integer representation of bytes,
* e.g. 1M to 1024768
*
* @param string $setting The value in human readable format, e.g. "1M"
*
* @return integer The value in bytes
*/
protected function humanToIntegerBytes($setting)
{
$val = trim($setting);
$last = strtolower($val[strlen($val) - 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;
}
/**
* Get the PHP memory limit in bytes
*
* @return int|null Memory limit in bytes or null if we can't figure it out.
*/
protected function getMemoryLimit()
{
if (!function_exists('ini_get'))
{
return null;
}
$memLimit = ini_get("memory_limit");
if ((is_numeric($memLimit) && ($memLimit < 0)) || !is_numeric($memLimit))
{
$memLimit = 0; // 1.2a3 -- Rare case with memory_limit < 0, e.g. -1Mb!
}
$memLimit = $this->humanToIntegerBytes($memLimit);
return $memLimit;
}
/**
* Enable storing of symlink target if we are not on Windows
*
* @return void
*/
protected function enableSymlinkTargetStorage()
{
$configuration = Factory::getConfiguration();
$dereferenceSymlinks = $configuration->get('engine.archiver.common.dereference_symlinks', true);
if ($dereferenceSymlinks)
{
return;
}
// We are told not to dereference symlinks. Are we on Windows?
$isWindows = (DIRECTORY_SEPARATOR == '\\');
if (function_exists('php_uname'))
{
$isWindows = stristr(php_uname(), 'windows');
}
// If we are not on Windows, enable symlink target storage
$this->storeSymlinkTarget = !$isWindows;
}
/**
* Gets the file size and last modification time (also works on virtual files and symlinks)
*
* @param string $sourceNameOrData File path to the source file or source data (if $isVirtual is true)
* @param bool $isVirtual Is this a virtual file?
* @param bool $isSymlink Is this a symlink?
* @param bool $isDir Is this a directory?
*
* @return array
*/
protected function getFileSizeAndModificationTime(&$sourceNameOrData, $isVirtual, $isSymlink, $isDir)
{
// Get real size before compression
if ($isVirtual)
{
$fileSize = akstrlen($sourceNameOrData);
$fileModTime = time();
return [$fileSize, $fileModTime];
}
if ($isSymlink)
{
$fileSize = akstrlen(@readlink($sourceNameOrData));
$fileModTime = 0;
return [$fileSize, $fileModTime];
}
// Is the file readable?
if (!is_readable($sourceNameOrData) && !$isDir)
{
// Really, REALLY check if it is readable (PHP sometimes lies, dammit!)
$myFP = @$this->fopen($sourceNameOrData, 'r');
if ($myFP === false)
{
// Unreadable file, skip it.
throw new WarningException('Unreadable file ' . $sourceNameOrData . '. Check permissions');
}
@$this->fclose($myFP);
}
// Get the file size
$fileSize = $isDir ? 0 : @filesize($sourceNameOrData);
$fileModTime = $isDir ? 0 : @filemtime($sourceNameOrData);
return [$fileSize, $fileModTime];
}
/**
* Get the preferred compression method for a file
*
* @param int $fileSize File size in bytes
* @param int $memLimit Memory limit in bytes
* @param bool $isDir Is it a directory?
* @param bool $isSymlink Is it a symlink?
*
* @return int Compression method to use: 0 (uncompressed) or 1 (gzip deflate)
*/
protected function getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink)
{
// If we don't have gzip installed we can't compress anything
if (!function_exists("gzcompress"))
{
return 0;
}
// Don't compress directories or symlinks
if ($isDir || $isSymlink)
{
return 0;
}
// Do not compress files over the compression threshold
if ($fileSize >= _AKEEBA_COMPRESSION_THRESHOLD)
{
return 0;
}
// No memory limit, file smaller than the compression threshold: always compress.
if (is_numeric($memLimit) && ($memLimit == 0))
{
return 1;
}
// Non-zero memory limit, PHP can report memory usage, see if there's enough memory.
if (is_numeric($memLimit) && function_exists("memory_get_usage"))
{
$availableRAM = $memLimit - memory_get_usage();
// Conservative approach: if the file size is over 40% of the available memory we won't compress.
$compressionMethod = (($availableRAM / 2.5) >= $fileSize) ? 1 : 0;
return $compressionMethod;
}
// Non-zero memory limit, PHP can't report memory usage, compress only files up to 512Kb (very conservative)
return ($fileSize <= 524288) ? 1 : 0;
}
/**
* Checks if the file exists and is readable
*
* @param string $sourceNameOrData The path to the file being compressed, or the raw file data for virtual files
* @param bool $isVirtual Is this a virtual file?
* @param bool $isSymlink Is this a symlink?
* @param bool $isDir Is this a directory?
*
* @return void
*
* @throws WarningException
*/
protected function testIfFileExists(&$sourceNameOrData, &$isVirtual, &$isDir, &$isSymlink)
{
if ($isVirtual || $isDir)
{
return;
}
if (!@file_exists($sourceNameOrData))
{
if ($isSymlink)
{
throw new WarningException('The symlink ' . $sourceNameOrData . ' points to a file or folder that no longer exists and will NOT be backed up.');
}
throw new WarningException('The file ' . $sourceNameOrData . ' no longer exists and will NOT be backed up. Are you backing up temporary or cache data?');
}
if (!@is_readable($sourceNameOrData))
{
throw new WarningException('Unreadable file ' . $sourceNameOrData . '. Check permissions.');
}
}
/**
* Try to get the compressed data for a file
*
* @param string $sourceNameOrData
* @param bool $isVirtual
* @param int $compressionMethod
* @param string $zdata
* @param int $unc_len
* @param int $c_len
*
* @return void
*/
protected function getZData(&$sourceNameOrData, &$isVirtual, &$compressionMethod, &$zdata, &$unc_len, &$c_len)
{
// Get uncompressed data
$udata =& $sourceNameOrData;
if (!$isVirtual)
{
$udata = @file_get_contents($sourceNameOrData);
}
// If the compression fails, we will let it behave like no compression was available
$c_len = $unc_len;
$compressionMethod = 0;
// Proceed with compression
$zdata = @gzcompress($udata);
if ($zdata !== false)
{
// The compression succeeded
unset($udata);
$compressionMethod = 1;
$zdata = aksubstr($zdata, 2, -4);
$c_len = akstrlen($zdata);
}
}
/**
* Returns the bytes available for writing data to the current part file (i.e. part size minus current offset)
*
* @return int
*/
protected function getPartFreeSize()
{
clearstatcache();
$current_part_size = @filesize($this->_dataFileName);
return (int) $this->partSize - ($current_part_size === false ? 0 : $current_part_size);
}
/**
* Enable split archive creation where possible
*
* @return void
*/
protected function enableSplitArchives()
{
$configuration = Factory::getConfiguration();
$partSize = $configuration->get('engine.archiver.common.part_size', 0);
// If the part size is less than 64Kb we won't enable split archives
if ($partSize < 65536)
{
return;
}
$extension = $this->getExtension();
$altExtension = substr($extension, 0, 2) . '01';
$archiveTypeUppercase = strtoupper(substr($extension, 1));
Factory::getLog()->info(__CLASS__ . " :: Split $archiveTypeUppercase creation enabled");
$this->useSplitArchive = true;
$this->partSize = $partSize;
$this->dataFileNameWithoutExtension =
dirname($this->_dataFileName) . '/' . basename($this->_dataFileName, $extension);
$this->_dataFileName = $this->dataFileNameWithoutExtension . $altExtension;
// Indicate that we have at least 1 part
$statistics = Factory::getStatistics();
$statistics->updateMultipart(1);
}
/**
* Write a file's GZip compressed data to the archive, taking into account archive splitting
*
* @param string $zdata The compressed data to write to the archive
*
* @return void
*/
protected function putRawDataIntoArchive(&$zdata)
{
// Single part archive. Just dump the compressed data.
if (!$this->useSplitArchive)
{
$this->fwrite($this->fp, $zdata);
return;
}
// Split JPA. Check if we need to split the part in the middle of the data.
$freeSpaceInPart = $this->getPartFreeSize();
// Nope. We have enough space to write all of the data in this part.
if ($freeSpaceInPart >= akstrlen($zdata))
{
$this->fwrite($this->fp, $zdata);
return;
}
$bytesLeftInData = akstrlen($zdata);
while ($bytesLeftInData > 0)
{
// Try to write to the archive. We can only write as much bytes as the free space in the backup archive OR
// the total data bytes left, whichever is lower.
$bytesWritten = $this->fwrite($this->fp, $zdata, min($bytesLeftInData, $freeSpaceInPart));
// Since we may have written fewer bytes than anticipated we use the real bytes written for calculations
$freeSpaceInPart -= $bytesWritten;
$bytesLeftInData -= $bytesWritten;
// If we still have data to write, remove the part already written and keep the rest
if ($bytesLeftInData > 0)
{
$zdata = aksubstr($zdata, -$bytesLeftInData);
}
// If the part file is full create a new one
if ($freeSpaceInPart <= 0)
{
// Create new part
$this->createAndOpenNewPart();
// Get its free space
$freeSpaceInPart = $this->getPartFreeSize();
}
}
// Tell PHP to free up some memory
$zdata = null;
}
/**
* Begin or resume adding an uncompressed file into the archive.
*
* IMPORTANT! Only this case can be spanned across steps: uncompressed, non-virtual data
*
* @param string $sourceNameOrData The path to the file we are reading from.
* @param int $fileLength The file size we are supposed to read, in bytes.
* @param int $resumeOffset Offset in the file to resume reading from
*
* @return bool True to indicate more processing is required in the next step
*/
protected function putUncompressedFileIntoArchive(&$sourceNameOrData, $fileLength = 0, $resumeOffset = null)
{
// Copy the file contents, ignore directories
$sourceFilePointer = @fopen($sourceNameOrData, "r");
if ($sourceFilePointer === false)
{
// If we have already written the file header and can't read the data your archive is busted.
throw new ErrorException('Unreadable file ' . $sourceNameOrData . '. Check permissions. Your archive is corrupt!');
}
// Seek to the resume point if required
if (!is_null($resumeOffset))
{
// Seek to new offset
$seek_result = @fseek($sourceFilePointer, $resumeOffset);
if ($seek_result === -1)
{
// What?! We can't resume!
$this->conditionalFileClose($sourceFilePointer);
throw new ErrorException(sprintf('Could not resume packing of file %s. Your archive is damaged!', $sourceNameOrData));
}
// Change the uncompressed size to reflect the remaining data
$fileLength -= $resumeOffset;
}
$mustBreak = $this->putDataFromFileIntoArchive($sourceFilePointer, $fileLength);
$this->conditionalFileClose($sourceFilePointer);
return $mustBreak;
}
/**
* Return the requested permissions for the backup archive file.
*
* @return int
* @since 8.0.0
*/
protected function getPermissions(): int
{
if (!is_null($this->permissions))
{
return $this->permissions;
}
$configuration = Factory::getConfiguration();
$permissions = $configuration->get('engine.archiver.common.permissions', '0666') ?: '0666';
$this->permissions = octdec($permissions);
return $this->permissions;
}
/**
* Put up to $fileLength bytes of the file pointer $sourceFilePointer into the backup archive. Returns true if we
* ran out of time and need to perform a step break. Returns false when the whole quantity of data has been copied.
* Throws an ErrorException if soemthing really bad happens.
*
* @param resource $sourceFilePointer The pointer to the input file
* @param int $fileLength How many bytes to copy
*
* @return bool True to indicate we need to resume packing the file in the next step
*/
private function putDataFromFileIntoArchive(&$sourceFilePointer, &$fileLength)
{
// Get references to engine objects we're going to be using
$configuration = Factory::getConfiguration();
$timer = Factory::getTimer();
// Quick copy data into the archive, AKEEBA_CHUNK bytes at a time
while (!feof($sourceFilePointer) && ($timer->getTimeLeft() > 0) && ($fileLength > 0))
{
// Normally I read up to AKEEBA_CHUNK bytes at a time
$chunkSize = AKEEBA_CHUNK;
// Do I have a split ZIP?
if ($this->useSplitArchive)
{
// I must only read up to the free space in the part file if it's less than AKEEBA_CHUNK.
$free_space = $this->getPartFreeSize();
$chunkSize = min($free_space, AKEEBA_CHUNK);
// If I ran out of free space I have to create a new part file.
if ($free_space <= 0)
{
$this->createAndOpenNewPart();
// We have created the part. If the user asked for immediate post-proc, break step now.
if ($configuration->get('engine.postproc.common.after_part', 0))
{
$resumeOffset = @ftell($sourceFilePointer);
$this->conditionalFileClose($sourceFilePointer);
$configuration->set('volatile.engine.archiver.resume', $resumeOffset);
$configuration->set('volatile.engine.archiver.processingfile', true);
$configuration->set('volatile.breakflag', true);
// Always close the open part when immediate post-processing is requested
@$this->fclose($this->fp);
$this->fp = null;
return true;
}
// No immediate post-proc. Recalculate the optimal chunk size.
$free_space = $this->getPartFreeSize();
$chunkSize = min($free_space, AKEEBA_CHUNK);
}
}
// Read some data and write it to the backup archive part file
$data = fread($sourceFilePointer, $chunkSize);
$bytesWritten = $this->fwrite($this->fp, $data, akstrlen($data));
// Subtract the written bytes from the bytes left to write
$fileLength -= $bytesWritten;
}
/**
* According to the file size we read when we were writing the file header we have more data to write. However,
* we reached the end of the file. This means the file went away or shrunk. We can't reliably go back and
* change the file header since it may be in a previous part file that's already been post-processed. All we can
* do is try to warn the user.
*/
while (feof($sourceFilePointer) && ($timer->getTimeLeft() > 0) && ($fileLength > 0))
{
throw new ErrorException('The file shrunk or went away while putting it in the backup archive. Your archive is damaged! If this is a temporary or cache file we advise you to exclude the contents of the temporary / cache folder it is contained in.');
}
// WARNING!!! The extra $unc_len != 0 check is necessary as PHP won't reach EOF for 0-byte files.
if (!feof($sourceFilePointer) && ($fileLength != 0))
{
// We have to break, or we'll time out!
$resumeOffset = @ftell($sourceFilePointer);
$this->conditionalFileClose($sourceFilePointer);
$configuration->set('volatile.engine.archiver.resume', $resumeOffset);
$configuration->set('volatile.engine.archiver.processingfile', true);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,235 @@
<?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\Archiver;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Factory;
if (!function_exists('akstrlen'))
{
/**
* Attempt to use mbstring for calculating the binary string length.
*
* @param $string
*
* @return int
*/
function akstrlen($string)
{
return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string);
}
}
/**
* Abstract class for an archiver using managed file pointers
*/
abstract class BaseFileManagement extends Base
{
/** @var resource File pointer to the archive's central directory file (for ZIP) */
protected $cdfp = null;
/** @var resource File pointer to the archive being currently written to */
protected $fp = null;
/** @var array An array of the last open files for writing and their last written to offsets */
private $fileOffsets = [];
/** @var array An array of open file pointers */
private $filePointers = [];
/** @var null|string The last filename fwrite() wrote to */
private $lastFileName = null;
/** @var null|resource The last file pointer fwrite() wrote to */
private $lastFilePointer = null;
/**
* Release file pointers when the object is being destroyed
*
* @codeCoverageIgnore
*
* @return void
*/
public function __destruct()
{
$this->_closeAllFiles();
$this->fp = null;
$this->cdfp = null;
}
/**
* Release file pointers when the object is being serialized
*
* @codeCoverageIgnore
*
* @return void
*/
public function _onSerialize()
{
$this->_closeAllFiles();
$this->fp = null;
$this->cdfp = null;
}
/**
* Closes all open files known to this archiver object
*
* @return void
*/
protected function _closeAllFiles()
{
if (!empty($this->filePointers))
{
foreach ($this->filePointers as $file => $fp)
{
$this->conditionalFileClose($fp);
unset($this->filePointers[$file]);
}
}
}
/**
* Closes an already open file
*
* @param resource $fp The file pointer to close
*
* @return boolean
*/
protected function fclose(&$fp)
{
$result = true;
$offset = array_search($fp, $this->filePointers, true);
if (!is_null($fp) && is_resource($fp))
{
$result = $this->conditionalFileClose($fp);
}
if ($offset !== false)
{
unset($this->filePointers[$offset]);
}
$fp = null;
return $result;
}
protected function fcloseByName($file)
{
if (!array_key_exists($file, $this->filePointers))
{
return true;
}
$ret = $this->fclose($this->filePointers[$file]);
if (array_key_exists($file, $this->filePointers))
{
unset($this->filePointers[$file]);
}
return $ret;
}
/**
* Opens a file, if it's not already open, or returns its cached file pointer if it's already open
*
* @param string $file The filename to open
* @param string $mode File open mode, defaults to binary write
*
* @return resource
*/
protected function fopen($file, $mode = 'w')
{
if (!array_key_exists($file, $this->filePointers))
{
//Factory::getLog()->debug("Opening backup archive $file with mode $mode");
$this->filePointers[$file] = @fopen($file, $mode);
// If we open a file for append we have to seek to the correct offset
if (substr($mode, 0, 1) == 'a')
{
if (isset($this->fileOffsets[$file]))
{
Factory::getLog()->debug("Truncating backup archive file $file to " . $this->fileOffsets[$file] . " bytes");
@ftruncate($this->filePointers[$file], $this->fileOffsets[$file]);
}
fseek($this->filePointers[$file], 0, SEEK_END);
}
}
return $this->filePointers[$file];
}
/**
* Write to file, defeating magic_quotes_runtime settings (pure binary write)
*
* @param resource $fp Handle to a file
* @param string $data The data to write to the file
* @param integer $p_len Maximum length of data to write
*
* @return int The number of bytes written
*
* @throws ErrorException When writing to the file is not possible
*/
protected function fwrite($fp, $data, $p_len = null)
{
if ($fp !== $this->lastFilePointer)
{
$this->lastFilePointer = $fp;
$this->lastFileName = array_search($fp, $this->filePointers, true);
}
$len = is_null($p_len) ? (akstrlen($data)) : $p_len;
$ret = fwrite($fp, $data, $len);
if (($ret === false) || (abs(($ret - $len)) >= 1))
{
// Log debug information about the archive file's existence and current size. This helps us figure out if
// there is a server-imposed maximum file size limit.
clearstatcache();
$fileExists = @file_exists($this->lastFileName) ? 'exists' : 'does NOT exist';
$currentSize = @filesize($this->lastFileName);
Factory::getLog()->debug(sprintf("%s::_fwrite() ERROR!! Cannot write to archive file %s. The file %s. File size %s bytes after writing %s of %d bytes. Please check the output directory permissions and make sure you have enough disk space available. If this does not help, please set up a Part Size for Split Archives LOWER than this size and retry backing up.", __CLASS__, $this->lastFileName, $fileExists, $currentSize, $ret, $len));
throw new ErrorException(sprintf("Couldn\'t write to the archive file; check the output directory permissions and make sure you have enough disk space available. [len=%s / %s]", $ret, $len));
}
if ($this->lastFileName !== false)
{
$this->fileOffsets[$this->lastFileName] = @ftell($fp);
}
return $ret;
}
/**
* Removes a file path from the list of resumable offsets
*
* @param $filename
*/
protected function removeFromOffsetsList($filename)
{
if (isset($this->fileOffsets[$filename]))
{
unset($this->fileOffsets[$filename]);
}
}
}

View File

@@ -0,0 +1,540 @@
<?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\Archiver;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Factory;
use RuntimeException;
/**
* JPA creation class
*
* JPA Format 1.2 implemented, minus BZip2 compression support
*/
class Jpa extends BaseArchiver
{
/** @var integer How many files are contained in the archive */
private $totalFilesCount = 0;
/** @var integer The total size of files contained in the archive as they are stored */
private $totalCompressedSize = 0;
/** @var integer The total size of files contained in the archive when they are extracted to disk. */
private $totalUncompressedSize = 0;
/** @var string Standard Header signature */
private $archiveSignature = "\x4A\x50\x41";
/** @var string Entity Block signature */
private $fileHeaderSignature = "\x4A\x50\x46";
/** @var string Marks the split archive's extra header */
private $splitArchiveExtraHeader = "\x4A\x50\x01\x01"; //
/** @var int Current part file number */
private $currentPartNumber = 1;
/** @var int Total number of part files */
private $totalParts = 1;
/**
* Initialises the archiver class, creating the archive from an existent
* installer's JPA archive.
*
* @param string $targetArchivePath Absolute path to the generated archive
* @param array $options A named key array of options (optional)
*
* @return void
*/
public function initialize($targetArchivePath, $options = [])
{
Factory::getLog()->debug(__CLASS__ . " :: new instance - archive $targetArchivePath");
$this->_dataFileName = $targetArchivePath;
// Should we enable Split ZIP feature?
$this->enableSplitArchives();
// Should I use Symlink Target Storage?
$this->enableSymlinkTargetStorage();
// Try to kill the archive if it exists
$this->createNewBackupArchive();
// Write the initial instance of the archive header
$this->_writeArchiveHeader();
}
/**
* Updates the Standard Header with current information
*
* @return void
*/
public function finalize()
{
if (is_resource($this->fp))
{
$this->fclose($this->fp);
}
if (is_resource($this->cdfp))
{
$this->fclose($this->cdfp);
}
$this->_closeAllFiles();
// If Spanned JPA and there is no .jpa file, rename the last fragment to .jpa
if ($this->useSplitArchive)
{
$extension = substr($this->_dataFileName, -4);
if ($extension != '.jpa')
{
Factory::getLog()->debug('Renaming last JPA part to .JPA extension');
$newName = $this->dataFileNameWithoutExtension . '.jpa';
if (!@rename($this->_dataFileName, $newName))
{
throw new RuntimeException('Could not rename last JPA part to .JPA extension.');
}
$this->_dataFileName = $newName;
}
// Finally, point to the first part so that we can re-write the correct header information
if ($this->totalParts > 1)
{
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.j01';
}
}
// Re-write the archive header
$this->_writeArchiveHeader();
}
/**
* Returns a string with the extension (including the dot) of the files produced
* by this class.
*
* @return string
*/
public function getExtension()
{
return '.jpa';
}
/**
* Outputs a Standard Header at the top of the file
*
* @return void
*/
protected function _writeArchiveHeader()
{
if (!is_null($this->fp))
{
$this->fclose($this->fp);
$this->fp = null;
}
$this->fp = $this->fopen($this->_dataFileName, 'c');
if ($this->fp === false)
{
throw new ErrorException('Could not open ' . $this->_dataFileName . ' for writing. Check permissions and open_basedir restrictions.');
}
// Calculate total header size
$headerSize = 19; // Standard Header
if ($this->useSplitArchive)
{
// Spanned JPA header
$headerSize += 8;
}
$this->fwrite($this->fp, $this->archiveSignature); // ID string (JPA)
$this->fwrite($this->fp, pack('v', $headerSize)); // Header length; fixed to 19 bytes
$this->fwrite($this->fp, pack('C', _JPA_MAJOR)); // Major version
$this->fwrite($this->fp, pack('C', _JPA_MINOR)); // Minor version
$this->fwrite($this->fp, pack('V', $this->totalFilesCount)); // File count
$this->fwrite($this->fp, pack('V', $this->totalUncompressedSize)); // Size of files when extracted
$this->fwrite($this->fp, pack('V', $this->totalCompressedSize)); // Size of files when stored
// Do I need to add a split archive's header too?
if ($this->useSplitArchive)
{
$this->fwrite($this->fp, $this->splitArchiveExtraHeader); // Signature
$this->fwrite($this->fp, pack('v', 4)); // Extra field length
$this->fwrite($this->fp, pack('v', $this->totalParts)); // Number of parts
}
$this->fclose($this->fp);
@chmod($this->_dataFileName, $this->getPermissions());
}
/**
* Extend the bootstrap code to add some define's used by the JPA format engine
*
* @codeCoverageIgnore
*
* @return void
*/
protected function __bootstrap_code()
{
if (!defined('_AKEEBA_COMPRESSION_THRESHOLD'))
{
$config = Factory::getConfiguration();
define("_AKEEBA_COMPRESSION_THRESHOLD", $config->get('engine.archiver.common.big_file_threshold')); // Don't compress files over this size
/**
* Akeeba Backup and JPA Format version change chart:
* Akeeba Backup 3.0: JPA Format 1.1 is used
* Akeeba Backup 3.1: JPA Format 1.2 with file modification timestamp is used
*/
define('_JPA_MAJOR', 1); // JPA Format major version number
define('_JPA_MINOR', 2); // JPA Format minor version number
}
parent::__bootstrap_code();
}
/**
* The most basic file transaction: add a single entry (file or directory) to
* the archive.
*
* @param bool $isVirtual If true, the next parameter contains file data instead of a file name
* @param string $sourceNameOrData Absolute file name to read data from or the file data itself is $isVirtual is
* true
* @param string $targetName The (relative) file name under which to store the file in the archive
*
* @return boolean True on success, false otherwise
*
* @since 1.2.1
*/
protected function _addFile($isVirtual, &$sourceNameOrData, $targetName)
{
// Get references to engine objects we're going to be using
$configuration = Factory::getConfiguration();
// Is this a virtual file?
$isVirtual = (bool) $isVirtual;
// Open data file for output
$this->openArchiveForOutput();
// Should I continue backing up a file from the previous step?
$continueProcessingFile = $configuration->get('volatile.engine.archiver.processingfile', false);
// Initialize with the default values. Why are *these* values default? If we are continuing file packing, by
// definition we have an uncompressed, non-virtual file. Hence the default values.
$isDir = false;
$isSymlink = false;
$compressionMethod = 0;
$zdata = null;
// If we are continuing file packing we have an uncompressed, non-virtual file.
$isVirtual = $continueProcessingFile ? false : $isVirtual;
$resume = $continueProcessingFile ? 0 : null;
if (!$continueProcessingFile)
{
// Log the file being added
$messageSource = $isVirtual ? '(virtual data)' : "(source: $sourceNameOrData)";
Factory::getLog()->debug("-- Adding $targetName to archive $messageSource");
// Write a file header
$this->writeFileHeader($sourceNameOrData, $targetName, $isVirtual, $isSymlink, $isDir, $compressionMethod, $zdata, $unc_len);
}
else
{
$sourceNameOrData = $configuration->get('volatile.engine.archiver.sourceNameOrData', '');
$unc_len = $configuration->get('volatile.engine.archiver.unc_len', 0);
$resume = $configuration->get('volatile.engine.archiver.resume', 0);
// Log the file we continue packing
Factory::getLog()->debug("-- Resuming adding file $sourceNameOrData to archive from position $resume (total size $unc_len)");
}
/* "File data" segment. */
if ($compressionMethod == 1)
{
// Compressed data. Put into the archive.
$this->putRawDataIntoArchive($zdata);
}
elseif ($isVirtual)
{
// Virtual data. Put into the archive.
$this->putRawDataIntoArchive($sourceNameOrData);
}
elseif ($isSymlink)
{
// Symlink. Just put the link target into the archive.
$this->fwrite($this->fp, @readlink($sourceNameOrData));
}
elseif ((!$isDir) && (!$isSymlink))
{
// Uncompressed file.
if ($this->putUncompressedFileIntoArchive($sourceNameOrData, $unc_len, $resume) === true)
{
// If it returns true we are doing a step break to resume packing in the next step. So we need to return
// true here to avoid running the final bit of code which uncaches the file resume data.
return true;
}
}
// Factory::getLog()->debug("DEBUG -- Added $targetName to archive");
// Uncache data
$configuration->set('volatile.engine.archiver.sourceNameOrData', null);
$configuration->set('volatile.engine.archiver.unc_len', null);
$configuration->set('volatile.engine.archiver.resume', null);
$configuration->set('volatile.engine.archiver.processingfile', false);
// ... and return TRUE = success
return true;
}
/**
* Write the file header to the backup archive.
*
* Only the first three parameters are input. All other are ignored for input and are overwritten.
*
* @param string $sourceNameOrData The path to the file being compressed, or the raw file data for virtual files
* @param string $targetName The target path to be stored inside the archive
* @param bool $isVirtual Is this a virtual file?
* @param bool $isSymlink Is this a symlink?
* @param bool $isDir Is this a directory?
* @param int $compressionMethod The compression method chosen for this file
* @param string $zdata If we have compression method other than 0 this holds the compressed data.
* We return that from this method to avoid having to compress the same data
* twice (once to write the compressed data length in the header and once to
* write the compressed data to the archive).
* @param int $unc_len The uncompressed size of the file / source data
*
* @return void
*/
protected function writeFileHeader(&$sourceNameOrData, &$targetName, &$isVirtual, &$isSymlink, &$isDir, &$compressionMethod, &$zdata, &$unc_len)
{
static $memLimit = null;
if (is_null($memLimit))
{
$memLimit = $this->getMemoryLimit();
}
$configuration = Factory::getConfiguration();
// Uncache data -- WHY DO THAT?!
/**
* $configuration->set('volatile.engine.archiver.sourceNameOrData', null);
* $configuration->set('volatile.engine.archiver.unc_len', null);
* $configuration->set('volatile.engine.archiver.resume', null);
* $configuration->set('volatile.engine.archiver.processingfile',false);
* /**/
// See if it's a directory
$isDir = $isVirtual ? false : is_dir($sourceNameOrData);
// See if it's a symlink (w/out dereference)
$isSymlink = false;
if ($this->storeSymlinkTarget && !$isVirtual)
{
$isSymlink = is_link($sourceNameOrData);
}
// Get real size before compression
[$fileSize, $fileModTime] =
$this->getFileSizeAndModificationTime($sourceNameOrData, $isVirtual, $isSymlink, $isDir);
// Decide if we will compress
$compressionMethod = $this->getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink);
$storedName = $targetName;
/* "Entity Description Block" segment. */
$unc_len = $fileSize; // File size
$storedName .= ($isDir) ? "/" : "";
/**
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* !!!! WARNING!!! DO NOT MOVE THIS BLOCK OF CODE AFTER THE testIfFileExists OR getZData!!!! !!!!
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*
* PHP 5.6.3 IS BROKEN. Possibly the same applies for all old versions of PHP. If you try to get the file
* permissions after reading its contents PHP segfaults.
*/
// Get file permissions
$perms = 0644;
if (!$isVirtual)
{
$perms = @fileperms($sourceNameOrData);
}
// Test for non-existing or unreadable files
$this->testIfFileExists($sourceNameOrData, $isVirtual, $isDir, $isSymlink);
// Default compressed (archived) length = uncompressed length valid unless we can actually compress the data.
$c_len = $unc_len;
if ($compressionMethod == 1)
{
$this->getZData($sourceNameOrData, $isVirtual, $compressionMethod, $zdata, $unc_len, $c_len);
}
$this->totalCompressedSize += $c_len; // Update global data
$this->totalUncompressedSize += $fileSize; // Update global data
$this->totalFilesCount++;
// Calculate Entity Description Block length
$blockLength = 21 + akstrlen($storedName);
// If we need to store the file mod date
if ($fileModTime > 0)
{
$blockLength += 8;
}
// Get file type
$fileType = 1;
if ($isSymlink)
{
$fileType = 2;
}
elseif ($isDir)
{
$fileType = 0;
}
// If it's a split JPA file, we've got to make sure that the header can fit in the part
if ($this->useSplitArchive)
{
// Compare to free part space
$free_space = $this->getPartFreeSize();
if ($free_space <= $blockLength)
{
// Not enough space on current part, create new part
$this->createAndOpenNewPart();
}
}
$this->fwrite($this->fp, $this->fileHeaderSignature); // Entity Description Block header
$this->fwrite($this->fp, pack('v', $blockLength)); // Entity Description Block header length
$this->fwrite($this->fp, pack('v', akstrlen($storedName))); // Length of entity path
$this->fwrite($this->fp, $storedName); // Entity path
$this->fwrite($this->fp, pack('C', $fileType)); // Entity type
$this->fwrite($this->fp, pack('C', $compressionMethod)); // Compression method
$this->fwrite($this->fp, pack('V', $c_len)); // Compressed size
$this->fwrite($this->fp, pack('V', $unc_len)); // Uncompressed size
$this->fwrite($this->fp, pack('V', $perms)); // Entity permissions
// Timestamp Extra Field, only for files
if ($fileModTime > 0)
{
$this->fwrite($this->fp, "\x00\x01"); // Extra Field Identifier
$this->fwrite($this->fp, pack('v', 8)); // Extra Field Length
$this->fwrite($this->fp, pack('V', $fileModTime)); // Timestamp
}
// Cache useful information about the file
if (!$isDir && !$isSymlink && !$isVirtual)
{
$configuration->set('volatile.engine.archiver.unc_len', $unc_len);
$configuration->set('volatile.engine.archiver.sourceNameOrData', $sourceNameOrData);
}
}
/**
* Creates a new part for the spanned archive
*
* @param bool $finalPart Is this the final archive part?
*
* @return bool True on success
*/
protected function createNewPartFile($finalPart = false)
{
// Close any open file pointers
if (!is_resource($this->fp))
{
$this->fclose($this->fp);
}
if (is_resource($this->cdfp))
{
$this->fclose($this->cdfp);
}
// Remove the just finished part from the list of resumable offsets
$this->removeFromOffsetsList($this->_dataFileName);
// Set the file pointers to null
$this->fp = null;
$this->cdfp = null;
// Push the previous part if we have to post-process it immediately
$configuration = Factory::getConfiguration();
if ($configuration->get('engine.postproc.common.after_part', 0))
{
// The first part needs its header overwritten during archive
// finalization. Skip it from immediate processing.
if ($this->currentPartNumber != 1)
{
$this->finishedPart[] = $this->_dataFileName;
}
}
$this->totalParts++;
$this->currentPartNumber = $this->totalParts;
if ($finalPart)
{
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.jpa';
}
else
{
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.j' . sprintf('%02d', $this->currentPartNumber);
}
Factory::getLog()->info('Creating new JPA part #' . $this->currentPartNumber . ', file ' . $this->_dataFileName);
$statistics = Factory::getStatistics();
$statistics->updateMultipart($this->totalParts);
// Try to remove any existing file
@unlink($this->_dataFileName);
// Touch the new file
$result = @touch($this->_dataFileName);
chmod($this->_dataFileName, $this->getPermissions());
// Try to write 6 bytes to it
if ($result)
{
$result = @file_put_contents($this->_dataFileName, 'AKEEBA') == 6;
}
if ($result)
{
@unlink($this->_dataFileName);
$result = @touch($this->_dataFileName);
@chmod($this->_dataFileName, $this->getPermissions());
}
return $result;
}
}

View File

@@ -0,0 +1,913 @@
<?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\Archiver;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Base\Exceptions\WarningException;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Util\CRC32;
use RuntimeException;
class Zip extends BaseArchiver
{
/** @var string Beginning of central directory record. */
private $centralDirectoryRecordStartSignature = "\x50\x4b\x01\x02";
/** @var string End of central directory record. */
private $centralDirectoryRecordEndSignature = "\x50\x4b\x05\x06";
/** @var string Beginning of file contents. */
private $fileHeaderSignature = "\x50\x4b\x03\x04";
/** @var string The name of the temporary file holding the ZIP's Central Directory */
private $centralDirectoryFilename;
/** @var integer The total number of files and directories stored in the ZIP archive */
private $totalFilesCount;
/** @var integer The total size of data in the archive. Note: On 32-bit versions of PHP, this will overflow for archives over 2Gb! */
private $totalCompressedSize = 0;
/** @var integer The chunk size for CRC32 calculations */
private $AkeebaPackerZIP_CHUNK_SIZE;
/** @var int Current part file number */
private $currentPartNumber = 1;
/** @var int Total number of part files */
private $totalParts = 1;
/** @var CRC32 The CRC32 calculations object */
private $crcCalculator = null;
/**
* Class constructor - initializes internal operating parameters
*
* @return void
*/
public function __construct()
{
Factory::getLog()->debug(__CLASS__ . " :: New instance");
// Find the optimal chunk size for ZIP archive processing
$this->findOptimalChunkSize();
Factory::getLog()->debug("Chunk size for CRC is now " . $this->AkeebaPackerZIP_CHUNK_SIZE . " bytes");
// Should I use Symlink Target Storage?
$this->enableSymlinkTargetStorage();
parent::__construct();
}
/**
* Initialises the archiver class, creating the archive from an existent
* installer's JPA archive.
*
* @param string $sourceJPAPath Absolute path to an installer's JPA archive
* @param string $targetArchivePath Absolute path to the generated archive
* @param array $options A named key array of options (optional). This is currently not supported
*
* @return void
*/
public function initialize($targetArchivePath, $options = [])
{
Factory::getLog()->debug(__CLASS__ . " :: initialize - archive $targetArchivePath");
// Get names of temporary files
$this->_dataFileName = $targetArchivePath;
// Should we enable split archive feature?
$this->enableSplitArchives();
// Create the Central Directory temporary file
$this->createCentralDirectoryTempFile();
// Try to kill the archive if it exists
$this->createNewBackupArchive();
// On split archives, include the "Split ZIP" header, for PKZIP 2.50+ compatibility
if ($this->useSplitArchive)
{
$this->openArchiveForOutput();
$this->fwrite($this->fp, "\x50\x4b\x07\x08");
}
}
public function finalize()
{
$this->finalizeZIPFile();
}
/**
* Glues the Central Directory of the ZIP file to the archive and takes care about the differences between single
* and multipart archives.
*
* Official ZIP file format: http://www.pkware.com/appnote.txt
*
* @return void
*/
public function finalizeZIPFile()
{
// 1. Get size of central directory
clearstatcache();
$cdOffset = @filesize($this->_dataFileName);
$this->totalCompressedSize += $cdOffset;
$cdSize = @filesize($this->centralDirectoryFilename);
// 2. Append Central Directory to data file and remove the CD temp file afterwards
if (!is_null($this->fp))
{
$this->fclose($this->fp);
}
if (!is_null($this->cdfp))
{
$this->fclose($this->cdfp);
}
$this->openArchiveForOutput(true);
/**
* Do not remove the fcloseByName line! This is required for post-processing multipart archives when for any
* reason $this->filePointers[$this->centralDirectoryFilename] contains null instead of boolean false. In this
* case the while loop would be stuck forever and the backup would fail. This HAS happened and I have been able
* to reproduce it but I did not have enough time to identify the real root cause. This workaround, however,
* works.
*/
$this->fcloseByName($this->centralDirectoryFilename);
$this->cdfp = $this->fopen($this->centralDirectoryFilename, "r");
if ($this->cdfp === false)
{
// Already glued, return
$this->fclose($this->fp);
$this->fp = null;
$this->cdfp = null;
return;
}
// Comment length (I need it before I start gluing the archive)
$comment_length = akstrlen($this->_comment);
// Special consideration for split ZIP files
if ($this->useSplitArchive)
{
// Calculate size of Central Directory + EOCD records
$total_cd_eocd_size = $cdSize + 22 + $comment_length;
// Free space on the part
$free_space = $this->getPartFreeSize();
if (($free_space < $total_cd_eocd_size) && ($total_cd_eocd_size > 65536))
{
// Not enough space on archive for CD + EOCD, will go on separate part
$this->createAndOpenNewPart(true);
}
}
/**
* Write the CD record
*
* Note about is_resource: in some circumstances where multipart ZIP files are generated, the $this->cdfp will
* contain a null value. This seems to happen when $this->fopen returns null, i.e. $this->filePointers has a
* null value instead of a file pointer (resource). Why this happens is unclear but the workaround is to remove
* the null value from $this->filePointers and retry $this->fopen. Normally this should not be required since we
* already to the fcloseByName/fopen dance above. This if-block is our last hope to catch a potential issue
* which would either make the while loop go infinite (not anymore, I've patched it) or the Central Directory
* not get written to the archive, which results in a broken archive.
*/
if (!is_resource($this->cdfp))
{
$this->fcloseByName($this->centralDirectoryFilename);
$this->cdfp = $this->fopen($this->centralDirectoryFilename, "r");
// We tried reopening the central directory file and failed again. Time to report a fatal error.
if (!$this->cdfp)
{
throw new RuntimeException("Cannot open central directory temporary file {$this->centralDirectoryFilename} for reading.");
}
}
while (!feof($this->cdfp) && is_resource($this->cdfp))
{
/**
* Why not split the Central Directory between parts?
*
* APPNOTE.TXT §8.5.2 "The central directory may span segment boundaries, but no single record in the
* central directory should be split across segments."
*
* This would require parsing the CD temp file to prevent any CD record from spanning across two parts.
* But how many bytes is each CD record? It's about 100 bytes per file which gives us about 10,400 files
* per MB. Even a 2MB part size holds more than 20,000 file records. A typical 10Mb part size holds more
* files than the largest backup I've ever seen. Therefore there is no need to waste computational power
* to see if we need to span the Central Directory between parts.
*/
$chunk = fread($this->cdfp, _AKEEBA_DIRECTORY_READ_CHUNK);
$this->fwrite($this->fp, $chunk);
}
unset($chunk);
// Delete the temporary CD file
$this->fclose($this->cdfp);
$this->cdfp = null;
Factory::getTempFiles()->unregisterAndDeleteTempFile($this->centralDirectoryFilename);
// 3. Write the rest of headers to the end of the ZIP file
$this->fwrite($this->fp, $this->centralDirectoryRecordEndSignature);
if ($this->useSplitArchive)
{
// Split ZIP files, enter relevant disk number information
$this->fwrite($this->fp, pack('v', $this->totalParts - 1)); /* Number of this disk. */
$this->fwrite($this->fp, pack('v', $this->totalParts - 1)); /* Disk with central directory start. */
}
else
{
// Non-split ZIP files, the disk number MUST be 0
$this->fwrite($this->fp, pack('V', 0));
}
$this->fwrite($this->fp, pack('v', $this->totalFilesCount)); /* Total # of entries "on this disk". */
$this->fwrite($this->fp, pack('v', $this->totalFilesCount)); /* Total # of entries overall. */
$this->fwrite($this->fp, pack('V', $cdSize)); /* Size of central directory. */
$this->fwrite($this->fp, pack('V', $cdOffset)); /* Offset to start of central dir. */
// 2.0.b2 -- Write a ZIP file comment
$this->fwrite($this->fp, pack('v', $comment_length)); /* ZIP file comment length. */
$this->fwrite($this->fp, $this->_comment);
$this->fclose($this->fp);
// If Split ZIP and there is no .zip file, rename the last fragment to .ZIP
if ($this->useSplitArchive)
{
$extension = substr($this->_dataFileName, -3);
if ($extension != '.zip')
{
Factory::getLog()->debug('Renaming last ZIP part to .ZIP extension');
$newName = $this->dataFileNameWithoutExtension . '.zip';
if (!@rename($this->_dataFileName, $newName))
{
throw new RuntimeException('Could not rename last ZIP part to .ZIP extension.');
}
$this->_dataFileName = $newName;
}
// If Split ZIP and only one fragment, change the signature
if ($this->totalParts == 1)
{
$this->fp = $this->fopen($this->_dataFileName, 'r+');
$this->fwrite($this->fp, "\x50\x4b\x30\x30");
}
}
@chmod($this->_dataFileName, $this->getPermissions());
}
/**
* Returns a string with the extension (including the dot) of the files produced
* by this class.
*
* @return string
*/
public function getExtension()
{
return '.zip';
}
/**
* Extend the bootstrap code to add some define's used by the ZIP format engine
*
* @return void
*/
protected function __bootstrap_code()
{
if (!defined('_AKEEBA_COMPRESSION_THRESHOLD'))
{
$config = Factory::getConfiguration();
define("_AKEEBA_COMPRESSION_THRESHOLD", $config->get('engine.archiver.common.big_file_threshold')); // Don't compress files over this size
define("_AKEEBA_DIRECTORY_READ_CHUNK", $config->get('engine.archiver.zip.cd_glue_chunk_size')); // How much data to read at once when finalizing ZIP archives
}
$this->crcCalculator = Factory::getCRC32Calculator();
parent::__bootstrap_code();
}
/**
* The most basic file transaction: add a single entry (file or directory) to
* the archive.
*
* @param bool $isVirtual If true, the next parameter contains file data instead of a file name
* @param string $sourceNameOrData Absolute file name to read data from or the file data itself is $isVirtual is
* true
* @param string $targetName The (relative) file name under which to store the file in the archive
*
* @return bool True on success, false otherwise
*/
protected function _addFile($isVirtual, &$sourceNameOrData, $targetName)
{
$configuration = Factory::getConfiguration();
// Note down the starting disk number for Split ZIP archives
$starting_disk_number_for_this_file = 0;
if ($this->useSplitArchive)
{
$starting_disk_number_for_this_file = $this->currentPartNumber - 1;
}
// Open data file for output
$this->openArchiveForOutput();
// Should I continue backing up a file from the previous step?
$continueProcessingFile = $configuration->get('volatile.engine.archiver.processingfile', false);
// Initialize with the default values. Why are *these* values default? If we are continuing file packing, by
// definition we have an uncompressed, non-virtual file. Hence the default values.
$isDir = false;
$isSymlink = false;
$compressionMethod = 1;
$zdata = null;
// If we are continuing file packing we have an uncompressed, non-virtual file.
$isVirtual = $continueProcessingFile ? false : $isVirtual;
$resume = $continueProcessingFile ? 0 : null;
if (!$continueProcessingFile)
{
// Log the file being added
$messageSource = $isVirtual ? '(virtual data)' : "(source: $sourceNameOrData)";
Factory::getLog()->debug("-- Adding $targetName to archive $messageSource");
$this->writeFileHeader($sourceNameOrData, $targetName, $isVirtual, $isSymlink, $isDir,
$compressionMethod, $zdata, $unc_len,
$storedName, $crc, $c_len, $hexdtime, $old_offset);
}
else
{
// Since we are continuing archiving, it's an uncompressed regular file. Set up the variables.
$sourceNameOrData = $configuration->get('volatile.engine.archiver.sourceNameOrData', '');
$resume = $configuration->get('volatile.engine.archiver.resume', 0);
$unc_len = $configuration->get('volatile.engine.archiver.unc_len');
$storedName = $configuration->get('volatile.engine.archiver.storedName');
$crc = $configuration->get('volatile.engine.archiver.crc');
$c_len = $configuration->get('volatile.engine.archiver.c_len');
$hexdtime = $configuration->get('volatile.engine.archiver.hexdtime');
$old_offset = $configuration->get('volatile.engine.archiver.old_offset');
// Log the file we continue packing
Factory::getLog()->debug("-- Resuming adding file $sourceNameOrData to archive from position $resume (total size $unc_len)");
}
/* "File data" segment. */
if ($compressionMethod == 8)
{
$this->putRawDataIntoArchive($zdata);
}
elseif ($isVirtual)
{
// Virtual data. Put into the archive.
$this->putRawDataIntoArchive($sourceNameOrData);
}
elseif ($isSymlink)
{
$this->fwrite($this->fp, @readlink($sourceNameOrData));
}
elseif ((!$isDir) && (!$isSymlink))
{
// Uncompressed file.
if ($this->putUncompressedFileIntoArchive($sourceNameOrData, $unc_len, $resume) === true)
{
// If it returns true we are doing a step break to resume packing in the next step. So we need to return
// true here to avoid running the final bit of code which writes the central directory record and
// uncaches the file resume data.
return true;
}
}
// Open the central directory file for append
if (is_null($this->cdfp))
{
$this->cdfp = @$this->fopen($this->centralDirectoryFilename, "a");
}
if ($this->cdfp === false)
{
throw new ErrorException("Could not open Central Directory temporary file for append!");
}
$this->fwrite($this->cdfp, $this->centralDirectoryRecordStartSignature);
if (!$isSymlink)
{
$this->fwrite($this->cdfp, "\x14\x00"); /* Version made by (always set to 2.0). */
$this->fwrite($this->cdfp, "\x14\x00"); /* Version needed to extract */
$this->fwrite($this->cdfp, pack('v', 2048)); /* General purpose bit flag */
$this->fwrite($this->cdfp, ($compressionMethod == 8) ? "\x08\x00" : "\x00\x00"); /* Compression method. */
}
else
{
// Symlinks get special treatment
$this->fwrite($this->cdfp, "\x14\x03"); /* Version made by (version 2.0 with UNIX extensions). */
$this->fwrite($this->cdfp, "\x0a\x03"); /* Version needed to extract */
$this->fwrite($this->cdfp, pack('v', 2048)); /* General purpose bit flag */
$this->fwrite($this->cdfp, "\x00\x00"); /* Compression method. */
}
$this->fwrite($this->cdfp, $hexdtime); /* Last mod time/date. */
$this->fwrite($this->cdfp, pack('V', $crc)); /* CRC 32 information. */
$this->fwrite($this->cdfp, pack('V', $c_len)); /* Compressed filesize. */
if ($compressionMethod == 0)
{
// When we are not compressing, $unc_len is being reduced to 0 while backing up.
// With this trick, we always store the correct length, as in this case the compressed
// and uncompressed length is always the same.
$this->fwrite($this->cdfp, pack('V', $c_len)); /* Uncompressed filesize. */
}
else
{
// When compressing, the uncompressed length differs from compressed length
// and this line writes the correct value.
$this->fwrite($this->cdfp, pack('V', $unc_len)); /* Uncompressed filesize. */
}
$fn_length = akstrlen($storedName);
$this->fwrite($this->cdfp, pack('v', $fn_length)); /* Length of filename. */
$this->fwrite($this->cdfp, pack('v', 0)); /* Extra field length. */
$this->fwrite($this->cdfp, pack('v', 0)); /* File comment length. */
$this->fwrite($this->cdfp, pack('v', $starting_disk_number_for_this_file)); /* Disk number start. */
$this->fwrite($this->cdfp, pack('v', 0)); /* Internal file attributes. */
/* External file attributes */
if (!$isSymlink)
{
// Archive bit set
$this->fwrite($this->cdfp, pack('V', $isDir ? 0x41FF0010 : 0xFE49FFE0));
}
else
{
// For SymLinks we store UNIX file attributes
$this->fwrite($this->cdfp, "\x20\x80\xFF\xA1");
}
$this->fwrite($this->cdfp, pack('V', $old_offset)); /* Relative offset of local header. */
$this->fwrite($this->cdfp, $storedName); /* File name. */
/* Optional extra field, file comment goes here. */
// Finally, increase the file counter by one
$this->totalFilesCount++;
// Uncache data
$configuration->set('volatile.engine.archiver.sourceNameOrData', null);
$configuration->set('volatile.engine.archiver.unc_len', null);
$configuration->set('volatile.engine.archiver.resume', null);
$configuration->set('volatile.engine.archiver.hexdtime', null);
$configuration->set('volatile.engine.archiver.crc', null);
$configuration->set('volatile.engine.archiver.c_len', null);
$configuration->set('volatile.engine.archiver.fn_length', null);
$configuration->set('volatile.engine.archiver.old_offset', null);
$configuration->set('volatile.engine.archiver.storedName', null);
$configuration->set('volatile.engine.archiver.sourceNameOrData', null);
$configuration->set('volatile.engine.archiver.processingfile', false);
// ... and return TRUE = success
return true;
}
/**
* Write the file header before putting the file data into the archive
*
* @param string $sourceNameOrData The path to the file being compressed, or the raw file data for virtual files
* @param string $targetName The target path to be stored inside the archive
* @param bool $isVirtual Is this a virtual file?
* @param bool $isSymlink Is this a symlink?
* @param bool $isDir Is this a directory?
* @param int $compressionMethod The compression method chosen for this file
* @param string $zdata If we have compression method other than 0 this holds the compressed data.
* We return that from this method to avoid having to compress the same data
* twice (once to write the compressed data length in the header and once to
* write the compressed data to the archive).
* @param int $unc_len The uncompressed size of the file / source data
*
* @param string $storedName The file path stored in the archive
* @param string $crc CRC-32 for the file
* @param int $c_len Compressed data length
* @param string $hexdtime ZIP's hexadecimal notation if the file's modification date
* @param int $old_offset Offset of the file header in the part file
*/
protected function writeFileHeader(&$sourceNameOrData, $targetName, &$isVirtual, &$isSymlink, &$isDir,
&$compressionMethod, &$zdata, &$unc_len, &$storedName, &$crc, &$c_len,
&$hexdtime, &$old_offset)
{
static $memLimit = null;
if (is_null($memLimit))
{
$memLimit = $this->getMemoryLimit();
}
$configuration = Factory::getConfiguration();
// See if it's a directory
$isDir = $isVirtual ? false : is_dir($sourceNameOrData);
// See if it's a symlink (w/out dereference)
$isSymlink = false;
if ($this->storeSymlinkTarget && !$isVirtual)
{
$isSymlink = is_link($sourceNameOrData);
}
// Get real size before compression
[$unc_len, $fileModTime] =
$this->getFileSizeAndModificationTime($sourceNameOrData, $isVirtual, $isSymlink, $isDir);
// Decide if we will compress
$compressionMethod = $this->getCompressionMethod($unc_len, $memLimit, $isDir, $isSymlink);
if ($isVirtual)
{
Factory::getLog()->debug(' Virtual add:' . $targetName . ' (' . $unc_len . ') - ' . $compressionMethod);
}
/* "Local file header" segment. */
$crc = $this->getCRCForEntity($sourceNameOrData, $isVirtual, $isDir, $isSymlink);
$storedName = $targetName;
if (!$isSymlink && $isDir)
{
$storedName .= "/";
$unc_len = 0;
}
// Test for non-existing or unreadable files
$this->testIfFileExists($sourceNameOrData, $isVirtual, $isDir, $isSymlink);
// Default compressed (archived) length = uncompressed length valid unless we can actually compress the data.
$c_len = $unc_len;
// If we have to compress, read the data in memory and compress it
if ($compressionMethod == 8)
{
$this->getZData($sourceNameOrData, $isVirtual, $compressionMethod, $zdata, $unc_len, $c_len);
// The method modifies $compressionMethod to 0 (uncompressed) or 1 (Deflate) but the ZIP format needs it
// to be 0 (uncompressed) or 8 (Deflate). So I just multiply by 8.
$compressionMethod *= 8;
}
// Get the hex time.
$dtime = dechex($this->unix2DOSTime($fileModTime));
if (akstrlen($dtime) < 8)
{
$dtime = "00000000";
}
$hexdtime = chr(hexdec($dtime[6] . $dtime[7])) .
chr(hexdec($dtime[4] . $dtime[5])) .
chr(hexdec($dtime[2] . $dtime[3])) .
chr(hexdec($dtime[0] . $dtime[1]));
// If it's a split ZIP file, we've got to make sure that the header can fit in the part
if ($this->useSplitArchive)
{
// Get header size, taking into account any extra header necessary
$header_size = 30 + akstrlen($storedName);
// Compare to free part space
$free_space = $this->getPartFreeSize();
if ($free_space <= $header_size)
{
// Not enough space on current part, create new part
$this->createAndOpenNewPart();
}
}
$old_offset = @ftell($this->fp);
if ($this->useSplitArchive && ($old_offset == 0))
{
// Because in split ZIPs we have the split ZIP marker in the first four bytes.
@fseek($this->fp, 4);
$old_offset = @ftell($this->fp);
}
// Get the file name length in bytes
$fn_length = akstrlen($storedName);
$this->fwrite($this->fp, $this->fileHeaderSignature); /* Begin creating the ZIP data. */
/* Version needed to extract. */
if (!$isSymlink)
{
$this->fwrite($this->fp, "\x14\x00");
}
else
{
$this->fwrite($this->fp, "\x0a\x03");
}
$this->fwrite($this->fp, pack('v', 2048)); /* General purpose bit flag. Bit 11 set = use UTF-8 encoding for filenames & comments */
$this->fwrite($this->fp, ($compressionMethod == 8) ? "\x08\x00" : "\x00\x00"); /* Compression method. */
$this->fwrite($this->fp, $hexdtime); /* Last modification time/date. */
$this->fwrite($this->fp, pack('V', $crc)); /* CRC 32 information. */
$this->fwrite($this->fp, pack('V', $c_len)); /* Compressed filesize. */
$this->fwrite($this->fp, pack('V', $unc_len)); /* Uncompressed filesize. */
$this->fwrite($this->fp, pack('v', $fn_length)); /* Length of filename. */
$this->fwrite($this->fp, pack('v', 0)); /* Extra field length. */
$this->fwrite($this->fp, $storedName); /* File name. */
// Cache useful information about the file
if (!$isDir && !$isSymlink && !$isVirtual)
{
$configuration->set('volatile.engine.archiver.unc_len', $unc_len);
$configuration->set('volatile.engine.archiver.hexdtime', $hexdtime);
$configuration->set('volatile.engine.archiver.crc', $crc);
$configuration->set('volatile.engine.archiver.c_len', $c_len);
$configuration->set('volatile.engine.archiver.fn_length', $fn_length);
$configuration->set('volatile.engine.archiver.old_offset', $old_offset);
$configuration->set('volatile.engine.archiver.storedName', $storedName);
$configuration->set('volatile.engine.archiver.sourceNameOrData', $sourceNameOrData);
}
}
/**
* Get the preferred compression method for a file
*
* @param int $fileSize File size in bytes
* @param int $memLimit Memory limit in bytes
* @param bool $isDir Is it a directory?
* @param bool $isSymlink Is it a symlink?
*
* @return int Compression method to use
*/
protected function getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink)
{
// ZIP uses 0 for uncompressed and 8 for GZip Deflate whereas the parent method returns 0 and 1 respectively
return 8 * parent::getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink);
}
/**
* Calculate the CRC-32 checksum
*
* @param string $sourceNameOrData The path to the file being compressed, or the raw file data for virtual files
* @param bool $isVirtual Is this a virtual file?
* @param bool $isSymlink Is this a symlink?
* @param bool $isDir Is this a directory?
*
* @return int The CRC-32
*/
protected function getCRCForEntity(&$sourceNameOrData, &$isVirtual, &$isDir, &$isSymlink)
{
if (!$isSymlink && $isDir)
{
// Dummy CRC for dirs
$crc = 0;
return $crc;
}
if ($isSymlink)
{
$crc = \crc32(@readlink($sourceNameOrData));
return $crc;
}
if ($isVirtual)
{
$crc = \crc32($sourceNameOrData);
return $crc;
}
// This is supposed to be the fast way to calculate CRC32 of a (large) file.
$crc = $this->crcCalculator->crc32_file($sourceNameOrData, $this->AkeebaPackerZIP_CHUNK_SIZE);
// If the file was unreadable, $crc will be false, so we skip the file
if ($crc === false)
{
throw new WarningException('Could not calculate CRC32 for ' . $sourceNameOrData . '. Looks like it is an unreadable file.');
}
return $crc;
}
/**
* Converts a UNIX timestamp to a 4-byte DOS date and time format
* (date in high 2-bytes, time in low 2-bytes allowing magnitude
* comparison).
*
* @param integer $unixtime The current UNIX timestamp.
*
* @return integer The current date in a 4-byte DOS format.
*/
protected function unix2DOSTime($unixtime = null)
{
$timearray = (is_null($unixtime)) ? getdate() : getdate($unixtime);
if ($timearray['year'] < 1980)
{
$timearray['year'] = 1980;
$timearray['mon'] = 1;
$timearray['mday'] = 1;
$timearray['hours'] = 0;
$timearray['minutes'] = 0;
$timearray['seconds'] = 0;
}
return (($timearray['year'] - 1980) << 25) |
($timearray['mon'] << 21) |
($timearray['mday'] << 16) |
($timearray['hours'] << 11) |
($timearray['minutes'] << 5) |
($timearray['seconds'] >> 1);
}
/**
* Creates a new part for the spanned archive
*
* @param bool $finalPart Is this the final archive part?
*
* @return bool True on success
*/
protected function createNewPartFile($finalPart = false)
{
// Close any open file pointers
if (is_resource($this->fp))
{
$this->fclose($this->fp);
}
if (is_resource($this->cdfp))
{
$this->fclose($this->cdfp);
}
// Remove the just finished part from the list of resumable offsets
$this->removeFromOffsetsList($this->_dataFileName);
// Set the file pointers to null
$this->fp = null;
$this->cdfp = null;
// Push the previous part if we have to post-process it immediately
$configuration = Factory::getConfiguration();
if ($configuration->get('engine.postproc.common.after_part', 0))
{
$this->finishedPart[] = $this->_dataFileName;
}
// Add the part's size to our rolling sum
clearstatcache();
$this->totalCompressedSize += filesize($this->_dataFileName);
$this->totalParts++;
$this->currentPartNumber = $this->totalParts;
if ($finalPart)
{
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.zip';
}
else
{
$this->_dataFileName = $this->dataFileNameWithoutExtension . '.z' . sprintf('%02d', $this->currentPartNumber);
}
Factory::getLog()->info('Creating new ZIP part #' . $this->currentPartNumber . ', file ' . $this->_dataFileName);
// Inform the backup engine that we have changed the multipart number
$statistics = Factory::getStatistics();
$statistics->updateMultipart($this->totalParts);
// Try to remove any existing file
@unlink($this->_dataFileName);
// Touch the new file
$result = @touch($this->_dataFileName);
@chmod($this->_dataFileName, $this->getPermissions());
return $result;
}
/**
* Find the optimal chunk size for CRC32 calculations and file processing
*
* @return void
*/
private function findOptimalChunkSize()
{
$configuration = Factory::getConfiguration();
// The user has entered their own preference
if ($configuration->get('engine.archiver.common.chunk_size', 0) > 0)
{
$this->AkeebaPackerZIP_CHUNK_SIZE = AKEEBA_CHUNK;
return;
}
// Get the PHP memory limit
$memLimit = $this->getMemoryLimit();
// Can't get a PHP memory limit? Use 2Mb chunks (fairly large, right?)
if (is_null($memLimit))
{
$this->AkeebaPackerZIP_CHUNK_SIZE = 2097152;
return;
}
if (!function_exists("memory_get_usage"))
{
// PHP can't report memory usage, use a conservative 512Kb
$this->AkeebaPackerZIP_CHUNK_SIZE = 524288;
return;
}
// PHP *can* report memory usage, see if there's enough available memory
$availableRAM = $memLimit - memory_get_usage();
if ($availableRAM > 0)
{
$this->AkeebaPackerZIP_CHUNK_SIZE = $availableRAM * 0.5;
return;
}
// NEGATIVE AVAILABLE MEMORY?!! Some borked PHP implementations also return the size of the httpd footprint.
if (($memLimit - 6291456) > 0)
{
$this->AkeebaPackerZIP_CHUNK_SIZE = $memLimit - 6291456;
return;
}
// If all else fails, use 2Mb and cross your fingers
$this->AkeebaPackerZIP_CHUNK_SIZE = 2097152;
}
/**
* Create a Central Directory temporary file
*
* @return void
*
* @throws ErrorException
*/
private function createCentralDirectoryTempFile()
{
$configuration = Factory::getConfiguration();
$this->centralDirectoryFilename = tempnam($configuration->get('akeeba.basic.output_directory'), 'akzcd');
$this->centralDirectoryFilename = basename($this->centralDirectoryFilename);
$pos = strrpos($this->centralDirectoryFilename, '/');
if ($pos !== false)
{
$this->centralDirectoryFilename = substr($this->centralDirectoryFilename, $pos + 1);
}
$pos = strrpos($this->centralDirectoryFilename, '\\');
if ($pos !== false)
{
$this->centralDirectoryFilename = substr($this->centralDirectoryFilename, $pos + 1);
}
$this->centralDirectoryFilename = Factory::getTempFiles()->registerTempFile($this->centralDirectoryFilename);
Factory::getLog()->debug(__CLASS__ . " :: CntDir Tempfile = " . $this->centralDirectoryFilename);
// Create temporary file
if (!@touch($this->centralDirectoryFilename))
{
throw new ErrorException("Could not open temporary file for ZIP archiver. Please check your temporary directory's permissions!");
}
@chmod($this->centralDirectoryFilename, $this->getPermissions());
}
}

View File

@@ -0,0 +1,53 @@
{
"_information": {
"title": "COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_JPA_TITLE",
"description": "COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_JPA_DESCRIPTION"
},
"engine.archiver.common.dereference_symlinks": {
"default": "0",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_TITLE",
"description": "COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_DESCRIPTION"
},
"engine.archiver.common.part_size": {
"default": "0",
"type": "integer",
"min": "0",
"max": "2147483648",
"shortcuts": "0|131072|262144|524288|1048576|2097152|5242880|10485760|20971520|52428800|104857600|268435456|536870912|1073741824|1610612736|2097152000",
"scale": "1048576",
"uom": "MB",
"title": "COM_AKEEBA_CONFIG_PARTSIZE_TITLE",
"description": "COM_AKEEBA_CONFIG_PARTSIZE_DESCRIPTION"
},
"engine.archiver.common.permissions": {
"default": "0666",
"type": "enum",
"enumkeys": "COM_AKEEBA_CONFIG_PERMISSIONS_0600|COM_AKEEBA_CONFIG_PERMISSIONS_0644|COM_AKEEBA_CONFIG_PERMISSIONS_0666",
"enumvalues": "0600|0644|0666",
"title": "COM_AKEEBA_CONFIG_PERMISSIONS_TITLE",
"description": "COM_AKEEBA_CONFIG_PERMISSIONS_DESCRIPTION"
},
"engine.archiver.common.chunk_size": {
"default": "1048576",
"type": "integer",
"min": "65536",
"max": "10485760",
"shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
"scale": "1048576",
"uom": "MB",
"title": "COM_AKEEBA_CONFIG_CHUNKSIZE_TITLE",
"description": "COM_AKEEBA_CONFIG_CHUNKSIZE_DESCRIPTION"
},
"engine.archiver.common.big_file_threshold": {
"default": "1048576",
"type": "integer",
"min": "65536",
"max": "10485760",
"shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
"scale": "1048576",
"uom": "MB",
"title": "COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_TITLE",
"description": "COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_DESCRIPTION"
}
}

View File

@@ -0,0 +1,64 @@
{
"_information": {
"title": "COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_ZIP_TITLE",
"description": "COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_ZIP_DESCRIPTION"
},
"engine.archiver.common.dereference_symlinks": {
"default": "0",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_TITLE",
"description": "COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_DESCRIPTION"
},
"engine.archiver.common.part_size": {
"default": "0",
"type": "integer",
"min": "0",
"max": "2147483648",
"shortcuts": "0|131072|262144|524288|1048576|2097152|5242880|10485760|20971520|52428800|104857600|268435456|536870912|1073741824|1610612736|2097152000",
"scale": "1048576",
"uom": "MB",
"title": "COM_AKEEBA_CONFIG_PARTSIZE_TITLE",
"description": "COM_AKEEBA_CONFIG_PARTSIZE_DESCRIPTION"
},
"engine.archiver.common.chunk_size": {
"default": "1048576",
"type": "integer",
"min": "65536",
"max": "10485760",
"shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
"scale": "1048576",
"uom": "MB",
"title": "COM_AKEEBA_CONFIG_CHUNKSIZE_TITLE",
"description": "COM_AKEEBA_CONFIG_CHUNKSIZE_DESCRIPTION"
},
"engine.archiver.common.permissions": {
"default": "0666",
"type": "enum",
"enumkeys": "COM_AKEEBA_CONFIG_PERMISSIONS_0600|COM_AKEEBA_CONFIG_PERMISSIONS_0644|COM_AKEEBA_CONFIG_PERMISSIONS_0666",
"enumvalues": "0600|0644|0666",
"title": "COM_AKEEBA_CONFIG_PERMISSIONS_TITLE",
"description": "COM_AKEEBA_CONFIG_PERMISSIONS_DESCRIPTION"
},
"engine.archiver.common.big_file_threshold": {
"default": "1048576",
"type": "integer",
"min": "65536",
"max": "10485760",
"shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
"scale": "1048576",
"uom": "MB",
"title": "COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_TITLE",
"description": "COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_DESCRIPTION"
},
"engine.archiver.zip.cd_glue_chunk_size": {
"default": "1048576",
"type": "integer",
"min": "65536",
"max": "10485760",
"shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
"scale": "1048576",
"uom": "MB",
"title": "COM_AKEEBA_CONFIG_ZIPCDGLUECHUNKSIZE_TITLE",
"description": "COM_AKEEBA_CONFIG_ZIPCDGLUECHUNKSIZE_DESCRIPTION"
}
}

View File

@@ -0,0 +1,181 @@
<?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;
defined('AKEEBAENGINE') || die();
/**
* The main class autoloader for AkeebaEngine
*/
class Autoloader
{
/**
* An instance of this autoloader
*
* @var Autoloader
*/
public static $autoloader = null;
/**
* The path to the Akeeba Engine root directory
*
* @var string
*/
public static $enginePath = null;
/**
* The directories where Akeeba Engine platforms are stored
*
* @var array
*/
public static $platformDirs = null;
/**
* Public constructor. Registers the autoloader with PHP.
*/
public function __construct()
{
self::$enginePath = __DIR__;
spl_autoload_register([$this, 'autoload_akeeba_engine']);
spl_autoload_register([$this, 'autoload_psr3']);
}
/**
* Initialise this autoloader
*
* @return Autoloader
*/
public static function init()
{
if (self::$autoloader == null)
{
self::$autoloader = new self;
}
return self::$autoloader;
}
/**
* The actual autoloader
*
* @param string $className The name of the class to load
*
* @return void
*/
public function autoload_akeeba_engine($className)
{
// Trim the trailing backslash
$className = ltrim($className, '\\');
// Make sure the class has an Akeeba\Engine prefix
if (substr($className, 0, 13) != 'Akeeba\\Engine')
{
return;
}
// Remove the prefix and explode on backslashes
$className = substr($className, 14);
$class = explode('\\', $className);
// Do we have a list of platform directories?
if (is_null(self::$platformDirs) && class_exists('\\Akeeba\\Engine\\Platform', false))
{
self::$platformDirs = Platform::getPlatformDirectories();
if (!is_array(self::$platformDirs))
{
self::$platformDirs = [];
}
}
$rootPaths = [self::$enginePath];
if (is_array(self::$platformDirs))
{
$rootPaths = array_merge(
self::$platformDirs, [self::$enginePath]
);
}
foreach ($rootPaths as $rootPath)
{
// First try finding in structured directory format (preferred)
$path = $rootPath . '/' . implode('/', $class) . '.php';
if (@file_exists($path))
{
include_once $path;
}
// Then try the duplicate last name structured directory format (not recommended)
if (!class_exists($className, false))
{
reset($class);
$lastPart = end($class);
$path = $rootPath . '/' . implode('/', $class) . '/' . $lastPart . '.php';
if (@file_exists($path))
{
include_once $path;
}
}
}
}
/**
* An autoloader for our copy of PSR-3
*
* @param string $className The name of the class to load
*
* @return void
*/
public function autoload_psr3($className)
{
// Trim the trailing backslash
$className = ltrim($className, '\\');
// Make sure the class has an Akeeba\Engine prefix
if (substr($className, 0, 7) != 'Psr\\Log')
{
return;
}
// Remove the prefix and explode on backslashes
$className = substr($className, 7);
$class = explode('\\', $className);
$rootPath = self::$enginePath . '/Psr/Log';
// First try finding in structured directory format (preferred)
$path = $rootPath . '/' . implode('/', $class) . '.php';
if (@file_exists($path))
{
include_once $path;
}
// Then try the duplicate last name structured directory format (not recommended)
if (!class_exists($className, false))
{
reset($class);
$lastPart = end($class);
$path = $rootPath . '/' . implode('/', $class) . '/' . $lastPart . '.php';
if (@file_exists($path))
{
include_once $path;
}
}
}
}
// Register the Akeeba Engine autoloader
Autoloader::init();

View File

@@ -0,0 +1,303 @@
<?php
/**
* Akeeba Engine
* The PHP-only site backup engine
*
* @copyright Copyright (c)2006-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or, at your option, any later version
* @package akeebaengine
*/
namespace Akeeba\Engine\Base;
// Protection against direct access
use Akeeba\Engine\Factory;
defined('AKEEBAENGINE') or die();
/**
* The base class of Akeeba Engine objects. Allows for error and warnings logging
* and propagation. Largely based on the Joomla! 1.5 JObject class.
*/
abstract class BaseObject
{
/** @var array An array of errors */
private $_errors = array();
/** @var array The queue size of the $_errors array. Set to 0 for infinite size. */
protected $_errors_queue_size = 0;
/** @var array An array of warnings */
private $_warnings = array();
/** @var array The queue size of the $_warnings array. Set to 0 for infinite size. */
protected $_warnings_queue_size = 0;
/**
* This method should be overridden by descendant classes. It is called when the factory is being
* serialized and can be used to perform any necessary cleanup steps.
*
* @return void
*/
public function _onSerialize()
{
}
/**
* Get the most recent error message
*
* @param integer $i Optional error index
*
* @return string Error message
*/
public function getError($i = null)
{
return $this->getItemFromArray($this->_errors, $i);
}
/**
* Return all errors, if any
*
* @return array Array of error messages
*/
public function getErrors()
{
return $this->_errors;
}
/**
* Add an error message
*
* @param string $error Error message
* @param bool $log Should I log the message automatically?
*/
public function setError($error, $log = true)
{
if ($log)
{
Factory::getLog()->error($error);
}
if ($this->_errors_queue_size > 0)
{
if (count($this->_errors) >= $this->_errors_queue_size)
{
array_shift($this->_errors);
}
}
array_push($this->_errors, $error);
}
/**
* Resets all error messages
*
* @return void
*/
public function resetErrors()
{
$this->_errors = array();
}
/**
* Get the most recent warning message
*
* @param integer $i Optional warning index
*
* @return string Error message
*/
public function getWarning($i = null)
{
return $this->getItemFromArray($this->_warnings, $i);
}
/**
* Return all warnings, if any
*
* @return array Array of error messages
*/
public function getWarnings()
{
return $this->_warnings;
}
/**
* Add a warning message
*
* @param string $warning Warning message
* @param bool $log Should I log the message automatically?
*
* @return void
*/
public function setWarning($warning, $log = true)
{
if ($log)
{
Factory::getLog()->warning($warning);
}
if ($this->_warnings_queue_size > 0)
{
if (count($this->_warnings) >= $this->_warnings_queue_size)
{
array_shift($this->_warnings);
}
}
array_push($this->_warnings, $warning);
}
/**
* Resets all warning messages
*
* @return void
*/
public function resetWarnings()
{
$this->_warnings = array();
}
/**
* Propagates errors and warnings to a foreign object. Propagated items will be removed from our own instance.
*
* @param Object $object The object to propagate errors and warnings to.
*
* @return void
*/
public function propagateToObject(&$object)
{
// Skip non-objects
if (!is_object($object))
{
return;
}
if (method_exists($object, 'setError'))
{
if (!empty($this->_errors))
{
foreach ($this->_errors as $error)
{
$object->setError($error);
}
$this->_errors = array();
}
}
if (method_exists($object, 'setWarning'))
{
if (!empty($this->_warnings))
{
foreach ($this->_warnings as $warning)
{
$object->setWarning($warning);
}
$this->_warnings = array();
}
}
}
/**
* Propagates errors and warnings from a foreign object. Each propagated list is
* then cleared on the foreign object, as long as it implements resetErrors() and/or
* resetWarnings() methods.
*
* @param self $object The object to propagate errors and warnings from
*
* @return void
*/
public function propagateFromObject(&$object)
{
if (method_exists($object, 'getErrors'))
{
$errors = $object->getErrors();
if (!empty($errors))
{
foreach ($errors as $error)
{
$this->setError($error, false);
}
}
if (method_exists($object, 'resetErrors'))
{
$object->resetErrors();
}
}
if (method_exists($object, 'getWarnings'))
{
$warnings = $object->getWarnings();
if (!empty($warnings))
{
foreach ($warnings as $warning)
{
$this->setWarning($warning, false);
}
}
if (method_exists($object, 'resetWarnings'))
{
$object->resetWarnings();
}
}
}
/**
* Sets the size of the error queue (acts like a LIFO buffer)
*
* @param int $newSize The new queue size. Set to 0 for infinite length.
*
* @return void
*/
protected function setErrorsQueueSize($newSize = 0)
{
$this->_errors_queue_size = (int)$newSize;
}
/**
* Sets the size of the warnings queue (acts like a LIFO buffer)
*
* @param int $newSize The new queue size. Set to 0 for infinite length.
*
* @return void
*/
protected function setWarningsQueueSize($newSize = 0)
{
$this->_warnings_queue_size = (int)$newSize;
}
/**
* Returns the last item of a LIFO string message queue, or a specific item
* if so specified.
*
* @param array $array An array of strings, holding messages
* @param int $i Optional message index
*
* @return mixed The message string, or false if the key doesn't exist
*/
protected function getItemFromArray($array, $i = null)
{
// Find the item
if ($i === null)
{
// Default, return the last item
$item = end($array);
}
elseif (!array_key_exists($i, $array))
{
// If $i has been specified but does not exist, return false
return false;
}
else
{
$item = $array[$i];
}
return $item;
}
}

View File

@@ -0,0 +1,22 @@
<?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\Base\Exceptions;
defined('AKEEBAENGINE') || die();
use RuntimeException;
/**
* An exception which leads to an error (and complete halt) in the backup process
*/
class ErrorException extends RuntimeException
{
}

View File

@@ -0,0 +1,22 @@
<?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\Base\Exceptions;
defined('AKEEBAENGINE') || die();
use RuntimeException;
/**
* An exception which leads to a warning in the backup process
*/
class WarningException extends RuntimeException
{
}

View File

@@ -0,0 +1,583 @@
<?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\Base;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Factory;
use Exception;
use Psr\Log\LogLevel;
use Throwable;
/**
* Base class for all Akeeba Engine parts.
*
* Parts are objects which perform a specific function during the backup process, e.g. backing up files or dumping
* database contents. They have a fully defined and controlled lifecycle, from initialization to finalization. The
* transition between lifecycle phases is handled by the `tick()` method which is essentially the only public interface
* to interacting with an engine part.
*/
abstract class Part
{
public const STATE_INIT = 0;
public const STATE_PREPARED = 1;
public const STATE_RUNNING = 2;
public const STATE_POSTRUN = 3;
public const STATE_FINISHED = 4;
public const STATE_ERROR = 99;
/**
* The current state of this part; see the constants at the top of this class
*
* @var int
*/
protected $currentState = self::STATE_INIT;
/**
* The name of the engine part (a.k.a. Domain), used in return table
* generation.
*
* @var string
*/
protected $activeDomain = "";
/**
* The step this engine part is in. Used verbatim in return table and
* should be set by the code in the _run() method.
*
* @var string
*/
protected $activeStep = "";
/**
* A more detailed description of the step this engine part is in. Used
* verbatim in return table and should be set by the code in the _run()
* method.
*
* @var string
*/
protected $activeSubstep = "";
/**
* Any configuration variables, in the form of an array.
*
* @var array
*/
protected $_parametersArray = [];
/**
* The database root key
*
* @var string
*/
protected $databaseRoot = [];
/**
* Should we log the step nesting?
*
* @var bool
*/
protected $nest_logging = false;
/**
* Embedded installer preferences
*
* @var object
*/
protected $installerSettings;
/**
* How much milliseconds should we wait to reach the min exec time
*
* @var int
*/
protected $waitTimeMsec = 0;
/**
* Should I ignore the minimum execution time altogether?
*
* @var bool
*/
protected $ignoreMinimumExecutionTime = false;
/**
* The last exception thrown during the tick() method's execution.
*
* @var null|Exception
*/
protected $lastException = null;
/**
* Public constructor
*
* @return void
*/
public function __construct()
{
// Fetch the installer settings
$this->installerSettings = (object) [
'installerroot' => 'installation',
'sqlroot' => 'installation/sql',
'databasesini' => 1,
'readme' => 1,
'extrainfo' => 1,
'password' => 0,
];
$config = Factory::getConfiguration();
$installerKey = $config->get('akeeba.advanced.embedded_installer');
$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
// Fall back to default ANGIE installer if the selected installer is not found
if (!array_key_exists($installerKey, $installerDescriptors))
{
$installerKey = 'angie';
}
if (array_key_exists($installerKey, $installerDescriptors))
{
$this->installerSettings = (object) $installerDescriptors[$installerKey];
}
}
/**
* Nested logging of exceptions
*
* The message is logged using the specified log level. The detailed information of the Throwable and its trace are
* logged using the DEBUG level.
*
* If the Throwable is nested, its parents are logged recursively. This should create a thorough trace leading to
* the root cause of an error.
*
* @param Exception|Throwable $exception The Exception or Throwable to log
* @param string $logLevel The log level to use, default ERROR
*/
protected static function logErrorsFromException($exception, $logLevel = LogLevel::ERROR)
{
$logger = Factory::getLog();
$logger->log($logLevel, $exception->getMessage());
$logger->debug(sprintf('[%s] %s(%u) #%u %s', get_class($exception), $exception->getFile(), $exception->getLine(), $exception->getCode(), $exception->getMessage()));
foreach (explode("\n", $exception->getTraceAsString()) as $line)
{
$logger->debug(rtrim($line));
}
$previous = $exception->getPrevious();
if (!is_null($previous))
{
self::logErrorsFromException($previous, $logLevel);
}
}
/**
* The public interface to an engine part. This method takes care for
* calling the correct method in order to perform the initialisation -
* run - finalisation cycle of operation and return a proper response array.
*
* @param int $nesting
*
* @return array A response array
*/
public function tick($nesting = 0)
{
$configuration = Factory::getConfiguration();
$timer = Factory::getTimer();
$this->waitTimeMsec = 0;
$this->lastException = null;
/**
* Call the right action method, depending on engine part state.
*
* The action method may throw an exception to signal failure, hence the try-catch. If there is an exception we
* will set the part's state to STATE_ERROR and store the last exception.
*/
try
{
switch ($this->getState())
{
case self::STATE_INIT:
$this->_prepare();
break;
case self::STATE_PREPARED:
case self::STATE_RUNNING:
$this->_run();
break;
case self::STATE_POSTRUN:
$this->_finalize();
break;
}
}
catch (Exception $e)
{
$this->lastException = $e;
$this->setState(self::STATE_ERROR);
}
// If there is still time, we are not finished and there is no break flag set, re-run the tick()
// method.
$breakFlag = $configuration->get('volatile.breakflag', false);
if (
!in_array($this->getState(), [self::STATE_FINISHED, self::STATE_ERROR]) &&
($timer->getTimeLeft() > 0) &&
!$breakFlag &&
($nesting < 20) &&
($this->nest_logging)
)
{
// Nesting is only applied if $this->nest_logging == true (currently only Kettenrad has this)
$nesting++;
if ($this->nest_logging)
{
Factory::getLog()->debug("*** Batching successive steps (nesting level $nesting)");
}
return $this->tick($nesting);
}
// Return the output array
$out = $this->makeReturnTable();
// If it's not a nest-logged part (basically, anything other than Kettenrad) return the output array.
if (!$this->nest_logging)
{
return $out;
}
// From here on: things to do for nest-logged parts (i.e. Kettenrad)
if ($breakFlag)
{
Factory::getLog()->debug("*** Engine steps batching: Break flag detected.");
}
// Reset the break flag
$configuration->set('volatile.breakflag', false);
// Log that we're breaking the step
Factory::getLog()->debug("*** Batching of engine steps finished. I will now return control to the caller.");
// Detect whether I need server-side sleep
$serverSideSleep = $this->needsServerSideSleep();
// Enforce minimum execution time
if (!$this->ignoreMinimumExecutionTime)
{
$timer = Factory::getTimer();
$this->waitTimeMsec = (int) $timer->enforce_min_exec_time(true, $serverSideSleep);
}
// Send a Return Table back to the caller
return $out;
}
/**
* Returns a copy of the class's status array
*
* @return array The response array
*/
public function getStatusArray()
{
return $this->makeReturnTable();
}
/**
* Sends any kind of setup information to the engine part. Using this,
* we avoid passing parameters to the constructor of the class. These
* parameters should be passed as an indexed array and should be taken
* into account during the preparation process only. This function will
* set the error flag if it's called after the engine part is prepared.
*
* @param array $parametersArray The parameters to be passed to the engine part.
*
* @return void
*/
public function setup($parametersArray)
{
if ($this->currentState == self::STATE_PREPARED)
{
$this->setState(self::STATE_ERROR);
throw new ErrorException(__CLASS__ . ":: Can't modify configuration after the preparation of " . $this->activeDomain);
}
$this->_parametersArray = $parametersArray;
if (array_key_exists('root', $parametersArray))
{
$this->databaseRoot = $parametersArray['root'];
}
}
/**
* Returns the state of this engine part.
*
* @return int The state of this engine part.
*/
public function getState()
{
if (!is_null($this->lastException))
{
$this->currentState = self::STATE_ERROR;
}
return $this->currentState;
}
/**
* Translate the integer state to a string, used by consumers of the public Engine API.
*
* @param int $state The part state to translate to string
*
* @return string
*/
public function stateToString($state)
{
switch ($state)
{
case self::STATE_ERROR:
return 'error';
break;
case self::STATE_INIT:
return 'init';
break;
case self::STATE_PREPARED:
return 'prepared';
break;
case self::STATE_RUNNING:
return 'running';
break;
case self::STATE_POSTRUN:
return 'postrun';
break;
case self::STATE_FINISHED:
return 'finished';
break;
}
return 'init';
}
/**
* Get the current domain of the engine
*
* @return string The current domain
*/
public function getDomain()
{
return $this->activeDomain;
}
/**
* Get the current step of the engine
*
* @return string The current step
*/
public function getStep()
{
return $this->activeStep;
}
/**
* Get the current sub-step of the engine
*
* @return string The current sub-step
*/
public function getSubstep()
{
return $this->activeSubstep;
}
/**
* Implement this if your Engine Part can return the percentage of its work already complete
*
* @return float A number from 0 (nothing done) to 1 (all done)
*/
public function getProgress()
{
return 0;
}
/**
* Get the value of the minimum execution time ignore flag.
*
* DO NOT REMOVE. It is used by the Engine consumers.
*
* @return boolean
*/
public function isIgnoreMinimumExecutionTime()
{
return $this->ignoreMinimumExecutionTime;
}
/**
* Set the value of the minimum execution time ignore flag. When set, the nested logging parts (basically,
* Kettenrad) will ignore the minimum execution time parameter.
*
* DO NOT REMOVE. It is used by the Engine consumers.
*
* @param boolean $ignoreMinimumExecutionTime
*/
public function setIgnoreMinimumExecutionTime($ignoreMinimumExecutionTime)
{
$this->ignoreMinimumExecutionTime = $ignoreMinimumExecutionTime;
}
/**
* Runs any initialization code. Must set the state to STATE_PREPARED.
*
* @return void
*/
abstract protected function _prepare();
/**
* Runs any finalisation code. Must set the state to STATE_FINISHED.
*
* @return void
*/
abstract protected function _finalize();
/**
* Performs the main objective of this part. While still processing the state must be set to STATE_RUNNING. When the
* main objective is complete and we're ready to proceed to finalization the state must be set to STATE_POSTRUN.
*
* @return void
*/
abstract protected function _run();
/**
* Sets the BREAKFLAG, which instructs this engine part that the current step must break immediately,
* in fear of timing out.
*
* @return void
*/
protected function setBreakFlag()
{
$registry = Factory::getConfiguration();
$registry->set('volatile.breakflag', true);
}
/**
* Sets the engine part's internal state, in an easy to use manner
*
* @param int $state The part state to set
*
* @return void
*/
protected function setState($state = self::STATE_INIT)
{
$this->currentState = $state;
}
/**
* Constructs a Response Array based on the engine part's state.
*
* @return array The Response Array for the current state
*/
protected function makeReturnTable()
{
$errors = [];
$e = $this->lastException;
while (!empty($e))
{
$errors[] = $e->getMessage();
$e = $e->getPrevious();
}
return [
'HasRun' => $this->currentState != self::STATE_FINISHED,
'Domain' => $this->activeDomain,
'Step' => $this->activeStep,
'Substep' => $this->activeSubstep,
'Error' => implode("\n", $errors),
'Warnings' => [],
'ErrorException' => $this->lastException,
];
}
/**
* Set the current domain of the engine
*
* @param string $new_domain The domain to set
*
* @return void
*/
protected function setDomain($new_domain)
{
$this->activeDomain = $new_domain;
}
/**
* Set the current step of the engine
*
* @param string $new_step The step to set
*
* @return void
*/
protected function setStep($new_step)
{
$this->activeStep = $new_step;
}
/**
* Set the current sub-step of the engine
*
* @param string $new_substep The sub-step to set
*
* @return void
*/
protected function setSubstep($new_substep)
{
$this->activeSubstep = $new_substep;
}
/**
* Do I need to apply server-side sleep for the time difference between the elapsed time and the minimum execution
* time?
*
* @return bool
*/
private function needsServerSideSleep()
{
/**
* If the part doesn't support tagging, i.e. I can't determine if this is a backend backup or not, I will always
* use server-side sleep.
*/
if (!method_exists($this, 'getTag'))
{
return true;
}
/**
* If this is not a backend backup I will always use server-side sleep. That is to say that legacy front-end,
* remote JSON API and CLI backups must always use server-side sleep since they do not support client-side
* sleep.
*/
if (!in_array($this->getTag(), ['backend']))
{
return true;
}
return Factory::getConfiguration()->get('akeeba.basic.clientsidewait', 0) == 0;
}
}

View File

@@ -0,0 +1,655 @@
<?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;
defined('AKEEBAENGINE') || die();
use DirectoryIterator;
use stdClass;
/**
* The Akeeba Engine configuration registry class
*/
class Configuration
{
/**
* The currently loaded profile
*
* @var integer
*/
public $activeProfile = null;
/**
* Default namespace
*
* @var string
*/
private $defaultNameSpace = 'global';
/**
* Array keys which may contain stock directory definitions
*
* @var array
*/
private $directory_containing_keys = [
'akeeba.basic.output_directory',
];
/**
* Keys whose default values should never be overridden
*
* @var array
*/
private $protected_nodes = [];
/** The registry data
*
* @var array
*/
private $registry = [];
/**
* Constructor
*
* @return void
*/
public function __construct()
{
// Create the default namespace
$this->makeNameSpace($this->defaultNameSpace);
// Create a default configuration
$this->reset();
}
/**
* Create a namespace
*
* @param string $namespace Name of the namespace to create
*
* @return void
*/
public function makeNameSpace($namespace)
{
$this->registry[$namespace] = ['data' => new stdClass()];
}
/**
* Get the list of namespaces
*
* @return array List of namespaces
*/
public function getNameSpaces()
{
return array_keys($this->registry);
}
/**
* Get a registry value
*
* @param string $regpath Registry path (e.g. global.directory.temporary)
* @param mixed $default Optional default value
* @param boolean $process_special_vars Optional. If true (default), it processes special variables, e.g.
* [SITEROOT] in folder names
*
* @return mixed Value of entry or null
*/
public function get($regpath, $default = null, $process_special_vars = true)
{
// Cache the platform-specific stock directories
static $stock_directories = [];
if (empty($stock_directories))
{
$stock_directories = Platform::getInstance()->get_stock_directories();
}
$result = $default;
// Explode the registry path into an array
if ($nodes = explode('.', $regpath))
{
// Get the namespace
$count = count($nodes);
if ($count < 2)
{
$namespace = $this->defaultNameSpace;
$nodes[1] = $nodes[0];
}
else
{
$namespace = $nodes[0];
}
if (isset($this->registry[$namespace]))
{
$ns = $this->registry[$namespace]['data'];
$pathNodes = $count - 1;
for ($i = 1; $i < $pathNodes; $i++)
{
if ((isset($ns->{$nodes[$i]})))
{
$ns = $ns->{$nodes[$i]};
}
}
if (isset($ns->{$nodes[$i]}))
{
$result = $ns->{$nodes[$i]};
}
}
}
// Post-process certain directory-containing variables
if ($process_special_vars && in_array($regpath, $this->directory_containing_keys))
{
if (!empty($stock_directories))
{
foreach ($stock_directories as $tag => $content)
{
$result = str_replace($tag, $content, $result);
}
}
}
return $result;
}
/**
* Set a registry value
*
* @param string $regpath Registry Path (e.g. global.directory.temporary)
* @param mixed $value Value of entry
* @param bool $process_special_vars Optional. If true (default), it processes special variables, e.g.
* [SITEROOT] in folder names
*
* @return mixed Value of old value or boolean false if operation failed
*/
public function set($regpath, $value, $process_special_vars = true)
{
// Cache the platform-specific stock directories
static $stock_directories = [];
if (empty($stock_directories))
{
$stock_directories = Platform::getInstance()->get_stock_directories();
}
if (in_array($regpath, $this->protected_nodes))
{
return $this->get($regpath);
}
// Explode the registry path into an array
$nodes = explode('.', $regpath);
// Get the namespace
$count = count($nodes);
if ($count < 2)
{
$namespace = $this->defaultNameSpace;
}
else
{
$namespace = array_shift($nodes);
$count--;
}
if (!isset($this->registry[$namespace]))
{
$this->makeNameSpace($namespace);
}
$ns = $this->registry[$namespace]['data'];
$pathNodes = $count - 1;
if ($pathNodes < 0)
{
$pathNodes = 0;
}
for ($i = 0; $i < $pathNodes; $i++)
{
// If any node along the registry path does not exist, create it
if (!isset($ns->{$nodes[$i]}))
{
$ns->{$nodes[$i]} = new stdClass();
}
$ns = $ns->{$nodes[$i]};
}
// Set the new values
if (is_string($value))
{
if (substr($value, 0, 10) == '###json###')
{
$value = json_decode(substr($value, 10));
}
}
// Post-process certain directory-containing variables
if ($process_special_vars && in_array($regpath, $this->directory_containing_keys))
{
if (!empty($stock_directories))
{
$data = $value;
foreach ($stock_directories as $tag => $content)
{
$data = str_replace($tag, $content, $data);
}
$ns->{$nodes[$i]} = $data;
return $ns->{$nodes[$i]};
}
}
// This is executed if any of the previous two if's is false
if (empty($nodes[$i]))
{
return false;
}
$ns->{$nodes[$i]} = $value;
return $ns->{$nodes[$i]};
}
/**
* Unset (remove) a registry value
*
* @param string $regpath Registry Path (e.g. global.directory.temporary)
*
* @return boolean True if the node was removed
*/
public function remove($regpath)
{
// Explode the registry path into an array
$nodes = explode('.', $regpath);
// Get the namespace
$count = count($nodes);
if ($count < 2)
{
$namespace = $this->defaultNameSpace;
}
else
{
$namespace = array_shift($nodes);
$count--;
}
if (!isset($this->registry[$namespace]))
{
$this->makeNameSpace($namespace);
}
$ns = $this->registry[$namespace]['data'];
$pathNodes = $count - 1;
if ($pathNodes < 0)
{
$pathNodes = 0;
}
for ($i = 0; $i < $pathNodes; $i++)
{
// If any node along the registry path does not exist, return false
if (!isset($ns->{$nodes[$i]}))
{
return false;
}
$ns = $ns->{$nodes[$i]};
}
unset($ns->{$nodes[$i]});
return true;
}
/**
* Resets the registry to the default values
*/
public function reset()
{
// Load the Akeeba Engine INI files
$root_path = __DIR__;
$paths = [
$root_path . '/Core',
$root_path . '/Archiver',
$root_path . '/Dump',
$root_path . '/Scan',
$root_path . '/Writer',
$root_path . '/Proc',
$root_path . '/Platform/Filter/Stack',
$root_path . '/Filter/Stack',
];
$platform_paths = Platform::getInstance()->getPlatformDirectories();
foreach ($platform_paths as $p)
{
$paths[] = $p . '/Filter/Stack';
$paths[] = $p . '/Config';
}
foreach ($paths as $root)
{
if (!(is_dir($root) || is_link($root)))
{
continue;
}
if (!is_readable($root))
{
continue;
}
$di = new DirectoryIterator($root);
/** @var DirectoryIterator $file */
foreach ($di as $file)
{
if (!$file->isFile())
{
continue;
}
if ($file->getExtension() == 'json')
{
$this->mergeEngineJSON($file->getRealPath());
}
}
}
}
/**
* Merges an associative array of key/value pairs into the registry.
* If noOverride is set, only non set or null values will be applied.
*
* @param array $array An associative array. Its keys are registry paths.
* @param bool $noOverride [optional] Do not override pre-set values.
* @param bool $process_special_vars Optional. If true (default), it processes special variables, e.g.
* [SITEROOT] in folder names
*/
public function mergeArray($array, $noOverride = false, $process_special_vars = true)
{
if (!$noOverride)
{
foreach ($array as $key => $value)
{
$this->set($key, $value, $process_special_vars);
}
}
else
{
foreach ($array as $key => $value)
{
if (is_null($this->get($key, null)))
{
$this->set($key, $value, $process_special_vars);
}
}
}
}
/**
* Merges a JSON file into the registry. Its top level keys are registry paths,
* child keys are appended to the section-defined paths and then set equal to the
* values. If noOverride is set, only non set or null values will be applied.
* Top level keys beginning with an underscore will be ignored.
*
* @param string $jsonPath The full path to the INI file to load
* @param boolean $noOverride [optional] Do not override pre-set values.
*
* @return boolean True on success
*/
public function mergeJSON($jsonPath, $noOverride = false)
{
if (!file_exists($jsonPath))
{
return false;
}
$rawData = file_get_contents($jsonPath);
$jsonData = empty($rawData) ? [] : json_decode($rawData, true);
foreach ($jsonData as $rootkey => $rootvalue)
{
if (!is_array($rootvalue))
{
if (!$noOverride)
{
$this->set($rootkey, $rootvalue);
}
elseif (is_null($this->get($rootkey, null)))
{
$this->set($rootkey, $rootvalue);
}
}
elseif (substr($rootkey, 0, 1) != '_')
{
foreach ($rootvalue as $key => $value)
{
if (!$noOverride)
{
$this->set($rootkey . '.' . $key, $rootvalue);
}
elseif (is_null($this->get($rootkey . '.' . $key, null)))
{
$this->set($rootkey . '.' . $key, $rootvalue);
}
}
}
}
return true;
}
/**
* Merges an engine JSON file to the configuration. Each top level key defines a full
* registry path (section.subsection.key). It searches each top level key for the
* child key named "default" and merges its value to the configuration. The other keys
* are simply ignored.
*
* @param string $jsonPath The absolute path to an JSON file
* @param bool $noOverride [optional] If true, values from the JSON file will not override the configuration
*
* @return boolean True on success
*/
public function mergeEngineJSON($jsonPath, $noOverride = false)
{
if (!file_exists($jsonPath))
{
return false;
}
$rawData = file_get_contents($jsonPath);
$jsonData = empty($rawData) ? [] : json_decode($rawData, true);
foreach ($jsonData ?? [] as $section => $nodes)
{
if (is_array($nodes))
{
if (substr($section, 0, 1) != '_')
{
// Is this a protected node?
$protected = false;
if (array_key_exists('protected', $nodes))
{
$protected = $nodes['protected'];
}
// If overrides are allowed, unprotect until we can set the value
if (!$noOverride)
{
if (in_array($section, $this->protected_nodes))
{
$pnk = array_search($section, $this->protected_nodes);
unset($this->protected_nodes[$pnk]);
}
}
if (array_key_exists('remove', $nodes))
{
// Remove a node if it has "remove" set
$this->remove($section);
}
elseif (isset($nodes['default']))
{
if (!$noOverride)
{
// Update the default value if No Override is set
$this->set($section, $nodes['default']);
}
elseif (is_null($this->get($section, null)))
{
// Set the default value if it does not exist
$this->set($section, $nodes['default']);
}
}
// Finally, if it's a protected node, enable the protection
if ($protected)
{
$this->protected_nodes[] = $section;
}
else
{
$idx = array_search($section, $this->protected_nodes);
if ($idx !== false)
{
unset($this->protected_nodes[$idx]);
}
}
}
}
}
return true;
}
/**
* Exports the current registry snapshot as a JSON-encoded object. Each namespace is a property of the top-level
* JSON object.
*
* @return string The JSON-encoded representation of the registry
*
* @since 6.4.1
*/
public function exportAsJSON()
{
$forJSON = [];
$namespaces = $this->getNameSpaces();
foreach ($namespaces as $namespace)
{
if ($namespace == 'volatile')
{
continue;
}
$forJSON[$namespace] = $this->registry[$namespace]['data'];
}
return json_encode($forJSON, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_FORCE_OBJECT | JSON_PRETTY_PRINT);
}
/**
* Sets the protection status for a specific configuration key
*
* @param string|array $node The node to protect/unprotect
* @param boolean $protect True to protect, false to unprotect
*
* @return void
*/
public function setKeyProtection($node, $protect = false)
{
if (is_array($node))
{
foreach ($node as $k)
{
$this->setKeyProtection($k, $protect);
}
}
elseif (is_string($node))
{
if (is_array($this->protected_nodes))
{
$protected = in_array($node, $this->protected_nodes);
}
else
{
$this->protected_nodes = [];
$protected = false;
}
if ($protect)
{
if (!$protected)
{
$this->protected_nodes[] = $node;
}
}
else
{
if ($protected)
{
$pnk = array_search($node, $this->protected_nodes);
unset($this->protected_nodes[$pnk]);
}
}
}
}
/**
* Returns a list of protected keys
*
* @return array
*/
public function getProtectedKeys()
{
return $this->protected_nodes;
}
/**
* Resets the protected keys
*
* @return void
*/
public function resetProtectedKeys()
{
$this->protected_nodes = [];
}
/**
* Sets the protected keys
*
* @param array $keys A list of keys to protect
*
* @return void
*/
public function setProtectedKeys($keys)
{
$this->protected_nodes = $keys;
}
}

View File

@@ -0,0 +1,39 @@
{
"_group": {
"description": "COM_AKEEBA_CONFIG_HEADER_BASIC"
},
"akeeba.basic.output_directory": {
"default": "[DEFAULT_OUTPUT]",
"type": "browsedir",
"title": "COM_AKEEBA_CONFIG_OUTDIR_TITLE",
"description": "COM_AKEEBA_CONFIG_OUTDIR_DESCRIPTION"
},
"akeeba.basic.log_level": {
"default": "4",
"type": "enum",
"enumkeys": "COM_AKEEBA_CONFIG_LOGLEVEL_NONE|COM_AKEEBA_CONFIG_LOGLEVEL_WARNING|COM_AKEEBA_CONFIG_LOGLEVEL_DEBUG",
"enumvalues": "0|2|4",
"title": "COM_AKEEBA_CONFIG_LOGLEVEL_TITLE",
"description": "COM_AKEEBA_CONFIG_LOGLEVEL_DESCRIPTION"
},
"akeeba.basic.archive_name": {
"default": "site-[HOST]-[DATE]-[TIME_TZ]-[RANDOM]",
"type": "string",
"title": "COM_AKEEBA_CONFIG_ARCHIVENAME_TITLE",
"description": "COM_AKEEBA_CONFIG_ARCHIVENAME_DESCRIPTION"
},
"akeeba.basic.backup_type": {
"default": "full",
"type": "enum",
"enumkeys": "COM_AKEEBA_CONFIG_BACKUPTYPE_FULL|COM_AKEEBA_CONFIG_BACKUPTYPE_DBONLY",
"enumvalues": "full|dbonly",
"title": "COM_AKEEBA_CONFIG_BACKUPTYPE_TITLE",
"description": "COM_AKEEBA_CONFIG_BACKUPTYPE_DESCRIPTION"
},
"akeeba.basic.clientsidewait": {
"default": "0",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_CLIENTSIDEWAIT_TITLE",
"description": "COM_AKEEBA_CONFIG_CLIENTSIDEWAIT_DESCRIPTION"
}
}

View File

@@ -0,0 +1,43 @@
{
"_group": {
"description": "COM_AKEEBA_CONFIG_ADVANCED"
},
"akeeba.advanced.dump_engine": {
"default": "native",
"type": "engine",
"subtype": "dump",
"protected": "1",
"title": "COM_AKEEBA_CONFIG_DUMPENGINE_TITLE",
"description": "COM_AKEEBA_CONFIG_DUMPENGINE_DESCRIPTION"
},
"akeeba.advanced.scan_engine": {
"default": "smart",
"type": "engine",
"subtype": "scan",
"protected": "1",
"title": "COM_AKEEBA_CONFIG_SCANENGINE_TITLE",
"description": "COM_AKEEBA_CONFIG_SCANENGINE_DESCRIPTION"
},
"akeeba.advanced.archiver_engine": {
"default": "jpa",
"type": "engine",
"subtype": "archiver",
"title": "COM_AKEEBA_CONFIG_ARCHIVERENGINE_TITLE",
"description": "COM_AKEEBA_CONFIG_ARCHIVERENGINE_DESCRIPTION"
},
"akeeba.advanced.postproc_engine": {
"default": "none",
"type": "none",
"protected": "1"
},
"akeeba.advanced.embedded_installer": {
"default": "angie",
"type": "none",
"protected": "1"
},
"engine.installer.angie.key": {
"default": "",
"type": "none",
"protected": "1"
}
}

View File

@@ -0,0 +1,60 @@
{
"_group": {
"description": "COM_AKEEBA_CONFIG_HEADER_QUOTA"
},
"akeeba.quota.remote": {
"default": "0",
"type": "none",
"protected": "1"
},
"akeeba.quota.maxage.enable": {
"default": "0",
"type": "none",
"protected": "1"
},
"akeeba.quota.obsolete_quota": {
"default": "50",
"type": "integer",
"min": "0",
"max": "500",
"shortcuts": "1|10|20|30|40|50",
"scale": "1",
"uom": "items",
"title": "COM_AKEEBA_CONFIG_OBSOLETEQUOTA_ENABLE_TITLE",
"description": "COM_AKEEBA_CONFIG_OBSOLETEQUOTA_ENABLE_DESCRIPTION"
},
"akeeba.quota.enable_size_quota": {
"default": "0",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_SIZEQUOTA_ENABLE_TITLE",
"description": "COM_AKEEBA_CONFIG_SIZEQUOTA_ENABLE_DESCRIPTION"
},
"akeeba.quota.size_quota": {
"default": "15728640",
"type": "integer",
"min": "1",
"max": "1125899906842624",
"shortcuts": "15728640|52428800|104857600|268435456|536870912|1073741824|2147483648|5368709120|10737418240|21474836480|1099511627776",
"scale": "1048576",
"uom": "MB",
"title": "COM_AKEEBA_CONFIG_SIZEQUOTA_VALUE_TITLE",
"description": "COM_AKEEBA_CONFIG_SIZEQUOTA_VALUE_DESCRIPTION"
},
"akeeba.quota.enable_count_quota": {
"default": "1",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_COUNTQUOTA_ENABLE_TITLE",
"description": "COM_AKEEBA_CONFIG_COUNTQUOTA_ENABLE_DESCRIPTION"
},
"akeeba.quota.count_quota": {
"default": "3",
"type": "integer",
"min": "1",
"max": "200",
"shortcuts": "1|5|10|50|100|200",
"scale": "1",
"uom": "",
"title": "COM_AKEEBA_CONFIG_COUNTQUOTA_VALUE_TITLE",
"description": "COM_AKEEBA_CONFIG_COUNTQUOTA_VALUE_DESCRIPTION"
}
}

View File

@@ -0,0 +1,95 @@
{
"_group": {
"description": "COM_AKEEBA_CONFIG_HEADER_TUNING"
},
"akeeba.tuning.min_exec_time": {
"default": "2000",
"type": "integer",
"min": "0",
"max": "20000",
"shortcuts": "0|250|500|1000|2000|3000|4000|5000|7500|10000|15000|20000",
"scale": "1000",
"uom": "s",
"title": "COM_AKEEBA_CONFIG_MINEXECTIME_TITLE",
"description": "COM_AKEEBA_CONFIG_MINEXECTIME_DESCRIPTION"
},
"akeeba.tuning.max_exec_time": {
"default": "14",
"type": "integer",
"min": "0",
"max": "180",
"shortcuts": "1|2|3|5|7|10|14|15|20|23|25|30|45|60|90|120|180",
"scale": "1",
"uom": "s",
"title": "COM_AKEEBA_CONFIG_MAXEXECTIME_TITLE",
"description": "COM_AKEEBA_CONFIG_MAXEXECTIME_DESCRIPTION"
},
"akeeba.tuning.run_time_bias": {
"default": "75",
"type": "integer",
"min": "10",
"max": "100",
"shortcuts": "10|20|25|30|40|50|60|75|80|90|100",
"scale": "1",
"uom": "%",
"title": "COM_AKEEBA_CONFIG_RUNTIMEBIAS_TITLE",
"description": "COM_AKEEBA_CONFIG_RUNTIMEBIAS_DESCRIPTION"
},
"akeeba.advanced.autoresume": {
"default": "1",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_AUTORESUME_TITLE",
"description": "COM_AKEEBA_CONFIG_AUTORESUME_DESCRIPTION"
},
"akeeba.advanced.autoresume_timeout": {
"default": "10",
"type": "integer",
"min": "1",
"max": "36000",
"scale": "1",
"uom": "s",
"shortcuts": "3|5|10|15|20|30|45|60|90|120|300|600|900|1800|3600",
"title": "COM_AKEEBA_CONFIG_AUTORESUME_TIMEOUT_TITLE",
"description": "COM_AKEEBA_CONFIG_AUTORESUME_TIMEOUT_DESCRIPTION"
},
"akeeba.advanced.autoresume_maxretries": {
"default": "3",
"type": "integer",
"min": "1",
"max": "1000",
"scale": "1",
"shortcuts": "1|3|5|7|10|15|20|30|50|100",
"title": "COM_AKEEBA_CONFIG_AUTORESUME_MAXRETRIES_TITLE",
"description": "COM_AKEEBA_CONFIG_AUTORESUME_MAXRETRIES_DESCRIPTION"
},
"akeeba.tuning.nobreak.beforelargefile": {
"default": "0",
"type": "none",
"protected": "1"
},
"akeeba.tuning.nobreak.afterlargefile": {
"default": "0",
"type": "none",
"protected": "1"
},
"akeeba.tuning.nobreak.proactive": {
"default": "0",
"type": "none",
"protected": "1"
},
"akeeba.tuning.nobreak.domains": {
"default": "0",
"type": "none",
"protected": "1"
},
"akeeba.tuning.nobreak.finalization": {
"default": "0",
"type": "none",
"protected": "1"
},
"akeeba.tuning.settimelimit": {
"default": "1",
"type": "none",
"protected": "1"
}
}

View File

@@ -0,0 +1,101 @@
<?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\Core;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Driver\Base as DriverBase;
use Akeeba\Engine\Driver\Mysqli;
use Akeeba\Engine\Platform;
use Exception;
/**
* A utility class to return a database connection object
*/
class Database
{
private static $instances = [];
/**
* Returns a database connection object. It caches the created objects for future use.
*
* @param array $options Options to use when instantiating the database connection
*
* @return DriverBase
*/
public static function &getDatabase($options, $unset = false)
{
if (!is_array(self::$instances))
{
self::$instances = [];
}
$signature = serialize($options);
if ($unset)
{
if (!empty(self::$instances[$signature]))
{
$db = self::$instances[$signature];
$db = null;
unset(self::$instances[$signature]);
}
$null = null;
return $null;
}
if (empty(self::$instances[$signature]))
{
$driver = array_key_exists('driver', $options) ? $options['driver'] : '';
$select = array_key_exists('select', $options) ? $options['select'] : true;
$database = array_key_exists('database', $options) ? $options['database'] : null;
$driver = preg_replace('/[^A-Z0-9_\\\.-]/i', '', $driver);
if (empty($driver))
{
// No driver specified; try to guess
$default_signature = serialize(Platform::getInstance()->get_platform_database_options());
if ($signature == $default_signature)
{
$driver = Platform::getInstance()->get_default_database_driver(true);
}
else
{
$driver = Platform::getInstance()->get_default_database_driver(false);
}
}
else
{
// Make sure a full driver name was given
if ((substr($driver, 0, 7) != '\\Akeeba') && substr($driver, 0, 7) != 'Akeeba\\')
{
$driver = '\\Akeeba\\Engine\\Driver\\' . ucfirst($driver);
}
}
// Useful for PHP 7 which does NOT have the ancient mysql adapter
if (($driver == '\\Akeeba\\Engine\\Driver\\Mysql') && !function_exists('mysql_connect'))
{
$driver = Mysqli::class;
}
self::$instances[$signature] = new $driver($options);
}
return self::$instances[$signature];
}
public static function unsetDatabase($options)
{
self::getDatabase($options, true);
}
}

View File

@@ -0,0 +1,323 @@
<?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\Core\Domain;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Dump\Base as DumpBase;
use Akeeba\Engine\Factory;
use RuntimeException;
/**
* Multiple database backup engine.
*/
class Db extends Part
{
/** @var array A list of the databases to be packed */
private $database_list = [];
/** @var array The current database configuration data */
private $database_config = null;
/** @var DumpBase The current dumper engine used to backup tables */
private $dump_engine = null;
/** @var string The contents of the databases.json file */
private $databases_json = '';
/** @var array An array containing the database definitions of all dumped databases so far */
private $dumpedDatabases = [];
/** @var int Total number of databases left to be processed */
private $total_databases = 0;
/**
* Implements the constructor of the class
*
* @return void
*/
public function __construct()
{
parent::__construct();
Factory::getLog()->debug(__CLASS__ . " :: New instance");
}
/**
* Implements the getProgress() percentage calculation based on how many
* databases we have fully dumped and how much of the current database we
* have dumped.
*
* @return float
*/
public function getProgress()
{
if (!$this->total_databases)
{
return 0;
}
// Get the overall percentage (based on databases fully dumped so far)
$remaining_steps = count($this->database_list);
$remaining_steps++;
$overall = 1 - ($remaining_steps / $this->total_databases);
// How much is this step worth?
$this_max = 1 / $this->total_databases;
// Get the percentage done of the current database
$local = is_object($this->dump_engine) ? $this->dump_engine->getProgress() : 0;
$percentage = $overall + $local * $this_max;
if ($percentage < 0)
{
$percentage = 0;
}
elseif ($percentage > 1)
{
$percentage = 1;
}
return $percentage;
}
/**
* Implements the _prepare abstract method
*
* @return void
*/
protected function _prepare()
{
Factory::getLog()->debug(__CLASS__ . " :: Preparing instance");
// Populating the list of databases
$this->populate_database_list();
$this->total_databases = count($this->database_list);
$this->setState(self::STATE_PREPARED);
}
/**
* Implements the _run() abstract method
*
* @return void
*/
protected function _run()
{
if ($this->getState() == self::STATE_POSTRUN)
{
Factory::getLog()->debug(__CLASS__ . " :: Already finished");
$this->setStep('');
$this->setSubstep('');
}
else
{
$this->setState(self::STATE_RUNNING);
}
// Make sure we have a dumper instance loaded!
if (is_null($this->dump_engine) && !empty($this->database_list))
{
Factory::getLog()->debug(__CLASS__ . " :: Iterating next database");
// Reset the volatile key holding the table names for this database
Factory::getConfiguration()->set('volatile.database.table_names', []);
// Create a new instance
$this->dump_engine = Factory::getDumpEngine(true);
// Configure the dumper instance and pass on the volatile database root registry key
$registry = Factory::getConfiguration();
$rootkeys = array_keys($this->database_list);
$root = array_shift($rootkeys);
$registry->set('volatile.database.root', $root);
$this->database_config = array_shift($this->database_list);
$this->database_config['root'] = $root;
$this->database_config['process_empty_prefix'] = ($root == '[SITEDB]') ? true : false;
Factory::getLog()->debug(sprintf("%s :: Now backing up %s (%s)", __CLASS__, $root, $this->database_config['database']));
$this->dump_engine->setup($this->database_config);
}
elseif (is_null($this->dump_engine) && empty($this->database_list))
{
throw new RuntimeException('Current dump engine died while resuming the step');
}
// Try to step the instance
$retArray = $this->dump_engine->tick();
// Error propagation
$this->lastException = $retArray['ErrorException'];
if (!is_null($this->lastException))
{
throw $this->lastException;
}
$this->setStep($retArray['Step']);
$this->setSubstep($retArray['Substep']);
// Check if the instance has finished
if (!$retArray['HasRun'])
{
// Set the number of parts
$this->database_config['parts'] = $this->dump_engine->partNumber + 1;
// Push the list of tables in the database into the definition of the last database backed up
$this->database_config['tables'] = Factory::getConfiguration()->get('volatile.database.table_names', []);
Factory::getConfiguration()->set('volatile.database.table_names', []);
// Push the definition of the last database backed up into dumpedDatabases
array_push($this->dumpedDatabases, $this->database_config);
// Go to the next entry in the list and dispose the old AkeebaDumperDefault instance
$this->dump_engine = null;
// Are we past the end of the list?
if (empty($this->database_list))
{
Factory::getLog()->debug(__CLASS__ . " :: No more databases left to iterate");
$this->setState(self::STATE_POSTRUN);
}
}
}
/**
* Implements the _finalize() abstract method
*
* @return void
*/
protected function _finalize()
{
$this->setState(self::STATE_FINISHED);
// If we are in db backup mode, don't create a databases.json
$configuration = Factory::getConfiguration();
if (!Factory::getEngineParamsProvider()->getScriptingParameter('db.databasesini', 1))
{
Factory::getLog()->debug(__CLASS__ . " :: Skipping databases.json");
}
// Create the databases.json contents
// P.A. This still has the old name with the "ini" string. That's for legacy support. Must update it in the future
elseif ($this->installerSettings->databasesini)
{
$this->createDatabasesJSON();
Factory::getLog()->debug(__CLASS__ . " :: Creating databases.json");
// Create a new string
$databasesJSON = json_encode($this->databases_json, JSON_PRETTY_PRINT);
Factory::getLog()->debug(__CLASS__ . " :: Writing databases.json contents");
$archiver = Factory::getArchiverEngine();
$virtualLocation = (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'short') ? '' : $this->installerSettings->sqlroot;
$archiver->addFileVirtual('databases.json', $virtualLocation, $databasesJSON);
}
// On alldb mode, we have to finalize the archive as well
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.finalizearchive', 0))
{
Factory::getLog()->info("Finalizing database dump archive");
$archiver = Factory::getArchiverEngine();
$archiver->finalize();
}
// In CLI mode we'll also close the database connection
if (defined('AKEEBACLI'))
{
Factory::getLog()->info("Closing the database connection to the main database");
Factory::unsetDatabase();
}
return;
}
/**
* Populates database_list with the list of databases in the settings
*
* @return void
*/
protected function populate_database_list()
{
// Get database inclusion filters
$filters = Factory::getFilters();
$this->database_list = $filters->getInclusions('db');
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.skipextradb', 0))
{
// On database only backups we prune extra databases
Factory::getLog()->debug(__CLASS__ . " :: Adding only main database");
if (count($this->database_list) > 1)
{
$this->database_list = array_slice($this->database_list, 0, 1);
}
}
}
protected function createDatabasesJSON()
{
// caching databases.json contents
Factory::getLog()->debug(__CLASS__ . " :: Creating databases.json data");
// Create a new array
$this->databases_json = [];
$registry = Factory::getConfiguration();
$blankOutPass = $registry->get('engine.dump.common.blankoutpass', 0);
$siteRoot = $registry->get('akeeba.platform.newroot', '');
// Loop through databases list
foreach ($this->dumpedDatabases as $definition)
{
$section = basename($definition['dumpFile']);
$dboInstance = Factory::getDatabase($definition);
$type = $dboInstance->name;
$tech = $dboInstance->getDriverType();
// If the database is a sqlite one, we have to process the database name which contains the path
// At the moment we only handle the case where the db file is UNDER site root
if ($tech == 'sqlite')
{
$definition['database'] = str_replace($siteRoot, '#SITEROOT#', $definition['database']);
}
$this->databases_json[$section] = [
'dbtype' => $type,
'dbtech' => $tech,
'dbname' => $definition['database'],
'sqlfile' => $definition['dumpFile'],
'marker' => "\n/**ABDB**/",
'dbhost' => $definition['host'],
'dbuser' => $definition['username'],
'dbpass' => $definition['password'],
'prefix' => $definition['prefix'],
'parts' => $definition['parts'],
'tables' => $definition['tables'],
];
if ($blankOutPass)
{
$this->databases_json[$section]['dbuser'] = '';
$this->databases_json[$section]['dbpass'] = '';
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,485 @@
<?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\Core\Domain;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use RuntimeException;
/**
* Backup initialization domain
*/
class Init extends Part
{
/** @var string The backup description */
private $description = '';
/** @var string The backup comment */
private $comment = '';
/**
* Implements the constructor of the class
*
* @return void
*/
public function __construct()
{
parent::__construct();
Factory::getLog()->debug(__CLASS__ . " :: New instance");
}
/**
* Converts a PHP error to a string
*
* @return string
*/
public static function error2string()
{
if (!function_exists('error_reporting'))
{
return "Not applicable; host too restrictive";
}
$value = error_reporting();
$level_names = [
E_ERROR => 'E_ERROR', E_WARNING => 'E_WARNING',
E_PARSE => 'E_PARSE', E_NOTICE => 'E_NOTICE',
E_CORE_ERROR => 'E_CORE_ERROR', E_CORE_WARNING => 'E_CORE_WARNING',
E_COMPILE_ERROR => 'E_COMPILE_ERROR', E_COMPILE_WARNING => 'E_COMPILE_WARNING',
E_USER_ERROR => 'E_USER_ERROR', E_USER_WARNING => 'E_USER_WARNING',
E_USER_NOTICE => 'E_USER_NOTICE',
];
if (defined('E_STRICT'))
{
$level_names[E_STRICT] = 'E_STRICT';
}
$levels = [];
if (($value & E_ALL) == E_ALL)
{
$levels[] = 'E_ALL';
$value &= ~E_ALL;
}
foreach ($level_names as $level => $name)
{
if (($value & $level) == $level)
{
$levels[] = $name;
}
}
return implode(' | ', $levels);
}
/**
* Reports whether the error display (output to HTML) is enabled or not
*
* @return string
*/
public static function errordisplay()
{
if (!function_exists('ini_get'))
{
return "Not applicable; host too restrictive";
}
return ini_get('display_errors') ? 'on' : 'off';
}
/**
* Implements the _prepare abstract method
*
* @return void
*/
protected function _prepare()
{
// Load parameters (description and comment)
$jpskey = '';
$angiekey = '';
if (!empty($this->_parametersArray))
{
$params = $this->_parametersArray;
if (isset($params['description']))
{
$this->description = $params['description'];
}
if (isset($params['comment']))
{
$this->comment = $params['comment'];
}
if (isset($params['jpskey']))
{
$jpskey = $params['jpskey'];
}
if (isset($params['angiekey']))
{
$angiekey = $params['angiekey'];
}
}
// Load configuration -- No. This is already done by the model. Doing it again removes all overrides.
// Platform::getInstance()->load_configuration();
// Initialize counters
$registry = Factory::getConfiguration();
if (!empty($jpskey))
{
$registry->set('engine.archiver.jps.key', $jpskey);
}
if (!empty($angiekey))
{
$registry->set('engine.installer.angie.key', $angiekey);
}
// Initialize temporary storage
Factory::getFactoryStorage()->reset();
// Force load the tag -- do not delete!
$kettenrad = Factory::getKettenrad();
$tag = $kettenrad->getTag(); // Yes, this is an unused variable by we MUST run this method. DO NOT DELETE.
// Push the comment and description in temp vars for use in the installer phase
$registry->set('volatile.core.description', $this->description);
$registry->set('volatile.core.comment', $this->comment);
$this->setState(self::STATE_PREPARED);
}
/**
* Implements the _run() abstract method
*
* @return void
*/
protected function _run()
{
if ($this->getState() == self::STATE_POSTRUN)
{
Factory::getLog()->debug(__CLASS__ . " :: Already finished");
$this->setStep('');
$this->setSubstep('');
return;
}
else
{
$this->setState(self::STATE_RUNNING);
}
// Initialise the extra notes variable, used by platform classes to return warnings and errors
$extraNotes = null;
// Load the version defines
Platform::getInstance()->load_version_defines();
$registry = Factory::getConfiguration();
// Write log file's header
$version = defined('AKEEBABACKUP_VERSION') ? AKEEBABACKUP_VERSION : AKEEBA_VERSION;
$date = defined('AKEEBABACKUP_DATE') ? AKEEBABACKUP_DATE : AKEEBA_DATE;
Factory::getLog()->info("--------------------------------------------------------------------------------");
Factory::getLog()->info("Akeeba Backup " . $version . ' (' . $date . ')');
Factory::getLog()->info("--------------------------------------------------------------------------------");
// PHP configuration variables are tried to be logged only for debug and info log levels
if ($registry->get('akeeba.basic.log_level') >= 2)
{
Factory::getLog()->info("--- System Information ---");
Factory::getLog()->info("PHP Version :" . PHP_VERSION);
Factory::getLog()->info("PHP OS :" . PHP_OS);
Factory::getLog()->info("PHP SAPI :" . PHP_SAPI);
if (function_exists('php_uname'))
{
Factory::getLog()->info("OS Version :" . php_uname('s'));
}
$db = Factory::getDatabase();
Factory::getLog()->info("DB Version :" . $db->getVersion());
if (isset($_SERVER['SERVER_SOFTWARE']))
{
$server = $_SERVER['SERVER_SOFTWARE'];
}
elseif (($sf = getenv('SERVER_SOFTWARE')))
{
$server = $sf;
}
else
{
$server = 'n/a';
}
Factory::getLog()->info("Web Server :" . $server);
$platform = 'Unknown platform';
$version = '(unknown version)';
$platformData = Platform::getInstance()->getPlatformVersion();
Factory::getLog()->info($platformData['name'] . " version :" . $platformData['version']);
if (isset($_SERVER['HTTP_USER_AGENT']))
{
Factory::getLog()->info("User agent :" . $_SERVER['HTTP_USER_AGENT']);
}
Factory::getLog()->info("Safe mode :" . ini_get("safe_mode"));
Factory::getLog()->info("Display errors :" . ini_get("display_errors"));
Factory::getLog()->info("Error reporting :" . self::error2string());
Factory::getLog()->info("Error display :" . self::errordisplay());
Factory::getLog()->info("Disabled functions :" . ini_get("disable_functions"));
Factory::getLog()->info("open_basedir restr.:" . ini_get('open_basedir'));
Factory::getLog()->info("Max. exec. time :" . ini_get("max_execution_time"));
Factory::getLog()->info("Memory limit :" . ini_get("memory_limit"));
if (function_exists("memory_get_usage"))
{
Factory::getLog()->info("Current mem. usage :" . memory_get_usage());
}
if (function_exists("gzcompress"))
{
Factory::getLog()->info("GZIP Compression : available (good)");
}
else
{
Factory::getLog()->info("GZIP Compression : n/a (no compression)");
}
$extraNotes = Platform::getInstance()->log_platform_special_directories();
if (!empty($extraNotes) && is_array($extraNotes))
{
if (isset($extraNotes['warnings']) && is_array($extraNotes['warnings']))
{
foreach ($extraNotes['warnings'] as $warning)
{
Factory::getLog()->warning($warning);
}
}
if (isset($extraNotes['errors']) && is_array($extraNotes['errors']))
{
foreach ($extraNotes['errors'] as $error)
{
Factory::getLog()->error($error);
}
if (!empty($extraNotes['errors']))
{
throw new RuntimeException($extraNotes['errors'][0]);
}
}
}
$min_time = $registry->get('akeeba.tuning.min_exec_time');
$max_time = $registry->get('akeeba.tuning.max_exec_time');
$bias = $registry->get('akeeba.tuning.run_time_bias');
Factory::getLog()->info("Min/Max/Bias :" . $min_time . '/' . $max_time . '/' . $bias);
Factory::getLog()->info("Output directory :" . $registry->get('akeeba.basic.output_directory'), ['root_translate' => false]);
Factory::getLog()->info("Part size (bytes) :" . $registry->get('engine.archiver.common.part_size', 0));
Factory::getLog()->info("--------------------------------------------------------------------------------");
}
// Quirks reporting
$quirks = Factory::getConfigurationChecks()->getDetailedStatus(true);
if (!empty($quirks))
{
Factory::getLog()->info("Akeeba Backup has detected the following potential problems:");
foreach ($quirks as $q)
{
Factory::getLog()->info('- ' . $q['code'] . ' ' . $q['description'] . ' (' . $q['severity'] . ')');
}
Factory::getLog()->info("You probably do not have to worry about them, but you should be aware of them.");
Factory::getLog()->info("--------------------------------------------------------------------------------");
}
$phpVersion = PHP_VERSION;
if (version_compare($phpVersion, '7.3.0', 'lt'))
{
Factory::getLog()->warning("You are using PHP $phpVersion which is officially End of Life. We recommend using PHP 7.4 or later for best results. Your version of PHP, $phpVersion, will stop being supported by this backup software in the future.");
}
// Report profile ID
$profile_id = Platform::getInstance()->get_active_profile();
Factory::getLog()->info("Loaded profile #$profile_id");
// Get archive name
[$relativeArchiveName, $absoluteArchiveName] = $this->getArchiveName();
// ==== Stats initialisation ===
$origin = Platform::getInstance()->get_backup_origin(); // Get backup origin
$profile_id = Platform::getInstance()->get_active_profile(); // Get active profile
$registry = Factory::getConfiguration();
$backupType = $registry->get('akeeba.basic.backup_type');
Factory::getLog()->debug("Backup type is now set to '" . $backupType . "'");
// Substitute "variables" in the archive name
$fsUtils = Factory::getFilesystemTools();
$description = $fsUtils->replace_archive_name_variables($this->description);
$comment = $fsUtils->replace_archive_name_variables($this->comment);
if ($registry->get('volatile.writer.store_on_server', true))
{
// Archive files are stored on our server
$stat_relativeArchiveName = $relativeArchiveName;
$stat_absoluteArchiveName = $absoluteArchiveName;
}
else
{
// Archive files are not stored on our server (FTP backup, cloud backup, sent by email, etc)
$stat_relativeArchiveName = '';
$stat_absoluteArchiveName = '';
}
$kettenrad = Factory::getKettenrad();
$temp = [
'description' => $description,
'comment' => $comment,
'backupstart' => Platform::getInstance()->get_timestamp_database(),
'status' => 'run',
'origin' => $origin,
'type' => $backupType,
'profile_id' => $profile_id,
'archivename' => $stat_relativeArchiveName,
'absolute_path' => $stat_absoluteArchiveName,
'multipart' => 0,
'filesexist' => 1,
'tag' => $kettenrad->getTag(),
'backupid' => $kettenrad->getBackupId(),
];
// Save the entry
$statistics = Factory::getStatistics();
$statistics->setStatistics($temp);
$statistics->release_multipart_lock();
// Initialize the archive.
if (Factory::getEngineParamsProvider()->getScriptingParameter('core.createarchive', true))
{
Factory::getLog()->debug("Expanded archive file name: " . $absoluteArchiveName);
Factory::getLog()->debug("Initializing archiver engine");
$archiver = Factory::getArchiverEngine();
$archiver->initialize($absoluteArchiveName);
$archiver->setComment($comment); // Add the comment to the archive itself.
}
$this->setState(self::STATE_POSTRUN);
}
/**
* Implements the abstract _finalize method
*
* @return void
*/
protected function _finalize()
{
$this->setState(self::STATE_FINISHED);
}
/**
* Returns the relative and absolute path to the archive
*/
protected function getArchiveName()
{
$registry = Factory::getConfiguration();
// Import volatile scripting keys to the registry
Factory::getEngineParamsProvider()->importScriptingToRegistry();
// Determine the extension
$force_extension = Factory::getEngineParamsProvider()->getScriptingParameter('core.forceextension', null);
if (is_null($force_extension))
{
$archiver = Factory::getArchiverEngine();
$extension = $archiver->getExtension();
}
else
{
$extension = $force_extension;
}
// Get the template name
$templateName = $registry->get('akeeba.basic.archive_name');
Factory::getLog()->debug("Archive template name: $templateName");
/**
* Security: Protect archives in the default backup output directory
*
* If the configured backup output directory is the same as the default backup output directory the following
* actions are taken:
*
* 1. The backup archive name must include [RANDOM]. If it doesn't, '-[RANDOM]' will be appended to it.
* 2. We make sure that the direct web access blocking files .htaccess, web.config, index.html, index.htm and
* index.php exist in that directory. If they do not they will be forcibly added.
*/
$configuredOutputPath = $registry->get('akeeba.basic.output_directory');
$stockDirs = Platform::getInstance()->get_stock_directories();
$defaultOutputPath = $stockDirs['[DEFAULT_OUTPUT]'];
$fsUtils = Factory::getFilesystemTools();
if (@realpath($configuredOutputPath) === @realpath($defaultOutputPath))
{
$this->ensureHasRandom($templateName);
$fsUtils->ensureNoAccess($defaultOutputPath);
}
// Parse all tags
$fsUtils = Factory::getFilesystemTools();
$templateName = $fsUtils->replace_archive_name_variables($templateName);
Factory::getLog()->debug("Expanded template name: $templateName");
$relative_path = $templateName . $extension;
$absolute_path = $fsUtils->TranslateWinPath($configuredOutputPath . DIRECTORY_SEPARATOR . $relative_path);
return [$relative_path, $absolute_path];
}
/**
* Make sure that the archive template name contains the [RANDOM] variable.
*
* @param string $templateName
*
* @return void
*/
protected function ensureHasRandom(&$templateName)
{
if (strpos($templateName, '[RANDOM]') !== false)
{
return;
}
$templateName .= '-[RANDOM]';
}
}

View File

@@ -0,0 +1,237 @@
<?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\Core\Domain;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
/**
* Installer deployment
*/
class Installer extends Part
{
/** @var int Installer image file offset last read */
private $offset;
/** @var int How much installer data I have processed yet */
private $runningSize = 0;
/** @var int Installer image file index last read */
private $xformIndex = 0;
/** @var int Percentage of process done */
private $progress = 0;
/**
* Public constructor
*
* @return void
*/
public function __construct()
{
parent::__construct();
Factory::getLog()->debug(__CLASS__ . " :: New instance");
}
/**
* Implements the _prepare abstract method
*
*/
function _prepare()
{
$archive = Factory::getArchiverEngine();
// Add the backup description and comment in a README.html file in the
// installation directory. This makes it the first file in the archive.
if (!empty($this->installerSettings->readme ?? ''))
{
$data = $this->createReadme();
$archive->addFileVirtual('README.html', $this->installerSettings->installerroot, $data);
}
if (!empty($this->installerSettings->extrainfo ?? ''))
{
$data = $this->createExtrainfo();
$archive->addFileVirtual('extrainfo.json', $this->installerSettings->installerroot, $data);
}
if (!empty($this->installerSettings->password ?? ''))
{
$data = $this->createPasswordFile();
if (!empty($data))
{
$archive->addFileVirtual('password.php', $this->installerSettings->installerroot, $data);
}
}
$this->progress = 0;
// Set our state to prepared
$this->setState(self::STATE_PREPARED);
}
/**
* Implements the _run() abstract method
*/
function _run()
{
if ($this->getState() == self::STATE_POSTRUN)
{
Factory::getLog()->debug(__CLASS__ . " :: Already finished");
$this->setStep('');
$this->setSubstep('');
}
else
{
$this->setState(self::STATE_RUNNING);
}
// Try to step the archiver
$archive = Factory::getArchiverEngine();
$ret = $archive->transformJPA($this->xformIndex, $this->offset);
if ($ret !== false)
{
$this->offset = $ret['offset'];
$this->xformIndex = $ret['index'];
$this->setStep($ret['filename']);
}
// Check for completion
if ($ret['done'])
{
Factory::getLog()->debug(__CLASS__ . ":: archive is initialized");
$this->setState(self::STATE_FINISHED);
}
// Calculate percentage
$this->runningSize += $ret['chunkProcessed'] ?? 0;
if ($ret['filesize'] > 0)
{
$this->progress = $this->runningSize / $ret['filesize'];
}
}
/**
* Implements the _finalize() abstract method
*
*/
function _finalize()
{
$this->setState(self::STATE_FINISHED);
$this->progress = 1;
}
/**
* Implements the progress calculation based on how much of the installer image
* archive we have processed so far.
*/
public function getProgress()
{
return $this->progress;
}
/**
* Creates the contents of an HTML file with the description and comment of
* the backup. This file will be saved as README.html in the installer's root
* directory, as specified by the embedded installer's settings.
*
* @return string The contents of the HTML file.
*/
protected function createReadme()
{
$config = Factory::getConfiguration();
$version = defined('AKEEBABACKUP_VERSION') ? AKEEBABACKUP_VERSION : AKEEBA_VERSION;
$date = defined('AKEEBABACKUP_DATE') ? AKEEBABACKUP_DATE : AKEEBA_DATE;
$pro = defined('AKEEBABACKUP_PRO') ? AKEEBABACKUP_PRO : AKEEBA_PRO;
$lbl_version = $version . ' (' . $date . ')';
$lbl_coreorpro = ($pro == 1) ? 'Professional' : 'Core';
$description = $config->get('volatile.core.description', '');
$comment = $config->get('volatile.core.comment', '');
$config->set('volatile.core.description', null);
$config->set('volatile.core.comment', null);
return <<<ENDHTML
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Akeeba Backup Archive Identity</title>
</head>
<body>
<h1>Backup Description</h1>
<p id="description"><![CDATA[$description]]></p>
<h1>Backup Comment</h1>
<div id="comment">
$comment
</div>
<hr/>
<p>
Akeeba Backup $lbl_coreorpro $lbl_version
</p>
</body>
</html>
ENDHTML;
}
protected function createExtrainfo()
{
$abversion = defined('AKEEBABACKUP_VERSION') ? AKEEBABACKUP_VERSION : AKEEBA_VERSION;
$host = Platform::getInstance()->get_host();
$backupdate = gmdate('Y-m-d H:i:s');
$phpversion = PHP_VERSION;
$rootPath = Platform::getInstance()->get_site_root();
$data = [
'host' => $host,
'backup_date' => $backupdate,
'akeeba_version' => $abversion,
'php_version' => $phpversion,
'root' => $rootPath,
];
$ret = json_encode($data, JSON_PRETTY_PRINT);
return $ret;
}
protected function createPasswordFile()
{
$config = Factory::getConfiguration();
$ret = '';
$password = $config->get('engine.installer.angie.key', '');
if (empty($password))
{
return $ret;
}
$randVal = Factory::getRandval();
$salt = $randVal->generateString(32);
$passhash = md5($password . $salt) . ':' . $salt;
$ret = "<?php\n";
$ret .= "define('AKEEBA_PASSHASH', '" . $passhash . "');\n";
return $ret;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,426 @@
<?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\Core;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Factory;
use Akeeba\Engine\Filter\Base as FilterBase;
use Akeeba\Engine\Platform;
use DirectoryIterator;
use RuntimeException;
/**
* Akeeba filtering feature
*/
class Filters
{
/** @var array An array holding data for all defined filters */
private $filter_registry = [];
/** @var array Hash array with instances of all filters as $filter_name => filter_object */
private $filters = [];
/** @var bool True after the filter clean up has run */
private $cleanup_has_run = false;
/**
* Public constructor, loads filter data and filter classes
*/
public function __construct()
{
// Load filter data from platform's database
Factory::getLog()->debug('Fetching filter data from database');
$this->filter_registry = Platform::getInstance()->load_filters();
// Load platform, plugin and core filters
$this->filters = [];
$locations = [
Factory::getAkeebaRoot() . '/Filter',
];
$platform_paths = Platform::getInstance()->getPlatformDirectories();
foreach ($platform_paths as $p)
{
$locations[] = $p . '/Filter';
}
Factory::getLog()->debug('Loading filters');
foreach ($locations as $folder)
{
if (!@is_dir($folder))
{
continue;
}
if (!@is_readable($folder))
{
continue;
}
$di = new DirectoryIterator($folder);
foreach ($di as $file)
{
if (!$file->isFile())
{
continue;
}
// PHP 5.3.5 and earlier do not support getExtension
if ($file->getExtension() != 'php')
{
continue;
}
$filename = $file->getFilename();
// Skip filter files starting with dot or dash
if (in_array(substr($filename, 0, 1), ['.', '_']))
{
continue;
}
// 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
$bare_name = $file->getBasename('.php');
if (preg_match('/[^a-zA-Z0-9]/', $bare_name))
{
continue;
}
// Extract filter base name
$filter_name = ucfirst($bare_name);
// This is an abstract class; do not try to create instance
if ($filter_name == 'Base')
{
continue;
}
// Skip already loaded filters
if (array_key_exists($filter_name, $this->filters))
{
continue;
}
Factory::getLog()->debug('-- Loading filter ' . $filter_name);
// Add the filter
$this->filters[$filter_name] = Factory::getFilterObject($filter_name);
}
}
// Load platform, plugin and core stacked filters
$locations = [
Factory::getAkeebaRoot() . '/Filter/Stack',
];
$platform_paths = Platform::getInstance()->getPlatformDirectories();
$platform_stack_paths = [];
foreach ($platform_paths as $p)
{
$locations[] = $p . '/Filter';
$locations[] = $p . '/Filter/Stack';
$platform_stack_paths[] = $p . '/Filter/Stack';
}
$config = Factory::getConfiguration();
Factory::getLog()->debug('Loading optional filters');
foreach ($locations as $folder)
{
if (!@is_dir($folder))
{
continue;
}
if (!@is_readable($folder))
{
continue;
}
$di = new DirectoryIterator($folder);
/** @var DirectoryIterator $file */
foreach ($di as $file)
{
if (!$file->isFile())
{
continue;
}
// PHP 5.3.5 and earlier do not support getExtension
// if ($file->getExtension() != 'php')
if (substr($file->getBasename(), -4) != '.php')
{
continue;
}
// 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
$bare_name = strtolower($file->getBasename('.php'));
if (preg_match('/[^A-Za-z0-9]/', $bare_name))
{
continue;
}
// Extract filter base name
if (substr($bare_name, 0, 5) == 'stack')
{
$bare_name = substr($bare_name, 5);
}
$filter_name = 'Stack\\Stack' . ucfirst($bare_name);
// Skip already loaded filters
if (array_key_exists($filter_name, $this->filters))
{
continue;
}
// Make sure the JSON file also exists
if (!file_exists($folder . '/' . $bare_name . '.json'))
{
continue;
}
$key = "core.filters.$bare_name.enabled";
if ($config->get($key, 0))
{
Factory::getLog()->debug('-- Loading optional filter ' . $filter_name);
// Add the filter
$this->filters[$filter_name] = Factory::getFilterObject($filter_name);
}
}
}
}
/**
* Extended filtering information of a given object. Applies only to exclusion filters.
*
* @param string|array $test The string to check for filter status (e.g. filename, dir name, table name, etc)
* @param string $root The exclusion root test belongs to
* @param string $object What type of object is it? dir|file|dbobject
* @param string $subtype Filter subtype (all|content|children)
* @param string $by_filter [out] The filter name which first matched $test, or an empty string
*
* @return bool True if it is a filtered element
*/
public function isFilteredExtended($test, $root, $object, $subtype, &$by_filter)
{
if (!$this->cleanup_has_run)
{
// Loop the filters and clean up those with no data
/**
* @var string $filter_name
* @var FilterBase $filter
*/
foreach ($this->filters as $filter_name => $filter)
{
if (!$filter->hasFilters())
{
unset($this->filters[$filter_name]);
} // Remove empty filters
}
$this->cleanup_has_run = true;
}
$by_filter = '';
if (!empty($this->filters))
{
foreach ($this->filters as $filter_name => $filter)
{
if ($filter->isFiltered($test, $root, $object, $subtype))
{
$by_filter = strtolower($filter_name);
return true;
}
}
// If we are still here, no filter matched
return false;
}
else
{
return false;
}
}
/**
* Returns the filtering status of a given object
*
* @param string|array $test The string to check for filter status (e.g. filename, dir name, table name, etc)
* @param string $root The exclusion root test belongs to
* @param string $object What type of object is it? dir|file|dbobject
* @param string $subtype Filter subtype (all|content|children)
*
* @return bool True if it is a filtered element
*/
public function isFiltered($test, $root, $object, $subtype)
{
$by_filter = '';
return $this->isFilteredExtended($test, $root, $object, $subtype, $by_filter);
}
/**
* Returns the inclusion filters for a specific object type
*
* @param string $object The inclusion object (dir|db)
*
* @return array
*/
public function &getInclusions($object)
{
$inclusions = [];
if (!empty($this->filters))
{
/**
* @var string $filter_name
* @var FilterBase $filter
*/
foreach ($this->filters as $filter_name => $filter)
{
if (!is_object($filter))
{
throw new RuntimeException("Object for filter $filter_name not found. The engine will now crash.");
}
$new_inclusions = $filter->getInclusions($object);
if (!empty($new_inclusions))
{
$inclusions = array_merge($inclusions, $new_inclusions);
}
}
}
return $inclusions;
}
/**
* Returns the filter registry information for a specified filter class
*
* @param string $filter_name The name of the filter we want data for
*
* @return array The filter data for the requested filter
*/
public function &getFilterData($filter_name)
{
if (array_key_exists($filter_name, $this->filter_registry))
{
return $this->filter_registry[$filter_name];
}
else
{
$dummy = [];
return $dummy;
}
}
/**
* Replaces the filter data of a specific filter with the new data
*
* @param string $filter_name The filter for which to modify the stored data
* @param string $data The new data
*/
public function setFilterData($filter_name, &$data)
{
$this->filter_registry[$filter_name] = $data;
}
/**
* Saves all filters to the platform defined database
*
* @return bool True on success
*/
public function save()
{
return Platform::getInstance()->save_filters($this->filter_registry);
}
/**
* Get SQL statements to append to the database backup file
*
* @param string $root
*
* @return array
*/
public function getExtraSQL(string $root): array
{
if (count($this->filters) < 1)
{
return [];
}
$ret = [];
/**
* @var FilterBase $filter
*/
foreach ($this->filters as $filter)
{
$ret = array_merge($ret, $filter->getExtraSQL($root));
}
return $ret;
}
/**
* Checks if there is an active filter for the object/subtype requested.
*
* @param string $object The filtering object: dir|file|dbobject|db
* @param string $subtype The filtering subtype: all|content|children|inclusion
*
* @return bool
*/
public function hasFilterType($object, $subtype = null)
{
foreach ($this->filters as $filter_name => $filter)
{
if ($filter->object == $object)
{
if (is_null($subtype))
{
return true;
}
elseif ($filter->subtype == $subtype)
{
return true;
}
}
}
return false;
}
/**
* Resets all filters, reverting them to a blank state
*
* @return void
*
* @since 5.4.0
*/
public function reset()
{
$this->filter_registry = [];
}
}

View File

@@ -0,0 +1,818 @@
<?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\Core;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Exception;
use RuntimeException;
use Throwable;
/**
* Kettenrad is the main controller of Akeeba Engine. It's responsible for setting the engine into motion, running each
* and all domain objects to their completion.
*/
class Kettenrad extends Part
{
/**
* Set to true when deadOnTimeout is registered as a shutdown function
*
* @var bool
*/
public static $registeredShutdownCallback = false;
/**
* Set to true when akeebaBackupErrorHandler is registered as an error handler
*
* @var bool
*/
public static $registeredErrorHandler = false;
/**
* Cached copy of the response array
*
* @var array
*/
private $array_cache = null;
/**
* The list of remaining steps
*
* @var array
*/
private $domain_chain = [];
/**
* The current domain's name
*
* @var string
*/
private $domain = '';
/**
* The active domain's class name
*
* @var string
*/
private $class = '';
/**
* The current backup's tag (actually: the backup's origin)
*
* @var string
*/
private $tag = null;
/**
* How many steps the domain_chain array contained when the backup began. Used for percentage calculations.
*
* @var int
*/
private $total_steps = 0;
/**
* A unique backup ID which allows us to run multiple parallel backups using the same backup origin (tag)
*
* @var string
*/
private $backup_id = '';
/**
* Set to true when there are warnings available when getStatusArray() is called. This is used at the end of the
* backup to send a different push message depending on whether the backup completed with or without warnings.
*
* @var bool
*/
private $warnings_issued = false;
/**
* Kettenrad constructor.
*
* Overrides the Part constructor to initialize Kettenrad-specific properties.
*
* @return void
*/
public function __construct()
{
parent::__construct();
// Register the error handler
if (!static::$registeredErrorHandler)
{
static::$registeredErrorHandler = true;
set_error_handler('\\Akeeba\\Engine\\Core\\akeebaEngineErrorHandler');
}
}
/**
* Returns the unique Backup ID
*
* @return string
*/
public function getBackupId()
{
return $this->backup_id;
}
/**
* Sets the unique backup ID.
*
* @param string $backup_id
*
* @return void
*/
public function setBackupId($backup_id = null)
{
$this->backup_id = $backup_id;
}
/**
* Returns the current backup tag. If none is specified, it sets it to be the
* same as the current backup origin and returns the new setting.
*
* @return string
*/
public function getTag()
{
if (empty($this->tag))
{
// If no tag exists, we resort to the pre-set backup origin
$tag = Platform::getInstance()->get_backup_origin();
$this->tag = $tag;
}
return $this->tag;
}
/**
* The public interface to Kettenrad.
*
* Internally it calls Part::tick(), wrapped in a try-catch block which traps any runaway Exception (PHP 5) or
* Throwable we didn't manage to successfully suppress yet.
*
* @param int $nesting
*
* @return array A response array
*/
public function tick($nesting = 0)
{
$ret = null;
$e = null;
// PHP 7.x -- catch any unhandled Throwable, including PHP fatal errors
try
{
$ret = parent::tick($nesting);
}
catch (Throwable $e)
{
$this->setState(self::STATE_ERROR);
$this->lastException = $e;
}
// If an error occurred we don't have a return table. If that's the case create one and do log our errors.
if (!isset($ret))
{
// Log the existence of an unhandled exception
Factory::getLog()->warning("Kettenrad :: Caught unhandled exception. The backup will now fail.");
// Recursively log unhandled exceptions
self::logErrorsFromException($e);
// Create the missing return table
$ret = $this->makeReturnTable();
$this->array_cache = array_merge(is_null($this->array_cache) ? [] : $this->array_cache, $ret);
}
return $ret;
}
/**
* Returns a copy of the class's status array
*
* @return array
*/
public function getStatusArray()
{
// Get the cached array
if (!empty($this->array_cache))
{
return $this->array_cache;
}
// Get the default table
$array = $this->makeReturnTable();
// Add the warnings
$array['Warnings'] = Factory::getLog()->getWarnings();
// Did we have warnings?
if (is_array($array['Warnings']) || $array['Warnings'] instanceof \Countable ? count($array['Warnings']) : 0)
{
$this->warnings_issued = true;
}
// Get the current step number
$stepCounter = Factory::getConfiguration()->get('volatile.step_counter', 0);
// Add the archive name
$statistics = Factory::getStatistics();
$record = $statistics->getRecord();
$array['Archive'] = $record['archivename'] ?? '';
// Translate HasRun to what the rest of the suite expects
$array['HasRun'] = ($this->getState() == self::STATE_FINISHED) ? 1 : 0;
$array['Error'] = is_null($array['ErrorException']) ? '' : $array['Error'];
$array['tag'] = $this->tag;
$array['Progress'] = $this->getProgress();
$array['backupid'] = $this->getBackupId();
$array['sleepTime'] = $this->waitTimeMsec;
$array['stepNumber'] = $stepCounter;
$array['stepState'] = $this->stateToString($this->getState());
$this->array_cache = $array;
return $this->array_cache;
}
/**
* Gets the percentage of the backup process done so far.
*
* @return string
*/
public function getProgress()
{
// Get the overall percentage (based on domains complete so far)
$remainingSteps = count($this->domain_chain) + 1;
$totalSteps = max($this->total_steps, 1);
$overall = 1 - ($remainingSteps / $totalSteps);
// How much is this step worth?
$currentStepMaxContribution = 1 / $totalSteps;
// Get the percentage reported from the domain object, zero if we can't get a domain object.
$object = !empty($this->class) ? Factory::getDomainObject($this->class) : null;
$local = is_object($object) ? $object->getProgress() : 0;
// Calculate the percentage and apply [0, 100] bounds.
$percentage = (int) (100 * ($overall + $local * $currentStepMaxContribution));
$percentage = max(0, $percentage);
$percentage = min(100, $percentage);
return $percentage;
}
/**
* Obsolete method.
*
* @deprecated 7.0
*/
public function resetWarnings()
{
Factory::getLog()->debug('DEPRECATED: Akeeba Engine consumers must remove calls to resetWarnings()');
}
/**
* Initialization. Sets the state to STATE_PREPARED.
*
* @return void
*/
protected function _prepare()
{
// Initialize the timer class. Do not remove, even though we don't use the object it needs to be initialized!
$timer = Factory::getTimer();
// Do we have a tag?
if (!empty($this->_parametersArray['tag']))
{
$this->tag = $this->_parametersArray['tag'];
}
// Make sure a tag exists (or create a new one)
$this->tag = $this->getTag();
// Reset the log
$logTag = $this->getLogTag();
Factory::getLog()->open($logTag);
Factory::getLog()->reset($logTag);
// Reset the storage
$factoryStorageTag = $this->tag . (empty($this->backup_id) ? '' : ('.' . $this->backup_id));
Factory::getFactoryStorage()->reset($factoryStorageTag);
// Apply the configuration overrides
$overrides = Platform::getInstance()->configOverrides;
if (is_array($overrides) && @count($overrides))
{
$registry = Factory::getConfiguration();
$protected_keys = $registry->getProtectedKeys();
$registry->resetProtectedKeys();
foreach ($overrides as $k => $v)
{
$registry->set($k, $v);
}
$registry->setProtectedKeys($protected_keys);
}
// Get the domain chain
$this->domain_chain = Factory::getEngineParamsProvider()->getDomainChain();
$this->total_steps = count($this->domain_chain) - 1; // Init shouldn't count in the progress bar
// Mark this engine for Nesting Logging
$this->nest_logging = true;
// Preparation is over
$this->array_cache = null;
$this->setState(self::STATE_PREPARED);
// Send a push message to mark the start of backup
$platform = Platform::getInstance();
$timeStamp = date($platform->translate('DATE_FORMAT_LC2'));
$pushSubject = sprintf($platform->translate('COM_AKEEBA_PUSH_STARTBACKUP_SUBJECT'), $platform->get_site_name(), $platform->get_host());
$pushDetails = sprintf($platform->translate('COM_AKEEBA_PUSH_STARTBACKUP_BODY'), $platform->get_site_name(), $platform->get_host(), $timeStamp, $this->getLogTag());
Factory::getPush()->message($pushSubject, $pushDetails);
}
/**
* Main backup process. Sets the state to STATE_RUNNING or STATE_POSTRUN.
*
* @return void
*/
protected function _run()
{
$result = null;
$logTag = $this->getLogTag();
$logger = Factory::getLog();
$logger->open($logTag);
// Maybe we're already done or in an error state?
if (in_array($this->getState(), [self::STATE_POSTRUN, self::STATE_ERROR]))
{
return;
}
// Set running state
$this->setState(self::STATE_RUNNING);
// Do I even have enough time...?
$timer = Factory::getTimer();
$registry = Factory::getConfiguration();
if (($timer->getTimeLeft() <= 0))
{
// We need to set the break flag for the part processing to not batch successive steps
$registry->set('volatile.breakflag', true);
return;
}
// Initialize operation counter
$registry->set('volatile.operation_counter', 0);
// Advance step counter
$stepCounter = $registry->get('volatile.step_counter', 0);
$registry->set('volatile.step_counter', ++$stepCounter);
// Log step start number
$logger->debug('====== Starting Step number ' . $stepCounter . ' ======');
if (defined('AKEEBADEBUG'))
{
$root = Platform::getInstance()->get_site_root();
$logger->debug('Site root: ' . $root);
}
$finished = false;
$error = false;
// BREAKFLAG is optionally passed by domains to force-break current operation
$breakFlag = false;
// Apply an infinite time limit if required
if ($registry->get('akeeba.tuning.settimelimit', 0))
{
if (function_exists('set_time_limit'))
{
set_time_limit(0);
}
}
// Update statistics, marking the backup as currently processing a backup step.
Factory::getStatistics()->updateInStep(true);
// Loop until time's up, we're done or an error occurred, or BREAKFLAG is set
$this->array_cache = null;
$object = null;
while (($timer->getTimeLeft() > 0) && (!$finished) && (!$error) && (!$breakFlag))
{
// Reset the break flag
$registry->set('volatile.breakflag', false);
// Do we have to switch domains? This only happens if there is no active
// domain, or the current domain has finished
$have_to_switch = false;
$object = null;
if ($this->class == '')
{
$have_to_switch = true;
}
else
{
$object = Factory::getDomainObject($this->class);
if (!is_object($object))
{
$have_to_switch = true;
}
elseif (!in_array('getState', get_class_methods($object)))
{
$have_to_switch = true;
}
elseif ($object->getState() == self::STATE_FINISHED)
{
$have_to_switch = true;
}
}
// Switch domain if necessary
if ($have_to_switch)
{
$logger->debug('Kettenrad :: Switching domains');
if (!Factory::getConfiguration()->get('akeeba.tuning.nobreak.domains', 0))
{
$logger->debug("Kettenrad :: BREAKING STEP BEFORE SWITCHING DOMAIN");
$registry->set('volatile.breakflag', true);
}
// Free last domain
$object = null;
if (empty($this->domain_chain))
{
// Aw, we're done! No more domains to run.
$this->setState(self::STATE_POSTRUN);
$logger->debug("Kettenrad :: No more domains to process");
$logger->debug('====== Finished Step number ' . $stepCounter . ' ======');
$this->array_cache = null;
return;
}
// Shift the next definition off the stack
$this->array_cache = null;
$new_definition = array_shift($this->domain_chain);
if (array_key_exists('class', $new_definition))
{
$logger->debug("Switching to domain {$new_definition['domain']}, class {$new_definition['class']}");
$this->domain = $new_definition['domain'];
$this->class = $new_definition['class'];
// Get a working object
$object = Factory::getDomainObject($this->class);
$object->setup($this->_parametersArray);
}
else
{
$logger->warning("Kettenrad :: No class defined trying to switch domains. The backup will crash.");
$this->domain = null;
$this->class = null;
}
}
elseif (!is_object($object))
{
$logger->debug("Kettenrad :: Getting domain object of class {$this->class}");
$object = Factory::getDomainObject($this->class);
}
// Tick the object
$logger->debug('Kettenrad :: Ticking the domain object');
$this->lastException = null;
try
{
// We ask the domain object to execute and return its output array
$result = $object->tick();
$hasErrorException = array_key_exists('ErrorException', $result) && is_object($result['ErrorException']);
$hasErrorString = array_key_exists('Error', $result) && !empty($result['Error']);
/**
* Legacy objects may not be throwing exceptions on error, instead returning an Error string in the
* output array. The code below addresses this discrepancy.
*/
if (!$hasErrorException && $hasErrorString)
{
$result['ErrorException'] = new RuntimeException($result['Error']);
$hasErrorException = true;
}
/**
* Some domain objects may be acting as nested Parts, e.g. the Database domain. In this case the
* internal Engine (itself a Part object) is absorbing the thrown exception and relays it in the output
* table's ErrorException key. This means that the code above will NOT catch the error. This code below
* addresses that situation by rethrowing the exception.
*
* Practical example: cannot connect to MySQL is thrown by the MySQL Dump engine. The Native database
* backup engine absorbs the exception and reports it back to the Database domain object through the
* returned output array. However, the Database domain object does not rethrow it, simply relaying it
* back to Kettenrad through its own returned output array. As a result we enter an infinite loop where
* Kettenrad asks the Database domain to tick, it asks the Native engine to tick which asks the MySQL
* Dump object to tick. However the latter fails again to connect to MySQL and the whole process is
* repeated ad nauseam. By rethrowing the propagated ErrorException we alleviate this problem.
*/
if ($hasErrorException)
{
throw $result['ErrorException'];
}
$logger->debug('Kettenrad :: Domain object returned without errors; propagating');
}
catch (Exception $e)
{
/**
* Exceptions are used to propagate error conditions through the engine. Catching them and storing them
* in $this->lastException lets us detect and report the error condition in Kettenrad, the integration-
* facing interface of the backup engine.
*/
$this->lastException = $e;
$logger->debug('Kettenrad :: Domain object returned with errors; propagating');
self::logErrorsFromException($this->lastException);
$this->setState(self::STATE_ERROR);
}
// Advance operation counter
$currentOperationNumber = $registry->get('volatile.operation_counter', 0);
$currentOperationNumber++;
$registry->set('volatile.operation_counter', $currentOperationNumber);
// Process return array
$this->setDomain($this->domain);
$this->setStep($result['Step']);
$this->setSubstep($result['Substep']);
// Check for BREAKFLAG
$breakFlag = $registry->get('volatile.breakflag', false);
$logger->debug("Kettenrad :: Break flag status: " . ($breakFlag ? 'YES' : 'no'));
// Process errors
$error = $this->getState() === self::STATE_ERROR;
// Check if the backup procedure should finish now
$finished = $error ? true : !($result['HasRun']);
// Log operation end
$logger->debug('----- Finished operation ' . $currentOperationNumber . ' ------');
}
// Log the result
$objectStepType = is_object($object) ? get_class($object) : 'INVALID OBJECT';
if (!is_object($object))
{
$reason = ($timer->getTimeLeft() <= 0)
? 'we already ran out of time'
: 'a step break has already been requested';
$logger->debug(sprintf(
"Finishing step immediately because %s", $reason
));
}
elseif (!$error)
{
$logger->debug("Successful Smart algorithm on " . $objectStepType);
}
else
{
$logger->error("Failed Smart algorithm on " . $objectStepType);
}
// Log if we have to do more work or not
/**
* The domain object is not set in the following cases:
*
* - There is no time left, the while loop never ran.
* - The break flag was already set, the while loop never ran.
* - We are already finished, the while loop never ran. Shouldn't happen, the step status is set to POSTRUN.
* - There was an error, the while loop never ran. Shouldn't happen, we return immediately upon an error.
* - We tried to go to the next domain but something went wrong. Shouldn't happen.
*
* If we get to a condition that shouldn't happen we will throw a Runtime exception. In any other case we let
* the step finish.
*/
if (!is_object($object) && ($timer->getTimeLeft() > 0) && !$breakFlag)
{
throw new RuntimeException(sprintf(
"Kettenrad :: Empty object found when processing domain '%s'. This should never happen.",
$this->domain
));
}
/** @noinspection PhpStatementHasEmptyBodyInspection */
elseif (!is_object($object))
{
// This is an expected case.
// I have to use an empty case because $object->getState() below would cause a PHP error on a NULL variable.
}
elseif ($object->getState() == self::STATE_RUNNING)
{
$logger->debug("Kettenrad :: More work required in domain '" . $this->domain . "'");
// We need to set the break flag for the part processing to not batch successive steps
$registry->set('volatile.breakflag', true);
}
elseif ($object->getState() == self::STATE_FINISHED)
{
$logger->debug("Kettenrad :: Domain '" . $this->domain . "' has finished.");
$registry->set('volatile.breakflag', false);
}
elseif ($object->getState() == self::STATE_ERROR)
{
$logger->debug("Kettenrad :: Domain '" . $this->domain . "' has experienced an error.");
$registry->set('volatile.breakflag', false);
}
// Log step end
$logger->debug('====== Finished Step number ' . $stepCounter . ' ======');
// Update statistics, marking the backup as having just finished processing a backup step.
Factory::getStatistics()->updateInStep(false);
if (!$registry->get('akeeba.tuning.nobreak.domains', 0))
{
// Force break between steps
$logger->debug('Kettenrad :: Setting the break flag between domains');
$registry->set('volatile.breakflag', true);
}
}
/**
* Finalization. Sets the state to STATE_FINISHED.
*
* @return void
*/
protected function _finalize()
{
// Open the log
$logTag = $this->getLogTag();
Factory::getLog()->open($logTag);
// Kill the cached array
$this->array_cache = null;
// Remove the memory file
$tempVarsTag = $this->tag . (empty($this->backup_id) ? '' : ('.' . $this->backup_id));
Factory::getFactoryStorage()->reset($tempVarsTag);
// All done.
Factory::getLog()->debug("Kettenrad :: Just finished");
$this->setState(self::STATE_FINISHED);
// Send a push message to mark the end of backup
$pushSubjectKey = $this->warnings_issued ? 'COM_AKEEBA_PUSH_ENDBACKUP_WARNINGS_SUBJECT' : 'COM_AKEEBA_PUSH_ENDBACKUP_SUCCESS_SUBJECT';
$pushBodyKey = $this->warnings_issued ? 'COM_AKEEBA_PUSH_ENDBACKUP_WARNINGS_BODY' : 'COM_AKEEBA_PUSH_ENDBACKUP_SUCCESS_BODY';
$platform = Platform::getInstance();
$timeStamp = date($platform->translate('DATE_FORMAT_LC2'));
$pushSubject = sprintf($platform->translate($pushSubjectKey), $platform->get_site_name(), $platform->get_host());
$pushDetails = sprintf($platform->translate($pushBodyKey), $platform->get_site_name(), $platform->get_host(), $timeStamp);
Factory::getPush()->message($pushSubject, $pushDetails);
}
/**
* Returns the tag used to open the correct log file
*
* @return string
*/
protected function getLogTag()
{
$tag = $this->getTag();
if (!empty($this->backup_id))
{
$tag .= '.' . $this->backup_id;
}
return $tag;
}
}
/**
* Timeout error handler
*/
function akeebaEnginePHPTimeoutHandler()
{
if (connection_status() == 1)
{
Factory::getLog()->error('The process was aborted on user\'s request');
return;
}
if (connection_status() >= 2)
{
Factory::getLog()->error('Akeeba Backup has timed out. Please read the documentation.');
return;
}
}
// Register the timeout error handler
if (!Kettenrad::$registeredShutdownCallback)
{
Kettenrad::$registeredShutdownCallback = true;
register_shutdown_function("\\Akeeba\\Engine\\Core\\akeebaEnginePHPTimeoutHandler");
}
/**
* Custom PHP error handler to log catchable PHP errors to the backup log file
*
* @param int $errno
* @param string $errstr
* @param string $errfile
* @param int $errline
*
* @return bool|null
*/
function akeebaEngineErrorHandler($errno, $errstr, $errfile, $errline)
{
// Sanity check
if (!function_exists('error_reporting'))
{
return false;
}
// Do not proceed if the error springs from an @function() construct, or if
// the overall error reporting level is set to report no errors.
$error_reporting = error_reporting();
if ($error_reporting == 0)
{
return false;
}
switch ($errno)
{
case E_ERROR:
case E_USER_ERROR:
case E_RECOVERABLE_ERROR:
/**
* This will only work for E_RECOVERABLE_ERROR and E_USER_ERROR, not E_ERROR. In PHP 7 all errors throw an
* Error throwable (a special kind of exception) which propagates nicely within our architecture.
*/
Factory::getLog()->error("PHP FATAL ERROR on line $errline in file $errfile:");
Factory::getLog()->error($errstr);
Factory::getLog()->error("Execution aborted due to PHP fatal error");
break;
case E_WARNING:
case E_USER_WARNING:
// Log as debug messages so that we don't spook the user with warnings
Factory::getLog()->debug("PHP WARNING (not an error; you can ignore) on line $errline in file $errfile:");
Factory::getLog()->debug($errstr);
break;
case E_NOTICE:
case E_USER_NOTICE:
// Log as debug messages so that we don't spook the user with notices
Factory::getLog()->debug("PHP NOTICE (not an error; you can ignore) on line $errline in file $errfile:");
Factory::getLog()->debug($errstr);
break;
case E_DEPRECATED:
case E_USER_DEPRECATED:
// Log as debug messages so that we don't spook the user with deprecated notices
Factory::getLog()->debug("PHP DEPRECATED (not an error; you can ignore) on line $errline in file $errfile:");
Factory::getLog()->debug($errstr);
break;
default:
// These are E_DEPRECATED, E_STRICT etc. Let PHP handle them
return false;
break;
}
// Uncomment to prevent the execution of PHP's internal error handler
//return true;
// Let PHP's internal error handler take care of the error.
return false;
}

View File

@@ -0,0 +1,205 @@
<?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\Core;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Factory;
/**
* Timer class
*/
class Timer
{
/** @var float Maximum execution time allowance per step */
private $max_exec_time = null;
/** @var int Timestamp of execution start */
private $start_time = null;
/**
* Public constructor, creates the timer object and calculates the execution time limits
*
* @param int|null $maxExecTime Maximum execution time, in seconds (minimum 1 second)
* @param int|null $bias Execution time bias, in percentage points (10-100)
*/
public function __construct(?int $maxExecTime = null, ?int $bias = null)
{
// Initialize start time
$this->start_time = $this->microtime_float();
// Make sure we have max execution time and execution time bias or use the ones configured in the backup profile
$configuration = Factory::getConfiguration();
$maxExecTime = $maxExecTime ?? (int) $configuration->get('akeeba.tuning.max_exec_time', 14);
$bias = $bias ?? (int) $configuration->get('akeeba.tuning.run_time_bias', 75);
// Make sure both max exec time and bias are positive integers within the allowed range of values
$maxExecTime = max(1, $maxExecTime);
$bias = min(100, max(10, $bias));
$this->max_exec_time = $maxExecTime * $bias / 100;
}
/**
* Wake-up function to reset internal timer when we get unserialized
*/
public function __wakeup()
{
// Re-initialize start time on wake-up
$this->start_time = $this->microtime_float();
}
/**
* Gets the number of seconds left, before we hit the "must break" threshold
*
* @return float
*/
public function getTimeLeft()
{
return $this->max_exec_time - $this->getRunningTime();
}
/**
* Gets the time elapsed since object creation/unserialization, effectively how
* long Akeeba Engine has been processing data
*
* @return float
*/
public function getRunningTime()
{
return $this->microtime_float() - $this->start_time;
}
/**
* Enforce the minimum execution time
*
* @param bool $log Should I log what I'm doing? Default is true.
* @param bool $serverSideSleep Should I sleep on the server side? If false we return the amount of time to
* wait in msec
*
* @return int Wait time to reach min_execution_time in msec
*/
public function enforce_min_exec_time($log = true, $serverSideSleep = true)
{
// Try to get a sane value for PHP's maximum_execution_time INI parameter
if (@function_exists('ini_get'))
{
$php_max_exec = @ini_get("maximum_execution_time");
}
else
{
$php_max_exec = 10;
}
if (($php_max_exec == "") || ($php_max_exec == 0))
{
$php_max_exec = 10;
}
// Decrease $php_max_exec time by 500 msec we need (approx.) to tear down
// the application, as well as another 500msec added for rounding
// error purposes. Also make sure this is never gonna be less than 0.
$php_max_exec = max($php_max_exec * 1000 - 1000, 0);
// Get the "minimum execution time per step" Akeeba Backup configuration variable
$configuration = Factory::getConfiguration();
$minexectime = $configuration->get('akeeba.tuning.min_exec_time', 0);
if (!is_numeric($minexectime))
{
$minexectime = 0;
}
// Make sure we are not over PHP's time limit!
if ($minexectime > $php_max_exec)
{
$minexectime = $php_max_exec;
}
// Get current running time
$elapsed_time = $this->getRunningTime() * 1000;
$clientSideSleep = 0;
// Only run a sleep delay if we haven't reached the minexectime execution time
if (($minexectime > $elapsed_time) && ($elapsed_time > 0))
{
$sleep_msec = (int)($minexectime - $elapsed_time);
if (!$serverSideSleep)
{
Factory::getLog()->debug("Asking client to sleep for $sleep_msec msec");
$clientSideSleep = $sleep_msec;
}
elseif (function_exists('usleep'))
{
if ($log)
{
Factory::getLog()->debug("Sleeping for $sleep_msec msec, using usleep()");
}
usleep(1000 * $sleep_msec);
}
elseif (function_exists('time_nanosleep'))
{
if ($log)
{
Factory::getLog()->debug("Sleeping for $sleep_msec msec, using time_nanosleep()");
}
$sleep_sec = floor($sleep_msec / 1000);
$sleep_nsec = 1000000 * ($sleep_msec - ($sleep_sec * 1000));
time_nanosleep($sleep_sec, $sleep_nsec);
}
elseif (function_exists('time_sleep_until'))
{
if ($log)
{
Factory::getLog()->debug("Sleeping for $sleep_msec msec, using time_sleep_until()");
}
$until_timestamp = time() + $sleep_msec / 1000;
time_sleep_until($until_timestamp);
}
elseif (function_exists('sleep'))
{
$sleep_sec = ceil($sleep_msec / 1000);
if ($log)
{
Factory::getLog()->debug("Sleeping for $sleep_sec seconds, using sleep()");
}
sleep($sleep_sec);
}
}
elseif ($elapsed_time > 0)
{
// No sleep required, even if user configured us to be able to do so.
if ($log)
{
Factory::getLog()->debug("No need to sleep; execution time: $elapsed_time msec; min. exec. time: $minexectime msec");
}
}
return $clientSideSleep;
}
/**
* Reset the timer. It should only be used in CLI mode!
*/
public function resetTime()
{
$this->start_time = $this->microtime_float();
}
/**
* Returns the current timestamp in decimal seconds
*/
protected function microtime_float()
{
[$usec, $sec] = explode(" ", microtime());
return ((float) $usec + (float) $sec);
}
}

View File

@@ -0,0 +1,76 @@
{
"volatile.akeebaengine.domains": "init|installer|packdb|packing|finale",
"volatile.akeebaengine.scripts": "full|dbonly|fileonly|alldb|incfile|incfull",
"volatile.domain.init.domain": "init",
"volatile.domain.init.class": "Init",
"volatile.domain.init.text": "COM_AKEEBA_BACKUP_LABEL_DOMAIN_INIT",
"volatile.domain.installer.domain": "installer",
"volatile.domain.installer.class": "Installer",
"volatile.domain.installer.text": "COM_AKEEBA_BACKUP_LABEL_DOMAIN_INSTALLER",
"volatile.domain.packdb.domain": "PackDB",
"volatile.domain.packdb.class": "Db",
"volatile.domain.packdb.text": "COM_AKEEBA_BACKUP_LABEL_DOMAIN_PACKDB",
"volatile.domain.packing.domain": "Packing",
"volatile.domain.packing.class": "Pack",
"volatile.domain.packing.text": "COM_AKEEBA_BACKUP_LABEL_DOMAIN_PACKING",
"volatile.domain.finale.domain": "finale",
"volatile.domain.finale.class": "Finalization",
"volatile.domain.finale.text": "COM_AKEEBA_BACKUP_LABEL_DOMAIN_FINISHED",
"volatile.scripting.full.chain": "init|installer|packdb|packing|finale",
"volatile.scripting.full.text": "COM_AKEEBA_CONFIG_BACKUPTYPE_FULL",
"volatile.scripting.full.db.saveasname": "normal",
"volatile.scripting.full.db.databasesini": "1",
"volatile.scripting.full.db.skipextradb": "0",
"volatile.scripting.full.db.abstractnames": "1",
"volatile.scripting.full.db.dropstatements": "0",
"volatile.scripting.full.db.delimiterstatements": "0",
"volatile.scripting.full.core.createarchive": "1",
"volatile.scripting.dbonly.chain": "init|packdb|finale",
"volatile.scripting.dbonly.text": "COM_AKEEBA_CONFIG_BACKUPTYPE_DBONLY",
"volatile.scripting.dbonly.db.saveasname": "output",
"volatile.scripting.dbonly.db.databasesini": "0",
"volatile.scripting.dbonly.db.skipextradb": "1",
"volatile.scripting.dbonly.db.abstractnames": "0",
"volatile.scripting.dbonly.db.dropstatements": "1",
"volatile.scripting.dbonly.db.delimiterstatements": "1",
"volatile.scripting.dbonly.core.forceextension": ".sql",
"volatile.scripting.dbonly.core.createarchive": "0",
"volatile.scripting.fileonly.chain": "init|packing|finale",
"volatile.scripting.fileonly.text": "COM_AKEEBA_CONFIG_BACKUPTYPE_FILEONLY",
"volatile.scripting.fileonly.core.createarchive": "1",
"volatile.scripting.alldb.chain": "init|installer|packdb|finale",
"volatile.scripting.alldb.text": "COM_AKEEBA_CONFIG_BACKUPTYPE_ALLDB",
"volatile.scripting.alldb.db.tempfile": "temporary",
"volatile.scripting.alldb.db.saveasname": "normal",
"volatile.scripting.alldb.db.databasesini": "1",
"volatile.scripting.alldb.db.skipextradb": "0",
"volatile.scripting.alldb.db.abstractnames": "1",
"volatile.scripting.alldb.db.dropstatements": "0",
"volatile.scripting.alldb.db.delimiterstatements": "0",
"volatile.scripting.alldb.db.finalizearchive": "1",
"volatile.scripting.alldb.core.createarchive": "1",
"volatile.scripting.incfile.chain": "init|packing|finale",
"volatile.scripting.incfile.text": "COM_AKEEBA_CONFIG_BACKUPTYPE_INCFILE",
"volatile.scripting.incfile.filter.incremental": "1",
"volatile.scripting.incfull.chain": "init|installer|packdb|packing|finale",
"volatile.scripting.incfull.text": "COM_AKEEBA_CONFIG_BACKUPTYPE_INCFULL",
"volatile.scripting.incfull.db.saveasname": "normal",
"volatile.scripting.incfull.db.databasesini": "1",
"volatile.scripting.incfull.db.skipextradb": "0",
"volatile.scripting.incfull.db.abstractnames": "1",
"volatile.scripting.incfull.db.dropstatements": "0",
"volatile.scripting.incfull.db.delimiterstatements": "0",
"volatile.scripting.incfull.core.createarchive": "1",
"volatile.scripting.incfull.filter.incremental": "1"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,612 @@
<?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\Driver;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Driver\Query\Mysqli as QueryMysqli;
use mysqli_result;
use RuntimeException;
/**
* MySQL Improved (mysqli) database driver for Akeeba Engine
*
* Based on Joomla! Platform 11.2
*/
class Mysqli extends Mysql
{
/**
* The name of the database driver.
*
* @var string
* @since 11.1
*/
public $name = 'mysqli';
/** @var \mysqli|null The db connection resource */
protected $connection = '';
/** @var mysqli_result|null The database connection cursor from the last query. */
protected $cursor;
protected $port;
protected $socket;
/** @var bool Are we in the process of reconnecting to the database server? */
private $isReconnecting = false;
/**
* Database object constructor
*
* @param array $options List of options used to configure the connection
*/
public function __construct($options)
{
$this->driverType = 'mysql';
// Init
$this->nameQuote = '`';
$host = array_key_exists('host', $options) ? $options['host'] : 'localhost';
$port = array_key_exists('port', $options) ? $options['port'] : '';
$user = array_key_exists('user', $options) ? $options['user'] : '';
$password = array_key_exists('password', $options) ? $options['password'] : '';
$database = array_key_exists('database', $options) ? $options['database'] : '';
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
$select = array_key_exists('select', $options) ? $options['select'] : true;
$socket = null;
// Figure out if a port is included in the host name
$this->fixHostnamePortSocket($host, $port, $socket);
// Set the information
$this->host = $host;
$this->user = $user;
$this->password = $password;
$this->port = $port;
$this->socket = $socket;
$this->_database = $database;
$this->selectDatabase = $select;
// finalize initialization
parent::__construct($options);
// Open the connection
if (!is_object($this->connection))
{
$this->open();
}
}
/**
* Test to see if the MySQL connector is available.
*
* @return boolean True on success, false otherwise.
*/
public static function isSupported()
{
return (function_exists('mysqli_connect'));
}
public function close()
{
$return = false;
if (is_object($this->cursor) && ($this->cursor instanceof mysqli_result))
{
try
{
@$this->cursor->free();
}
catch (\Throwable $e)
{
}
$this->cursor = null;
}
if (is_object($this->connection) && ($this->connection instanceof \mysqli))
{
try
{
$return = @$this->connection->close();
}
catch (\Throwable $e)
{
$return = false;
}
}
$this->connection = null;
return $return;
}
/**
* Determines if the connection to the server is active.
*
* @return boolean True if connected to the database engine.
*/
public function connected()
{
if (is_object($this->connection))
{
return @mysqli_ping($this->connection);
}
return false;
}
/**
* Method to escape a string for usage in an SQL statement.
*
* @param string $text The string to be escaped.
* @param boolean $extra Optional parameter to provide extra escaping.
*
* @return string The escaped string.
*/
public function escape($text, $extra = false)
{
if (is_null($text))
{
return 'NULL';
}
$result = @mysqli_real_escape_string($this->getConnection(), $text);
if ($result === false)
{
// Attempt to reconnect.
try
{
$this->connection = null;
$this->open();
$result = @mysqli_real_escape_string($this->getConnection(), $text);;
}
catch (RuntimeException $e)
{
$result = $this->unsafe_escape($text);
}
}
if ($extra)
{
$result = addcslashes($result, '%_');
}
return $result;
}
/**
* Method to fetch a row from the result set cursor as an associative array.
*
* @param mixed $cursor The optional result set cursor from which to fetch the row.
*
* @return mixed Either the next row from the result set or false if there are no more rows.
*/
public function fetchAssoc($cursor = null)
{
return mysqli_fetch_assoc($cursor ?: $this->cursor);
}
/**
* Method to free up the memory used for the result set.
*
* @param mixed $cursor The optional result set cursor from which to fetch the row.
*
* @return void
*/
public function freeResult($cursor = null)
{
mysqli_free_result($cursor ?: $this->cursor);
}
/**
* Get the number of affected rows for the previous executed SQL statement.
*
* @return integer The number of affected rows.
*/
public function getAffectedRows()
{
return mysqli_affected_rows($this->connection);
}
/**
* Get the number of returned rows for the previous executed SQL statement.
*
* @param mysqli_result $cursor An optional database cursor resource to extract the row count from.
*
* @return integer The number of returned rows.
*/
public function getNumRows($cursor = null)
{
return mysqli_num_rows($cursor ?: $this->cursor);
}
/**
* Get the current or query, or new JDatabaseQuery object.
*
* @param boolean $new False to return the last query set, True to return a new JDatabaseQuery object.
*
* @return mixed The current value of the internal SQL variable or a new JDatabaseQuery object.
*/
public function getQuery($new = false)
{
if ($new)
{
return new QueryMysqli($this);
}
else
{
return $this->sql;
}
}
/**
* Get the version of the database connector.
*
* @return string The database connector version.
*/
public function getVersion()
{
return mysqli_get_server_info($this->connection);
}
/**
* Determines if the database engine supports UTF-8 character encoding.
*
* @return boolean True if supported.
*/
public function hasUTF()
{
$verParts = explode('.', $this->getVersion());
return ($verParts[0] == 5 || ($verParts[0] == 4 && $verParts[1] == 1 && (int) $verParts[2] >= 2));
}
/**
* Method to get the auto-incremented value from the last INSERT statement.
*
* @return integer The value of the auto-increment field from the last inserted row.
*/
public function insertid()
{
return mysqli_insert_id($this->connection);
}
public function open()
{
if ($this->connected())
{
return;
}
else
{
$this->close();
}
// perform a number of fatality checks, then return gracefully
if (!function_exists('mysqli_connect'))
{
$this->errorNum = 1;
$this->errorMsg = 'The MySQL adapter "mysqli" is not available.';
return;
}
// connect to the server
if (!($this->connection = @mysqli_connect($this->host, $this->user, $this->password, null, $this->port, $this->socket)))
{
$this->errorNum = 2;
$this->errorMsg = 'Could not connect to MySQL';
return;
}
// Set sql_mode to non_strict mode
mysqli_query($this->connection, "SET @@SESSION.sql_mode = '';");
if ($this->selectDatabase && !empty($this->_database))
{
if (!$this->select($this->_database))
{
$this->errorNum = 3;
$this->errorMsg = "Cannot select database {$this->_database}";
return;
}
}
$this->setUTF();
}
/**
* Execute the SQL statement.
*
* @return mixed A database cursor resource on success, boolean false on failure.
*/
public function query()
{
$this->open();
if (!is_object($this->connection))
{
throw new RuntimeException($this->errorMsg, $this->errorNum);
}
// Take a local copy so that we don't modify the original query and cause issues later
$query = $this->replacePrefix((string) $this->sql);
if ($this->limit > 0 || $this->offset > 0)
{
$query .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
}
// Increment the query counter.
$this->count++;
// If debugging is enabled then let's log the query.
if ($this->debug)
{
// Add the query to the object queue.
$this->log[] = $query;
}
// Reset the error values.
$this->errorNum = 0;
$this->errorMsg = '';
// Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost.
$this->cursor = @mysqli_query($this->connection, $query);
// If an error occurred handle it.
if (!$this->cursor)
{
$this->errorNum = (int) mysqli_errno($this->connection);
$this->errorMsg = (string) mysqli_error($this->connection) . ' SQL=' . $query;
// Check if the server was disconnected.
if (!$this->connected() && !$this->isReconnecting)
{
$this->isReconnecting = true;
try
{
// Attempt to reconnect.
$this->connection = null;
$this->open();
}
// If connect fails, ignore that exception and throw the normal exception.
catch (RuntimeException $e)
{
throw new RuntimeException($this->errorMsg, $this->errorNum);
}
// Since we were able to reconnect, run the query again.
$result = $this->query();
$this->isReconnecting = false;
return $result;
}
// The server was not disconnected.
elseif ($this->errorNum != 0)
{
throw new RuntimeException($this->errorMsg, $this->errorNum);
}
}
return $this->cursor;
}
/**
* Select a database for use.
*
* @param string $database The name of the database to select for use.
*
* @return boolean True if the database was successfully selected.
*/
public function select($database)
{
if (!$database)
{
return false;
}
if (!mysqli_select_db($this->connection, $database))
{
return false;
}
return true;
}
/**
* Set the connection to use UTF-8 character encoding.
*
* @return boolean True on success.
*/
public function setUTF()
{
$result = false;
if ($this->supportsUtf8mb4())
{
$result = @mysqli_set_charset($this->connection, 'utf8mb4');
}
if (!$result)
{
$result = @mysqli_set_charset($this->connection, 'utf8');
}
return $result;
}
/**
* Does this database server support UTF-8 four byte (utf8mb4) collation?
*
* libmysql supports utf8mb4 since 5.5.3 (same version as the MySQL server). mysqlnd supports utf8mb4 since 5.0.9.
*
* This method's code is based on WordPress' wpdb::has_cap() method
*
* @return bool
*/
public function supportsUtf8mb4()
{
$client_version = mysqli_get_client_info();
if (strpos($client_version, 'mysqlnd') !== false)
{
$client_version = preg_replace('/^\D+([\d.]+).*/', '$1', $client_version);
return version_compare($client_version, '5.0.9', '>=');
}
else
{
return version_compare($client_version, '5.5.3', '>=');
}
}
/**
* Method to fetch a row from the result set cursor as an array.
*
* @param mixed $cursor The optional result set cursor from which to fetch the row.
*
* @return mixed Either the next row from the result set or false if there are no more rows.
*/
protected function fetchArray($cursor = null)
{
return mysqli_fetch_row($cursor ?: $this->cursor);
}
/**
* Method to fetch a row from the result set cursor as an object.
*
* @param mixed $cursor The optional result set cursor from which to fetch the row.
* @param string $class The class name to use for the returned row object.
*
* @return mixed Either the next row from the result set or false if there are no more rows.
*/
protected function fetchObject($cursor = null, $class = 'stdClass')
{
return mysqli_fetch_object($cursor ?: $this->cursor, $class);
}
/**
* Tries to parse all the weird hostname definitions and normalize them into something that the MySQLi connector
* will understand. Please note that there are some differences to the old MySQL driver:
*
* * Port and socket MUST be provided separately from the hostname. Hostnames in the form of 127.0.0.1:8336 are no
* longer acceptable.
*
* * The hostname "localhost" has special meaning. It means "use named pipes / sockets". Anything else uses TCP/IP.
* This is the ONLY way to specify a. TCP/IP or b. named pipes / sockets connection.
*
* * You SHOULD NOT use a numeric TCP/IP port with hostname localhost. For some strange reason it's still allowed
* but the manual is self-contradicting over what this really does...
*
* * Likewise you CANNOT use a socket / named pipe path with hostname other than localhost. Named pipes and sockets
* can only be used with the local machine, therefore the hostname MUST be localhost.
*
* * You cannot give a TCP/IP port number in the socket parameter or a named pipe / socket path to the port
* parameter. This leads to an error.
*
* * You cannot use an empty string, 0 or any other non-null value when you want to omit either of the port or
* socket parameters.
*
* * Persistent connections must be prefixed with the string literal 'p:'. Therefore you cannot have a hostname
* called 'p' (not to mention that'd be daft). You can also not specify something like 'p:1234' to make a
* persistent connection to a port. This wasn't even supported by the old MySQL driver. As a result we don't even
* try to catch that degenerate case.
*
* This method will try to apply all of the aforementioned rules with one additional disambiguation rule:
*
* A port / socket set in the hostname overrides a port specified separately. A port specified separately overrides
* a socket specified separately.
*
* @param string $host The hostname. Can contain legacy hostname:port or hostname:sc=ocket definitions.
* @param int $port The port. Alternatively it can contain the path to the socket.
* @param string $socket The path to the socket. You could abuse it to enter the port number. DON'T!
*
* @return void All parameters are passed by reference.
*/
protected function fixHostnamePortSocket(&$host, &$port, &$socket)
{
// Is this a persistent connection? Persistent connections are indicated by the literal "p:" in front of the hostname
$isPersistent = (substr($host, 0, 2) == 'p:');
$host = $isPersistent ? substr($host, 2) : $host;
/*
* Unlike mysql_connect(), mysqli_connect() takes the port and socket as separate arguments. Therefore, we
* have to extract them from the host string.
*/
$port = !empty($port) ? $port : 3306;
$regex = '/^(?P<host>((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(:(?P<port>.+))?$/';
// It's an IPv4 address with or without port
if (preg_match($regex, $host, $matches))
{
$host = $matches['host'];
if (!empty($matches['port']))
{
$port = $matches['port'];
}
}
// Square-bracketed IPv6 address with or without port, e.g. [fe80:102::2%eth1]:3306
elseif (preg_match('/^(?P<host>\[.*\])(:(?P<port>.+))?$/', $host, $matches))
{
$host = $matches['host'];
if (!empty($matches['port']))
{
$port = $matches['port'];
}
}
// Named host (e.g example.com or localhost) with or without port
elseif (preg_match('/^(?P<host>(\w+:\/{2,3})?[a-z0-9\.\-]+)(:(?P<port>[^:]+))?$/i', $host, $matches))
{
$host = $matches['host'];
if (!empty($matches['port']))
{
$port = $matches['port'];
}
}
// Empty host, just port, e.g. ':3306'
elseif (preg_match('/^:(?P<port>[^:]+)$/', $host, $matches))
{
$host = 'localhost';
$port = $matches['port'];
}
// ... else we assume normal (naked) IPv6 address, so host and port stay as they are or default
// Get the port number or socket name
if (is_numeric($port))
{
$port = (int) $port;
$socket = '';
}
else
{
$socket = $port;
// If we're going to use sockets, port MUST BE null, otherwise mysqli_connect will try to use it ignoring
// the socket, causing a connection error
$port = null;
}
// Finally, if it's a persistent connection we have to prefix the hostname with 'p:'
$host = $isPersistent ? "p:$host" : $host;
}
}

View File

@@ -0,0 +1,373 @@
<?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\Driver;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Driver\Query\Base as QueryBase;
/**
* Dummy driver class for flat-file CMS
*/
class None extends Base
{
public static $dbtech = 'none';
/**
* The name of the database driver.
*
* @var string
* @since 1.0
*/
public $name = 'none';
public function __construct(array $options)
{
$this->driverType = 'none';
parent::__construct($options);
}
/**
* Test to see if this db driver is available
*
* @return boolean True on success, false otherwise.
*
* @since 1.0
*/
public static function isSupported()
{
return true;
}
public function open()
{
return $this;
}
/**
* Closes the database connection
*/
public function close()
{
return;
}
/**
* Determines if the connection to the server is active.
*
* @return boolean True if connected to the database engine.
*/
public function connected()
{
return true;
}
/**
* Drops a table from the database.
*
* @param string $table The name of the database table to drop.
* @param boolean $ifExists Optionally specify that the table must exist before it is dropped.
*
* @return Base Returns this object to support chaining.
*/
public function dropTable($table, $ifExists = true)
{
return $this;
}
/**
* Method to escape a string for usage in an SQL statement.
*
* @param string $text The string to be escaped.
* @param boolean $extra Optional parameter to provide extra escaping.
*
* @return string The escaped string.
*/
public function escape($text, $extra = false)
{
return '';
}
/**
* Method to fetch a row from the result set cursor as an associative array.
*
* @param mixed $cursor The optional result set cursor from which to fetch the row.
*
* @return mixed Either the next row from the result set or false if there are no more rows.
*/
public function fetchAssoc($cursor = null)
{
return false;
}
/**
* Method to free up the memory used for the result set.
*
* @param mixed $cursor The optional result set cursor from which to fetch the row.
*
* @return void
*/
public function freeResult($cursor = null)
{
return;
}
/**
* Get the number of affected rows for the previous executed SQL statement.
*
* @return integer The number of affected rows.
*/
public function getAffectedRows()
{
return 0;
}
/**
* Method to get the database collation in use by sampling a text field of a table in the database.
*
* @return mixed The collation in use by the database or boolean false if not supported.
*/
public function getCollation()
{
return false;
}
/**
* Get the number of returned rows for the previous executed SQL statement.
*
* @param resource $cursor An optional database cursor resource to extract the row count from.
*
* @return integer The number of returned rows.
*/
public function getNumRows($cursor = null)
{
return 0;
}
/**
* Get the current query object or a new QueryBase object.
*
* @param boolean $new False to return the current query object, True to return a new QueryBase object.
*
* @return QueryBase The current query object or a new object extending the QueryBase class.
*/
public function getQuery($new = false)
{
return $this->sql;
}
/**
* Retrieves field information about the given tables.
*
* @param string $table The name of the database table.
* @param boolean $typeOnly True (default) to only return field types.
*
* @return array An array of fields by table.
*/
public function getTableColumns($table, $typeOnly = true)
{
return [];
}
/**
* Shows the table CREATE statement that creates the given tables.
*
* @param mixed $tables A table name or a list of table names.
*
* @return array A list of the create SQL for the tables.
*/
public function getTableCreate($tables)
{
return [];
}
/**
* Retrieves field information about the given tables.
*
* @param mixed $tables A table name or a list of table names.
*
* @return array An array of keys for the table(s).
*/
public function getTableKeys($tables)
{
return [];
}
/**
* Method to get an array of all tables in the database.
*
* @return array An array of all the tables in the database.
*/
public function getTableList()
{
return [];
}
/**
* Returns an array with the names of tables, views, procedures, functions and triggers
* in the database. The table names are the keys of the tables, whereas the value is
* the type of each element: table, view, merge, temp, procedure, function or trigger.
* Note that merge are MRG_MYISAM tables and temp is non-permanent data table, usually
* set up as temporary, black hole or federated tables. These two types should never,
* ever, have their data dumped in the SQL dump file.
*
* @param bool $abstract Return or normal names? Defaults to true (names)
*
* @return array
*/
public function getTables($abstract = true)
{
return [];
}
/**
* Get the version of the database connector
*
* @return string The database connector version.
*/
public function getVersion()
{
return '0.0.0';
}
/**
* Method to get the auto-incremented value from the last INSERT statement.
*
* @return integer The value of the auto-increment field from the last inserted row.
*/
public function insertid()
{
return 0;
}
/**
* Locks a table in the database.
*
* @param string $tableName The name of the table to unlock.
*
* @return Base Returns this object to support chaining.
*/
public function lockTable($tableName)
{
return $this;
}
/**
* Execute the SQL statement.
*
* @return mixed A database cursor resource on success, boolean false on failure.
*/
public function query()
{
return false;
}
/**
* Renames a table in the database.
*
* @param string $oldTable The name of the table to be renamed
* @param string $newTable The new name for the table.
* @param string $backup Table prefix
* @param string $prefix For the table - used to rename constraints in non-mysql databases
*
* @return Base Returns this object to support chaining.
*/
public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
{
return $this;
}
/**
* Select a database for use.
*
* @param string $database The name of the database to select for use.
*
* @return boolean True if the database was successfully selected.
*/
public function select($database)
{
return true;
}
/**
* Set the connection to use UTF-8 character encoding.
*
* @return boolean True on success.
*/
public function setUTF()
{
return true;
}
/**
* Method to commit a transaction.
*
* @return void
*/
public function transactionCommit()
{
return;
}
/**
* Method to roll back a transaction.
*
* @return void
*/
public function transactionRollback()
{
return;
}
/**
* Method to initialize a transaction.
*
* @return void
*/
public function transactionStart()
{
return;
}
/**
* Unlocks tables in the database.
*
* @return Base Returns this object to support chaining.
*/
public function unlockTables()
{
return $this;
}
/**
* Method to fetch a row from the result set cursor as an array.
*
* @param mixed $cursor The optional result set cursor from which to fetch the row.
*
* @return mixed Either the next row from the result set or false if there are no more rows.
*/
protected function fetchArray($cursor = null)
{
return false;
}
/**
* Method to fetch a row from the result set cursor as an object.
*
* @param mixed $cursor The optional result set cursor from which to fetch the row.
* @param string $class The class name to use for the returned row object.
*
* @return mixed Either the next row from the result set or false if there are no more rows.
*/
protected function fetchObject($cursor = null, $class = 'stdClass')
{
return false;
}
}

View File

@@ -0,0 +1,737 @@
<?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\Driver;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Driver\Query\Pdomysql as QueryPdomysql;
use Exception;
use PDO;
use PDOException;
use PDOStatement;
use ReflectionClass;
use RuntimeException;
/**
* PDO MySQL database driver for Akeeba Engine
*
* Based on Joomla! Platform 12.1
*/
class Pdomysql extends Mysql
{
/**
* The name of the database driver.
*
* @var string
*/
public $name = 'pdomysql';
/** @var string Connection character set */
protected $charset = 'utf8mb4';
/** @var PDO The db connection resource */
protected $connection = null;
/** @var PDOStatement The database connection cursor from the last query. */
protected $cursor;
/** @var array Driver options for PDO */
protected $driverOptions = [];
/** @var bool Are we in the process of reconnecting to the database server? */
private $isReconnecting = false;
/**
* Database object constructor
*
* @param array $options List of options used to configure the connection
*/
public function __construct($options)
{
$this->driverType = 'mysql';
// Init
$this->nameQuote = '`';
$host = array_key_exists('host', $options) ? $options['host'] : 'localhost';
$port = array_key_exists('port', $options) ? $options['port'] : '';
$user = array_key_exists('user', $options) ? $options['user'] : '';
$password = array_key_exists('password', $options) ? $options['password'] : '';
$database = array_key_exists('database', $options) ? $options['database'] : '';
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
$select = array_key_exists('select', $options) ? $options['select'] : true;
$charset = array_key_exists('charset', $options) ? $options['charset'] : 'utf8mb4';
$driverOptions = array_key_exists('driverOptions', $options) ? $options['driverOptions'] : [];
$connection = array_key_exists('connection', $options) ? $options['connection'] : null;
$socket = null;
// Figure out if a port is included in the host name
if (empty($port))
{
// Unlike mysql_connect(), mysqli_connect() takes the port and socket
// as separate arguments. Therefore, we have to extract them from the
// host string.
$port = null;
$socket = null;
$targetSlot = substr(strstr($host, ":"), 1);
if (!empty($targetSlot))
{
// Get the port number or socket name
if (is_numeric($targetSlot))
{
$port = $targetSlot;
}
else
{
$socket = $targetSlot;
}
// Extract the host name only
$host = substr($host, 0, strlen($host) - (strlen($targetSlot) + 1));
// This will take care of the following notation: ":3306"
if ($host == '')
{
$host = 'localhost';
}
}
}
// Open the connection
$this->host = $host;
$this->user = $user;
$this->password = $password;
$this->port = $port;
$this->socket = $socket;
$this->charset = $charset;
$this->_database = $database;
$this->selectDatabase = $select;
$this->driverOptions = $driverOptions;
$this->tablePrefix = $prefix;
$this->connection = $connection;
$this->errorNum = 0;
$this->count = 0;
$this->log = [];
$this->options = $options;
if (!is_object($this->connection))
{
$this->open();
}
}
/**
* Test to see if the MySQL connector is available.
*
* @return boolean True on success, false otherwise.
*/
public static function isSupported()
{
if (!defined('\PDO::ATTR_DRIVER_NAME'))
{
return false;
}
return in_array('mysql', PDO::getAvailableDrivers());
}
/**
* PDO does not support serialize
*
* @return array
*/
public function __sleep()
{
$serializedProperties = [];
$reflect = new ReflectionClass($this);
// Get properties of the current class
$properties = $reflect->getProperties();
foreach ($properties as $property)
{
// Do not serialize properties that are \PDO
if ($property->isStatic() == false && !($this->{$property->name} instanceof PDO))
{
array_push($serializedProperties, $property->name);
}
}
return $serializedProperties;
}
/**
* Wake up after serialization
*
* @return array
*/
public function __wakeup()
{
// Get connection back
$this->__construct($this->options);
}
public function close()
{
$return = false;
if (is_object($this->cursor))
{
$this->cursor->closeCursor();
}
$this->connection = null;
return $return;
}
/**
* Determines if the connection to the server is active.
*
* @return boolean True if connected to the database engine.
*/
public function connected()
{
if (!is_object($this->connection))
{
return false;
}
try
{
/** @var PDOStatement $statement */
$statement = $this->connection->prepare('SELECT 1');
$executed = $statement->execute();
$ret = 0;
if ($executed)
{
$row = [0];
if (!empty($statement) && $statement instanceof PDOStatement)
{
$row = $statement->fetch(PDO::FETCH_NUM);
}
$ret = $row[0];
}
$status = $ret == 1;
$statement->closeCursor();
$statement = null;
}
// If we catch an exception here, we must not be connected.
catch (Exception $e)
{
$status = false;
}
return $status;
}
/**
* Method to escape a string for usage in an SQL statement.
*
* @param string $text The string to be escaped.
* @param boolean $extra Optional parameter to provide extra escaping.
*
* @return string The escaped string.
*/
public function escape($text, $extra = false)
{
if (is_int($text) || is_float($text))
{
return $text;
}
if (is_null($text))
{
return 'NULL';
}
$result = substr($this->connection->quote($text), 1, -1);
if ($extra)
{
$result = addcslashes($result, '%_');
}
return $result;
}
/**
* Method to fetch a row from the result set cursor as an associative array.
*
* @param mixed $cursor The optional result set cursor from which to fetch the row.
*
* @return mixed Either the next row from the result set or false if there are no more rows.
*/
public function fetchAssoc($cursor = null)
{
$ret = null;
if (!empty($cursor) && $cursor instanceof PDOStatement)
{
$ret = $cursor->fetch(PDO::FETCH_ASSOC);
}
elseif ($this->cursor instanceof PDOStatement)
{
$ret = $this->cursor->fetch(PDO::FETCH_ASSOC);
}
return $ret;
}
/**
* Method to free up the memory used for the result set.
*
* @param mixed $cursor The optional result set cursor from which to fetch the row.
*
* @return void
*/
public function freeResult($cursor = null)
{
if ($cursor instanceof PDOStatement)
{
$cursor->closeCursor();
$cursor = null;
}
if ($this->cursor instanceof PDOStatement)
{
$this->cursor->closeCursor();
$this->cursor = null;
}
}
/**
* Get the number of affected rows for the previous executed SQL statement.
*
* @return integer The number of affected rows.
*/
public function getAffectedRows()
{
if ($this->cursor instanceof PDOStatement)
{
return $this->cursor->rowCount();
}
return 0;
}
/**
* Get the number of returned rows for the previous executed SQL statement.
*
* @param resource $cursor An optional database cursor resource to extract the row count from.
*
* @return integer The number of returned rows.
*/
public function getNumRows($cursor = null)
{
if ($cursor instanceof PDOStatement)
{
return $cursor->rowCount();
}
if ($this->cursor instanceof PDOStatement)
{
return $this->cursor->rowCount();
}
return 0;
}
/**
* Get the current or query, or new JDatabaseQuery object.
*
* @param boolean $new False to return the last query set, True to return a new JDatabaseQuery object.
*
* @return mixed The current value of the internal SQL variable or a new JDatabaseQuery object.
*/
public function getQuery($new = false)
{
if ($new)
{
return new QueryPdomysql($this);
}
else
{
return $this->sql;
}
}
/**
* Get the version of the database connector.
*
* @return string The database connector version.
*/
public function getVersion()
{
return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION);
}
/**
* Determines if the database engine supports UTF-8 character encoding.
*
* @return boolean True if supported.
*/
public function hasUTF()
{
$verParts = explode('.', $this->getVersion());
return ($verParts[0] == 5 || ($verParts[0] == 4 && $verParts[1] == 1 && (int) $verParts[2] >= 2));
}
/**
* Method to get the auto-incremented value from the last INSERT statement.
*
* @return integer The value of the auto-increment field from the last inserted row.
*/
public function insertid()
{
// Error suppress this to prevent PDO warning us that the driver doesn't support this operation.
return @$this->connection->lastInsertId();
}
/**
* Method to get the next row in the result set from the database query as an object.
*
* @param string $class The class name to use for the returned row object.
*
* @return mixed The result of the query as an array, false if there are no more rows.
*/
public function loadNextObject($class = 'stdClass')
{
// Execute the query and get the result set cursor.
if (!$this->cursor)
{
if (!($this->execute()))
{
return $this->errorNum ? null : false;
}
}
// Get the next row from the result set as an object of type $class.
if ($row = $this->fetchObject(null, $class))
{
return $row;
}
// Free up system resources and return.
$this->freeResult();
return false;
}
/**
* Method to get the next row in the result set from the database query as an array.
*
* @return mixed The result of the query as an array, false if there are no more rows.
*/
public function loadNextRow()
{
// Execute the query and get the result set cursor.
if (!$this->cursor)
{
if (!($this->execute()))
{
return $this->errorNum ? null : false;
}
}
// Get the next row from the result set as an object of type $class.
if ($row = $this->fetchArray())
{
return $row;
}
// Free up system resources and return.
$this->freeResult();
return false;
}
public function open()
{
if ($this->connected())
{
return;
}
else
{
$this->close();
}
if (!isset($this->charset))
{
$this->charset = 'utf8mb4';
}
$this->port = $this->port ?: 3306;
$format = 'mysql:host=#HOST#;port=#PORT#;dbname=#DBNAME#;charset=#CHARSET#';
if ($this->socket)
{
$format = 'mysql:socket=#SOCKET#;dbname=#DBNAME#;charset=#CHARSET#';
}
$replace = ['#HOST#', '#PORT#', '#SOCKET#', '#DBNAME#', '#CHARSET#'];
$with = [$this->host, $this->port, $this->socket, $this->_database, $this->charset];
// Create the connection string:
$connectionString = str_replace($replace, $with, $format);
// connect to the server
try
{
$this->connection = new PDO(
$connectionString,
$this->user,
$this->password,
$this->driverOptions
);
}
catch (PDOException $e)
{
// If we tried connecting through utf8mb4 and we failed let's retry with regular utf8
if ($this->charset == 'utf8mb4')
{
$this->charset = 'UTF8';
$this->open();
return;
}
$this->errorNum = 2;
$this->errorMsg = 'Could not connect to MySQL via PDO: ' . $e->getMessage();
return;
}
// Reset the SQL mode of the connection
try
{
$this->connection->exec("SET @@SESSION.sql_mode = '';");
}
// Ignore any exceptions (incompatible MySQL versions)
catch (Exception $e)
{
}
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
if ($this->selectDatabase && !empty($this->_database))
{
$this->select($this->_database);
}
$this->freeResult();
}
/**
* Execute the SQL statement.
*
* @return mixed A database cursor resource on success, boolean false on failure.
*/
public function query()
{
if (!is_object($this->connection))
{
$this->open();
}
$this->freeResult();
// Take a local copy so that we don't modify the original query and cause issues later
$query = $this->replacePrefix((string) $this->sql);
if ($this->limit > 0 || $this->offset > 0)
{
$query .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
}
// Increment the query counter.
$this->count++;
// If debugging is enabled then let's log the query.
if ($this->debug)
{
// Add the query to the object queue.
$this->log[] = $query;
}
// Reset the error values.
$this->errorNum = 0;
$this->errorMsg = '';
// Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost.
try
{
$this->cursor = $this->connection->query($query);
}
catch (Exception $e)
{
}
// If an error occurred handle it.
if (!$this->cursor)
{
$errorInfo = $this->connection->errorInfo();
$this->errorNum = $errorInfo[1];
$this->errorMsg = $errorInfo[2] . ' SQL=' . $query;
// Check if the server was disconnected.
if (!$this->connected() && !$this->isReconnecting)
{
$this->isReconnecting = true;
try
{
// Attempt to reconnect.
$this->connection = null;
$this->open();
}
// If connect fails, ignore that exception and throw the normal exception.
catch (RuntimeException $e)
{
throw new RuntimeException($this->errorMsg, $this->errorNum);
}
// Since we were able to reconnect, run the query again.
$result = $this->query();
$this->isReconnecting = false;
return $result;
}
// The server was not disconnected.
else
{
throw new RuntimeException($this->errorMsg, $this->errorNum);
}
}
return $this->cursor;
}
/**
* Select a database for use.
*
* @param string $database The name of the database to select for use.
*
* @return boolean True if the database was successfully selected.
*/
public function select($database)
{
try
{
$this->connection->exec('USE ' . $this->quoteName($database));
}
catch (Exception $e)
{
$errorInfo = $this->connection->errorInfo();
$this->errorNum = $errorInfo[1];
$this->errorMsg = $errorInfo[2];
return false;
}
return true;
}
/**
* Set the connection to use UTF-8 character encoding.
*
* @return boolean True on success.
*/
public function setUTF()
{
return true;
}
/**
* Method to commit a transaction.
*
* @return void
*/
public function transactionCommit()
{
$this->connection->commit();
}
/**
* Method to roll back a transaction.
*
* @return void
*/
public function transactionRollback()
{
$this->connection->rollBack();
}
/**
* Method to initialize a transaction.
*
* @return void
*/
public function transactionStart()
{
$this->connection->beginTransaction();
}
/**
* Method to fetch a row from the result set cursor as an array.
*
* @param mixed $cursor The optional result set cursor from which to fetch the row.
*
* @return mixed Either the next row from the result set or false if there are no more rows.
*/
protected function fetchArray($cursor = null)
{
$ret = null;
if (!empty($cursor) && $cursor instanceof PDOStatement)
{
$ret = $cursor->fetch(PDO::FETCH_NUM);
}
elseif ($this->cursor instanceof PDOStatement)
{
$ret = $this->cursor->fetch(PDO::FETCH_NUM);
}
return $ret;
}
/**
* Method to fetch a row from the result set cursor as an object.
*
* @param mixed $cursor The optional result set cursor from which to fetch the row.
* @param string $class The class name to use for the returned row object.
*
* @return mixed Either the next row from the result set or false if there are no more rows.
*/
protected function fetchObject($cursor = null, $class = 'stdClass')
{
$ret = null;
if (!empty($cursor) && $cursor instanceof PDOStatement)
{
$ret = $cursor->fetchObject($class);
}
elseif ($this->cursor instanceof PDOStatement)
{
$ret = $this->cursor->fetchObject($class);
}
return $ret;
}
}

File diff suppressed because it is too large Load Diff

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\Driver\Query;
defined('AKEEBAENGINE') || die();
/**
* Query Element Class.
*
* Based on Joomla! Platform 11.3
*/
class Element
{
/**
* @var string The name of the element.
*/
protected $name = null;
/**
* @var array An array of elements.
*/
protected $elements = null;
/**
* @var string Glue piece.
*/
protected $glue = null;
/**
* Constructor.
*
* @param string $name The name of the element.
* @param mixed $elements String or array.
* @param string $glue The glue for elements.
*/
public function __construct($name, $elements, $glue = ',')
{
$this->elements = [];
$this->name = $name;
$this->glue = $glue;
$this->append($elements);
}
/**
* Magic function to convert the query element to a string.
*
* @return string
*/
public function __toString()
{
if (substr($this->name, -2) == '()')
{
return PHP_EOL . substr($this->name, 0, -2) . '(' . implode($this->glue, $this->elements) . ')';
}
else
{
return PHP_EOL . $this->name . ' ' . implode($this->glue, $this->elements);
}
}
/**
* Appends element parts to the internal list.
*
* @param mixed $elements String or array.
*
* @return void
*/
public function append($elements)
{
if (is_array($elements))
{
$this->elements = array_merge($this->elements, $elements);
}
else
{
$this->elements = array_merge($this->elements, [$elements]);
}
}
/**
* Gets the elements of this element.
*
* @return string
*/
public function getElements()
{
return $this->elements;
}
/**
* Method to provide deep copy support to nested objects and arrays
* when cloning.
*
* @return void
*/
public function __clone()
{
foreach ($this as $k => $v)
{
if (is_object($v) || is_array($v))
{
$this->{$k} = unserialize(serialize($v));
}
}
}
}

View File

@@ -0,0 +1,58 @@
<?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\Driver\Query;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Driver\Query\Base as BaseQuery;
/**
* Query Limitable Interface.
* Adds bind/unbind methods as well as a getBounded() method
* to retrieve the stored bounded variables on demand prior to
* query execution.
*
* Based on Joomla! Platform 11.2
*/
interface Limitable
{
/**
* Method to modify a query already in string format with the needed
* additions to make the query limited to a particular number of
* results, or start at a particular offset. This method is used
* automatically by the __toString() method if it detects that the
* query implements the Limitable interface.
*
* @param string $query The query in string format
* @param integer $limit The limit for the result set
* @param integer $offset The offset for the result set
*
* @return string
*
* @since 12.1
*/
public function processLimit($query, $limit, $offset = 0);
/**
* Sets the offset and limit for the result set, if the database driver supports it.
*
* Usage:
* $query->setLimit(100, 0); (retrieve 100 rows, starting at first record)
* $query->setLimit(50, 50); (retrieve 50 rows, starting at 50th record)
*
* @param integer $limit The limit for the result set
* @param integer $offset The offset for the result set
*
* @return BaseQuery Returns this object to allow chaining.
*
* @since 12.1
*/
public function setLimit($limit = 0, $offset = 0);
}

View File

@@ -0,0 +1,21 @@
<?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\Driver\Query;
defined('AKEEBAENGINE') || die();
/**
* Query Building Class.
*
* Based on Joomla! Platform 11.3
*/
class Mysql extends Mysqli
{
}

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\Driver\Query;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Driver\Query\Base as BaseQuery;
/**
* Query Building Class.
*
* Based on Joomla! Platform 11.3
*/
class Mysqli extends Base implements Limitable
{
/**
* @var integer The offset for the result set.
*/
protected $offset;
/**
* @var integer The limit for the result set.
*/
protected $limit;
/**
* Method to modify a query already in string format with the needed
* additions to make the query limited to a particular number of
* results, or start at a particular offset.
*
* @param string $query The query in string format
* @param integer $limit The limit for the result set
* @param integer $offset The offset for the result set
*
* @return string
*/
public function processLimit($query, $limit, $offset = 0)
{
if ($limit > 0 || $offset > 0)
{
$query .= ' LIMIT ' . $offset . ', ' . $limit;
}
return $query;
}
/**
* Sets the offset and limit for the result set, if the database driver supports it.
*
* Usage:
* $query->setLimit(100, 0); (retrieve 100 rows, starting at first record)
* $query->setLimit(50, 50); (retrieve 50 rows, starting at 50th record)
*
* @param integer $limit The limit for the result set
* @param integer $offset The offset for the result set
*
* @return BaseQuery Returns this object to allow chaining.
*/
public function setLimit($limit = 0, $offset = 0)
{
$this->limit = (int) $limit;
$this->offset = (int) $offset;
return $this;
}
/**
* Concatenates an array of column names or values.
*
* @param array $values An array of values to concatenate.
* @param string $separator As separator to place between each value.
*
* @return string The concatenated values.
*/
public function concatenate($values, $separator = null)
{
if ($separator)
{
$concat_string = 'CONCAT_WS(' . $this->quote($separator);
foreach ($values as $value)
{
$concat_string .= ', ' . $value;
}
return $concat_string . ')';
}
else
{
return 'CONCAT(' . implode(',', $values) . ')';
}
}
}

View File

@@ -0,0 +1,21 @@
<?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\Driver\Query;
defined('AKEEBAENGINE') || die();
/**
* Query Building Class.
*
* Based on Joomla! Platform 11.3
*/
class Pdomysql extends Mysqli implements Limitable
{
}

View File

@@ -0,0 +1,58 @@
<?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\Driver\Query;
defined('AKEEBAENGINE') || die();
use PDO;
/**
* Database Query Preparable Interface.
*
* Adds bind/unbind methods as well as a getBounded() method
* to retrieve the stored bounded variables on demand prior to
* query execution.
*
* @since 1.0
*
* @codeCoverageIgnore
*/
interface Preparable
{
/**
* Method to add a variable to an internal array that will be bound to a prepared SQL statement before query execution. Also
* removes a variable that has been bounded from the internal bounded array when the passed in value is null.
*
* @param string|integer $key The key that will be used in your SQL query to reference the value. Usually of
* the form ':key', but can also be an integer.
* @param mixed &$value The value that will be bound. The value is passed by reference to support output
* parameters such as those possible with stored procedures.
* @param integer $dataType Constant corresponding to a SQL datatype.
* @param integer $length The length of the variable. Usually required for OUTPUT parameters.
* @param array $driverOptions Optional driver options to be used.
*
* @return Preparable
*
* @since 1.0
*/
public function bind($key = null, &$value = null, $dataType = PDO::PARAM_STR, $length = 0, $driverOptions = []);
/**
* Retrieves the bound parameters array when key is null and returns it by reference. If a key is provided then that item is
* returned.
*
* @param mixed $key The bounded variable key to retrieve.
*
* @return mixed
*
* @since 1.0
*/
public function &getBounded($key = null);
}

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\Driver\Query;
defined('AKEEBAENGINE') || die();
use PDO;
use stdClass;
/**
* SQLite Query Building Class.
*
* @since 1.0
*/
class Sqlite extends Base implements Preparable, Limitable
{
/**
* The limit for the result set.
*
* @var integer
* @since 1.0
*/
protected $limit;
/**
* The offset for the result set.
*
* @var integer
* @since 1.0
*/
protected $offset;
/**
* Holds key / value pair of bound objects.
*
* @var mixed
* @since 1.0
*/
protected $bounded = [];
/**
* Method to add a variable to an internal array that will be bound to a prepared SQL statement before query execution. Also
* removes a variable that has been bounded from the internal bounded array when the passed in value is null.
*
* @param string|integer $key The key that will be used in your SQL query to reference the value. Usually of
* the form ':key', but can also be an integer.
* @param mixed &$value The value that will be bound. The value is passed by reference to support output
* parameters such as those possible with stored procedures.
* @param integer $dataType Constant corresponding to a SQL datatype.
* @param integer $length The length of the variable. Usually required for OUTPUT parameters.
* @param array $driverOptions Optional driver options to be used.
*
* @return Sqlite Returns this object to allow chaining.
*
* @since 1.0
*/
public function bind($key = null, &$value = null, $dataType = PDO::PARAM_STR, $length = 0, $driverOptions = [])
{
// Case 1: Empty Key (reset $bounded array)
if (empty($key))
{
$this->bounded = [];
return $this;
}
// Case 2: Key Provided, null value (unset key from $bounded array)
if (is_null($value))
{
if (isset($this->bounded[$key]))
{
unset($this->bounded[$key]);
}
return $this;
}
$obj = new stdClass;
$obj->value = &$value;
$obj->dataType = $dataType;
$obj->length = $length;
$obj->driverOptions = $driverOptions;
// Case 3: Simply add the Key/Value into the bounded array
$this->bounded[$key] = $obj;
return $this;
}
/**
* Retrieves the bound parameters array when key is null and returns it by reference. If a key is provided then that item is
* returned.
*
* @param mixed $key The bounded variable key to retrieve.
*
* @return mixed
*
* @since 1.0
*/
public function &getBounded($key = null)
{
if (empty($key))
{
return $this->bounded;
}
else
{
if (isset($this->bounded[$key]))
{
return $this->bounded[$key];
}
}
}
/**
* Gets the number of characters in a string.
*
* Note, use 'length' to find the number of bytes in a string.
*
* Usage:
* $query->select($query->charLength('a'));
*
* @param string $field A value.
* @param string $operator Comparison operator between charLength integer value and $condition
* @param string $condition Integer value to compare charLength with.
*
* @return string The required char length call.
*
* @since 1.1.0
*/
public function charLength($field, $operator = null, $condition = null)
{
return 'length(' . $field . ')' . (isset($operator) && isset($condition) ? ' ' . $operator . ' ' . $condition : '');
}
/**
* Clear data from the query or a specific clause of the query.
*
* @param string $clause Optionally, the name of the clause to clear, or nothing to clear the whole query.
*
* @return Sqlite Returns this object to allow chaining.
*
* @since 1.0
*/
public function clear($clause = null)
{
switch ($clause)
{
case null:
$this->bounded = [];
break;
}
return parent::clear($clause);
}
/**
* Concatenates an array of column names or values.
*
* Usage:
* $query->select($query->concatenate(array('a', 'b')));
*
* @param array $values An array of values to concatenate.
* @param string $separator As separator to place between each value.
*
* @return string The concatenated values.
*
* @since 1.1.0
*/
public function concatenate($values, $separator = null)
{
if ($separator)
{
return implode(' || ' . $this->quote($separator) . ' || ', $values);
}
else
{
return implode(' || ', $values);
}
}
/**
* Method to modify a query already in string format with the needed
* additions to make the query limited to a particular number of
* results, or start at a particular offset. This method is used
* automatically by the __toString() method if it detects that the
* query implements the LimitableInterface.
*
* @param string $query The query in string format
* @param integer $limit The limit for the result set
* @param integer $offset The offset for the result set
*
* @return string
*
* @since 1.0
*/
public function processLimit($query, $limit, $offset = 0)
{
if ($limit > 0 || $offset > 0)
{
$query .= ' LIMIT ' . $offset . ', ' . $limit;
}
return $query;
}
/**
* Sets the offset and limit for the result set, if the database driver supports it.
*
* Usage:
* $query->setLimit(100, 0); (retrieve 100 rows, starting at first record)
* $query->setLimit(50, 50); (retrieve 50 rows, starting at 50th record)
*
* @param integer $limit The limit for the result set
* @param integer $offset The offset for the result set
*
* @return Sqlite Returns this object to allow chaining.
*
* @since 1.0
*/
public function setLimit($limit = 0, $offset = 0)
{
$this->limit = (int) $limit;
$this->offset = (int) $offset;
return $this;
}
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* Akeeba Engine
* The PHP-only site backup engine
*
* @copyright Copyright (c)2006-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU GPL version 3 or, at your option, any later version
* @package akeebaengine
*/
namespace Akeeba\Engine\Driver;
// Protection against direct access
defined('AKEEBAENGINE') or die();
class QueryException extends \Exception {};

View File

@@ -0,0 +1,909 @@
<?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\Driver;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Driver\Query\Base as QueryBase;
use Akeeba\Engine\Driver\Query\Limitable;
use Akeeba\Engine\Driver\Query\Preparable;
use PDO;
use PDOException;
use PDOStatement;
use RuntimeException;
use SQLite3;
/**
* SQLite database driver supporting PDO based connections
*
* @see http://php.net/manual/en/ref.pdo-sqlite.php
* @since 1.0
*/
class Sqlite extends Base
{
public static $dbtech = 'sqlite';
/**
* The name of the database driver.
*
* @var string
* @since 1.0
*/
public $name = 'sqlite';
/** @var PDOStatement The database connection cursor from the last query. */
protected $cursor;
/** @var array Contains the current query execution status */
protected $executed = false;
/**
* The character(s) used to quote SQL statement names such as table names or field names,
* etc. The child classes should define this as necessary. If a single character string the
* same character is used for both sides of the quoted name, else the first character will be
* used for the opening quote and the second for the closing quote.
*
* @var string
* @since 1.0
*/
protected $nameQuote = '`';
/** @var resource The prepared statement. */
protected $prepared;
/** @var bool Are we in the process of reconnecting to the database server? */
private $isReconnecting = false;
public function __construct(array $options)
{
$this->driverType = 'sqlite';
parent::__construct($options);
if (!is_object($this->connection))
{
$this->open();
}
}
/**
* Test to see if the PDO ODBC connector is available.
*
* @return boolean True on success, false otherwise.
*
* @since 1.0
*/
public static function isSupported()
{
return class_exists('\\PDO') && in_array('sqlite', PDO::getAvailableDrivers());
}
/**
* Destructor.
*
* @since 1.0
*/
public function __destruct()
{
$this->freeResult();
unset($this->connection);
}
public function close()
{
$return = false;
if (is_object($this->cursor))
{
$this->cursor->closeCursor();
}
$this->connection = null;
return $return;
}
/**
* Determines if the connection to the server is active.
*
* @return boolean True if connected to the database engine.
*/
public function connected()
{
return !empty($this->connection);
}
/**
* Disconnects the database.
*
* @return void
*
* @since 1.0
*/
public function disconnect()
{
$this->freeResult();
unset($this->connection);
}
/**
* Drops a table from the database.
*
* @param string $tableName The name of the database table to drop.
* @param boolean $ifExists Optionally specify that the table must exist before it is dropped.
*
* @return Sqlite Returns this object to support chaining.
*
* @since 1.0
*/
public function dropTable($tableName, $ifExists = true)
{
$this->open();
$query = $this->getQuery(true);
$this->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $query->quoteName($tableName));
$this->execute();
return $this;
}
/**
* Method to escape a string for usage in an SQLite statement.
*
* Note: Using query objects with bound variables is preferable to the below.
*
* @param string $text The string to be escaped.
* @param boolean $extra Unused optional parameter to provide extra escaping.
*
* @return string The escaped string.
*
* @since 1.0
*/
public function escape($text, $extra = false)
{
if (is_int($text) || is_float($text))
{
return $text;
}
if (is_null($text))
{
return 'NULL';
}
return SQLite3::escapeString($text);
}
public function fetchAssoc($cursor = null)
{
if (!empty($cursor) && $cursor instanceof PDOStatement)
{
return $cursor->fetch(PDO::FETCH_ASSOC);
}
if ($this->prepared instanceof PDOStatement)
{
return $this->prepared->fetch(PDO::FETCH_ASSOC);
}
}
public function freeResult($cursor = null)
{
$this->executed = false;
if ($cursor instanceof PDOStatement)
{
$cursor->closeCursor();
$cursor = null;
}
if ($this->prepared instanceof PDOStatement)
{
$this->prepared->closeCursor();
$this->prepared = null;
}
}
public function getAffectedRows()
{
$this->open();
if ($this->prepared instanceof PDOStatement)
{
return $this->prepared->rowCount();
}
else
{
return 0;
}
}
/**
* Method to get the database collation in use by sampling a text field of a table in the database.
*
* @return mixed The collation in use by the database or boolean false if not supported.
*
* @since 1.0
*/
public function getCollation()
{
return $this->charset;
}
public function getNumRows($cursor = null)
{
$this->open();
if ($cursor instanceof PDOStatement)
{
return $cursor->rowCount();
}
elseif ($this->prepared instanceof PDOStatement)
{
return $this->prepared->rowCount();
}
else
{
return 0;
}
}
/**
* Retrieve a PDO database connection attribute
* http://www.php.net/manual/en/pdo.getattribute.php
*
* Usage: $db->getOption(PDO::ATTR_CASE);
*
* @param mixed $key One of the PDO::ATTR_* Constants
*
* @return mixed
*
* @since 1.0
*/
public function getOption($key)
{
$this->open();
return $this->connection->getAttribute($key);
}
/**
* Get the current query object or a new Query object.
* We have to override the parent method since it will always return a PDO query, while we have a
* specialized class for SQLite
*
* @param boolean $new False to return the current query object, True to return a new Query object.
*
* @return QueryBase The current query object or a new object extending the Query class.
*
* @throws RuntimeException
*/
public function getQuery($new = false)
{
if ($new)
{
return new Query\Sqlite($this);
}
return $this->sql;
}
/**
* Retrieves field information about a given table.
*
* @param string $table The name of the database table.
* @param boolean $typeOnly True to only return field types.
*
* @return array An array of fields for the database table.
*
* @throws RuntimeException
* @since 1.0
*/
public function getTableColumns($table, $typeOnly = true)
{
$this->open();
$columns = [];
$query = $this->getQuery(true);
$fieldCasing = $this->getOption(PDO::ATTR_CASE);
$this->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER);
$table = strtoupper($table);
$query->setQuery('pragma table_info(' . $table . ')');
$this->setQuery($query);
$fields = $this->loadObjectList();
if ($typeOnly)
{
foreach ($fields as $field)
{
$columns[$field->NAME] = $field->TYPE;
}
}
else
{
foreach ($fields as $field)
{
// Do some dirty translation to MySQL output.
$columns[$field->NAME] = (object) [
'Field' => $field->NAME,
'Type' => $field->TYPE,
'Null' => ($field->NOTNULL == '1' ? 'NO' : 'YES'),
'Default' => $field->DFLT_VALUE,
'Key' => ($field->PK == '1' ? 'PRI' : ''),
];
}
}
$this->setOption(PDO::ATTR_CASE, $fieldCasing);
return $columns;
}
/**
* Shows the table CREATE statement that creates the given tables.
*
* Note: Doesn't appear to have support in SQLite
*
* @param mixed $tables A table name or a list of table names.
*
* @return array A list of the create SQL for the tables.
*
* @throws RuntimeException
* @since 1.0
*/
public function getTableCreate($tables)
{
$this->open();
// Sanitize input to an array and iterate over the list.
$tables = (array) $tables;
return $tables;
}
/**
* Get the details list of keys for a table.
*
* @param string $table The name of the table.
*
* @return array An array of the column specification for the table.
*
* @throws RuntimeException
* @since 1.0
*/
public function getTableKeys($table)
{
$this->open();
$keys = [];
$query = $this->getQuery(true);
$fieldCasing = $this->getOption(PDO::ATTR_CASE);
$this->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER);
$table = strtoupper($table);
$query->setQuery('pragma table_info( ' . $table . ')');
// $query->bind(':tableName', $table);
$this->setQuery($query);
$rows = $this->loadObjectList();
foreach ($rows as $column)
{
if ($column->PK == 1)
{
$keys[$column->NAME] = $column;
}
}
$this->setOption(PDO::ATTR_CASE, $fieldCasing);
return $keys;
}
/**
* Method to get an array of all tables in the database (schema).
*
* @return array An array of all the tables in the database.
*
* @throws RuntimeException
* @since 1.0
*/
public function getTableList()
{
$this->open();
/* @type Query\Sqlite $query */
$query = $this->getQuery(true);
$type = 'table';
$query->select('name');
$query->from('sqlite_master');
$query->where('type = :type');
$query->bind(':type', $type);
$query->order('name');
$this->setQuery($query);
$tables = $this->loadColumn();
return $tables;
}
/**
* There's no point on return "a list of tables" inside a SQLite database: we are simple going to
* copy the whole database file in the new location
*
* @param bool $abstract
*
* @return array
*/
public function getTables($abstract = true)
{
return [];
}
/**
* Get the version of the database connector.
*
* @return string The database connector version.
*
* @since 1.0
*/
public function getVersion()
{
$this->open();
$this->setQuery("SELECT sqlite_version()");
return $this->loadResult();
}
public function insertid()
{
$this->open();
// Error suppress this to prevent PDO warning us that the driver doesn't support this operation.
return @$this->connection->lastInsertId();
}
/**
* Locks a table in the database.
*
* @param string $table The name of the table to unlock.
*
* @return Sqlite Returns this object to support chaining.
*
* @throws RuntimeException
* @since 1.0
*/
public function lockTable($table)
{
return $this;
}
public function open()
{
if ($this->connected())
{
return;
}
else
{
$this->close();
}
if (isset($this->options['version']) && $this->options['version'] == 2)
{
$format = 'sqlite2:#DBNAME#';
}
else
{
$format = 'sqlite:#DBNAME#';
}
$replace = ['#DBNAME#'];
$with = [$this->options['database']];
// Create the connection string:
$connectionString = str_replace($replace, $with, $format);
try
{
$this->connection = new PDO(
$connectionString,
$this->options['user'],
$this->options['password']
);
}
catch (PDOException $e)
{
throw new RuntimeException('Could not connect to PDO' . ': ' . $e->getMessage(), 2, $e);
}
}
public function query()
{
$this->open();
if (!is_object($this->connection))
{
throw new RuntimeException($this->errorMsg, $this->errorNum);
}
// Take a local copy so that we don't modify the original query and cause issues later
$sql = $this->replacePrefix((string) $this->sql);
if ($this->limit > 0 || $this->offset > 0)
{
$sql .= ' LIMIT ' . $this->limit;
if ($this->offset > 0)
{
$sql .= ' OFFSET ' . $this->offset;
}
}
// Increment the query counter.
$this->count++;
// If debugging is enabled then let's log the query.
if ($this->debug)
{
// Add the query to the object queue.
$this->log[] = $sql;
}
// Reset the error values.
$this->errorNum = 0;
$this->errorMsg = '';
// Execute the query.
$this->executed = false;
if ($this->prepared instanceof PDOStatement)
{
// Bind the variables:
if ($this->sql instanceof Preparable)
{
$bounded =& $this->sql->getBounded();
foreach ($bounded as $key => $obj)
{
$this->prepared->bindParam($key, $obj->value, $obj->dataType, $obj->length, $obj->driverOptions);
}
}
$this->executed = $this->prepared->execute();
}
// If an error occurred handle it.
if (!$this->executed)
{
// Get the error number and message before we execute any more queries.
$errorNum = (int) $this->connection->errorCode();
$errorMsg = (string) 'SQL: ' . implode(", ", $this->connection->errorInfo());
// Check if the server was disconnected.
if (!$this->connected() && !$this->isReconnecting)
{
$this->isReconnecting = true;
try
{
// Attempt to reconnect.
$this->connection = null;
$this->open();
}
catch (RuntimeException $e)
// If connect fails, ignore that exception and throw the normal exception.
{
// Get the error number and message.
$this->errorNum = (int) $this->connection->errorCode();
$this->errorMsg = (string) 'SQL: ' . implode(", ", $this->connection->errorInfo());
// Throw the normal query exception.
throw new RuntimeException($this->errorMsg, $this->errorNum);
}
// Since we were able to reconnect, run the query again.
$result = $this->query();
$this->isReconnecting = false;
return $result;
}
else
// The server was not disconnected.
{
// Get the error number and message from before we tried to reconnect.
$this->errorNum = $errorNum;
$this->errorMsg = $errorMsg;
// Throw the normal query exception.
throw new RuntimeException($this->errorMsg, $this->errorNum);
}
}
return $this->prepared;
}
/**
* Renames a table in the database.
*
* @param string $oldTable The name of the table to be renamed
* @param string $newTable The new name for the table.
* @param string $backup Not used by Sqlite.
* @param string $prefix Not used by Sqlite.
*
* @return Sqlite Returns this object to support chaining.
*
* @throws RuntimeException
* @since 1.0
*/
public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
{
$this->setQuery('ALTER TABLE ' . $oldTable . ' RENAME TO ' . $newTable)->execute();
return $this;
}
/**
* Select a database for use.
*
* @param string $database The name of the database to select for use.
*
* @return boolean True if the database was successfully selected.
*
* @throws RuntimeException
* @since 1.0
*/
public function select($database)
{
$this->open();
$this->_database = $database;
return true;
}
/**
* Sets an attribute on the PDO database handle.
* http://www.php.net/manual/en/pdo.setattribute.php
*
* Usage: $db->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER);
*
* @param integer $key One of the PDO::ATTR_* Constants
* @param mixed $value One of the associated PDO Constants
* related to the particular attribute
* key.
*
* @return boolean
*
* @since 1.0
*/
public function setOption($key, $value)
{
$this->open();
return $this->connection->setAttribute($key, $value);
}
/**
* Sets the SQL statement string for later execution.
*
* @param mixed $query The SQL statement to set either as a JDatabaseQuery object or a string.
* @param integer $offset The affected row offset to set.
* @param integer $limit The maximum affected rows to set.
* @param array $driverOptions The optional PDO driver options
*
* @return Base This object to support method chaining.
*
* @since 1.0
*/
public function setQuery($query, $offset = null, $limit = null, $driverOptions = [])
{
$this->open();
$this->freeResult();
if (is_string($query))
{
// Allows taking advantage of bound variables in a direct query:
$query = $this->getQuery(true)->setQuery($query);
}
if ($query instanceof Limitable && !is_null($offset) && !is_null($limit))
{
$query->setLimit($limit, $offset);
}
$sql = $this->replacePrefix((string) $query);
$this->prepared = $this->connection->prepare($sql, $driverOptions);
// Store reference to the DatabaseQuery instance:
parent::setQuery($query, $offset, $limit);
return $this;
}
/**
* Set the connection to use UTF-8 character encoding.
*
* Returns false automatically for the Oracle driver since
* you can only set the character set when the connection
* is created.
*
* @return boolean True on success.
*
* @since 1.0
*/
public function setUTF()
{
$this->open();
return false;
}
/**
* Method to commit a transaction.
*
* @param boolean $toSavepoint If true, commit to the last savepoint.
*
* @return void
*
* @throws RuntimeException
* @since 1.0
*/
public function transactionCommit($toSavepoint = false)
{
$this->open();
if (!$toSavepoint || $this->transactionDepth <= 1)
{
$this->open();
if (!$toSavepoint || $this->transactionDepth == 1)
{
$this->connection->commit();
}
$this->transactionDepth--;
}
else
{
$this->transactionDepth--;
}
}
/**
* Method to roll back a transaction.
*
* @param boolean $toSavepoint If true, rollback to the last savepoint.
*
* @return void
*
* @throws RuntimeException
* @since 1.0
*/
public function transactionRollback($toSavepoint = false)
{
$this->connected();
if (!$toSavepoint || $this->transactionDepth <= 1)
{
$this->open();
if (!$toSavepoint || $this->transactionDepth == 1)
{
$this->connection->rollBack();
}
$this->transactionDepth--;
}
else
{
$savepoint = 'SP_' . ($this->transactionDepth - 1);
$this->setQuery('ROLLBACK TO ' . $this->quoteName($savepoint));
if ($this->execute())
{
$this->transactionDepth--;
}
}
}
/**
* Method to initialize a transaction.
*
* @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created.
*
* @return void
*
* @throws RuntimeException
* @since 1.0
*/
public function transactionStart($asSavepoint = false)
{
$this->connected();
if (!$asSavepoint || !$this->transactionDepth)
{
$this->open();
if (!$asSavepoint || !$this->transactionDepth)
{
$this->connection->beginTransaction();
}
$this->transactionDepth++;
}
else
{
$savepoint = 'SP_' . $this->transactionDepth;
$this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint));
if ($this->execute())
{
$this->transactionDepth++;
}
}
}
/**
* Unlocks tables in the database.
*
* @return Sqlite Returns this object to support chaining.
*
* @throws RuntimeException
* @since 1.0
*/
public function unlockTables()
{
return $this;
}
protected function fetchArray($cursor = null)
{
if (!empty($cursor) && $cursor instanceof PDOStatement)
{
return $cursor->fetch(PDO::FETCH_NUM);
}
if ($this->prepared instanceof PDOStatement)
{
return $this->prepared->fetch(PDO::FETCH_NUM);
}
}
protected function fetchObject($cursor = null, $class = 'stdClass')
{
if (!empty($cursor) && $cursor instanceof PDOStatement)
{
return $cursor->fetchObject($class);
}
if ($this->prepared instanceof PDOStatement)
{
return $this->prepared->fetchObject($class);
}
}
}

View File

@@ -0,0 +1,986 @@
<?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\Dump;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Core\Domain\Pack;
use Akeeba\Engine\Driver\Base as DriverBase;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Util\FileCloseAware;
use Exception;
use RuntimeException;
abstract class Base extends Part
{
use FileCloseAware;
// **********************************************************************
// Configuration parameters
// **********************************************************************
/** @var int Current dump file part number */
public $partNumber = 0;
/** @var string Prefix to this database */
protected $prefix = '';
/** @var string MySQL database server host name or IP address */
protected $host = '';
/** @var string MySQL database server port (optional) */
protected $port = '';
/** @var string MySQL user name, for authentication */
protected $username = '';
/** @var string MySQL password, for authentication */
protected $password = '';
/** @var string MySQL database */
protected $database = '';
/** @var string The database driver to use */
protected $driver = '';
// **********************************************************************
// File handling fields
// **********************************************************************
/** @var boolean Should I post process quoted values */
protected $postProcessValues = false;
/** @var string Absolute path to dump file; must be writable (optional; if left blank it is automatically calculated) */
protected $dumpFile = '';
/** @var string Data cache, used to cache data before being written to disk */
protected $data_cache = '';
/** @var int */
protected $largest_query = 0;
/** @var int Size of the data cache, default 128Kb */
protected $cache_size = 131072;
/** @var bool Should I process empty prefixes when creating abstracted names? */
protected $processEmptyPrefix = true;
/** @var string Absolute path to the temp file */
protected $tempFile = '';
/** @var string Relative path of how the file should be saved in the archive */
protected $saveAsName = '';
/** @var array Contains the sorted (by dependencies) list of tables/views to backup */
protected $tables = [];
// **********************************************************************
// Protected fields (data handling)
// **********************************************************************
/** @var array Contains the configuration data of the tables */
protected $tables_data = [];
/** @var array Maps database table names to their abstracted format */
protected $table_name_map = [];
/** @var array Contains the dependencies of tables and views (temporary) */
protected $dependencies = [];
/** @var string The next table to backup */
protected $nextTable;
/** @var integer The next row of the table to start backing up from */
protected $nextRange;
/** @var integer Current table's row count */
protected $maxRange;
/** @var bool Use extended INSERTs */
protected $extendedInserts = false;
/** @var integer Maximum packet size for extended INSERTs, in bytes */
protected $packetSize = 0;
/** @var string Extended INSERT query, while it's being constructed */
protected $query = '';
/** @var int Dump part's maximum size */
protected $partSize = 0;
/** @var resource Filepointer to the current dump part */
private $fp = null;
/**
* Should I be using an abstract prefix (#__) for table names?
*
* @var bool
* @since 9.1.0
*/
private $useAbstractPrefix;
/**
* Public constructor.
*
* @return void
* @since 9.1.0
*/
public function __construct()
{
$this->useAbstractPrefix =
Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') !== 'output';
parent::__construct();
}
/**
* This method is called when the factory is being serialized and is used to perform necessary cleanup steps.
*
* @return void
*/
public function _onSerialize()
{
$this->closeFile();
}
/**
* This method is called when the object is destroyed and is used to perform necessary cleanup steps.
*/
public function __destruct()
{
$this->closeFile();
}
/**
* Close any open SQL dump (output) file.
*/
public function closeFile()
{
if (!is_resource($this->fp))
{
return;
}
Factory::getLog()->debug("Closing SQL dump file.");
$this->conditionalFileClose($this->fp);
$this->fp = null;
}
/**
* Call a specific stage of the dump engine
*
* @param string $stage
*
* @throws Exception
*/
public function callStage($stage)
{
switch ($stage)
{
case '_prepare':
$this->_prepare();
break;
case '_run':
$this->_run();
break;
case '_finalize':
$this->_finalize();
break;
}
}
/**
* Find where to store the backup files
*
* @param $partNumber int The SQL part number, default is 0 (.sql)
*/
protected function getBackupFilePaths($partNumber = 0)
{
Factory::getLog()->debug(__CLASS__ . " :: Getting temporary file");
$this->tempFile = Factory::getTempFiles()->registerTempFile(dechex(crc32(microtime())) . '.sql');
Factory::getLog()->debug(__CLASS__ . " :: Temporary file is {$this->tempFile}");
// Get the base name of the dump file
$partNumber = intval($partNumber);
$baseName = $this->dumpFile;
if ($partNumber > 0)
{
// The file names are in the format dbname.sql, dbname.s01, dbname.s02, etc
if (strtolower(substr($baseName, -4)) == '.sql')
{
$baseName = substr($baseName, 0, -4) . '.s' . sprintf('%02u', $partNumber);
}
else
{
$baseName = $baseName . '.s' . sprintf('%02u', $partNumber);
}
}
if (empty($this->installerSettings))
{
// Fetch the installer settings
$this->installerSettings = (object) [
'installerroot' => 'installation',
'sqlroot' => 'installation/sql',
'databasesini' => 1,
'readme' => 1,
'extrainfo' => 1,
];
$config = Factory::getConfiguration();
$installerKey = $config->get('akeeba.advanced.embedded_installer');
$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
if (array_key_exists($installerKey, $installerDescriptors))
{
// The selected installer exists, use it
$this->installerSettings = (object) $installerDescriptors[$installerKey];
}
elseif (array_key_exists('angie', $installerDescriptors))
{
// The selected installer doesn't exist, but ANGIE exists; use that instead
$this->installerSettings = (object) $installerDescriptors['angie'];
}
}
switch (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal'))
{
case 'output':
// The SQL file will be stored uncompressed in the output directory
$statistics = Factory::getStatistics();
$statRecord = $statistics->getRecord();
$this->saveAsName = $statRecord['absolute_path'];
break;
case 'normal':
// The SQL file will be stored in the SQL root of the archive, as
// specified by the particular embedded installer's settings
$this->saveAsName = $this->installerSettings->sqlroot . '/' . $baseName;
break;
case 'short':
// The SQL file will be stored on archive's root
$this->saveAsName = $baseName;
break;
}
if ($partNumber > 0)
{
Factory::getLog()->debug("AkeebaDomainDBBackup :: Creating new SQL dump part #$partNumber");
}
Factory::getLog()->debug("AkeebaDomainDBBackup :: SQL temp file is " . $this->tempFile);
Factory::getLog()->debug("AkeebaDomainDBBackup :: SQL file location in archive is " . $this->saveAsName);
}
/**
* Deletes any leftover files from previous backup attempts
*
*/
protected function removeOldFiles()
{
Factory::getLog()->debug("AkeebaDomainDBBackup :: Deleting leftover files, if any");
if (file_exists($this->tempFile))
{
@unlink($this->tempFile);
}
}
/**
* Populates the table arrays with the information for the db entities to backup
*
* @return null
*/
protected abstract function getTablesToBackup();
/**
* Runs a step of the database dump
*
* @return null
*/
protected abstract function stepDatabaseDump();
/**
* Implements the _prepare abstract method
*
* @throws Exception
*/
protected function _prepare()
{
$this->setStep('Initialization');
$this->setSubstep('');
// Process parameters, passed to us using the setup() public method
Factory::getLog()->debug(__CLASS__ . " :: Processing parameters");
if (is_array($this->_parametersArray))
{
$this->driver = array_key_exists('driver', $this->_parametersArray) ? $this->_parametersArray['driver'] : $this->driver;
$this->host = array_key_exists('host', $this->_parametersArray) ? $this->_parametersArray['host'] : $this->host;
$this->port = array_key_exists('port', $this->_parametersArray) ? $this->_parametersArray['port'] : $this->port;
$this->username = array_key_exists('username', $this->_parametersArray) ? $this->_parametersArray['username'] : $this->username;
$this->username = array_key_exists('user', $this->_parametersArray) ? $this->_parametersArray['user'] : $this->username;
$this->password = array_key_exists('password', $this->_parametersArray) ? $this->_parametersArray['password'] : $this->password;
$this->database = array_key_exists('database', $this->_parametersArray) ? $this->_parametersArray['database'] : $this->database;
$this->prefix = array_key_exists('prefix', $this->_parametersArray) ? $this->_parametersArray['prefix'] : $this->prefix;
$this->dumpFile = array_key_exists('dumpFile', $this->_parametersArray) ? $this->_parametersArray['dumpFile'] : $this->dumpFile;
$this->processEmptyPrefix = array_key_exists('process_empty_prefix', $this->_parametersArray) ? $this->_parametersArray['process_empty_prefix'] : $this->processEmptyPrefix;
}
// Make sure we have self-assigned the first part
$this->partNumber = 0;
// Get DB backup only mode
$configuration = Factory::getConfiguration();
// Find tables to be included and put them in the $_tables variable
$this->getTablesToBackup();
// Find where to store the database backup files
$this->getBackupFilePaths($this->partNumber);
// Remove any leftovers
$this->removeOldFiles();
// Initialize the extended INSERTs feature
$this->extendedInserts = ($configuration->get('engine.dump.common.extended_inserts', 0) != 0);
$this->packetSize = (int) $configuration->get('engine.dump.common.packet_size', 0);
if ($this->packetSize == 0)
{
$this->extendedInserts = false;
}
// Initialize the split dump feature
$this->partSize = $configuration->get('engine.dump.common.splitsize', 1048576);
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'output')
{
$this->partSize = 0;
}
if (($this->partSize != 0) && ($this->packetSize != 0) && ($this->packetSize > $this->partSize))
{
$this->packetSize = floor($this->partSize / 2);
}
// Initialize the algorithm
Factory::getLog()->debug(__CLASS__ . " :: Initializing algorithm for first run");
$this->nextTable = array_shift($this->tables);
// If there is no table to back up we are done with the database backup
if (empty($this->nextTable))
{
$this->setState(self::STATE_POSTRUN);
return;
}
$this->nextRange = 0;
$this->query = '';
// FIX 2.2: First table of extra databases was not being written to disk.
// This deserved a place in the Bug Fix Hall Of Fame. In subsequent calls to _init, the $fp in
// _writeline() was not nullified. Therefore, the first dump chunk (that is, the first table's
// definition and first chunk of its data) were not written to disk. This call causes $fp to be
// nullified, causing it to be recreated, pointing to the correct file.
$null = null;
$this->writeline($null);
// Finally, mark ourselves "prepared".
$this->setState(self::STATE_PREPARED);
}
/**
* Implements the _run() abstract method
*
* @throws Exception
*/
protected function _run()
{
// Check if we are already done
if ($this->getState() == self::STATE_POSTRUN)
{
Factory::getLog()->debug(__CLASS__ . " :: Already finished");
$this->setStep("");
$this->setSubstep("");
return;
}
// Mark ourselves as still running (we will test if we actually do towards the end ;) )
$this->setState(self::STATE_RUNNING);
/**
* Resume packing / post-processing part files if necessary.
*
* @see \Akeeba\Engine\Archiver\BaseArchiver::putDataFromFileIntoArchive
*
* Sometimes the SQL part file may be bigger than the big file threshold (engine.archiver.common.
* big_file_threshold). In this case when we try to add it to the backup archive the archiver engine figures
* out it has to be added uncompressed, one chunk (engine.archiver.common.chunk_size) bytes at a time. This
* happens in a loop. We read a chunk, push it to the archive, rinse and repeat.
*
* There are two cases when we might break the loop:
*
* 1. Not enough free space in the backup archive part and engine.postproc.common.after_part (immediate post-
* processing) is enabled. We break the step to let the backup part be post-processed.
*
* 2. We ran out of time copying data.
*
* The following if-blocks deal with these two cases.
*/
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') != 'output')
{
$archiver = Factory::getArchiverEngine();
$configuration = Factory::getConfiguration();
// Check whether we need to immediately post-processing a done part
if (Pack::postProcessDonePartFile($archiver, $configuration))
{
return;
}
// We had already started putting the DB dump file into the archive but it needs more time
if ($configuration->get('volatile.engine.archiver.processingfile', false))
{
/**
* We MUST NOT try to continue adding the file to the backup archive manually. Instead, we have to go
* through getNextDumpPart. This method will continue adding the part to the backup archive and when
* this is done it will remove the file and create a new one.
*
* If that method returns false it means that we either hit an error or the archiver didn't have enough
* time to add the part to the backup archive. In either case we have to return and let the Engine step.
*/
if ($this->getNextDumpPart() === false)
{
return;
}
}
}
$this->stepDatabaseDump();
$null = null;
$this->writeline($null);
}
/**
* Implements the _finalize() abstract method
*
* @throws Exception
*/
protected function _finalize()
{
Factory::getLog()->debug("Adding any extra SQL statements imposed by the filters");
foreach (Factory::getFilters()->getExtraSQL($this->databaseRoot) as $sqlStatement)
{
$sqlStatement = trim($sqlStatement) . "\n";
$this->writeDump($sqlStatement, true);
}
// We need this to write out the cached extra SQL statements before closing the file.
$this->writeDump(null);
// Close the file pointer (otherwise the SQL file is left behind)
$this->closeFile();
// If we are not just doing a main db only backup, add the SQL file to the archive
$finished = true;
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') != 'output')
{
$archiver = Factory::getArchiverEngine();
$configuration = Factory::getConfiguration();
if ($configuration->get('volatile.engine.archiver.processingfile', false))
{
// We had already started archiving the db file, but it needs more time
Factory::getLog()->debug("Continuing adding the SQL dump to the archive");
$archiver->addFile(null, null, null);
$finished = !$configuration->get('volatile.engine.archiver.processingfile', false);
}
else
{
// We have to add the dump file to the archive
Factory::getLog()->debug("Adding the final SQL dump to the archive");
$archiver->addFileRenamed($this->tempFile, $this->saveAsName);
$finished = !$configuration->get('volatile.engine.archiver.processingfile', false);
}
}
else
{
// We just have to move the dump file to its final destination
Factory::getLog()->debug("Moving the SQL dump to its final location");
$result = Platform::getInstance()->move($this->tempFile, $this->saveAsName);
if (!$result)
{
Factory::getLog()->debug("Removing temporary file of final SQL dump");
Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true);
throw new RuntimeException('Could not move the SQL dump to its final location');
}
}
// Make sure that if the archiver needs more time to process the file we can supply it
if ($finished)
{
Factory::getLog()->debug("Removing temporary file of final SQL dump");
Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true);
$this->setState(self::STATE_FINISHED);
}
}
/**
* Creates a new dump part
*
* @return bool
* @throws Exception
*/
protected function getNextDumpPart()
{
// On database dump only mode we mustn't create part files!
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'output')
{
return false;
}
// Is the archiver still processing?
$configuration = Factory::getConfiguration();
$archiver = Factory::getArchiverEngine();
$stillProcessing = $configuration->get('volatile.engine.archiver.processingfile', false);
if ($stillProcessing)
{
/**
* The archiver is still adding the previous dump part. This means that we are called from the top few lines
* of the _run method. We must continue adding the previous dump part.
*/
Factory::getLog()->debug("Continuing adding the SQL dump part to the archive");
$archiver->addFile('', '', '');
}
else
{
/**
* There is no other dump part being processed. Therefore the current SQL dump part is still open. We must
* close it and ask the archiver to add it to the backup archive.
*/
$this->closeFile();
Factory::getLog()->debug("Adding the SQL dump part to the archive");
$archiver->addFileRenamed($this->tempFile, $this->saveAsName);
}
// Return false if the file didn't finish getting added to the archive
if ($configuration->get('volatile.engine.archiver.processingfile', false))
{
Factory::getLog()->debug("The SQL dump file has not been processed thoroughly by the archiver. Resuming in the next step.");
return false;
}
/**
* If you are here the SQL dump part file is completely added to the backup archive. All we have to do now is
* remove it and create a new dump part file.
*/
// Remove the old file
Factory::getLog()->debug("Removing dump part's temporary file");
Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true);
// Create the new dump part
$this->partNumber++;
$this->getBackupFilePaths($this->partNumber);
$null = null;
$this->writeline($null);
return true;
}
/**
* Creates a new dump part, but only if required to do so
*
* @return bool
* @throws Exception
*/
protected function createNewPartIfRequired()
{
if ($this->partSize == 0)
{
return true;
}
$filesize = 0;
if (@file_exists($this->tempFile))
{
$filesize = @filesize($this->tempFile);
}
$projectedSize = $filesize + strlen($this->query);
if ($this->extendedInserts)
{
$projectedSize = $filesize + $this->packetSize;
}
if ($projectedSize > $this->partSize)
{
return $this->getNextDumpPart();
}
return true;
}
/**
* Returns a table's abstract name (replacing the prefix with the magic #__ string)
*
* @param string $tableName The canonical name, e.g. 'jos_content'
*
* @return string The abstract name, e.g. '#__content'
*/
protected function getAbstract($tableName)
{
// Don't return abstract names for non-CMS tables
if (is_null($this->prefix))
{
return $tableName;
}
switch ($this->prefix)
{
case '':
if ($this->processEmptyPrefix)
{
// This is more of a hack; it assumes all tables are core CMS tables if the prefix is empty.
return '#__' . $tableName;
}
// If $this->processEmptyPrefix (the process_empty_prefix config flag) is false, we don't
// assume anything.
return $tableName;
break;
default:
// Normal behaviour for 99% of sites. Start by assuming the table has no prefix, therefore is non-core.
$tableAbstract = $tableName;
// If there's a prefix use the abstract name
if (!empty($this->prefix) && (substr($tableName, 0, strlen($this->prefix)) == $this->prefix))
{
$tableAbstract = '#__' . substr($tableName, strlen($this->prefix));
}
return $tableAbstract;
break;
}
}
/**
* Writes the SQL dump into the output files. If it fails, it sets the error
*
* @param string $data Data to write to the dump file. Pass NULL to force flushing to file.
* @param bool $addMarker Should I prefix the data with a marker?
*
* @return boolean TRUE on successful write, FALSE otherwise
* @throws Exception
*/
protected function writeDump($data, $addMarker = false)
{
if (!empty($data))
{
if ($addMarker && $this->useAbstractPrefix)
{
$this->data_cache .= '/**ABDB**/';
}
elseif (!$this->useAbstractPrefix)
{
// Replace #__ with the prefix when writing plain .sql files
$db = $this->getDB();
$data = $db->replacePrefix($data) . "\n";
}
$this->data_cache .= $data;
if (strlen($data) > $this->largest_query)
{
$this->largest_query = strlen($data);
Factory::getConfiguration()->set('volatile.database.largest_query', $this->largest_query);
}
}
if ((strlen($this->data_cache) >= $this->cache_size) || (is_null($data) && (!empty($this->data_cache))))
{
Factory::getLog()->debug("Writing " . strlen($this->data_cache) . " bytes to the dump file");
$result = $this->writeline($this->data_cache);
if (!$result)
{
$errorMessage = sprintf('Couldn\'t write to the SQL dump file %s; check the temporary directory permissions and make sure you have enough disk space available.', $this->tempFile);
throw new RuntimeException($errorMessage);
}
$this->data_cache = '';
}
return true;
}
/**
* Saves the string in $fileData to the file $backupfile. Returns TRUE. If saving
* failed, return value is FALSE.
*
* @param string $fileData Data to write. Set to null to close the file handle.
*
* @return boolean TRUE is saving to the file succeeded
* @throws Exception
*/
protected function writeline(&$fileData)
{
if (!is_resource($this->fp))
{
$this->fp = @fopen($this->tempFile, 'a');
if ($this->fp === false)
{
throw new RuntimeException('Could not open ' . $this->tempFile . ' for append, in DB dump.');
}
}
if (is_null($fileData))
{
$this->conditionalFileClose($this->fp);
$this->fp = null;
return true;
}
else
{
if ($this->fp)
{
$ret = fwrite($this->fp, $fileData);
@clearstatcache();
// Make sure that all data was written to disk
return ($ret == strlen($fileData));
}
return false;
}
}
/**
* Return an instance of DriverBase
*
* @return DriverBase|bool
*
* @throws Exception
*/
protected function &getDB()
{
$host = $this->host . ($this->port != '' ? ':' . $this->port : '');
$user = $this->username;
$password = $this->password;
$driver = $this->driver;
$database = $this->database;
$prefix = is_null($this->prefix) ? '' : $this->prefix;
$options = [
'driver' => $driver, 'host' => $host, 'user' => $user, 'password' => $password,
'database' => $database, 'prefix' => $prefix,
];
$db = Factory::getDatabase($options);
if ($db->getErrorNum() > 0)
{
$error = $db->getErrorMsg();
throw new RuntimeException(__CLASS__ . ' :: Database Error: ' . $error);
}
return $db;
}
/**
* Return the current database name by querying the database connection object (e.g. SELECT DATABASE() in MySQL)
*
* @return string
*/
abstract protected function getDatabaseNameFromConnection();
/**
* Returns the database name. If the name was not declared when the object was created we will go through the
* getDatabaseNameFromConnection method to populate it.
*
* @return string
*/
protected function getDatabaseName()
{
if (empty($this->database))
{
$this->database = $this->getDatabaseNameFromConnection();
}
return $this->database;
}
/**
* Post process a quoted value before it's written to the database dump.
* So far it's only required for SQL Server which has a problem escaping
* newline characters...
*
* @param string $value The quoted value to post-process
*
* @return string
*/
protected function postProcessQuotedValue($value)
{
return $value;
}
/**
* Returns a preamble for the data dump portion of the SQL backup. This is
* used to output commands before the first INSERT INTO statement for a
* table when outputting a plain SQL file.
*
* Practical use: the SET IDENTITY_INSERT sometable ON required for SQL Server
*
* @param string $tableAbstract Abstract name of the table, e.g. #__foobar
* @param string $tableName Real name of the table, e.g. abc_foobar
* @param integer $maxRange Row count on this table
*
* @return string The SQL commands you want to be written in the dump file
*/
protected function getDataDumpPreamble($tableAbstract, $tableName, $maxRange)
{
return '';
}
/**
* Returns an epilogue for the data dump portion of the SQL backup. This is
* used to output commands after the last INSERT INTO statement for a
* table when outputting a plain SQL file.
*
* Practical use: the SET IDENTITY_INSERT sometable OFF required for SQL Server
*
* @param string $tableAbstract Abstract name of the table, e.g. #__foobar
* @param string $tableName Real name of the table, e.g. abc_foobar
* @param integer $maxRange Row count on this table
*
* @return string The SQL commands you want to be written in the dump file
*/
protected function getDataDumpEpilogue($tableAbstract, $tableName, $maxRange)
{
return '';
}
/**
* Return a list of field names for the INSERT INTO statements. This is only
* required for Microsoft SQL Server because without it the SET IDENTITY_INSERT
* has no effect.
*
* @param array|string $fieldNames A list of field names in array format or '*' if it's all fields
*
* @return string
* @throws Exception
*/
protected function getFieldListSQL($fieldNames)
{
// If we get a literal '*' we dumped all columns so we don't need to add column names in the INSERT.
if ($fieldNames === '*')
{
return '';
}
return '(' . implode(', ', array_map([$this->getDB(), 'qn'], $fieldNames)) . ')';
}
/**
* Return a list of columns to use in the SELECT query for dumping table data.
*
* This is used to filter out all generated rows.
*
* @param string $tableAbstract
*
* @return string|array An array of table columns or the string literal '*' to quickly select all columns.
*
* @see https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html
*/
protected function getSelectColumns($tableAbstract)
{
return '*';
}
/**
* Converts a human formatted size to integer representation of bytes,
* e.g. 1M to 1024768
*
* @param string $setting The value in human readable format, e.g. "1M"
*
* @return integer The value in bytes
*/
protected function humanToIntegerBytes($setting)
{
$val = trim($setting);
$last = strtolower($val[strlen($val) - 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;
}
/**
* Get the PHP memory limit in bytes
*
* @return int|null Memory limit in bytes or null if we can't figure it out.
*/
protected function getMemoryLimit()
{
if (!function_exists('ini_get'))
{
return null;
}
$memLimit = ini_get("memory_limit");
if ((is_numeric($memLimit) && ($memLimit < 0)) || !is_numeric($memLimit))
{
$memLimit = 0; // 1.2a3 -- Rare case with memory_limit < 0, e.g. -1Mb!
}
$memLimit = $this->humanToIntegerBytes($memLimit);
return $memLimit;
}
}

View File

@@ -0,0 +1,135 @@
<?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\Dump;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Dump\Base as DumpBase;
use Akeeba\Engine\Factory;
use RuntimeException;
class Native extends Part
{
/** @var DumpBase */
private $_engine = null;
/**
* Implements the constructor of the class
*
* @return void
*/
public function __construct()
{
parent::__construct();
Factory::getLog()->debug(__CLASS__ . " :: New instance");
}
/**
* Runs the preparation for this part. Should set _isPrepared
* to true
*
* @return void
*/
protected function _prepare()
{
Factory::getLog()->debug(__CLASS__ . " :: Processing parameters");
$options = null;
// Get the DB connection parameters
if (is_array($this->_parametersArray))
{
$driver = array_key_exists('driver', $this->_parametersArray) ? $this->_parametersArray['driver'] : 'mysql';
$host = array_key_exists('host', $this->_parametersArray) ? $this->_parametersArray['host'] : '';
$port = array_key_exists('port', $this->_parametersArray) ? $this->_parametersArray['port'] : '';
$username = array_key_exists('username', $this->_parametersArray) ? $this->_parametersArray['username'] : '';
$username = array_key_exists('user', $this->_parametersArray) ? $this->_parametersArray['user'] : $username;
$password = array_key_exists('password', $this->_parametersArray) ? $this->_parametersArray['password'] : '';
$database = array_key_exists('database', $this->_parametersArray) ? $this->_parametersArray['database'] : '';
$prefix = array_key_exists('prefix', $this->_parametersArray) ? $this->_parametersArray['prefix'] : '';
if (($driver == 'mysql') && !function_exists('mysql_connect'))
{
$driver = 'mysqli';
}
$options = [
'driver' => $driver,
'host' => $host . ($port != '' ? ':' . $port : ''),
'user' => $username,
'password' => $password,
'database' => $database,
'prefix' => is_null($prefix) ? '' : $prefix,
];
}
$db = Factory::getDatabase($options);
if ($db->getErrorNum() > 0)
{
$error = $db->getErrorMsg();
throw new RuntimeException(__CLASS__ . ' :: Database Error: ' . $error);
}
$driverType = $db->getDriverType();
$className = '\\Akeeba\\Engine\\Dump\\Native\\' . ucfirst($driverType);
// Check if we have a native dump driver
if (!class_exists($className, true))
{
$this->setState(self::STATE_ERROR);
throw new ErrorException('Akeeba Engine does not have a native dump engine for ' . $driverType . ' databases');
}
Factory::getLog()->debug(__CLASS__ . " :: Instanciating new native database dump engine $className");
$this->_engine = new $className;
$this->_engine->setup($this->_parametersArray);
$this->_engine->callStage('_prepare');
$this->setState($this->_engine->getState());
}
/**
* Runs the finalisation process for this part. Should set
* _isFinished to true.
*
* @return void
*/
protected function _finalize()
{
$this->_engine->callStage('_finalize');
$this->setState($this->_engine->getState());
}
/**
* Runs the main functionality loop for this part. Upon calling,
* should set the _isRunning to true. When it finished, should set
* the _hasRan to true. If an error is encountered, setError should
* be used.
*
* @return void
*/
protected function _run()
{
$this->_engine->callStage('_run');
$this->setState($this->_engine->getState());
$this->setStep($this->_engine->getStep());
$this->setSubstep($this->_engine->getSubstep());
$this->partNumber = $this->_engine->partNumber;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
<?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\Dump\Native;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Dump\Base;
use Akeeba\Engine\Factory;
/**
* Dump class for the "None" database driver (ie no database used by the application)
*/
class None extends Base
{
public function __construct()
{
parent::__construct();
}
/**
* Populates the table arrays with the information for the db entities to backup
*
* @return void
*/
protected function getTablesToBackup()
{
}
/**
* Runs a step of the database dump
*
* @return void
*/
protected function stepDatabaseDump()
{
Factory::getLog()->info("Reminder: database definitions using the 'None' driver result in no data being backed up.");
$this->setState(self::STATE_FINISHED);
}
/**
* Return the current database name by querying the database connection object (e.g. SELECT DATABASE() in MySQL)
*
* @return string
*/
protected function getDatabaseNameFromConnection()
{
return '';
}
protected function _run()
{
Factory::getLog()->info("Reminder: database definitions using the 'None' driver result in no data being backed up.");
$this->setState(self::STATE_POSTRUN);
}
protected function _finalize()
{
Factory::getLog()->info("Reminder: database definitions using the 'None' driver result in no data being backed up.");
$this->setState(self::STATE_FINISHED);
}
}

View File

@@ -0,0 +1,28 @@
<?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\Dump\Native;
defined('AKEEBAENGINE') || die();
use RuntimeException;
/**
* Dump class for the "None" database driver (ie no database used by the application)
*/
class Sqlite extends None
{
public function __construct()
{
parent::__construct();
throw new RuntimeException("Please do not add SQLite databases, they are files. If they are under your site's root they are backed up automatically. Otherwise use the Off-site Directories Inclusion to include them in the backup.");
}
}

View File

@@ -0,0 +1,81 @@
{
"_information": {
"title": "COM_AKEEBA_CONFIG_ENGINE_DUMP_NATIVE_TITLE",
"description": "COM_AKEEBA_CONFIG_ENGINE_DUMP_NATIVE_DESCRIPTION"
},
"engine.dump.divider.common": {
"default": "0",
"type": "separator",
"title": "COM_AKEEBA_CONFIG_DUMP_DIVIDER_COMMON",
"bold": "1"
},
"engine.dump.common.blankoutpass": {
"default": "0",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_BLANKOUTPASS_TITLE",
"description": "COM_AKEEBA_CONFIG_BLANKOUTPASS_DESCRIPTION"
},
"engine.dump.common.extended_inserts": {
"default": "1",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_EXTENDEDINSERTS_TITLE",
"description": "COM_AKEEBA_CONFIG_EXTENDEDINSERTS_DESCRIPTION"
},
"engine.dump.common.packet_size": {
"default": "131072",
"type": "integer",
"min": "1",
"max": "1048576",
"shortcuts": "16384|32768|65536|131072|262144|524288|1048576",
"scale": "1024",
"uom": "KB",
"title": "COM_AKEEBA_CONFIG_MAXPACKET_TITLE",
"description": "COM_AKEEBA_CONFIG_MAXPACKET_DESCRIPTION"
},
"engine.dump.common.splitsize": {
"default": "524288",
"type": "integer",
"min": "0",
"max": "10485760",
"shortcuts": "524288|1048576|2097152|5242880|10485760",
"scale": "1048576",
"uom": "MB",
"title": "COM_AKEEBA_CONFIG_SPLITDBDUMP_TITLE",
"description": "COM_AKEEBA_CONFIG_SPLITDBDUMP_DESCRIPTION"
},
"engine.dump.common.batchsize": {
"default": "1000",
"type": "integer",
"min": "0",
"max": "100000",
"shortcuts": "10|20|50|100|200|500|1000",
"scale": "1",
"uom": "queries",
"title": "COM_AKEEBA_CONFIG_BACTHSIZE_TITLE",
"description": "COM_AKEEBA_CONFIG_BACTHSIZE_DESCRIPTION"
},
"engine.dump.divider.mysql": {
"default": "0",
"type": "separator",
"title": "COM_AKEEBA_CONFIG_DUMP_DIVIDER_MYSQL",
"bold": "1"
},
"engine.dump.native.advanced_entitites": {
"default": "0",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_MYSQL5FEATURES_ENABLE_TITLE",
"description": "COM_AKEEBA_CONFIG_MYSQL5FEATURES_ENABLE_DESCRIPTION"
},
"engine.dump.native.nodependencies": {
"default": "0",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_NODEPENDENCIES_TITLE",
"description": "COM_AKEEBA_CONFIG_NODEPENDENCIES_DESCRIPTION"
},
"engine.dump.native.nobtree": {
"default": "1",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_MYSQLNOBTREE_TITLE",
"description": "COM_AKEEBA_CONFIG_MYSQLNOBTREE_TIP"
}
}

View File

@@ -0,0 +1,916 @@
<?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;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Core\Database;
use Akeeba\Engine\Core\Filters;
use Akeeba\Engine\Core\Kettenrad;
use Akeeba\Engine\Core\Timer;
use Akeeba\Engine\Driver\Base;
use Akeeba\Engine\Postproc\PostProcInterface;
use Akeeba\Engine\Util\ConfigurationCheck;
use Akeeba\Engine\Util\CRC32;
use Akeeba\Engine\Util\Encrypt;
use Akeeba\Engine\Util\EngineParameters;
use Akeeba\Engine\Util\FactoryStorage;
use Akeeba\Engine\Util\FileLister;
use Akeeba\Engine\Util\FileSystem;
use Akeeba\Engine\Util\Logger;
use Akeeba\Engine\Util\PushMessages;
use Akeeba\Engine\Util\RandomValue;
use Akeeba\Engine\Util\SecureSettings;
use Akeeba\Engine\Util\Statistics;
use Akeeba\Engine\Util\TemporaryFiles;
use Exception;
use RuntimeException;
// Try to kill errors display
if (function_exists('ini_set') && !defined('AKEEBADEBUG'))
{
ini_set('display_errors', false);
}
// Make sure the class autoloader is loaded
require_once __DIR__ . '/Autoloader.php';
/**
* The Akeeba Engine Factory class
*
* This class is responsible for instantiating all Akeeba Engine classes
*/
abstract class Factory
{
/**
* The absolute path to Akeeba Engine's installation
*
* @var string
*/
private static $root;
/**
* Partial class names of the loaded engines e.g. 'archiver' => 'Archiver\\Jpa'. Survives serialization.
*
* @var array
*/
private static $engineClassnames = [];
/**
* A list of instantiated objects which will persist after serialisation / unserialisation
*
* @var array
*/
private static $objectList = [];
/**
* A list of instantiated objects which will NOT persist after serialisation / unserialisation
*
* @var array
*/
private static $temporaryObjectList = [];
/**
* Gets a serialized snapshot of the Factory for safekeeping (hibernate)
*
* @return string The serialized snapshot of the Factory
*/
public static function serialize()
{
// Call _onSerialize in all objects known to the factory
foreach (static::$objectList as $class_name => $object)
{
if (method_exists($object, '_onSerialize'))
{
call_user_func([$object, '_onSerialize']);
}
}
// Serialise an array with all the engine information
$engineInfo = [
'root' => static::$root,
'objectList' => static::$objectList,
'engineClassnames' => static::$engineClassnames,
];
// Serialize the factory
return serialize($engineInfo);
}
/**
* Regenerates the full Factory state from a serialized snapshot (resume)
*
* @param string $serialized_data The serialized snapshot to resume from
*
* @return void
*/
public static function unserialize($serialized_data)
{
static::nuke();
$engineInfo = unserialize($serialized_data);
static::$root = $engineInfo['root'] ?? '';
static::$objectList = $engineInfo['objectList'] ?? [];
static::$engineClassnames = $engineInfo['engineClassnames'] ?? [];
static::$temporaryObjectList = [];
}
/**
* Reset the internal factory state, freeing all previously created objects
*
* @return void
*/
public static function nuke()
{
foreach (static::$objectList as $key => $object)
{
$object = null;
}
foreach (static::$temporaryObjectList as $key => $object)
{
$object = null;
}
static::$objectList = [];
static::$temporaryObjectList = [];
}
/**
* Saves the engine state to temporary storage
*
* @param string $tag The backup origin to save. Leave empty to get from already loaded Kettenrad instance.
* @param string $backupId The backup ID to save. Leave empty to get from already loaded Kettenrad instance.
*
* @return void
*
* @throws RuntimeException When the state save fails for any reason
*/
public static function saveState($tag = null, $backupId = null)
{
$kettenrad = static::getKettenrad();
$tag = $tag ?: $kettenrad->getTag();
$backupId = $backupId ?: $kettenrad->getBackupId();
$saveTag = rtrim($tag . '.' . ($backupId ?: ''), '.');
$ret = $kettenrad->getStatusArray();
if ($ret['HasRun'] == 1)
{
Factory::getLog()->debug("Will not save a finished Kettenrad instance");
return;
}
Factory::getLog()->debug("Saving Kettenrad instance $tag");
// Save a Factory snapshot
$factoryStorage = static::getFactoryStorage();
$logger = static::getLog();
$logger->resetWarnings();
$serializedFactoryData = static::serialize();
$memoryFileExtension = 'php';
$result = $factoryStorage->set($serializedFactoryData, $saveTag, $memoryFileExtension);
/**
* Some hosts, such as WPEngine, do not allow us to save the memory files in .php files. In this case we use the
* far more insecure .dat extension.
*/
if ($result === false)
{
$memoryFileExtension = 'dat';
$result = $factoryStorage->set($serializedFactoryData, $saveTag, $memoryFileExtension);
}
if ($result === false)
{
$saveKey = $factoryStorage->get_storage_filename($saveTag, $memoryFileExtension);
$errorMessage = "Cannot save factory state in storage, storage filename $saveKey";
$logger->error($errorMessage);
throw new RuntimeException($errorMessage);
}
}
/**
* Loads the engine state from the storage (if it exists).
*
* When failIfMissing is true (default) an exception will be thrown if the memory file / database record is no
* longer there. This is a clear indication of an issue with the storage engine, e.g. the host deleting the memory
* files in the middle of the backup step. Therefore we'll switch the storage engine type before throwing the
* exception.
*
* When failIfMissing is false we do NOT throw an exception. Instead, we do a hard reset of the backup factory. This
* is required by the resetState method when we ask it to reset multiple origins at once.
*
* @param string $tag The backup origin to load
* @param string $backupId The backup ID to load
* @param bool $failIfMissing Throw an exception if the memory data is no longer there
*
* @return void
*/
public static function loadState($tag = null, $backupId = null, $failIfMissing = true)
{
$tag = $tag ?: (defined('AKEEBA_BACKUP_ORIGIN') ? AKEEBA_BACKUP_ORIGIN : 'backend');
$loadTag = rtrim($tag . '.' . ($backupId ?: ''), '.');
// In order to load anything, we need to have the correct profile loaded. Let's assume
// that the latest backup record in this tag has the correct profile number set.
$config = static::getConfiguration();
if (empty($config->activeProfile))
{
$profile = Platform::getInstance()->get_active_profile();
if (empty($profile) || ($profile <= 1))
{
// Only bother loading a configuration if none has been already loaded
$filters = [
['field' => 'tag', 'value' => $tag],
];
if (!empty($backupId))
{
$filters[] = ['field' => 'backupid', 'value' => $backupId];
}
$statList = Platform::getInstance()->get_statistics_list([
'filters' => $filters, 'order' => [
'by' => 'id', 'order' => 'DESC',
],
]
);
if (is_array($statList))
{
$stat = array_pop($statList) ?? [];
$profile = $stat['profile_id'] ?? 1;
}
}
Platform::getInstance()->load_configuration($profile);
}
$profile = $config->activeProfile;
Factory::getLog()->open($loadTag);
Factory::getLog()->debug("Kettenrad :: Attempting to load from database ($tag) [$loadTag]");
$serialized_factory = static::getFactoryStorage()->get($loadTag);
if ($serialized_factory === false)
{
if ($failIfMissing)
{
throw new RuntimeException("Akeeba Engine detected a problem while saving temporary data. Please restart your backup.", 500);
}
// There is no serialized factory. Nuke the in-memory factory.
Factory::getLog()->debug(" -- Stored Akeeba Factory ($tag) [$loadTag] not found - hard reset");
static::nuke();
Platform::getInstance()->load_configuration($profile);
}
Factory::getLog()->debug(" -- Loaded stored Akeeba Factory ($tag) [$loadTag]");
static::unserialize($serialized_factory);
unset($serialized_factory);
}
// ========================================================================
// Public factory interface
// ========================================================================
/**
* Resets the engine state, wiping out any pending backups and/or stale temporary data.
*
* The configuration parameters are:
*
* * global `bool` True to reset all backups, regardless of the origin or profile ID
* * log `bool` True to log our actions (default: false)
* * maxrun `int` Only backup records older than this number of seconds will be reset (default: 180)
*
* Special considerations:
*
* * If global = true all backups from all origins are taken into account to determine which ones are stuck (over
* the maxrun threshold since their last database entry).
*
* * If global = false only backups from the current backup origin are taken into account.
*
* * If global = false AND the current origin is 'backend' all pending and idle backups with the 'backup' origin are
* considered stuck regardless of their age. In other words, maxrun is effectively set to 0. The idea is that only
* a single person, from a single browser, should be taking backend backups at a time. Resetting single origin
* backups is only ever meant to be called by the consumer when starting a backup.
*
* * Corollary to the above: starting a frontend, CLI or JSON API backup with the same backup profile DOES NOT reset
* a previously failed backup if the new backup starts less than 'maxrun' seconds since the last step of the
* failed backup started.
*
* * The time information for the backup age is taken from the database, namely the backupend field. If no time
* is recorded for the last step we use the backupstart field instead.
*
* @param array $config Configuration parameters for the reset operation
*
* @return void
* @throws Exception
*/
public static function resetState($config = [])
{
$default_config = [
'global' => true,
'log' => false,
'maxrun' => 180,
];
$config = (object) array_merge($default_config, $config);
// Pause logging if so desired
if (!$config->log)
{
Factory::getLog()->pause();
}
// Get the origin to clear, depending on the 'global' setting
$originTag = $config->global ? null : Platform::getInstance()->get_backup_origin();
// Cache the factory before proceeding
$factory = static::serialize();
// Get all running backups for the selected origin (or all origins, if global was false).
$runningList = Platform::getInstance()->get_running_backups($originTag);
// Sanity check
if (!is_array($runningList))
{
$runningList = [];
}
// If the current origin is 'backend' we assume maxrun = 0 per the method docblock notes.
$maxRun = ($originTag == 'backend') ? 0 : $config->maxrun;
// Filter out entries by backup age
$now = time();
$cutOff = $now - $maxRun;
$runningList = array_filter($runningList, function (array $running) use ($cutOff, $maxRun) {
// No cutoff time: include all currently running backup records
if ($maxRun == 0)
{
return true;
}
// Try to get the last backup tick timestamp
try
{
$backupTickTime = !empty($running['backupend']) ? $running['backupend'] : $running['backupstart'];
$tz = new \DateTimeZone('UTC');
$tstamp = (new \DateTime($backupTickTime, $tz))->getTimestamp();
}
catch (Exception $e)
{
$tstamp = Factory::getLog()->getLastTimestamp($running['origin']);
}
if (is_null($tstamp))
{
return false;
}
// Only include still running backups whose last tick was BEFORE the cutoff time
return $tstamp <= $cutOff;
});
// Mark running backups as failed
foreach ($runningList as $running)
{
// Delete the failed backup's leftover archive parts
$filenames = Factory::getStatistics()->get_all_filenames($running, false);
$filenames = is_null($filenames) ? [] : $filenames;
$totalSize = 0;
foreach ($filenames as $failedArchive)
{
if (!@file_exists($failedArchive))
{
continue;
}
$totalSize += (int) @filesize($failedArchive);
Platform::getInstance()->unlink($failedArchive);
}
// Mark the backup failed
$running['status'] = 'fail';
$running['instep'] = 0;
$running['total_size'] = empty($running['total_size']) ? $totalSize : $running['total_size'];
$running['multipart'] = 0;
Platform::getInstance()->set_or_update_statistics($running['id'], $running);
// Remove the temporary data
$backupId = isset($running['backupid']) ? ('.' . $running['backupid']) : '';
self::removeTemporaryData($running['origin'] . $backupId);
}
// Reload the factory
static::unserialize($factory);
unset($factory);
// Unpause logging if it was previously paused
if (!$config->log)
{
Factory::getLog()->unpause();
}
}
/**
* Returns an Akeeba Configuration object
*
* @return Configuration The Akeeba Configuration object
*/
public static function getConfiguration()
{
return static::getObjectInstance('Configuration');
}
/**
* Returns a statistics object, used to track current backup's progress
*
* @return Statistics
*/
public static function getStatistics()
{
return static::getObjectInstance('Util\\Statistics');
}
/**
* Returns the currently configured archiver engine
*
* @param bool $reset Should I try to forcible create a new instance?
*
* @return Archiver\Base
*/
public static function getArchiverEngine($reset = false)
{
return static::getEngineInstance(
'archiver', 'akeeba.advanced.archiver_engine',
'Archiver\\', 'Archiver\\Jpa',
$reset
);
}
/**
* Returns the currently configured dump engine
*
* @param boolean $reset Should I try to forcible create a new instance?
*
* @return Dump\Base
*/
public static function getDumpEngine($reset = false)
{
return static::getEngineInstance(
'dump', 'akeeba.advanced.dump_engine',
'Dump\\', 'Dump\\Native',
$reset
);
}
/**
* Returns the filesystem scanner engine instance
*
* @param bool $reset Should I try to forcible create a new instance?
*
* @return Scan\Base The scanner engine
*/
public static function getScanEngine($reset = false)
{
return static::getEngineInstance(
'scan', 'akeeba.advanced.scan_engine',
'Scan\\', 'Scan\\Large',
$reset
);
}
/**
* Returns the current post-processing engine. If no class is specified we
* return the post-processing engine configured in akeeba.advanced.postproc_engine
*
* @param string $engine The name of the post-processing class to forcibly return
*
* @return PostProcInterface
*/
public static function getPostprocEngine($engine = null)
{
if (!is_null($engine))
{
static::$engineClassnames['postproc'] = 'Postproc\\' . ucfirst($engine);
return static::getObjectInstance(static::$engineClassnames['postproc']);
}
return static::getEngineInstance(
'postproc', 'akeeba.advanced.postproc_engine',
'Postproc\\', 'Postproc\\None',
true
);
}
// ========================================================================
// Core objects which are part of the engine state
// ========================================================================
/**
* Returns an instance of the Filters feature class
*
* @return Filters The Filters feature class' object instance
*/
public static function getFilters()
{
return static::getObjectInstance('Core\\Filters');
}
/**
* Returns an instance of the specified filter group class. Do note that it does not
* work with platform filter classes. They are handled internally by AECoreFilters.
*
* @param string $filter_name The filter class to load, without AEFilter prefix
*
* @return Filter\Base The filter class' object instance
*/
public static function getFilterObject($filter_name)
{
return static::getObjectInstance('Filter\\' . ucfirst($filter_name));
}
/**
* Loads an engine domain class and returns its associated object
*
* @param string $domain_name The name of the domain, e.g. installer for AECoreDomainInstaller
*
* @return Part
*/
public static function getDomainObject($domain_name)
{
return static::getObjectInstance('Core\\Domain\\' . ucfirst($domain_name));
}
/**
* Returns a database connection object. It's an alias of AECoreDatabase::getDatabase()
*
* @param array $options Options to use when instantiating the database connection
*
* @return Base
*/
public static function getDatabase($options = null)
{
if (is_null($options))
{
$options = Platform::getInstance()->get_platform_database_options();
}
if (isset($options['username']) && !isset($options['user']))
{
$options['user'] = $options['username'];
}
return Database::getDatabase($options);
}
/**
* Returns a database connection object. It's an alias of AECoreDatabase::getDatabase()
*
* @param array $options Options to use when instantiating the database connection
*
* @return void
*/
public static function unsetDatabase($options = null)
{
if (is_null($options))
{
$options = Platform::getInstance()->get_platform_database_options();
}
$db = Database::getDatabase($options);
$db->close();
Database::unsetDatabase($options);
}
/**
* Get the a reference to the Akeeba Engine's timer
*
* @return Timer
*/
public static function getTimer()
{
return static::getObjectInstance('Core\\Timer');
}
/**
* Get a reference to Akeeba Engine's main controller called Kettenrad
*
* @return Kettenrad
*/
public static function getKettenrad()
{
return static::getObjectInstance('Core\\Kettenrad');
}
/**
* Returns an instance of the factory storage class (formerly Tempvars)
*
* @return FactoryStorage
*/
public static function getFactoryStorage()
{
return static::getTempObjectInstance('Util\\FactoryStorage');
}
/**
* Returns an instance of the encryption class
*
* @return Encrypt
*/
public static function getEncryption()
{
return static::getTempObjectInstance('Util\\Encrypt');
}
/**
* Returns an instance of the CRC32 calculations class
*
* @return CRC32
*/
public static function getCRC32Calculator()
{
return static::getTempObjectInstance('Util\\CRC32');
}
/**
* Returns an instance of the crypto-safe random value generator class
*
* @return RandomValue
*/
public static function getRandval()
{
return static::getTempObjectInstance('Util\\RandomValue');
}
/**
* Returns an instance of the filesystem tools class
*
* @return FileSystem
*/
public static function getFilesystemTools()
{
return static::getTempObjectInstance('Util\\FileSystem');
}
/**
* Returns an instance of the filesystem tools class
*
* @return FileLister
*/
public static function getFileLister()
{
return static::getTempObjectInstance('Util\\FileLister');
}
// ========================================================================
// Temporary objects which are not part of the engine state
// ========================================================================
/**
* Returns an instance of the engine parameters provider which provides information on scripting, GUI configuration
* elements and engine parts
*
* @return EngineParameters
*/
public static function getEngineParamsProvider()
{
return static::getTempObjectInstance('Util\\EngineParameters');
}
/**
* Returns an instance of the log object
*
* @return Logger
*/
public static function getLog()
{
return static::getTempObjectInstance('Util\\Logger');
}
/**
* Returns an instance of the configuration checks object
*
* @return ConfigurationCheck
*/
public static function getConfigurationChecks()
{
return static::getTempObjectInstance('Util\\ConfigurationCheck');
}
/**
* Returns an instance of the secure settings handling object
*
* @return SecureSettings
*/
public static function getSecureSettings()
{
return static::getTempObjectInstance('Util\\SecureSettings');
}
/**
* Returns an instance of the secure settings handling object
*
* @return TemporaryFiles
*/
public static function getTempFiles()
{
return static::getTempObjectInstance('Util\\TemporaryFiles');
}
/**
* Get the connector object for push messages
*
* @return PushMessages
*/
public static function getPush()
{
return static::getObjectInstance('Util\\PushMessages');
}
/**
* Returns the absolute path to Akeeba Engine's installation
*
* @return string
*/
public static function getAkeebaRoot()
{
if (empty(static::$root))
{
static::$root = __DIR__;
}
return static::$root;
}
/**
* @param string $engineType Engine type, e.g. 'archiver', 'postproc', ...
* @param string $configKey Profile config key with configured engine e.g. 'akeeba.advanced.archiver_engine'
* @param string $prefix Prefix for engine classes, e.g. 'Archiver\\'
* @param string $fallback Fallback class if the configured one doesn't exist e.g. 'Archiver\\Jpa'. Empty for
* no fallback.
* @param bool $reset Should I force-reload the engine? Default: false.
*
* @return mixed The Singleton engine object instance
*/
protected static function getEngineInstance($engineType, $configKey, $prefix, $fallback, $reset = false)
{
if (!$reset && !empty(static::$engineClassnames[$engineType]))
{
return static::getObjectInstance(static::$engineClassnames[$engineType]);
}
// Unset the existing engine object
if (!empty(static::$engineClassnames[$engineType]))
{
static::unsetObjectInstance(static::$engineClassnames[$engineType]);
}
// Get the engine name from the backup profile, construct a class name and check if it exists
$registry = static::getConfiguration();
$engine = $registry->get($configKey);
static::$engineClassnames[$engineType] = $prefix . ucfirst($engine);
$object = static::getObjectInstance(static::$engineClassnames[$engineType]);
// If the engine object does not exist, fall back to the default
if (!empty($fallback) && $object === false)
{
static::unsetObjectInstance(static::$engineClassnames[$engineType]);
static::$engineClassnames[$engineType] = $fallback;
}
return static::getObjectInstance(static::$engineClassnames[$engineType]);
}
/**
* Internal function which instantiates an object of a class named $class_name.
*
* @param string $class_name
*
* @return mixed
*/
protected static function getObjectInstance($class_name)
{
$class_name = trim($class_name, '\\');
if (isset(static::$objectList[$class_name]))
{
return static::$objectList[$class_name];
}
static::$objectList[$class_name] = false;
$searchClass = '\\Akeeba\\Engine\\' . $class_name;
if (class_exists($searchClass))
{
static::$objectList[$class_name] = new $searchClass;
}
return static::$objectList[$class_name];
}
// ========================================================================
// Handy functions
// ========================================================================
/**
* Internal function which removes the object of the class named $class_name
*
* @param string $class_name
*
* @return void
*/
protected static function unsetObjectInstance($class_name)
{
if (isset(static::$objectList[$class_name]))
{
static::$objectList[$class_name] = null;
unset(static::$objectList[$class_name]);
}
}
/**
* Internal function which instantiates an object of a class named $class_name. This is a temporary instance which
* will not survive serialisation and subsequent unserialisation.
*
* @param string $class_name
*
* @return mixed
*/
protected static function getTempObjectInstance($class_name)
{
$class_name = trim($class_name, '\\');
if (!isset(static::$temporaryObjectList[$class_name]))
{
static::$temporaryObjectList[$class_name] = false;
$searchClassname = '\\Akeeba\\Engine\\' . $class_name;
if (class_exists($searchClassname))
{
static::$temporaryObjectList[$class_name] = new $searchClassname;
}
}
return static::$temporaryObjectList[$class_name];
}
/**
* Remote the temporary data for a specific backup tag.
*
* @param string $originTag The backup tag to reset e.g. 'backend.id123' or 'frontend'.
*
* @return void
*/
protected static function removeTemporaryData($originTag)
{
static::loadState($originTag, null, false);
// Remove temporary files
Factory::getTempFiles()->deleteTempFiles();
// Delete any stale temporary data
static::getFactoryStorage()->reset($originTag);
}
}
/**
* Timeout handler. It is registered as a global PHP shutdown function.
*
* If a PHP reports a timeout we will log this before letting PHP kill us.
*/
function AkeebaTimeoutTrap()
{
if (connection_status() >= 2)
{
Factory::getLog()->error('Akeeba Engine has timed out');
}
}
register_shutdown_function("\\Akeeba\\Engine\\AkeebaTimeoutTrap");

View File

@@ -0,0 +1,690 @@
<?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\Filter;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Factory;
use Akeeba\Engine\Util\FileSystem;
abstract class Base
{
/** @var string Filter's internal name; defaults to filename without .php extension */
public $filter_name = '';
/** @var string The filtering object: dir|file|dbobject|db */
public $object = 'dir';
/** @var string The filtering subtype (all|content|children|inclusion) */
public $subtype = null;
/** @var string The filtering method (direct|regex|api) */
public $method = 'direct';
/** @var bool Is the filter active? */
public $enabled = true;
/** @var array An array holding filter or regex strings per root, i.e. $filter_data[$root] = array() */
protected $filter_data = null;
/** @var FileSystem Used by treatDirectory */
protected $fsTools = null;
/**
* Public constructor
*/
public function __construct()
{
// Set the filter name if it's missing (filename in lowercase, minus the .php extension)
if (empty($this->filter_name))
{
$this->filter_name = strtolower(basename(__FILE__, '.php'));
}
}
/**
* Extra SQL statements to append to the SQL dump file. Useful for extension
* filters which have to filter out specific database records. This method
* must be overriden in children classes.
*
* @param string $root The database for which to get the extra SQL statements
*
* @return array Extra SQL statements
*/
public function getExtraSQL(string $root): array
{
return [];
}
/**
* Returns filtering (exclusion) status of the $test object
*
* @param string $test The string to check for filter status (e.g. filename, dir name, table name, etc)
* @param string $root The exclusion root test belongs to
* @param string $object What type of object is it? dir|file|dbobject
* @param string $subtype Filter subtype (all|content|children)
*
* @return bool True if it excluded, false otherwise
*/
public function isFiltered($test, $root, $object, $subtype)
{
if (!$this->enabled)
{
return false;
}
//Factory::getLog()->log(LogLevel::DEBUG,"Filtering [$object:$subtype] $root // $test");
// Inclusion filters do not qualify for exclusion
if ($this->subtype == 'inclusion')
{
return false;
}
// The object and subtype must match
if (($this->object != $object) || ($this->subtype != $subtype))
{
return false;
}
if (in_array($this->method, ['direct', 'regex']))
{
// -- Direct or regex based filters --
// Get a local reference of the filter data, if necessary
if (is_null($this->filter_data))
{
$filters = Factory::getFilters();
$this->filter_data = $filters->getFilterData($this->filter_name);
}
// Check if the root exists and if there's a filter for the $test
if (!array_key_exists($root, $this->filter_data))
{
// Root not found
return false;
}
else
{
// Root found, search in the array
if ($this->method == 'direct')
{
// Direct filtering
return in_array($test, $this->filter_data[$root]);
}
else
{
// Regex matching
foreach ($this->filter_data[$root] as $regex)
{
if (substr($regex, 0, 1) == '!')
{
// Custom Akeeba Backup extension to PCRE notation. If you put a ! before the PCRE, it negates the result of the PCRE.
if (!preg_match(substr($regex, 1), $test))
{
return true;
}
}
else
{
// Normal PCRE
if (preg_match($regex, $test))
{
return true;
}
}
}
// if we're here, no match exists
return false;
}
}
}
else
{
// -- API-based filters --
return $this->is_excluded_by_api($test, $root);
}
}
/**
* Returns the inclusion filters defined by this class for the requested $object
*
* @param string $object The object to get inclusions for (dir|db)
*
* @return array The inclusion filters
*/
public function &getInclusions($object)
{
$dummy = [];
if (!$this->enabled)
{
return $dummy;
}
if (($this->subtype != 'inclusion') || ($this->object != $object))
{
return $dummy;
}
switch ($this->method)
{
case 'api':
return $this->get_inclusions_by_api();
break;
case 'direct':
// Get a local reference of the filter data, if necessary
if (is_null($this->filter_data))
{
$filters = Factory::getFilters();
$this->filter_data = $filters->getFilterData($this->filter_name);
}
return $this->filter_data;
break;
default:
// regex inclusion is not supported at the moment
$dummy = [];
return $dummy;
break;
}
}
/**
* Adds an exclusion filter, or add/replace an inclusion filter
*
* @param string $root Filter's root
* @param mixed $test Exclusion: the filter string. Inclusion: the root definition data
*
* @return bool True on success
*/
public function set($root, $test)
{
if (in_array($this->subtype, ['all', 'content', 'children']))
{
return $this->setExclusion($root, $test);
}
else
{
return $this->setInclusion($root, $test);
}
}
/**
* Unsets a given filter
*
* @param string $root Filter's root
* @param string $test The filter to remove
*
* @return bool
*/
public function remove($root, $test = null)
{
if ($this->subtype == 'inclusion')
{
return $this->removeInclusion($root);
}
else
{
return $this->removeExclusion($root, $test);
}
}
/**
* Completely removes all filters off a specific root
*
* @param string $root
*
* @return bool
*/
public function reset($root)
{
switch ($this->method)
{
default:
case 'api':
return false;
break;
case 'direct':
case 'regex':
// Get a local reference of the filter data, if necessary
if (is_null($this->filter_data))
{
$filters = Factory::getFilters();
$this->filter_data = $filters->getFilterData($this->filter_name);
}
// Direct filters
if (array_key_exists($root, $this->filter_data))
{
unset($this->filter_data[$root]);
}
else
{
// Root not found
return false;
}
break;
}
$filters = Factory::getFilters();
$filters->setFilterData($this->filter_name, $this->filter_data);
return true;
}
/**
* Toggles a filter
*
* @param string $root The filter root object
* @param string $test The filter string to toggle
* @param bool $new_status The new filter status after the operation (true: enabled, false: disabled)
*
* @return bool True on successful change, false if we failed to change it
*/
public function toggle($root, $test, &$new_status)
{
// Can't toggle inclusion filters!
if ($this->subtype == 'inclusion')
{
return false;
}
$is_set = $this->isFiltered($test, $root, $this->object, $this->subtype);
$new_status = !$is_set;
if ($is_set)
{
$status = $this->remove($root, $test);
}
else
{
$status = $this->set($root, $test);
}
if (!$status)
{
$new_status = $is_set;
}
return $status;
}
/**
* Does this class has any filters? If it doesn't, its methods are never called by
* Akeeba's engine to speed things up.
* @return bool
*/
public function hasFilters()
{
if (!$this->enabled)
{
return false;
}
switch ($this->method)
{
default:
case 'api':
// API filters always have data!
return true;
break;
case 'direct':
case 'regex':
// Get a local reference of the filter data, if necessary
if (is_null($this->filter_data))
{
$filters = Factory::getFilters();
$this->filter_data = $filters->getFilterData($this->filter_name);
}
return !empty($this->filter_data);
break;
}
}
/**
* Returns a list of filter strings for the given root. Used by MySQLDump engine.
*
* @param string $root
*
* @return array
*/
public function getFilters($root)
{
$dummy = [];
if (!$this->enabled)
{
return $dummy;
}
switch ($this->method)
{
default:
case 'api':
// API filters never have a list
return $dummy;
break;
case 'direct':
case 'regex':
// Get a local reference of the filter data, if necessary
if (is_null($this->filter_data))
{
$filters = Factory::getFilters();
$this->filter_data = $filters->getFilterData($this->filter_name);
}
if (is_null($root))
{
// When NULL is passed as the root, we return all roots
return $this->filter_data;
}
elseif (array_key_exists($root, $this->filter_data))
{
// The root exists, return its data
return $this->filter_data[$root];
}
else
{
// The root doesn't exist, return an empty array
return $dummy;
}
break;
}
}
/**
* This method must be overriden by API-type exclusion filters.
*
* @param string $test The object to test for exclusion
* @param string $root The object's root
*
* @return bool Return true if it matches your filters
*
* @codeCoverageIgnore
*/
protected function is_excluded_by_api($test, $root)
{
return false;
}
/**
* This method must be overriden by API-type inclusion filters.
*
* @return array The inclusion filters
*
* @codeCoverageIgnore
*/
protected function &get_inclusions_by_api()
{
$dummy = [];
return $dummy;
}
/**
* Remove the root prefix from an absolute path
*
* @param string $directory The absolute path
*
* @return string The translated path, relative to the root directory of the backup job
*/
protected function treatDirectory($directory)
{
if (!is_object($this->fsTools))
{
$this->fsTools = Factory::getFilesystemTools();
}
// Get the site's root
$configuration = Factory::getConfiguration();
if ($configuration->get('akeeba.platform.override_root', 0))
{
$root = $configuration->get('akeeba.platform.newroot', '[SITEROOT]');
}
else
{
$root = '[SITEROOT]';
}
if (stristr($root, '['))
{
$root = $this->fsTools->translateStockDirs($root);
}
$site_root = $this->fsTools->TrimTrailingSlash($this->fsTools->TranslateWinPath($root));
$directory = $this->fsTools->TrimTrailingSlash($this->fsTools->TranslateWinPath($directory));
// Trim site root from beginning of directory
if (substr($directory, 0, strlen($site_root)) == $site_root)
{
$directory = substr($directory, strlen($site_root));
if (substr($directory, 0, 1) == '/')
{
$directory = substr($directory, 1);
}
}
return $directory;
}
/**
* Sets a filter, for direct and regex exclusion filter types
*
* @param string $root The filter root object
* @param string $test The filter string to set
*
* @return bool True on success
*
* @codeCoverageIgnore
*/
private function setExclusion($root, $test)
{
switch ($this->method)
{
default:
case 'api':
// we can't set new filter elements for API-type filters
return false;
break;
case 'direct':
case 'regex':
// Get a local reference of the filter data, if necessary
if (is_null($this->filter_data))
{
$filters = Factory::getFilters();
$this->filter_data = $filters->getFilterData($this->filter_name);
}
// Direct filters
if (array_key_exists($root, $this->filter_data))
{
if (!in_array($test, $this->filter_data[$root]))
{
$this->filter_data[$root][] = $test;
}
else
{
return false;
}
}
else
{
$this->filter_data[$root] = [$test];
}
break;
}
$filters = Factory::getFilters();
$filters->setFilterData($this->filter_name, $this->filter_data);
return true;
}
/**
* Sets a filter, for direct inclusion filter types
*
* @param string $root The inclusion filter key (root)
* @param string $test The inclusion filter raw data
*
* @return bool True on success
*
* @codeCoverageIgnore
*/
private function setInclusion($root, $test)
{
switch ($this->method)
{
default:
case 'api':
case 'regex':
// we can't set new filter elements for API or regex type filters
return false;
break;
case 'direct':
// Get a local reference of the filter data, if necessary
if (is_null($this->filter_data))
{
$filters = Factory::getFilters();
$this->filter_data = $filters->getFilterData($this->filter_name);
}
$this->filter_data[$root] = $test;
break;
}
$filters = Factory::getFilters();
$filters->setFilterData($this->filter_name, $this->filter_data);
return true;
}
/**
* Remove a key from direct and regex filters
*
* @param string $root The filter root object
* @param string $test The filter string to set
*
* @return bool True on success
*
* @codeCoverageIgnore
*/
private function removeExclusion($root, $test)
{
switch ($this->method)
{
default:
case 'api':
// we can't remove filter elements from API-type filters
return false;
break;
case 'direct':
case 'regex':
// Get a local reference of the filter data, if necessary
if (is_null($this->filter_data))
{
$filters = Factory::getFilters();
$this->filter_data = $filters->getFilterData($this->filter_name);
}
// Direct filters
if (array_key_exists($root, $this->filter_data))
{
if (in_array($test, $this->filter_data[$root]))
{
if (count($this->filter_data[$root]) == 1)
{
// If it's the only element, remove the entire root key
unset($this->filter_data[$root]);
}
else
{
// If there are more elements, remove just the $test value
$key = array_search($test, $this->filter_data[$root]);
unset($this->filter_data[$root][$key]);
}
}
else
{
// Filter object not found
return false;
}
}
else
{
// Root not found
return false;
}
break;
}
$filters = Factory::getFilters();
$filters->setFilterData($this->filter_name, $this->filter_data);
return true;
}
/**
* Remove an inclusion filter
*
* @param string $root The root of the filter to remove
*
* @return bool
*
* @codeCoverageIgnore
*/
private function removeInclusion($root)
{
switch ($this->method)
{
default:
case 'api':
case 'regex':
// we can't remove filter elements from API or regex type filters
return false;
break;
case 'direct':
// Get a local reference of the filter data, if necessary
if (is_null($this->filter_data))
{
$filters = Factory::getFilters();
$this->filter_data = $filters->getFilterData($this->filter_name);
}
if (array_key_exists($root, $this->filter_data))
{
unset($this->filter_data[$root]);
}
else
{
// Root not found
return false;
}
break;
}
$filters = Factory::getFilters();
$filters->setFilterData($this->filter_name, $this->filter_data);
return true;
}
}

View File

@@ -0,0 +1,32 @@
<?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\Filter;
defined('AKEEBAENGINE') || die();
/**
* Directory exclusion filter
*/
class Directories extends Base
{
public function __construct()
{
$this->object = 'dir';
$this->subtype = 'all';
$this->method = 'direct';
if (empty($this->filter_name))
{
$this->filter_name = strtolower(basename(__FILE__, '.php'));
}
parent::__construct();
}
}

View File

@@ -0,0 +1,32 @@
<?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\Filter;
defined('AKEEBAENGINE') || die();
/**
* Files exclusion filter
*/
class Files extends Base
{
public function __construct()
{
$this->object = 'file';
$this->subtype = 'all';
$this->method = 'direct';
if (empty($this->filter_name))
{
$this->filter_name = strtolower(basename(__FILE__, '.php'));
}
parent::__construct();
}
}

View File

@@ -0,0 +1,143 @@
<?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\Filter;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use DateTime;
use DateTimeZone;
/**
* Incremental file filter
*
* It will only backup files which are newer than the last backup taken with this profile
*/
class Incremental extends Base
{
public function __construct()
{
$this->object = 'file';
$this->subtype = 'all';
$this->method = 'api';
}
protected function is_excluded_by_api($test, $root)
{
static $filter_switch = null;
static $last_backup = null;
if (is_null($filter_switch))
{
$config = Factory::getConfiguration();
$filter_switch = Factory::getEngineParamsProvider()->getScriptingParameter('filter.incremental', 0);
$filter_switch = ($filter_switch == 1);
$last_backup = $config->get('volatile.filter.last_backup', null);
if (is_null($last_backup) && $filter_switch)
{
// Get a list of backups on this profile
$backups = Platform::getInstance()->get_statistics_list([
'filters' => [
[
'field' => 'profile_id',
'value' => Platform::getInstance()->get_active_profile(),
],
],
]);
// Find this backup's ID
$model = Factory::getStatistics();
$id = $model->getId();
if (is_null($id))
{
$id = -1;
}
// Initialise
$last_backup = time();
$now = $last_backup;
// Find the last time a successful backup with this profile was made
if (count($backups))
{
foreach ($backups as $backup)
{
// Skip the current backup
if ($backup['id'] == $id)
{
continue;
}
// Skip non-complete backups
if ($backup['status'] != 'complete')
{
continue;
}
$tzUTC = new DateTimeZone('UTC');
$dateTime = new DateTime($backup['backupstart'], $tzUTC);
$backuptime = $dateTime->getTimestamp();
$last_backup = $backuptime;
break;
}
}
if ($last_backup == $now)
{
// No suitable backup found; disable this filter
$config->set('volatile.scripting.incfile.filter.incremental', 0);
$filter_switch = false;
}
else
{
// Cache the last backup timestamp
$config->set('volatile.filter.last_backup', $last_backup);
}
}
}
if (!$filter_switch)
{
return false;
}
// Get the filesystem path for $root
$config = Factory::getConfiguration();
$fsroot = $config->get('volatile.filesystem.current_root', '');
$ds = ($fsroot == '') || ($fsroot == '/') ? '' : DIRECTORY_SEPARATOR;
$filename = $fsroot . $ds . $test;
// Get the timestamp of the file
$timestamp = @filemtime($filename);
// If we could not get this information, include the file in the archive
if ($timestamp === false)
{
return false;
}
// Compare it with the last backup timestamp and exclude if it's older than the last backup
if ($timestamp <= $last_backup)
{
//Factory::getLog()->debug("Excluding $filename due to incremental backup restrictions");
return true;
}
// No match? Just include the file!
return false;
}
}

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\Filter;
defined('AKEEBAENGINE') || die();
/**
* Database table records exclusion filter
*
* This is simple stuff. If a table's on the list, it will backup just its structure, not
* its contents. Fair and square...
*/
class Regextabledata extends Base
{
public function __construct()
{
$this->object = 'dbobject';
$this->subtype = 'content';
$this->method = 'regex';
if (empty($this->filter_name))
{
$this->filter_name = strtolower(basename(__FILE__, '.php'));
}
parent::__construct();
}
}

View File

@@ -0,0 +1,32 @@
<?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\Filter;
defined('AKEEBAENGINE') || die();
/**
* Database table exclusion filter
*/
class Regextables extends Base
{
public function __construct()
{
$this->object = 'dbobject';
$this->subtype = 'all';
$this->method = 'regex';
if (empty($this->filter_name))
{
$this->filter_name = strtolower(basename(__FILE__, '.php'));
}
parent::__construct();
}
}

View File

@@ -0,0 +1,32 @@
<?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\Filter;
defined('AKEEBAENGINE') || die();
/**
* Subdirectories exclusion filter
*/
class Skipdirs extends Base
{
public function __construct()
{
$this->object = 'dir';
$this->subtype = 'children';
$this->method = 'direct';
if (empty($this->filter_name))
{
$this->filter_name = strtolower(basename(__FILE__, '.php'));
}
parent::__construct();
}
}

View File

@@ -0,0 +1,32 @@
<?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\Filter;
defined('AKEEBAENGINE') || die();
/**
* Directory contents (files) exclusion filter
*/
class Skipfiles extends Base
{
public function __construct()
{
$this->object = 'dir';
$this->subtype = 'content';
$this->method = 'direct';
if (empty($this->filter_name))
{
$this->filter_name = strtolower(basename(__FILE__, '.php'));
}
parent::__construct();
}
}

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--~
~ Akeeba Engine
~
~ @package akeebaengine
~ @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
~ @license GNU General Public License version 3, or later
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>AkeebaBackup :: Filter Stack</title>
</head>
<body>
<h1>What is this directory?</h1>
<p>In this directory, Akeeba Backup and Akeeba Solo store the optional filters (Optional Filters in the Configuration
page). Unlike regular filters which are always loaded, optional filters are only loaded when the user chooses to
enable them. Each filter consists of two files: <var>filtername</var>.ini and <var>filtername</var>.php. The former
contains the filter-specific configuration options and the later contains the actual filter code.</p>
<p>
If you want to create new optional filters, put them in here. Do note that the INI file must always contain a
boolean key named core.filters.<var>filtername</var>.enabled which controls the loading of this particular filter.
</p>
<p>
Optional filters are always named Akeeba\Engine\Filter\Stack\Stack<var>Filtername</var> so that the autoloader can
find them. For the same reason their filename must be Stack<var>Filtername</var>.php Please watch out for the
letter case in the names, it's important.
</p>
</body>
</html>

View File

@@ -0,0 +1,68 @@
<?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\Filter\Stack;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Factory;
use Akeeba\Engine\Filter\Base;
/**
* Date conditional filter
*
* It will only backup files modified after a specific date and time
*/
class StackDateconditional extends Base
{
public function __construct()
{
$this->object = 'file';
$this->subtype = 'all';
$this->method = 'api';
}
protected function is_excluded_by_api($test, $root)
{
static $from_datetime;
$config = Factory::getConfiguration();
if (is_null($from_datetime))
{
$user_setting = $config->get('core.filters.dateconditional.start');
$from_datetime = strtotime($user_setting);
}
// Get the filesystem path for $root
$fsroot = $config->get('volatile.filesystem.current_root', '');
$ds = ($fsroot == '') || ($fsroot == '/') ? '' : DIRECTORY_SEPARATOR;
$filename = $fsroot . $ds . $test;
// Get the timestamp of the file
$timestamp = @filemtime($filename);
// If we could not get this information, include the file in the archive
if ($timestamp === false)
{
return false;
}
// Compare it with the user-defined minimum timestamp and exclude if it's older than that
if ($timestamp <= $from_datetime)
{
return true;
}
// No match? Just include the file!
return false;
}
}

View File

@@ -0,0 +1,52 @@
<?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\Filter\Stack;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Filter\Base;
/**
* Files exclusion filter based on regular expressions
*/
class StackErrorlogs extends Base
{
function __construct()
{
$this->object = 'file';
$this->subtype = 'all';
$this->method = 'api';
if (empty($this->filter_name))
{
$this->filter_name = strtolower(basename(__FILE__, '.php'));
}
parent::__construct();
}
protected function is_excluded_by_api($test, $root)
{
// Is it an error log? Exclude the file.
if (in_array(basename($test), [
'php_error',
'php_errorlog',
'error_log',
'error.log',
]))
{
return true;
}
// No match? Just include the file!
return false;
}
}

View File

@@ -0,0 +1,46 @@
<?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\Filter\Stack;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Filter\Base;
/**
* Exclude folders and files belonging to the host web stat (ie Webalizer)
*/
class StackHoststats extends Base
{
public function __construct()
{
$this->object = 'dir';
$this->subtype = 'all';
$this->method = 'api';
if (empty($this->filter_name))
{
$this->filter_name = strtolower(basename(__FILE__, '.php'));
}
parent::__construct();
}
protected function is_excluded_by_api($test, $root)
{
if ($test == 'stats')
{
return true;
}
// No match? Just include the file!
return false;
}
}

View File

@@ -0,0 +1,15 @@
{
"core.filters.dateconditional.enabled": {
"default": "0",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_DATECONDITIONAL_ENABLED_TITLE",
"description": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_DATECONDITIONAL_ENABLED_DESCRIPTION",
"bold": "1"
},
"core.filters.dateconditional.start": {
"default": "1970-01-01 00:00 GMT",
"type": "string",
"title": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_DATECONDITIONAL_START_TITLE",
"description": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_DATECONDITIONAL_START_DESCRIPTION"
}
}

View File

@@ -0,0 +1,9 @@
{
"core.filters.errorlogs.enabled": {
"default": "1",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_ERRORLOGS_ENABLED_TITLE",
"description": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_ERRORLOGS_ENABLED_DESCRIPTION",
"bold": "1"
}
}

View File

@@ -0,0 +1,9 @@
{
"core.filters.hoststats.enabled": {
"default": "1",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_HOSTSTATS_ENABLED_TITLE",
"description": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_HOSTSTATS_ENABLED_DESCRIPTION",
"bold": "1"
}
}

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\Filter;
defined('AKEEBAENGINE') || die();
/**
* Database table records exclusion filter
*
* This is simple stuff. If a table's on the list, it will backup just its structure, not
* its contents. Fair and square...
*/
class Tabledata extends Base
{
public function __construct()
{
$this->object = 'dbobject';
$this->subtype = 'content';
$this->method = 'direct';
if (empty($this->filter_name))
{
$this->filter_name = strtolower(basename(__FILE__, '.php'));
}
parent::__construct();
}
}

View File

@@ -0,0 +1,32 @@
<?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\Filter;
defined('AKEEBAENGINE') || die();
/**
* Database table exclusion filter
*/
class Tables extends Base
{
public function __construct()
{
$this->object = 'dbobject';
$this->subtype = 'all';
$this->method = 'direct';
if (empty($this->filter_name))
{
$this->filter_name = strtolower(basename(__FILE__, '.php'));
}
parent::__construct();
}
}

View File

@@ -0,0 +1,350 @@
<?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;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Platform\Base;
use Akeeba\Engine\Platform\PlatformInterface;
use DirectoryIterator;
use Exception;
/**
* Platform abstraction. Manages the loading of platform connector objects and delegates calls to itself the them.
*
* @property string $tableNameProfiles The name of the table where backup profiles are stored
* @property string $tableNameStats The name of the table where backup records are stored
*
* @since 3.4
*/
class Platform
{
/** @var Base|null The currently loaded platform connector object instance */
protected static $platformConnectorInstance = null;
/** @var array A list of additional directories where platform classes can be found */
protected static $knownPlatformsDirectories = [];
/** @var Platform The currently loaded object instance of this class. WARNING: This is NOT the platform connector! */
protected static $instance = null;
/**
* Public class constructor
*
* @param string $platform Optional; platform name. Leave blank to auto-detect.
*
* @throws Exception When the platform cannot be loaded
*/
public function __construct($platform = null)
{
if (empty($platform) || is_null($platform))
{
$platform = static::detectPlatform();
}
if (empty($platform))
{
throw new Exception('Can not find a suitable Akeeba Engine platform for your site');
}
static::$platformConnectorInstance = static::loadPlatform($platform);
if (!is_object(static::$platformConnectorInstance))
{
throw new Exception("Can not load Akeeba Engine platform $platform");
}
}
/**
* Implements the Singleton pattern for this class
*
* @staticvar Platform $instance The static object instance
*
* @param string $platform Optional; platform name. Autodetect if blank.
*
* @return PlatformInterface
*/
public static function &getInstance($platform = null)
{
if (!is_object(static::$instance))
{
static::$instance = new Platform($platform);
}
return static::$instance;
}
/**
* Get a list of all directories where platform classes can be found
*
* @return array
*/
public static function getPlatformDirectories()
{
$defaultPath = [];
if (is_object(static::$platformConnectorInstance))
{
$defaultPath[] = __DIR__ . '/Platform/' . static::$platformConnectorInstance->platformName;
}
return array_merge(
static::$knownPlatformsDirectories,
$defaultPath
);
}
/**
* Lists available platforms
*
* @staticvar array $platforms Static cache of the available platforms
*
* @return array The list of available platforms
*/
static public function listPlatforms()
{
if (empty(static::$knownPlatformsDirectories))
{
$di = new DirectoryIterator(__DIR__ . '/Platform');
/** @var DirectoryIterator $file */
foreach ($di as $file)
{
if (!$file->isDir())
{
continue;
}
if ($file->isDot())
{
continue;
}
if ($file->getExtension() !== 'php')
{
continue;
}
$shortName = $file->getFilename();
$bareName = basename($shortName, '.php');
/**
* We never have dots in our filenames but some hosts will rename files similar to foo.1.php when their
* broken security scanners detect a false positive. This is our defence against that.
*/
if (strpos($bareName, '.') !== false)
{
continue;
}
static::$knownPlatformsDirectories[$shortName] = $file->getRealPath();
}
}
return static::$knownPlatformsDirectories;
}
/**
* Add a platform to the list of known platforms
*
* @param string $slug Short name of the platform
* @param string $platformDirectory The path where you can find it
*
* @return void
*/
public static function addPlatform($slug, $platformDirectory)
{
if (empty(static::$knownPlatformsDirectories))
{
static::listPlatforms();
static::$knownPlatformsDirectories[$slug] = $platformDirectory;
}
}
/**
* Auto-detect the suitable platform for this site
*
* @return string
*
* @throws Exception When no platform is detected
*/
protected static function detectPlatform()
{
$platforms = static::listPlatforms();
if (empty($platforms))
{
throw new Exception('No Akeeba Engine platform class found');
}
$bestPlatform = (object) [
'name' => null,
'priority' => 0,
];
foreach ($platforms as $platform => $path)
{
$o = static::loadPlatform($platform, $path);
if (is_null($o))
{
continue;
}
if ($o->isThisPlatform())
{
if ($o->priority > $bestPlatform->priority)
{
$bestPlatform->priority = $o->priority;
$bestPlatform->name = $platform;
}
}
}
return $bestPlatform->name;
}
/**
* Load a given platform and return the platform object
*
* @param string $platform Platform name
* @param string $path The path to laod the platform from (optional)
*
* @return Base
*/
protected static function &loadPlatform($platform, $path = null)
{
if (empty($path))
{
if (isset(static::$knownPlatformsDirectories[$platform]))
{
$path = static::$knownPlatformsDirectories[$platform];
}
}
if (empty($path))
{
$path = dirname(__FILE__) . '/' . $platform;
}
$classFile = $path . '/Platform.php';
$className = '\\Akeeba\\Engine\\Platform\\' . ucfirst($platform);
$null = null;
if (!file_exists($classFile))
{
return $null;
}
require_once($classFile);
if (!class_exists($className, false))
{
return $null;
}
$o = new $className;
return $o;
}
/**
* Magic method to proxy all calls to the loaded platform object
*
* @param string $name The name of the method to call
* @param array $arguments The arguments to pass
*
* @return mixed The result of the method being called
*
* @throws Exception When the platform isn't loaded or an non-existent method is called
*/
public function __call($name, array $arguments)
{
if (is_null(static::$platformConnectorInstance))
{
throw new Exception('Akeeba Engine platform is not loaded');
}
if (method_exists(static::$platformConnectorInstance, $name))
{
// Call_user_func_array is ~3 times slower than direct method calls.
// See the on-line PHP documentation page of call_user_func_array for more information.
switch (count($arguments))
{
case 0 :
$result = static::$platformConnectorInstance->$name();
break;
case 1 :
$result = static::$platformConnectorInstance->$name($arguments[0]);
break;
case 2:
$result = static::$platformConnectorInstance->$name($arguments[0], $arguments[1]);
break;
case 3:
$result = static::$platformConnectorInstance->$name($arguments[0], $arguments[1], $arguments[2]);
break;
case 4:
$result = static::$platformConnectorInstance->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3]);
break;
case 5:
$result = static::$platformConnectorInstance->$name($arguments[0], $arguments[1], $arguments[2], $arguments[3], $arguments[4]);
break;
default:
// Resort to using call_user_func_array for many segments
$result = call_user_func_array([static::$platformConnectorInstance, $name], $arguments);
}
return $result;
}
else
{
throw new Exception('Method ' . $name . ' not found in Akeeba Platform');
}
}
/**
* Magic getter for the properties of the loaded platform
*
* @param string $name The name of the property to get
*
* @return mixed The value of the property
*/
public function __get($name)
{
if (!isset(static::$platformConnectorInstance->$name) || !property_exists(static::$platformConnectorInstance, $name))
{
static::$platformConnectorInstance->$name = null;
user_error(__CLASS__ . ' does not support property ' . $name, E_NOTICE);
}
return static::$platformConnectorInstance->$name;
}
/**
* Magic setter for the properties of the loaded platform
*
* @param string $name The name of the property to set
* @param mixed $value The value of the property to set
*/
public function __set($name, $value)
{
if (isset(static::$platformConnectorInstance->$name) || property_exists(static::$platformConnectorInstance, $name))
{
static::$platformConnectorInstance->$name = $value;
}
else
{
static::$platformConnectorInstance->$name = null;
user_error(__CLASS__ . ' does not support property ' . $name, E_NOTICE);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
<?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\Platform\Exception;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Platform;
use Exception;
use RuntimeException;
/**
* Thrown when the settings cannot be decrypted, e.g. when the server no longer has encyrption enabled or the key has
* changed.
*/
class DecryptionException extends RuntimeException
{
public function __construct($message = null, $code = 500, Exception $previous = null)
{
if (empty($message))
{
$message = Platform::getInstance()->translate('COM_AKEEBA_CONFIG_ERR_DECRYPTION');
}
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,413 @@
<?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\Platform;
defined('AKEEBAENGINE') || die();
use Exception;
/**
* Interface PlatformInterface
*
* @property string $tableNameProfiles The name of the table where backup profiles are stored
* @property string $tableNameStats The name of the table where backup records are stored
* @property array $configOverrides Configuration overrides
*/
interface PlatformInterface
{
/**
* Returns an array with the directory/-ies in which the magic autoloader should look for platform overrides.
*
* @return array
*/
public function getPlatformDirectories();
/**
* Performs heuristics to determine if this platform object is the ideal
* candidate for the environment Akeeba Engine is running in.
*
* @return bool
*/
public function isThisPlatform();
/**
* Saves the current configuration to the database table
*
* @param int $profile_id The profile where to save the configuration
* to, defaults to current profile
*
* @return bool True if everything was saved properly
*/
public function save_configuration($profile_id = null);
/**
* Loads the current configuration off the database table
*
* @param int $profile_id The profile where to read the configuration from, defaults to current profile
*
* @return bool True if everything was read properly
*/
public function load_configuration($profile_id = null);
/**
* Returns an associative array of stock platform directories
*
* @return array
*/
public function get_stock_directories();
/**
* Returns the absolute path to the site's root
*
* @return string
*/
public function get_site_root();
/**
* Returns the absolute path to the installer images directory
*
* @return string
*/
public function get_installer_images_path();
/**
* Returns the active profile number
*
* @return integer
*/
public function get_active_profile();
/**
* Returns the selected profile's name. If no ID is specified, the current
* profile's name is returned.
*
* @param integer|null $id The ID of the profile, skip for current profile
*
* @return string
*/
public function get_profile_name($id = null);
/**
* Returns the backup origin
*
* @return string Backup origin: backend|frontend
*/
public function get_backup_origin();
/**
* Returns a timestamp formatted for the current site's database driver
*
* @param string $date [optional] The timestamp to use. Omit to use current timestamp.
*
* @return string
*/
public function get_timestamp_database($date = 'now');
/**
* Returns the current timestamp, taking into account any TZ information,
* in the format specified by $format.
*
* @param string $format Timestamp format string (standard PHP format string)
*
* @return string
*/
public function get_local_timestamp($format);
/**
* Returns the current host name
*
* @return string
*/
public function get_host();
/**
* Returns the current site's name
*
* @return string
*/
public function get_site_name();
/**
* Creates or updates the statistics record of the current backup attempt
*
* @param int $id Backup record ID, use null for new record
* @param array $data The data to store
*
* @return int|null The new record id, or null if this doesn't apply
*
* @throws Exception On database error
*/
public function set_or_update_statistics($id = null, $data = []);
/**
* Loads and returns a backup statistics record as a hash array
*
* @param int $id Backup record ID
*
* @return array
*/
public function get_statistics($id);
/**
* Completely removes a backup statistics record
*
* @param int $id Backup record ID
*
* @return bool True on success
*/
public function delete_statistics($id);
/**
* Returns a list of backup statistics records, respecting the pagination
*
* The $config array allows the following options to be set:
* limitstart int Offset in the recordset to start from
* limit int How many records to return at once
* filters array An array of filters to apply to the results. Alternatively you can just pass a profile
* ID to filter by that profile. order array Record ordering information (by and ordering)
*
* @param array $config See above
*
* @return array
*/
function &get_statistics_list($config = []);
/**
* Return the total number of statistics records
*
* @param array $filters An array of filters to apply to the results. Alternatively you can just pass a profile
* ID to filter by that profile.
*
* @return integer
*/
function get_statistics_count($filters = null);
/**
* Returns an array with the specifics of running backups
*
* @param string $tag The backup type (e.g. backend)
*
* @return array
*/
public function get_running_backups($tag = null);
/**
* Multiple backup attempts can share the same backup file name. Only
* the last backup attempt's file is considered valid. Previous attempts
* have to be deemed "obsolete". This method returns a list of backup
* statistics ID's with "valid"-looking names. IT DOES NOT CHECK FOR THE
* EXISTENCE OF THE BACKUP FILE!
*
* @param bool $useprofile If true, it will only return backup records of the current profile
* @param array $tagFilters Which tags to include; leave blank for all. If the first item is "NOT", then all
* tags EXCEPT those listed will be included.
* @param string $ordering Ordering of the records, default DESC (descending), use DESC or ASC
*
* @return array A list of ID's for records w/ "valid"-looking backup files
*/
public function &get_valid_backup_records($useprofile = false, $tagFilters = [], $ordering = 'DESC');
/**
* Invalidates older records sharing the same $archivename
*
* @param string $archivename The archive name
*
* @return void
*/
public function remove_duplicate_backup_records($archivename);
/**
* Marks the specified backup records as having no files
*
* @param array $ids Array of backup record IDs to ivalidate
*
* @return void
*/
public function invalidate_backup_records($ids);
/**
* Gets a list of records with remotely stored files in the selected remote storage
* provider and profile.
*
* @param int $profile (optional) The profile to use. Skip or use null for active profile.
* @param string $engine (optional) The remote engine to looks for. Skip or use null for the active profile's
* engine.
*
* @return array
*/
public function get_valid_remote_records($profile = null, $engine = null);
/**
* Returns the filter data for the entire filter group collection
*
* @return array
*/
public function &load_filters();
/**
* Saves the nested filter data array $filter_data to the database
*
* @param array $filter_data The filter data to save
*
* @return bool True on success
*/
public function save_filters(&$filter_data);
/**
* Gets the best matching database driver class, according to CMS settings
*
* @param bool $use_platform If set to false, it will forcibly try to assign one of the primitive type
* (Mysql/Mysqli) and NEVER tell you to use an platform driver
*
* @return string
*/
public function get_default_database_driver($use_platform = true);
/**
* Returns a set of options to connect to the default database of the current CMS
*
* @return array
*/
public function get_platform_database_options();
/**
* Provides a platform-specific translation function
*
* @param string $key The translation key
*
* @return string
*/
public function translate($key);
/**
* Populates global constants holding the Akeeba version
*
* @return void
*/
public function load_version_defines();
/**
* Returns the platform name and version
*
* @return array Contains the platform name and version
*/
public function getPlatformVersion();
/**
* Logs platform-specific directories with LogLevel::INFO log level
*
* @return string If there are any extra notes / warnings on this platform
*/
public function log_platform_special_directories();
/**
* Loads a platform-specific software configuration option. These are
* configuration values for the backup engine, but unlike the rest of the
* configuration options they are not stored in the backup profile.
* Instead, these are stored globally using a platform-specific method.
* So these are not configuration values for the platform itself.
*
* @param string $key The configuration option to retrieve
* @param mixed $default The default value to return if it's not defined
*
* @return mixed
*/
public function get_platform_configuration_option($key, $default);
/**
* Returns a list of emails to the administrators
*
* @return array
*/
public function get_administrator_emails();
/**
* Sends a very simple email using the platform's emailer facility
*
* @param string $to Recipient address
* @param string $subject Email subject line
* @param string $body Email body (plain text)
* @param string $attachFile (optional) Full path to the file being attached
*
* @return bool True on success
*/
public function send_email($to, $subject, $body, $attachFile = null);
/**
* Deletes a file from the local server using direct file access or FTP
*
* @param string $file The file to unlink
*
* @return bool True on success
*/
public function unlink($file);
/**
* Moves a file around within the local server using direct file access or FTP
*
* @param string $from Full path of the file to move
* @param string $to Full path of where the file will be moved to
*
* @return bool True on success
*/
public function move($from, $to);
/**
* Stores a flash (temporary) variable in the session.
*
* @param string $name The name of the variable to store
* @param string $value The value of the variable to store
*
* @return void
*/
public function set_flash_variable($name, $value);
/**
* Return the value of a flash (temporary) variable from the session and
* immediately removes it.
*
* @param string $name The name of the flash variable
* @param mixed $default Default value, if the variable is not defined
*
* @return mixed The value of the variable or $default if it's not set
*/
public function get_flash_variable($name, $default = null);
/**
* Perform an immediate redirection to the defined URL
*
* @param string $url The URL to redirect to
*
* @return void
*/
public function redirect($url);
/**
* Get the proxy configuration for this platform.
*
* @return array{enabled: bool, host: string, port: int, user: string, pass: string}
* @since 9.0.7
*/
public function getProxySettings();
/**
* Set the proxy configuration for this platform
*
* @param false $useProxy Should I use a proxy at all?
* @param string $host Proxy hostname or IP address
* @param int $port Proxy port
* @param string $username Proxy username. Optional. Leavel blank to turn off authentication.
* @param string $password Proxy password.
*
* @return void
* @since 9.0.7
*/
public function setProxySettings($useProxy = false, $host = '', $port = 8080, $username = '', $password = '');
}

View File

@@ -0,0 +1,261 @@
<?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\Postproc;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Platform;
use Akeeba\Engine\Postproc\Exception\BadConfiguration;
use Akeeba\Engine\Postproc\Exception\DeleteNotSupported;
use Akeeba\Engine\Postproc\Exception\DownloadToBrowserNotSupported;
use Akeeba\Engine\Postproc\Exception\DownloadToServerNotSupported;
use Akeeba\Engine\Postproc\Exception\OAuthNotSupported;
use Akeeba\Engine\Util\FileCloseAware;
use Exception;
/**
* Akeeba Engine post-processing abstract class. Provides the default implementation of most of the PostProcInterface
* methods.
*/
abstract class Base implements PostProcInterface
{
use FileCloseAware;
/**
* Should we break the step before post-processing?
*
* The only engine which does not require a step break before is the None engine.
*
* @var bool
*/
protected $recommendsBreakBefore = true;
/**
* Should we break the step after post-processing?
*
* @var bool
*/
protected $recommendsBreakAfter = true;
/**
* Does this engine processes the files in a way that makes deleting the originals safe?
*
* @var bool
*/
protected $advisesDeletionAfterProcessing = true;
/**
* Does this engine support remote file deletes?
*
* @var bool
*/
protected $supportsDelete = false;
/**
* Does this engine support downloads to files?
*
* @var bool
*/
protected $supportsDownloadToFile = false;
/**
* Does this engine support downloads to browser?
*
* @var bool
*/
protected $supportsDownloadToBrowser = false;
/**
* Does this engine push raw data to the browser when downloading a file?
*
* Set to true if raw data will be dumped to the browser when downloading the file to the browser. Set to false if
* a URL is returned instead.
*
* @var bool
*/
protected $inlineDownloadToBrowser = false;
/**
* The remote absolute path to the file which was just processed. Leave null if the file is meant to
* be non-retrievable, i.e. sent to email or any other one way service.
*
* @var string
*/
protected $remotePath = null;
/**
* Whitelist of method names you can call using customAPICall().
*
* @var array
*/
protected $allowedCustomAPICallMethods = ['oauthCallback'];
/**
* The connector object for this post-processing engine
*
* @var object|null
*/
private $connector;
public function delete($path)
{
throw new DeleteNotSupported();
}
public function downloadToFile($remotePath, $localFile, $fromOffset = null, $length = null)
{
throw new DownloadToServerNotSupported();
}
public function downloadToBrowser($remotePath)
{
throw new DownloadToBrowserNotSupported();
}
public final function customAPICall($method, $params = [])
{
if (!in_array($method, $this->allowedCustomAPICallMethods) || !method_exists($this, $method))
{
header('HTTP/1.0 501 Not Implemented');
exit();
}
return call_user_func_array([$this, $method], [$params]);
}
public function oauthOpen($params = [])
{
$callback = $params['callbackURI'] . '&method=oauthCallback';
$url = $this->getOAuth2HelperUrl();
$url .= (strpos($url, '?') !== false) ? '&' : '?';
$url .= 'callback=' . urlencode($callback);
$url .= '&dlid=' . urlencode(Platform::getInstance()->get_platform_configuration_option('update_dlid', ''));
Platform::getInstance()->redirect($url);
}
/**
* Fetches the authentication token from the OAuth helper script, after you've run the first step of the OAuth
* authentication process. Must be overridden in subclasses.
*
* @param array $params
*
* @return void
*
* @throws OAuthNotSupported
*/
public function oauthCallback(array $params)
{
throw new OAuthNotSupported();
}
public function recommendsBreakBefore()
{
return $this->recommendsBreakBefore;
}
public function recommendsBreakAfter()
{
return $this->recommendsBreakAfter;
}
public function isFileDeletionAfterProcessingAdvisable()
{
return $this->advisesDeletionAfterProcessing;
}
public function supportsDelete()
{
return $this->supportsDelete;
}
public function supportsDownloadToFile()
{
return $this->supportsDownloadToFile;
}
public function supportsDownloadToBrowser()
{
return $this->supportsDownloadToBrowser;
}
public function doesInlineDownloadToBrowser()
{
return $this->inlineDownloadToBrowser;
}
public function getRemotePath()
{
return $this->remotePath;
}
/**
* Returns the URL to the OAuth2 helper script. Used by the oauthOpen method. Must be overridden in subclasses.
*
* @return string
*
* @throws OAuthNotSupported
*/
protected function getOAuth2HelperUrl()
{
throw new OAuthNotSupported();
}
/**
* Returns an instance of the connector object.
*
* @param bool $forceNew Should I force the creation of a new connector object?
*
* @return object The connector object
*
* @throws BadConfiguration If there is a configuration error which prevents creating a connector object.
* @throws Exception
*/
final protected function getConnector($forceNew = false)
{
if ($forceNew)
{
$this->resetConnector();
}
if (empty($this->connector))
{
$this->connector = $this->makeConnector();
}
return $this->connector;
}
/**
* Resets the connector.
*
* If the connector requires any special handling upon destruction you must handle it in its __destruct method.
*
* @return void
*/
final protected function resetConnector()
{
$this->connector = null;
}
/**
* Creates a new connector object based on the engine configuration stored in the backup profile.
*
* Do not use this method directly. Use getConnector() instead.
*
* @return object The connector object
*
* @throws BadConfiguration If there is a configuration error which prevents creating a connector object.
* @throws Exception Any other error when creating or initializing the connector object.
*/
protected abstract function makeConnector();
}

View File

@@ -0,0 +1,94 @@
<?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\Postproc;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Postproc\Exception\BadConfiguration;
use Awf\Text\Text;
use Joomla\CMS\Language\Text as JText;
use RuntimeException;
class Email extends Base
{
public function processPart($localFilepath, $remoteBaseName = null)
{
// Retrieve engine configuration data
$config = Factory::getConfiguration();
$address = trim($config->get('engine.postproc.email.address', ''));
$subject = $config->get('engine.postproc.email.subject', '0');
// Sanity checks
if (empty($address))
{
throw new BadConfiguration('You have not set up a recipient\'s email address for the backup files');
}
// Send the file
$basename = empty($remoteBaseName) ? basename($localFilepath) : $remoteBaseName;
Factory::getLog()->info(sprintf("Preparing to email %s to %s", $basename, $address));
if (empty($subject))
{
$subject = "You have a new backup part";
if (class_exists('\Awf\Text\Text'))
{
$subject = Text::_('COM_AKEEBA_COMMON_EMAIL_DEAFULT_SUBJECT');
if ($subject === 'COM_AKEEBA_COMMON_EMAIL_DEAFULT_SUBJECT')
{
$subject = JText::_('COM_AKEEBABACKUP_COMMON_EMAIL_DEAFULT_SUBJECT');
}
}
elseif (class_exists('\Joomla\CMS\Language\Text'))
{
$subject = JText::_('COM_AKEEBA_COMMON_EMAIL_DEAFULT_SUBJECT');
if ($subject === 'COM_AKEEBA_COMMON_EMAIL_DEAFULT_SUBJECT')
{
$subject = JText::_('COM_AKEEBABACKUP_COMMON_EMAIL_DEAFULT_SUBJECT');
}
}
}
$body = "Emailing $basename";
Factory::getLog()->debug("Subject: $subject");
Factory::getLog()->debug("Body: $body");
$result = Platform::getInstance()->send_email($address, $subject, $body, $localFilepath);
// Return the result
if ($result !== true)
{
// An error occurred
throw new RuntimeException($result);
}
// Return success
Factory::getLog()->info("Email sent successfully");
return true;
}
protected function makeConnector()
{
/**
* This method does not use a connector.
*/
return;
}
}

View File

@@ -0,0 +1,21 @@
<?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\Postproc\Exception;
defined('AKEEBAENGINE') || die();
use RuntimeException;
/**
* Indicates an error with the post-processing engine's configuration
*/
class BadConfiguration extends RuntimeException
{
}

View File

@@ -0,0 +1,20 @@
<?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\Postproc\Exception;
defined('AKEEBAENGINE') || die();
/**
* Indicates that the post-processing engine does not support deleting remotely stored files.
*/
class DeleteNotSupported extends EngineException
{
protected $messagePrototype = 'The %s post-processing engine does not support deletion of backup archives.';
}

View File

@@ -0,0 +1,20 @@
<?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\Postproc\Exception;
defined('AKEEBAENGINE') || die();
/**
* Indicates that the post-processing engine does not support downloading remotely stored files to the user's browser.
*/
class DownloadToBrowserNotSupported extends EngineException
{
protected $messagePrototype = 'The %s post-processing engine does not support downloading of backup archives to the browser.';
}

View File

@@ -0,0 +1,20 @@
<?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\Postproc\Exception;
defined('AKEEBAENGINE') || die();
/**
* Indicates that the post-processing engine does not support downloading remotely stored files to the server
*/
class DownloadToServerNotSupported extends EngineException
{
protected $messagePrototype = 'The %s post-processing engine does not support downloading of backup archives to the server.';
}

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\Postproc\Exception;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Postproc\Base;
use Exception;
use RuntimeException;
use Throwable;
class EngineException extends RuntimeException
{
protected $messagePrototype = 'The %s post-processing engine has experienced an unspecified error.';
/**
* Construct the exception. If a message is not defined the default message for the exception will be used.
*
* @param string $message [optional] The Exception message to throw.
* @param int $code [optional] The Exception code.
* @param Exception|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct($message = "", $code = 0, $previous = null)
{
if (empty($message))
{
$engineName = $this->getEngineKeyFromBacktrace();
$message = sprintf($this->messagePrototype, $engineName);
}
parent::__construct($message, $code, $previous);
}
/**
* Returns the engine name (class name without the namespace) from the PHP execution backtrace.
*
* @return mixed|string
*/
protected function getEngineKeyFromBacktrace()
{
// Make sure the backtrace is at least 3 levels deep
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 5);
// We need to be at least two levels deep
if (count($backtrace) < 2)
{
return 'current';
}
for ($i = 1; $i < count($backtrace); $i++)
{
// Get the fully qualified class
$object = $backtrace[$i]['object'];
// We need a backtrace element with an object attached.
if (!is_object($object))
{
continue;
}
// If the object is not a Postproc\Base object go to the next entry.
if (!($object instanceof Base))
{
continue;
}
// Get the bare class name
$fqnClass = $backtrace[$i]['class'];
$parts = explode('\\', $fqnClass);
$bareClass = array_pop($parts);
// Do not return the base object!
if ($bareClass == 'Base')
{
continue;
}
return $bareClass;
}
return 'current';
}
}

View File

@@ -0,0 +1,21 @@
<?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\Postproc\Exception;
defined('AKEEBAENGINE') || die();
/**
* Indicates that the post-processing engine does not support OAuth2 or similar redirection-based authentication with
* the remote storage provider.
*/
class OAuthNotSupported extends EngineException
{
protected $messagePrototype = 'The %s post-processing engine does not support opening an authentication window to the remote storage provider.';
}

View File

@@ -0,0 +1,20 @@
<?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\Postproc\Exception;
defined('AKEEBAENGINE') || die();
/**
* Indicates that the post-processing engine does not support range downloads.
*/
class RangeDownloadNotSupported extends EngineException
{
protected $messagePrototype = 'The %s post-processing engine does not support range downloads of backup archives to the server.';
}

View File

@@ -0,0 +1,37 @@
<?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\Postproc;
defined('AKEEBAENGINE') || die();
class None extends Base
{
public function __construct()
{
// No point in breaking the step; we simply do nothing :)
$this->recommendsBreakAfter = false;
$this->recommendsBreakBefore = false;
$this->advisesDeletionAfterProcessing = false;
}
public function processPart($localFilepath, $remoteBaseName = null)
{
// Really nothing to do!!
return true;
}
protected function makeConnector()
{
// I have to return an object to satisfy the definition.
return (object) [
'foo' => 'bar',
];
}
}

View File

@@ -0,0 +1,192 @@
<?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\Postproc;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Postproc\Exception\DeleteNotSupported;
use Akeeba\Engine\Postproc\Exception\DownloadToBrowserNotSupported;
use Akeeba\Engine\Postproc\Exception\DownloadToServerNotSupported;
use Akeeba\Engine\Postproc\Exception\OAuthNotSupported;
use Akeeba\Engine\Postproc\Exception\RangeDownloadNotSupported;
use Exception;
interface PostProcInterface
{
/**
* This function takes care of post-processing a file (typically a backup archive part file).
*
* If the process has ran to completion it returns true.
*
* If more work is required (the file has only been partially uploaded) it returns false.
*
* It the process has failed an Exception is thrown.
*
* @param string $localFilepath Absolute path to the part we'll have to process
* @param string|null $remoteBaseName Base name of the uploaded file, skip to use $absolute_filename's
*
* @return bool True on success, false if more work is required
*
* @throws Exception When an error occurred during post-processing
*/
public function processPart($localFilepath, $remoteBaseName = null);
/**
* Deletes a remote file
*
* @param string $path The absolute, remote storage path to the file we're deleting
*
* @return void
*
* @throws DeleteNotSupported When this feature is not supported at all.
* @throws Exception When an engine error occurs
*/
public function delete($path);
/**
* Downloads a remotely stored file back to the site's server. It can optionally do a range download. If range
* downloads are not supported we throw a RangeDownloadNotSupported exception. Any other type of Exception means
* that the download failed.
*
* @param string $remotePath The path to the remote file
* @param string $localFile The absolute path to the local file we're writing to
* @param int|null $fromOffset The offset (in bytes) to start downloading from
* @param int|null $length The amount of data (in bytes) to download
*
* @return void
*
* @throws DownloadToServerNotSupported When this feature is not supported at all.
* @throws RangeDownloadNotSupported When range downloads are not supported.
* @throws Exception On failure.
*/
public function downloadToFile($remotePath, $localFile, $fromOffset = null, $length = null);
/**
* Downloads a remotely stored file to the user's browser, without storing it on the site's web server first.
*
* If $this->inlineDownloadToBrowser is true the method outputs a byte stream to the browser and returns null.
*
* If $this->inlineDownloadToBrowser is false it returns a string containing a public download URL. The user's
* browser will be redirected to that URL.
*
* If this feature is not supported a DownloadToBrowserNotSupported exception will be thrown.
*
* Any other Exception indicates an error while trying to download to browser such as file not found, problem with
* the remote service etc.
*
* @param string $remotePath The absolute, remote storage path to the file we want to download
*
* @return string|null
*
* @throws DownloadToBrowserNotSupported When this feature is not supported at all.
* @throws Exception When an error occurs.
*/
public function downloadToBrowser($remotePath);
/**
* A proxy which allows us to execute arbitrary methods in this engine. Used for AJAX calls, typically to update UI
* elements with information fetched from the remote storage service.
*
* For security reasons, only methods whitelisted in the $this->allowedCustomAPICallMethods array can be called.
*
* @param string $method The method to call.
* @param array $params Any parameters to send to the method, in array format
*
* @return mixed The return value of the method.
*/
public function customAPICall($method, $params = []);
/**
* Opens an OAuth window (performs an HTTP redirection).
*
* @param array $params Any parameters required to launch OAuth
*
* @return void
*
* @throws OAuthNotSupported When not supported.
* @throws Exception When an error occurred.
*/
public function oauthOpen($params = []);
/**
* Fetches the authentication token from the OAuth helper script, after you've run the first step of the OAuth
* authentication process. Must be overridden in subclasses.
*
* @param array $params
*
* @return void
*
* @throws OAuthNotSupported
*/
public function oauthCallback(array $params);
/**
* Does the engine recommend doing a step break before post-processing backup archives with it?
*
* @return bool
*/
public function recommendsBreakBefore();
/**
* Does the engine recommend doing a step break after post-processing backup archives with it?
*
* @return bool
*/
public function recommendsBreakAfter();
/**
* Is it advisable to delete files successfully post-processed by this post-processing engine?
*
* Currently only the “None” method advises against deleting successfully post-processed files for the simple reason
* that it does absolutely nothing with the files. The only copy is still on the server.
*
* @return bool
*/
public function isFileDeletionAfterProcessingAdvisable();
/**
* Does this engine support deleting remotely stored files?
*
* Most engines support deletion. However, some engines such as “Send by email”, do not have a way to find files
* already processed and delete them. Or it may be that we are sending the file to a write-only storage service
* which does not support deletions.
*
* @return bool
*/
public function supportsDelete();
/**
* Does this engine support downloading backup archives back to the site's web server?
*
* @return bool
*/
public function supportsDownloadToFile();
/**
* Does this engine support downloading backup archives directly to the user's browser?
*
* @return bool
*/
public function supportsDownloadToBrowser();
/**
* Does this engine return a bytestream when asked to download backup archives directly to the user's browser?
*
* @return bool
*/
public function doesInlineDownloadToBrowser();
/**
* Returns the remote absolute path to the file which was just processed.
*
* @return string
*/
public function getRemotePath();
}

View File

@@ -0,0 +1,70 @@
<?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\Postproc;
use Akeeba\Engine\Platform;
trait ProxyAware
{
/**
* Apply the platform proxy configuration to the cURL resource.
*
* @param resource $ch The cURL resource, returned by curl_init();
*/
protected function applyProxySettingsToCurl($ch)
{
$proxySettings = Platform::getInstance()->getProxySettings();
if (!$proxySettings['enabled'])
{
return;
}
curl_setopt($ch, CURLOPT_PROXY, $proxySettings['host'] . ':' . $proxySettings['port']);
if (empty($proxySettings['user']))
{
return;
}
curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxySettings['user'] . ':' . $proxySettings['pass']);
}
protected function getProxyStreamContext()
{
$ret = [];
$proxySettings = Platform::getInstance()->getProxySettings();
if (!$proxySettings['enabled'])
{
return $ret;
}
$ret['http'] = [
'proxy' => $proxySettings['host'] . ':' . $proxySettings['port'],
'request_fulluri' => true,
];
$ret['ftp'] = [
'proxy' => $proxySettings['host'] . ':' . $proxySettings['port'],
// So, request_fulluri isn't documented for the FTP transport but seems to be required...?!
'request_fulluri' => true,
];
if (empty($proxySettings['user']))
{
return $ret;
}
$ret['http']['header'] = ['Proxy-Authorization: Basic ' . base64_encode($proxySettings['user'] . ':' . $proxySettings['pass'])];
$ret['ftp']['header'] = ['Proxy-Authorization: Basic ' . base64_encode($proxySettings['user'] . ':' . $proxySettings['pass'])];
return $ret;
}
}

View File

@@ -0,0 +1,30 @@
{
"_information": {
"title": "COM_AKEEBA_CONFIG_ENGINE_POSTPROC_EMAIL_TITLE",
"description": "COM_AKEEBA_CONFIG_ENGINE_POSTPROC_EMAIL_DESCRIPTION"
},
"engine.postproc.common.after_part": {
"default": "0",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_POSTPROCPARTS_TITLE",
"description": "COM_AKEEBA_CONFIG_POSTPROCPARTS_DESCRIPTION"
},
"engine.postproc.common.delete_after": {
"default": "1",
"type": "bool",
"title": "COM_AKEEBA_CONFIG_DELETEAFTER_TITLE",
"description": "COM_AKEEBA_CONFIG_DELETEAFTER_DESCRIPTION"
},
"engine.postproc.email.address": {
"default": "",
"type": "string",
"title": "COM_AKEEBA_CONFIG_PROCEMAIL_ADDRESS_TITLE",
"description": "COM_AKEEBA_CONFIG_PROCEMAIL_ADDRESS_DESCRIPTION"
},
"engine.postproc.email.subject": {
"default": "",
"type": "string",
"title": "COM_AKEEBA_CONFIG_PROCEMAIL_SUBJECT_TITLE",
"description": "COM_AKEEBA_CONFIG_PROCEMAIL_SUBJECT_DESCRIPTION"
}
}

View File

@@ -0,0 +1,6 @@
{
"_information": {
"title": "COM_AKEEBA_CONFIG_ENGINE_POSTPROC_NONE_TITLE",
"description": "COM_AKEEBA_CONFIG_ENGINE_POSTPROC_NONE_DESCRIPTION"
}
}

View File

@@ -0,0 +1,137 @@
<?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 Psr\Log;
defined('AKEEBAENGINE') || die();
/**
* This is a simple Logger implementation that other Loggers can inherit from.
*
* It simply delegates all log-level-specific methods to the `log` method to
* reduce boilerplate code that a simple Logger that does the same thing with
* messages regardless of the error level has to implement.
*/
abstract class AbstractLogger implements LoggerInterface
{
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return null
*/
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 null
*/
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 null
*/
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 null
*/
public function error($message, array $context = [])
{
$this->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 null
*/
public function warning($message, array $context = [])
{
$this->warning($message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return null
*/
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 null
*/
public function info($message, array $context = [])
{
$this->info($message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return null
*/
public function debug($message, array $context = [])
{
$this->debug($message, $context);
}
}

View File

@@ -0,0 +1,16 @@
<?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 Psr\Log;
defined('AKEEBAENGINE') || die();
class InvalidArgumentException extends \InvalidArgumentException
{
}

View File

@@ -0,0 +1,27 @@
<?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 Psr\Log;
defined('AKEEBAENGINE') || die();
/**
* Describes log levels
*/
class LogLevel
{
const EMERGENCY = 'emergency';
const ALERT = 'alert';
const CRITICAL = 'critical';
const ERROR = 'error';
const WARNING = 'warning';
const NOTICE = 'notice';
const INFO = 'info';
const DEBUG = 'debug';
}

View File

@@ -0,0 +1,27 @@
<?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 Psr\Log;
defined('AKEEBAENGINE') || die();
/**
* Describes a logger-aware instance
*/
interface LoggerAwareInterface
{
/**
* Sets a logger instance on the object
*
* @param LoggerInterface $logger
*
* @return null
*/
public function setLogger(LoggerInterface $logger);
}

View File

@@ -0,0 +1,31 @@
<?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 Psr\Log;
defined('AKEEBAENGINE') || die();
/**
* Basic Implementation of LoggerAwareInterface.
*/
trait LoggerAwareTrait
{
/** @var LoggerInterface */
protected $logger;
/**
* Sets a logger.
*
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
}

View File

@@ -0,0 +1,132 @@
<?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 Psr\Log;
defined('AKEEBAENGINE') || die();
/**
* Describes a logger instance
*
* The message MUST be a string or object implementing __toString().
*
* The message MAY contain placeholders in the form: {foo} where foo
* will be replaced by the context data in key "foo".
*
* The context array can contain arbitrary data, the only assumption that
* can be made by implementors is that if an Exception instance is given
* to produce a stack trace, it MUST be in a key named "exception".
*
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
* for the full interface specification.
*/
interface LoggerInterface
{
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return null
*/
public function emergency($message, array $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 null
*/
public function alert($message, array $context = []);
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*
* @return null
*/
public function critical($message, array $context = []);
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*
* @return null
*/
public function error($message, array $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 null
*/
public function warning($message, array $context = []);
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return null
*/
public function notice($message, array $context = []);
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*
* @return null
*/
public function info($message, array $context = []);
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return null
*/
public function debug($message, array $context = []);
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return null
*/
public function log($level, $message, array $context = []);
}

View File

@@ -0,0 +1,149 @@
<?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 Psr\Log;
defined('AKEEBAENGINE') || die();
/**
* This is a simple Logger trait that classes unable to extend AbstractLogger
* (because they extend another class, etc) can include.
*
* It simply delegates all log-level-specific methods to the `log` method to
* reduce boilerplate code that a simple Logger that does the same thing with
* messages regardless of the error level has to implement.
*/
trait LoggerTrait
{
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return null
*/
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 null
*/
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 null
*/
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 null
*/
public function error($message, array $context = [])
{
$this->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 null
*/
public function warning($message, array $context = [])
{
$this->warning($message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return null
*/
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 null
*/
public function info($message, array $context = [])
{
$this->info($message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return null
*/
public function debug($message, array $context = [])
{
$this->debug($message, $context);
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return null
*/
abstract public function log($level, $message, array $context = []);
}

View File

@@ -0,0 +1,37 @@
<?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 Psr\Log;
defined('AKEEBAENGINE') || die();
/**
* This Logger can be used to avoid conditional log calls
*
* Logging should always be optional, and if no logger is provided to your
* library creating a NullLogger instance to have something to throw logs at
* is a good way to avoid littering your code with `if ($this->logger) { }`
* blocks.
*/
class NullLogger extends AbstractLogger
{
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return null
*/
public function log($level, $message, array $context = [])
{
// noop
}
}

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\Scan;
defined('AKEEBAENGINE') || die();
abstract class Base
{
/**
* Gets all the files of a given folder
*
* @param string $folder The absolute path to the folder to scan for files
* @param integer $position The position in the file list to seek to. Use null for the start of list.
*
* @return array A simple array of files
*/
abstract public function getFiles($folder, &$position);
/**
* Gets all the folders (subdirectories) of a given folder
*
* @param string $folder The absolute path to the folder to scan for files
* @param integer $position The position in the file list to seek to. Use null for the start of list.
*
* @return array A simple array of folders
*/
abstract public function getFolders($folder, &$position);
}

View File

@@ -0,0 +1,164 @@
<?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\Scan;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Exceptions\WarningException;
use Akeeba\Engine\Factory;
use DirectoryIterator;
use Exception;
use RuntimeException;
/* 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 which uses opendir() and is smart enough to make large directories
* be scanned inside a step of their own.
*
* The idea is that if it's not the first operation of this step and the number of contained
* directories AND files is more than double the number of allowed files per fragment, we should
* break the step immediately.
*
*/
class Large extends Base
{
public function getFiles($folder, &$position)
{
return $this->scanFolder($folder, $position, false, 'file', 100);
}
public function getFolders($folder, &$position)
{
return $this->scanFolder($folder, $position, true, 'dir', 50);
}
protected function scanFolder($folder, &$position, $forFolders = true, $threshold_key = 'dir', $threshold_default = 50)
{
$registry = Factory::getConfiguration();
// Initialize variables
$arr = [];
$false = false;
if (!is_dir($folder) && !is_dir($folder . '/'))
{
throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP reports it as not a folder.');
}
if (!@is_readable($folder))
{
throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP reports it as not readable.');
}
try
{
$di = new DirectoryIterator($folder);
}
catch (Exception $e)
{
throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP\'s DirectoryIterator reports the path cannot be opened.', 0, $e);
}
if (!$di->valid())
{
throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP\'s DirectoryIterator could open the folder but immediately reports itself as not valid. If this happens your server is about to die.');
}
if (!empty($position))
{
$di->seek($position);
if ($di->key() != $position)
{
$position = null;
return $arr;
}
}
$counter = 0;
$maxCounter = $registry->get("engine.scan.large.{$threshold_key}_threshold", $threshold_default);
while ($di->valid())
{
/**
* If the directory entry is a link pointing somewhere outside the allowed directories per open_basedir we
* will get a RuntimeException (tested on PHP 5.3 onwards). Catching it lets us report the link as
* unreadable without suffering a PHP Fatal Error.
*/
try
{
$di->isLink();
}
catch (RuntimeException $e)
{
if (!in_array($di->getFilename(), ['.', '..']))
{
Factory::getLog()->warning(sprintf("Link %s is inaccessible. Check the open_basedir restrictions in your server's PHP configuration", $di->getPathname()));
}
$di->next();
continue;
}
if ($di->isDot())
{
$di->next();
continue;
}
if ($di->isDir() != $forFolders)
{
$di->next();
continue;
}
$ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR;
$dir = $folder . $ds . $di->getFilename();
$data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($dir) : $dir;
if ($data)
{
$counter++;
$arr[] = $data;
}
if ($counter == $maxCounter)
{
break;
}
$di->next();
}
// Determine the new value for the position
$di->next();
$position = $di->valid() ? ($di->key() - 1) : null;
return $arr;
}
}

View File

@@ -0,0 +1,266 @@
<?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\Scan;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Exceptions\WarningException;
use Akeeba\Engine\Factory;
use DirectoryIterator;
use Exception;
use RuntimeException;
/* 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 which uses opendir() and is smart enough to make large directories
* be scanned inside a step of their own.
*
* The idea is that if it's not the first operation of this step and the number of contained
* directories AND files is more than double the number of allowed files per fragment, we should
* break the step immediately.
*
*/
class Smart extends Base
{
public function getFiles($folder, &$position)
{
$registry = Factory::getConfiguration();
// Was the breakflag set BEFORE starting? -- This workaround is required due to PHP5 defaulting to assigning variables by reference
$breakflag_before_process = $registry->get('volatile.breakflag', false);
// Reset break flag before continuing
$breakflag = false;
// Initialize variables
$arr = [];
$false = false;
if (!@is_dir($folder) && !@is_dir($folder . '/'))
{
return $false;
}
$counter = 0;
$registry = Factory::getConfiguration();
$maxCounter = $registry->get('engine.scan.smart.large_dir_threshold', 100);
$allowBreakflag = ($registry->get('volatile.operation_counter', 0) != 0) && !$breakflag_before_process;
if (!@is_dir($folder))
{
throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP reports it as not a folder.');
}
if (!@is_readable($folder))
{
throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP reports it as not readable.');
}
try
{
$di = new DirectoryIterator($folder);
}
catch (Exception $e)
{
throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP\'s DirectoryIterator reports the path cannot be opened.', 0, $e);
}
if (!$di->valid())
{
throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP\'s DirectoryIterator could open the folder but immediately reports itself as not valid. If this happens your server is about to die.');
}
$ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR;
/** @var DirectoryIterator $file */
foreach ($di as $file)
{
if ($breakflag)
{
break;
}
/**
* If the directory entry is a link pointing somewhere outside the allowed directories per open_basedir we
* will get a RuntimeException (tested on PHP 5.3 onwards). Catching it lets us report the link as
* unreadable without suffering a PHP Fatal Error.
*/
try
{
$file->isLink();
}
catch (RuntimeException $e)
{
if (!in_array($di->getFilename(), ['.', '..']))
{
Factory::getLog()->warning(sprintf("Link %s is inaccessible. Check the open_basedir restrictions in your server's PHP configuration", $file->getPathname()));
}
continue;
}
if ($file->isDot())
{
continue;
}
if ($file->isDir())
{
continue;
}
$dir = $folder . $ds . $file->getFilename();
$data = $dir;
if (_AKEEBA_IS_WINDOWS)
{
$data = Factory::getFilesystemTools()->TranslateWinPath($dir);
}
if ($data)
{
$arr[] = $data;
}
$counter++;
if ($counter >= $maxCounter)
{
$breakflag = $allowBreakflag;
}
}
// Save break flag status
$registry->set('volatile.breakflag', $breakflag);
return $arr;
}
public function getFolders($folder, &$position)
{
// Was the breakflag set BEFORE starting? -- This workaround is required due to PHP5 defaulting to assigning variables by reference
$registry = Factory::getConfiguration();
$breakflag_before_process = $registry->get('volatile.breakflag', false);
// Reset break flag before continuing
$breakflag = false;
// Initialize variables
$arr = [];
$false = false;
if (!is_dir($folder) && !is_dir($folder . '/'))
{
throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP reports it as not a folder.');
}
if (!@is_readable($folder))
{
throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP reports it as not readable.');
}
$counter = 0;
$registry = Factory::getConfiguration();
$maxCounter = $registry->get('engine.scan.smart.large_dir_threshold', 100);
$allowBreakflag = ($registry->get('volatile.operation_counter', 0) != 0) && !$breakflag_before_process;
try
{
$di = new DirectoryIterator($folder);
}
catch (Exception $e)
{
throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP\'s DirectoryIterator reports the path cannot be opened.', 0, $e);
}
if (!$di->valid())
{
throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP\'s DirectoryIterator could open the folder but immediately reports itself as not valid. If this happens your server is about to die.');
}
$ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR;
/** @var DirectoryIterator $file */
foreach ($di as $file)
{
if ($breakflag)
{
break;
}
/**
* If the directory entry is a link pointing somewhere outside the allowed directories per open_basedir we
* will get a RuntimeException (tested on PHP 5.3 onwards). Catching it lets us report the link as
* unreadable without suffering a PHP Fatal Error.
*/
try
{
$file->isLink();
}
catch (RuntimeException $e)
{
if (!in_array($di->getFilename(), ['.', '..']))
{
Factory::getLog()->warning(sprintf("Link %s is inaccessible. Check the open_basedir restrictions in your server's PHP configuration", $file->getPathname()));
}
continue;
}
if ($file->isDot())
{
continue;
}
if (!$file->isDir())
{
continue;
}
$dir = $folder . $ds . $file->getFilename();
$data = $dir;
if (_AKEEBA_IS_WINDOWS)
{
$data = Factory::getFilesystemTools()->TranslateWinPath($dir);
}
if ($data)
{
$arr[] = $data;
}
$counter++;
if ($counter >= $maxCounter)
{
$breakflag = $allowBreakflag;
}
}
// Save break flag status
$registry->set('volatile.breakflag', $breakflag);
return $arr;
}
}

View File

@@ -0,0 +1,39 @@
{
"_information": {
"title": "COM_AKEEBA_CONFIG_ENGINE_SCAN_LARGE_TITLE",
"description": "COM_AKEEBA_CONFIG_ENGINE_SCAN_LARGE_DESCRIPTION"
},
"engine.scan.large.dir_threshold": {
"default": "100",
"type": "integer",
"min": "1",
"max": "1000",
"shortcuts": "20|50|100|200|300|400|500",
"scale": "1",
"uom": "",
"title": "COM_AKEEBA_CONFIG_LARGE_DIRTHRESHOLD_TITLE",
"description": "COM_AKEEBA_CONFIG_LARGE_DIRTHRESHOLD_DESCRIPTION"
},
"engine.scan.large.file_threshold": {
"default": "50",
"type": "integer",
"min": "1",
"max": "1000",
"shortcuts": "10|20|50|100|200|300|400|500",
"scale": "1",
"uom": "",
"title": "COM_AKEEBA_CONFIG_LARGE_FILESTHRESHOLD_TITLE",
"description": "COM_AKEEBA_CONFIG_LARGE_FILESTHRESHOLD_DESCRIPTION"
},
"engine.scan.common.largefile": {
"default": "10485760",
"type": "integer",
"min": "1048576",
"max": "1048576000",
"shortcuts": "1048576|2097152|5242880|10485760|15728640|20971520|26214400|31457280|41943040|52428800|78643200|104857600",
"scale": "1048576",
"uom": "MB",
"title": "COM_AKEEBA_CONFIG_LARGEFILE_TITLE",
"description": "COM_AKEEBA_CONFIG_LARGEFILE_DESCRIPTION"
}
}

View File

@@ -0,0 +1,28 @@
{
"_information": {
"title": "COM_AKEEBA_CONFIG_ENGINE_SCAN_SMART_TITLE",
"description": "COM_AKEEBA_CONFIG_ENGINE_SCAN_SMART_DESCRIPTION"
},
"engine.scan.smart.large_dir_threshold": {
"default": "100",
"type": "integer",
"min": "0",
"max": "500",
"shortcuts": "20|50|100|200|300|400|500",
"scale": "1",
"uom": "",
"title": "COM_AKEEBA_CONFIG_LARGEDIRTHRESHOLD_TITLE",
"description": "COM_AKEEBA_CONFIG_LARGEDIRTHRESHOLD_DESCRIPTION"
},
"engine.scan.common.largefile": {
"default": "10485760",
"type": "integer",
"min": "1048576",
"max": "1048576000",
"shortcuts": "1048576|2097152|5242880|10485760|15728640|20971520|26214400|31457280|41943040|52428800|78643200|104857600",
"scale": "1048576",
"uom": "MB",
"title": "COM_AKEEBA_CONFIG_LARGEFILE_TITLE",
"description": "COM_AKEEBA_CONFIG_LARGEFILE_DESCRIPTION"
}
}

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

Some files were not shown because too many files have changed in this diff Show More