first commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
<IfModule mod_authz_core.c>
|
||||
<RequireAll>
|
||||
Require all denied
|
||||
</RequireAll>
|
||||
</IfModule>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,971 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
if (!function_exists('hash_file') || !function_exists('hash'))
|
||||
{
|
||||
$action = version_compare(PHP_VERSION, '7.4.0', 'lt')
|
||||
? 'Please ask your host to enable the PHP hash extension and to make sure the hash_file() and hash() functions are not disabled.'
|
||||
: 'Please ask your host to change their PHP configuration so that the hash_file() and hash() functions are not disabled.';
|
||||
|
||||
Factory::getLog()->warning(
|
||||
sprintf(
|
||||
'Your server lacks support for the hash_file() and/or hash() functions. CRC32 checksum cannot be calculated. Third party ZIP extraction tools may report the backup archive as broken. %s',
|
||||
$action
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
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, $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.
|
||||
$hexdtime = pack('V', $this->unix2DOSTime($fileModTime));
|
||||
|
||||
// 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, $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)
|
||||
{
|
||||
// No hash? No CRC32!
|
||||
if (!function_exists('hash'))
|
||||
{
|
||||
return pack('V', 0);
|
||||
}
|
||||
|
||||
// Do I need to reverse the endianness of CRC32b data?
|
||||
static $reverseEndianness = null;
|
||||
|
||||
if ($reverseEndianness === null)
|
||||
{
|
||||
$reverseEndianness = hash('crc32b', 'The quick brown fox jumped over the lazy dog.', true) === pack('N', 2191738434);
|
||||
}
|
||||
|
||||
$entityType = 'file';
|
||||
$loggedName = null;
|
||||
|
||||
// Directories: dummy CRC-32
|
||||
if (!$isSymlink && $isDir)
|
||||
{
|
||||
$entityType = 'folder';
|
||||
$crc = pack('V', 0);
|
||||
}
|
||||
// Symlinks: CRC32 of the link source
|
||||
elseif ($isSymlink)
|
||||
{
|
||||
$entityType = 'symlink';
|
||||
$crc = hash('crc32b', @readlink($sourceNameOrData) ?: '', true);
|
||||
}
|
||||
// Virtual files: CRC32 of the contents
|
||||
elseif ($isVirtual)
|
||||
{
|
||||
$entityType = 'virtual file';
|
||||
$loggedName = sprintf('of size %u', strlen($sourceNameOrData));
|
||||
$crc = hash('crc32b', $sourceNameOrData ?: '', true);
|
||||
}
|
||||
// Files: CRC32 of the file contents
|
||||
else
|
||||
{
|
||||
// Get the CRC32 for the file
|
||||
$crc = function_exists("hash_file") ? @hash_file('crc32b', $sourceNameOrData, true) : null;
|
||||
|
||||
// If the file was unreadable skip it
|
||||
if ($crc === false)
|
||||
{
|
||||
throw new WarningException('Could not calculate CRC32 for ' . $sourceNameOrData . '. Looks like it is an unreadable file.');
|
||||
}
|
||||
|
||||
// If hash_file is not available use a fake CRC32
|
||||
$crc = $crc ?: pack('V', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* If CRC32 returns Big Endian data I'll have to convert it to Little Endian, as required by ZIP.
|
||||
*
|
||||
* I cannot unpack as Big Endian and repack as Little Endian. The intermediate conversion to integer might
|
||||
* overflow 32-bit versions of PHP as its internal integer type is platform-dependent.
|
||||
*
|
||||
* I cannot reverse the string using string functions because the internal character encoding may be something
|
||||
* other than ASCII / 8-bit and mb_string might not be available.
|
||||
*
|
||||
* Instead, I am unpacking the binary string as an array of unsigned bytes and then repacking the bytes, in
|
||||
* reverse order, into a binary string. pack() and unpack() are not affected by the character encoding. Using
|
||||
* unsigned bytes guarantees they will fit into internal integer variables regardless of the platform used.
|
||||
*/
|
||||
if ($crc !== "\000\000\000\000" && $reverseEndianness)
|
||||
{
|
||||
$temp = array_values(unpack('C4', $crc));
|
||||
$crc = pack('C*', $temp[3], $temp[2], $temp[1], $temp[0]);
|
||||
}
|
||||
|
||||
// Log the calculated CRC32 if the site is in debug mode
|
||||
if (defined('AKEEBADEBUG'))
|
||||
{
|
||||
$asHexChars = array_map(
|
||||
function ($c) {
|
||||
return dechex($c);
|
||||
}, unpack('C*', $crc)
|
||||
);
|
||||
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'%s %s - CRC32 = %s',
|
||||
$entityType,
|
||||
$loggedName ?? $sourceNameOrData,
|
||||
implode('', $reverseEndianness ? array_reverse($asHexChars) : $asHexChars)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
134
administrator/components/com_akeebabackup/engine/Autoloader.php
Normal file
134
administrator/components/com_akeebabackup/engine/Autoloader.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?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']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the Akeeba Engine autoloader
|
||||
Autoloader::init();
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
588
administrator/components/com_akeebabackup/engine/Base/Part.php
Normal file
588
administrator/components/com_akeebabackup/engine/Base/Part.php
Normal file
@@ -0,0 +1,588 @@
|
||||
<?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 Akeeba\Engine\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 function _onSerialize()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"_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",
|
||||
"showon": "akeeba.quota.size_quota:1"
|
||||
},
|
||||
"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",
|
||||
"showon": "akeeba.quota.enable_count_quota:1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"_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",
|
||||
"showon": "akeeba.advanced.autoresume:1"
|
||||
},
|
||||
"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",
|
||||
"showon": "akeeba.advanced.autoresume:1"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"akeeba.tuning.setmemlimit": {
|
||||
"default": "1",
|
||||
"type": "none",
|
||||
"protected": "1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* 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 The database driver connection options
|
||||
*
|
||||
* @return DriverBase|object A DriverBase object or something with magic methods that's compatible with it
|
||||
*/
|
||||
public static function &getDatabase(array $options)
|
||||
{
|
||||
// Get the options signature.
|
||||
$signature = md5(serialize($options));
|
||||
|
||||
// If there's a cached object return it.
|
||||
if (!empty(self::$instances[$signature]))
|
||||
{
|
||||
return self::$instances[$signature];
|
||||
}
|
||||
|
||||
// Get the driver name / class
|
||||
$driver = preg_replace('/[^A-Z0-9_\\\.-]/i', '', $options['driver'] ?? '');
|
||||
|
||||
// If there is no driver specified ask the Platform to guess it.
|
||||
if (empty($driver))
|
||||
{
|
||||
$default_signature = md5(serialize(Platform::getInstance()->get_platform_database_options()));
|
||||
$driver = Platform::getInstance()->get_default_database_driver($signature === $default_signature);
|
||||
}
|
||||
|
||||
// Ensure we have the FQN of the driver class
|
||||
if ((substr($driver, 0, 7) != '\\Akeeba') && substr($driver, 0, 7) != 'Akeeba\\')
|
||||
{
|
||||
$driver = '\\Akeeba\\Engine\\Driver\\' . ucfirst($driver);
|
||||
}
|
||||
|
||||
// Map the legacy MySQL driver to the newer MySQLi
|
||||
if (($driver == '\\Akeeba\\Engine\\Driver\\Mysql') && !function_exists('mysql_connect'))
|
||||
{
|
||||
$driver = Mysqli::class;
|
||||
}
|
||||
|
||||
// Translate MySQL SSL options
|
||||
if (!isset($options['ssl']) || !is_array($options['ssl']))
|
||||
{
|
||||
$options['ssl'] = [
|
||||
'enable' => (bool) ($options['dbencryption'] ?? false),
|
||||
'cipher' => ($options['dbsslcipher'] ?? '') ?: '',
|
||||
'ca' => ($options['dbsslca'] ?? '') ?: '',
|
||||
'capath' => ($options['dbsslcapath'] ?? '') ?: '',
|
||||
'key' => ($options['dbsslkey'] ?? '') ?: '',
|
||||
'cert' => ($options['dbsslcert'] ?? '') ?: '',
|
||||
'verify_server_cert' => ($options['dbsslverifyservercert'] ?? false) ?: false,
|
||||
];
|
||||
}
|
||||
|
||||
// Instantiate the database driver object and return it
|
||||
self::$instances[$signature] = new $driver($options);
|
||||
|
||||
return self::$instances[$signature];
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-cache a database driver object
|
||||
*
|
||||
* @param array $options The database driver connection options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function unsetDatabase(array $options): void
|
||||
{
|
||||
$signature = md5(serialize($options));
|
||||
|
||||
if (!isset(self::$instances[$signature]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
unset(self::$instances[$signature]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<?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.
|
||||
*/
|
||||
final 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_shift($this->database_list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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'] ?? '',
|
||||
'dbport' => $definition['port'] ?? '',
|
||||
'dbsocket' => $definition['socket'] ?? '',
|
||||
'dbuser' => $definition['username'] ?? '',
|
||||
'dbpass' => $definition['password'] ?? '',
|
||||
'prefix' => $definition['prefix'] ?? '',
|
||||
'dbencryption' => $definition['dbencryption'] ?? 0,
|
||||
'dbsslcipher' => $definition['dbsslcipher'] ?? '',
|
||||
'dbsslca' => $definition['dbsslca'] ?? '',
|
||||
'dbsslkey' => $definition['dbsslkey'] ?? '',
|
||||
'dbsslcert' => $definition['dbsslcert'] ?? '',
|
||||
'dbsslverifyservercert' => $definition['dbsslverifyservercert'] ?? 0,
|
||||
'parts' => $definition['parts'],
|
||||
'tables' => $definition['tables'],
|
||||
];
|
||||
|
||||
if ($blankOutPass)
|
||||
{
|
||||
$this->databases_json[$section]['dbuser'] = '';
|
||||
$this->databases_json[$section]['dbpass'] = '';
|
||||
$this->databases_json[$section]['dbsslca'] = '';
|
||||
$this->databases_json[$section]['dbsslkey'] = '';
|
||||
$this->databases_json[$section]['dbsslcert'] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<?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\Core\Domain\Finalizer\LocalQuotas;
|
||||
use Akeeba\Engine\Core\Domain\Finalizer\MailAdministrators;
|
||||
use Akeeba\Engine\Core\Domain\Finalizer\ObsoleteRecordsQuotas;
|
||||
use Akeeba\Engine\Core\Domain\Finalizer\PostProcessing;
|
||||
use Akeeba\Engine\Core\Domain\Finalizer\RemoteQuotas;
|
||||
use Akeeba\Engine\Core\Domain\Finalizer\RemoveTemporaryFiles;
|
||||
use Akeeba\Engine\Core\Domain\Finalizer\UpdateFileSizes;
|
||||
use Akeeba\Engine\Core\Domain\Finalizer\UpdateStatistics;
|
||||
use Akeeba\Engine\Core\Domain\Finalizer\UploadKickstart;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use Akeeba\Engine\Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Backup finalization domain
|
||||
*/
|
||||
final class Finalization extends Part
|
||||
{
|
||||
/** @var array The finalisation actions we have to execute (FIFO queue) */
|
||||
private $actionQueue = [];
|
||||
|
||||
/** @var string The current method, shifted from the action queye */
|
||||
private $currentActionClass = '';
|
||||
|
||||
private $currentActionObject = null;
|
||||
|
||||
/** @var int How many finalisation steps I have already done */
|
||||
private $stepsDone = 0;
|
||||
|
||||
/** @var int How many finalisation steps I have in total */
|
||||
private $stepsTotal = 0;
|
||||
|
||||
/** @var int How many finalisation substeps I have already done */
|
||||
private $subStepsDone = 0;
|
||||
|
||||
/** @var int How many finalisation substeps I have in total */
|
||||
private $subStepsTotal = 0;
|
||||
|
||||
/**
|
||||
* Get the percentage of finalization steps done
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getProgress()
|
||||
{
|
||||
if ($this->stepsTotal <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
$overall = $this->stepsDone / $this->stepsTotal;
|
||||
$local = 0;
|
||||
|
||||
if ($this->subStepsTotal > 0)
|
||||
{
|
||||
$local = $this->subStepsDone / $this->subStepsTotal;
|
||||
}
|
||||
|
||||
return $overall + ($local / $this->stepsTotal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relays and logs an exception
|
||||
*
|
||||
* @param \Throwable $e The exception or throwable to log
|
||||
* @param string $logLevel The log level to log it with
|
||||
*
|
||||
* @return void
|
||||
* @since 9.3.1
|
||||
*/
|
||||
public function relayException(\Throwable $e, string $logLevel = LogLevel::ERROR): void
|
||||
{
|
||||
self::logErrorsFromException($e, $logLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by additional handler classes to relay their step to us
|
||||
*
|
||||
* @param string $step The current step
|
||||
*/
|
||||
public function relayStep($step)
|
||||
{
|
||||
$this->setStep($step);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by additional handler classes to relay their substep to us
|
||||
*
|
||||
* @param string $substep The current sub-step
|
||||
*/
|
||||
public function relaySubstep($substep)
|
||||
{
|
||||
$this->setSubstep($substep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the abstract method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _finalize()
|
||||
{
|
||||
$this->setState(self::STATE_FINISHED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the finalisation engine
|
||||
*/
|
||||
protected function _prepare()
|
||||
{
|
||||
// Make sure the break flag is not set
|
||||
$configuration = Factory::getConfiguration();
|
||||
$configuration->get('volatile.breakflag', false);
|
||||
|
||||
// Get the quota actions
|
||||
$quotaActions = $configuration->get('volatile.core.finalization.quotaActions', null);
|
||||
|
||||
$quotaActions = is_array($quotaActions) ? $quotaActions : [
|
||||
LocalQuotas::class,
|
||||
RemoteQuotas::class,
|
||||
ObsoleteRecordsQuotas::class,
|
||||
];
|
||||
|
||||
// Get the default finalization actions
|
||||
$defaultActions = array_merge(
|
||||
[
|
||||
RemoveTemporaryFiles::class,
|
||||
UpdateStatistics::class,
|
||||
UpdateFileSizes::class,
|
||||
PostProcessing::class,
|
||||
UploadKickstart::class,
|
||||
],
|
||||
$quotaActions,
|
||||
[
|
||||
MailAdministrators::class,
|
||||
// Run it a second time to update the backup end time after post-processing, emails, etc
|
||||
UpdateStatistics::class,
|
||||
]
|
||||
);
|
||||
|
||||
// Populate the actions queue, if it's not already set in a subclass
|
||||
$this->actionQueue = $this->actionQueue ?: $defaultActions;
|
||||
|
||||
// Apply action queue customisations
|
||||
$customQueue = $configuration->get('volatile.core.finalization.action_queue', null);
|
||||
$customQueueBefore = $configuration->get('volatile.core.finalization.action_queue_before', null);
|
||||
$customQueueAfter = $configuration->get('volatile.core.finalization.action_queue_after', null);
|
||||
|
||||
if (is_array($customQueue) && !empty($customQueue))
|
||||
{
|
||||
Factory::getLog()->debug('Overriding action queue');
|
||||
$this->actionQueue = $customQueue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (is_array($customQueueBefore) && !empty($customQueueBefore))
|
||||
{
|
||||
Factory::getLog()->debug('Adding finalization actions before post-processing');
|
||||
$before = array_slice($this->actionQueue, 0, 3);
|
||||
$after = array_slice($this->actionQueue, 3);
|
||||
|
||||
$this->actionQueue = array_merge($before, $customQueueBefore, $after);
|
||||
}
|
||||
|
||||
if (is_array($customQueueAfter) && !empty($customQueueAfter))
|
||||
{
|
||||
Factory::getLog()->debug('Adding finalization actions at the end of the queue');
|
||||
$before = array_slice($this->actionQueue, 0, -1);
|
||||
$after = array_slice($this->actionQueue, -1, 1);
|
||||
|
||||
$this->actionQueue = array_merge($before, $customQueueAfter, $after);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the actions queue
|
||||
Factory::getLog()->debug('Finalization action queue: ' . implode(', ', $this->actionQueue));
|
||||
|
||||
// Initialise actions processing
|
||||
$this->stepsTotal = count($this->actionQueue);
|
||||
$this->stepsDone = 0;
|
||||
$this->subStepsTotal = 0;
|
||||
$this->subStepsDone = 0;
|
||||
|
||||
// Seed the method
|
||||
$this->currentActionClass = array_shift($this->actionQueue);
|
||||
|
||||
// Set ourselves to running state
|
||||
$this->setState(self::STATE_RUNNING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the abstract method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _run()
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
if ($this->getState() == self::STATE_POSTRUN)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$finished = (empty($this->actionQueue)) && ($this->currentActionClass == '');
|
||||
|
||||
if ($finished)
|
||||
{
|
||||
$this->setState(self::STATE_POSTRUN);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setState(self::STATE_RUNNING);
|
||||
|
||||
$timer = Factory::getTimer();
|
||||
|
||||
// Continue processing while we have still enough time and stuff to do
|
||||
while (($timer->getTimeLeft() > 0) && (!$finished) && (!$configuration->get('volatile.breakflag', false)))
|
||||
{
|
||||
if (empty($this->currentActionObject))
|
||||
{
|
||||
$className = $this->currentActionClass;
|
||||
|
||||
Factory::getLog()->debug(__CLASS__ . "::_run() Running new finalization object $className");
|
||||
|
||||
$this->currentActionObject = new $className($this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->debug(__CLASS__ . "::_run() Resuming finalization object $this->currentActionClass");
|
||||
}
|
||||
|
||||
$finalizer = $this->currentActionObject;
|
||||
|
||||
if ($finalizer() !== true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->currentActionClass = '';
|
||||
$this->currentActionObject = null;
|
||||
$this->stepsDone++;
|
||||
$finished = empty($this->actionQueue);
|
||||
|
||||
if ($finished)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->currentActionClass = array_shift($this->actionQueue);
|
||||
$this->subStepsTotal = 0;
|
||||
$this->subStepsDone = 0;
|
||||
}
|
||||
|
||||
if ($finished)
|
||||
{
|
||||
$this->setState(self::STATE_POSTRUN);
|
||||
$this->setStep('');
|
||||
$this->setSubstep('');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
* @subpackage
|
||||
*
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain\Finalizer;
|
||||
|
||||
use Akeeba\Engine\Core\Domain\Finalization;
|
||||
use Akeeba\Engine\Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Abstract implementation of a finalizer class
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
*/
|
||||
abstract class AbstractFinalizer implements FinalizerInterface
|
||||
{
|
||||
/**
|
||||
* The part we belong to
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @var Finalization
|
||||
*/
|
||||
private $finalizationPart;
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param Finalization $finalizationPart The part we belong to.
|
||||
*
|
||||
* @since 9.3.1
|
||||
*/
|
||||
public function __construct(Finalization $finalizationPart)
|
||||
{
|
||||
$this->finalizationPart = $finalizationPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relays an exception so it can be logged
|
||||
*
|
||||
* @param \Throwable $e
|
||||
* @param string $logLevel
|
||||
*
|
||||
* @return void
|
||||
* @since 9.3.1
|
||||
*/
|
||||
protected function logErrorsFromException(\Throwable $e, string $logLevel = LogLevel::ERROR): void
|
||||
{
|
||||
$this->finalizationPart->relayException($e, $logLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relays the current step back to the parent finalization engine part
|
||||
*
|
||||
* @param string $step The step name to set
|
||||
*
|
||||
* @return void
|
||||
* @since 9.3.1
|
||||
*/
|
||||
protected function setStep(string $step): void
|
||||
{
|
||||
$this->finalizationPart->relayStep($step);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relays the current sub-step back to the parent finalization engine part
|
||||
*
|
||||
* @param string $substep The sub-step name to set
|
||||
*
|
||||
* @return void
|
||||
* @since 9.3.1
|
||||
*/
|
||||
protected function setSubstep(string $substep): void
|
||||
{
|
||||
$this->finalizationPart->relaySubstep($substep);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
* @subpackage
|
||||
*
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain\Finalizer;
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
|
||||
abstract class AbstractQuotaManagement extends AbstractFinalizer
|
||||
{
|
||||
/**
|
||||
* Abstraction for the engine configuration keys used in the concrete quota management class.
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @var string[]
|
||||
*/
|
||||
protected $configKeys = [
|
||||
'maxAgeEnable' => 'akeeba.quota.maxage.enable',
|
||||
'maxAgeDays' => 'akeeba.quota.maxage.maxdays',
|
||||
'maxAgeKeep' => 'akeeba.quota.maxage.keepday',
|
||||
'countEnable' => 'akeeba.quota.enable_count_quota',
|
||||
'countValue' => 'akeeba.quota.count_quota',
|
||||
'sizeEnable' => 'akeeba.quota.enable_size_quota',
|
||||
'sizeValue' => 'akeeba.quota.size_quota',
|
||||
];
|
||||
|
||||
/**
|
||||
* The ID of the latest backup (the one we are running in right now)
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @var int
|
||||
*/
|
||||
protected $latestBackupId;
|
||||
|
||||
/**
|
||||
* Human-readable quote type e.g. 'local', 'remote', etc. for the concrete quota management class.
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @var string
|
||||
*/
|
||||
protected $quotaType = 'local';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
$this->setStep(
|
||||
sprintf(
|
||||
'Applying %s quotas',
|
||||
$this->quotaType
|
||||
)
|
||||
);
|
||||
$this->setSubstep('');
|
||||
|
||||
// If no quota settings are enabled, quit
|
||||
$configuration = Factory::getConfiguration();
|
||||
$timer = Factory::getTimer();
|
||||
$useDayQuotas = $configuration->get($this->configKeys['maxAgeEnable']);
|
||||
$useCountQuotas = $configuration->get($this->configKeys['countEnable']);
|
||||
$useSizeQuotas = $configuration->get($this->configKeys['sizeEnable']);
|
||||
|
||||
if (!($useDayQuotas || $useCountQuotas || $useSizeQuotas))
|
||||
{
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'No %s quotas were defined; old backup files will be kept intact',
|
||||
$this->quotaType
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the latest backup ID
|
||||
$statistics = Factory::getStatistics();
|
||||
$this->latestBackupId = $statistics->getId();
|
||||
|
||||
// Try to load the calculated quotas from the volatile keys
|
||||
$keyIsCalculated = sprintf('volatile.quotas.%s.calculated', $this->quotaType);
|
||||
$keyRemoveBackupIDs = sprintf('volatile.quotas.%s.removeBackupIDs', $this->quotaType);
|
||||
$keyRemoveLogPaths = sprintf('volatile.quotas.%s.removeLogPaths', $this->quotaType);
|
||||
$keyFilesToRemove = sprintf('volatile.quotas.%s.filesToRemove', $this->quotaType);
|
||||
|
||||
$isCalculated = $configuration->get($keyIsCalculated, false);
|
||||
$removeBackupIDs = $configuration->get($keyRemoveBackupIDs, []);
|
||||
$removeLogPaths = $configuration->get($keyRemoveLogPaths, []);
|
||||
$filesToRemove = $configuration->get($keyFilesToRemove, []);
|
||||
|
||||
// Calculate the quotas if nothing was calculated just yet.
|
||||
if (!$isCalculated)
|
||||
{
|
||||
// Calculate the quotas. If nothing is found, return immediately.
|
||||
if ($this->calculateQuotas($allRecords, $removeBackupIDs, $removeLogPaths, $filesToRemove) === false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->saveCalculatedQuotas($removeBackupIDs, $removeLogPaths, $filesToRemove);
|
||||
|
||||
// Do I have enough time to process removals?
|
||||
if ($timer->getTimeLeft() <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Process a chunk of removals
|
||||
if (!$this->processRemovals($removeBackupIDs, $filesToRemove, $removeLogPaths))
|
||||
{
|
||||
$this->saveCalculatedQuotas($removeBackupIDs, $removeLogPaths, $filesToRemove);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$removeBackupIDs = null;
|
||||
$removeLogPaths = null;
|
||||
$filesToRemove = null;
|
||||
|
||||
$this->saveCalculatedQuotas($removeBackupIDs, $removeLogPaths, $filesToRemove);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the backup records to apply quotas on.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 9.3.1
|
||||
*/
|
||||
abstract protected function getAllRecords(): array;
|
||||
|
||||
/**
|
||||
* Processes a list of records for removal. The removal DOES NOT take place here.
|
||||
*
|
||||
* @param array $allRecords The records to process
|
||||
* @param array $removeBackupIDs Running tally of backup IDs to remove files from
|
||||
* @param array $removeLogPaths Running tally of log entries to remove
|
||||
* @param array $ret Running tally of arrays of files to remove
|
||||
* @param array $leftover Leftover records to be processed by the next quota rule
|
||||
*
|
||||
* @since 9.3.1
|
||||
*/
|
||||
protected function markAllRecordsForRemoval(array &$allRecords, array &$removeBackupIDs, array &$removeLogPaths, array &$ret, array &$leftover)
|
||||
{
|
||||
foreach ($allRecords as $def)
|
||||
{
|
||||
if ($def['id'] != $this->latestBackupId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$temp = array_pop($leftover);
|
||||
$leftover[] = $def;
|
||||
array_unshift($allRecords, $temp);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($allRecords as $def)
|
||||
{
|
||||
$ret[] = $def['filenames'];
|
||||
$removeBackupIDs[] = $def['id'];
|
||||
|
||||
if (empty($def['logname']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$filePath = reset($def['filenames']);
|
||||
|
||||
if (empty($filePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$logPath = dirname($filePath) . '/' . $def['logname'];
|
||||
|
||||
if (@file_exists($logPath))
|
||||
{
|
||||
$removeLogPaths[] = $logPath;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$altLogPath = substr($logPath, 0, -4);
|
||||
|
||||
if (@file_exists($altLogPath))
|
||||
{
|
||||
/**
|
||||
* Bad host: the log file akeeba.tag.log.php may not exist but the akeeba.tag.log file
|
||||
* does. This code addresses this problem.
|
||||
*/
|
||||
$removeLogPaths[] = $altLogPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the actual removal.
|
||||
*
|
||||
* @param array $removeBackupIDs The backup IDs which will have their files removed
|
||||
* @param array $filesToRemove The flat list of files to remove
|
||||
* @param array $removeLogPaths The flat list of log paths to remove
|
||||
*
|
||||
* @return bool True if we are done, false to come back in the next step of the engine
|
||||
* @throws Exception
|
||||
* @since 9.3.1
|
||||
*/
|
||||
abstract protected function processRemovals(array &$removeBackupIDs, array &$filesToRemove, array &$removeLogPaths): bool;
|
||||
|
||||
/**
|
||||
* Applies the Count Quotas.
|
||||
*
|
||||
* @param array $allRecords All records left to process
|
||||
* @param array $removeBackupIDs Running tally of backup IDs to remove files from
|
||||
* @param array $removeLogPaths Running tally of log entries to remove
|
||||
* @param array $ret Running tally of arrays of files to remove
|
||||
*
|
||||
* @return void
|
||||
* @since 9.3.1
|
||||
*/
|
||||
private function applyCountQuotas(array &$allRecords, array &$removeBackupIDs, array &$removeLogPaths, array &$ret)
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$useCountQuotas = $configuration->get($this->configKeys['countEnable']);
|
||||
$countQuota = $configuration->get($this->configKeys['countValue']);
|
||||
|
||||
// Do we need to apply count quotas?
|
||||
if (!$useCountQuotas || !is_numeric($countQuota) || $countQuota <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// We should only run a count quota if there are more files than the set limit
|
||||
if (count($allRecords) <= $countQuota)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Processing %s count quotas',
|
||||
$this->quotaType
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Backups are sorted by reverse ID order, e.g. 200, 199, 198, 197, 196, 195, 194, 193, 192, 191, 190, 189, 188.
|
||||
* I need to keep the first $countQuota records in $leftover and process the remaining records.
|
||||
*/
|
||||
$leftover = array_slice($allRecords, 0, $countQuota);
|
||||
$allRecords = array_slice($allRecords, $countQuota);
|
||||
|
||||
$this->markAllRecordsForRemoval($allRecords, $removeBackupIDs, $removeLogPaths, $ret, $leftover);
|
||||
|
||||
$allRecords = $leftover;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the Day-Based Quotas.
|
||||
*
|
||||
* @param array $allRecords All records left to process
|
||||
* @param array $removeBackupIDs Running tally of backup IDs to remove files from
|
||||
* @param array $removeLogPaths Running tally of log entries to remove
|
||||
* @param array $ret Running tally of arrays of files to remove
|
||||
*
|
||||
* @return void
|
||||
* @since 9.3.1
|
||||
*/
|
||||
private function applyDayQuotas(array &$allRecords, array &$removeBackupIDs, array &$removeLogPaths, array &$ret): void
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$daysQuota = $configuration->get($this->configKeys['maxAgeDays']);
|
||||
$preserveDay = $configuration->get($this->configKeys['maxAgeKeep']);
|
||||
$leftover = [];
|
||||
|
||||
$killDatetime = new DateTime();
|
||||
$killDatetime->modify('-' . $daysQuota . ($daysQuota == 1 ? ' day' : ' days'));
|
||||
$killTS = $killDatetime->format('U');
|
||||
|
||||
/**
|
||||
* Move the following kind of records FROM allRecords TO leftover:
|
||||
* - Current backup record
|
||||
* - Backups on a preserve day
|
||||
* - Backups newer than the earliest removal date
|
||||
*/
|
||||
$allRecords = array_filter(
|
||||
$allRecords,
|
||||
function (array $def) use ($killDatetime, $killTS, $preserveDay, &$leftover): bool {
|
||||
if ($def['id'] == $this->latestBackupId)
|
||||
{
|
||||
$leftover[] = $def;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is this on a preserve day?
|
||||
if ($preserveDay > 0 && $def['day'] == $preserveDay)
|
||||
{
|
||||
$leftover[] = $def;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, check the timestamp
|
||||
if ($def['backupstart'] >= $killTS)
|
||||
{
|
||||
$leftover[] = $def;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
$this->markAllRecordsForRemoval($allRecords, $removeBackupIDs, $removeLogPaths, $ret, $leftover);
|
||||
|
||||
$allRecords = $leftover;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the Maximum Size Quotas.
|
||||
*
|
||||
* @param array $allRecords All records left to process
|
||||
* @param array $removeBackupIDs Running tally of backup IDs to remove files from
|
||||
* @param array $removeLogPaths Running tally of log entries to remove
|
||||
* @param array $ret Running tally of arrays of files to remove
|
||||
*
|
||||
* @return void
|
||||
* @since 9.3.1
|
||||
*/
|
||||
private function applySizeQuotas(array &$allRecords, array &$removeBackupIDs, array &$removeLogPaths, array &$ret)
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$useSizeQuotas = $configuration->get($this->configKeys['sizeEnable']);
|
||||
$sizeQuota = $configuration->get($this->configKeys['sizeValue']);
|
||||
|
||||
// Do we need to apply size quotas?
|
||||
if (!$useSizeQuotas || !is_numeric($sizeQuota) || $sizeQuota <= 0 || count($allRecords) <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Processing %s size quotas',
|
||||
$this->quotaType
|
||||
)
|
||||
);
|
||||
|
||||
// First I will find how many elements of the array I need to get to the $sizeQuota size.
|
||||
$runningSize = 0;
|
||||
$numberOfRecords = 0;
|
||||
|
||||
foreach ($allRecords as $def)
|
||||
{
|
||||
$numberOfRecords++;
|
||||
|
||||
if ($def['id'] == $this->latestBackupId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$runningSize += $def['size'];
|
||||
|
||||
if ($runningSize >= $sizeQuota)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$leftover = array_slice($allRecords, 0, $numberOfRecords);
|
||||
$allRecords = array_slice($allRecords, $numberOfRecords);
|
||||
|
||||
$this->markAllRecordsForRemoval($allRecords, $removeBackupIDs, $removeLogPaths, $ret, $leftover);
|
||||
|
||||
$allRecords = $leftover;
|
||||
}
|
||||
|
||||
private function calculateQuotas(&$allRecords, &$removeBackupIDs, &$removeLogPaths, &$filesToRemove): bool
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$useDayQuotas = $configuration->get($this->configKeys['maxAgeEnable']);
|
||||
|
||||
$allRecords = $this->getAllRecords();
|
||||
|
||||
// If there are no files, exit early
|
||||
if (count($allRecords) == 0)
|
||||
{
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'There were no old backup records to apply %s quotas on',
|
||||
$this->quotaType
|
||||
)
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Init arrays
|
||||
$removeBackupIDs = [];
|
||||
$removeLogPaths = [];
|
||||
$ret = [];
|
||||
|
||||
// Do we need to apply maximum backup age quotas?
|
||||
if ($useDayQuotas)
|
||||
{
|
||||
$this->applyDayQuotas($allRecords, $removeBackupIDs, $removeLogPaths, $ret);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->applyCountQuotas($allRecords, $removeBackupIDs, $removeLogPaths, $ret);
|
||||
$this->applySizeQuotas($allRecords, $removeBackupIDs, $removeLogPaths, $ret);
|
||||
}
|
||||
|
||||
// Convert the $ret 2-dimensional array to single dimensional
|
||||
$filesToRemove = [];
|
||||
|
||||
foreach ($ret as $temp)
|
||||
{
|
||||
$filesToRemove = array_merge($filesToRemove ?? [], $temp);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the calculated quotas into the volatile storage
|
||||
*
|
||||
* @param array|null $removeBackupIDs Running tally of backup IDs to remove files from
|
||||
* @param array|null $removeLogPaths Running tally of log entries to remove
|
||||
* @param array|null $filesToRemove The flat list of files to remove
|
||||
*
|
||||
* @return void
|
||||
* @since 9.3.1
|
||||
*/
|
||||
private function saveCalculatedQuotas(?array &$removeBackupIDs, ?array &$removeLogPaths, ?array &$filesToRemove): void
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$keyIsCalculated = sprintf('volatile.quotas.%s.calculated', $this->quotaType);
|
||||
$keyRemoveBackupIDs = sprintf('volatile.quotas.%s.removeBackupIDs', $this->quotaType);
|
||||
$keyRemoveLogPaths = sprintf('volatile.quotas.%s.removeLogPaths', $this->quotaType);
|
||||
$keyFilesToRemove = sprintf('volatile.quotas.%s.filesToRemove', $this->quotaType);
|
||||
|
||||
$isCalculated = $removeBackupIDs !== null || $removeLogPaths !== null || $filesToRemove !== null;
|
||||
|
||||
if ($isCalculated)
|
||||
{
|
||||
$configuration->set($keyIsCalculated, true);
|
||||
$configuration->set($keyRemoveBackupIDs, $removeBackupIDs);
|
||||
$configuration->set($keyRemoveLogPaths, $removeLogPaths);
|
||||
$configuration->set($keyFilesToRemove, $filesToRemove);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$configuration->remove($keyIsCalculated);
|
||||
$configuration->remove($keyRemoveBackupIDs);
|
||||
$configuration->remove($keyRemoveLogPaths);
|
||||
$configuration->remove($keyFilesToRemove);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
* @subpackage
|
||||
*
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain\Finalizer;
|
||||
|
||||
use Akeeba\Engine\Core\Domain\Finalization;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Interface to a finalizer invokable class.
|
||||
*
|
||||
* @since 9.3.1
|
||||
*/
|
||||
interface FinalizerInterface
|
||||
{
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param Finalization $finalizationPart The part we belong to.
|
||||
*
|
||||
* @since 9.3.1
|
||||
*/
|
||||
public function __construct(Finalization $finalizationPart);
|
||||
|
||||
/**
|
||||
* Executes the finalizer job. Returns true when done, false if it needs to run further.
|
||||
*
|
||||
* @return bool True if we are fully done. False if we must be called again.
|
||||
* @throws Exception When an error occurs.
|
||||
*
|
||||
* @since 9.3.1
|
||||
*/
|
||||
public function __invoke();
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
* @subpackage
|
||||
*
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain\Finalizer;
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
|
||||
final class LocalQuotas extends AbstractQuotaManagement
|
||||
{
|
||||
|
||||
/**
|
||||
* Get all the backup records to apply quotas on.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 9.3.1
|
||||
*/
|
||||
protected function getAllRecords(): array
|
||||
{
|
||||
// Get valid-looking backup ID's
|
||||
$validIDs = Platform::getInstance()->get_valid_backup_records(true) ?: [];
|
||||
|
||||
// Create a list of valid files
|
||||
$allFiles = [];
|
||||
$statistics = Factory::getStatistics();
|
||||
|
||||
foreach ($validIDs as $id)
|
||||
{
|
||||
$stat = Platform::getInstance()->get_statistics($id);
|
||||
|
||||
// Exclude frozen record from quota management
|
||||
if (isset($stat['frozen']) && $stat['frozen'])
|
||||
{
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Excluding frozen backup id %d from %s quota management',
|
||||
$id,
|
||||
$this->quotaType
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$backupstart = new DateTime($stat['backupstart']);
|
||||
$backupTS = $backupstart->format('U');
|
||||
$backupDay = $backupstart->format('d');
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
$backupTS = 0;
|
||||
$backupDay = 0;
|
||||
}
|
||||
|
||||
// Get the log file name
|
||||
$tag = $stat['tag'];
|
||||
$backupId = $stat['backupid'] ?? '';
|
||||
$logName = '';
|
||||
|
||||
if (!empty($backupId))
|
||||
{
|
||||
$logName = 'akeeba.' . $tag . '.' . $backupId . '.log.php';
|
||||
}
|
||||
|
||||
// Multipart processing
|
||||
$filenames = $statistics->get_all_filenames($stat, true);
|
||||
|
||||
// Only process existing files
|
||||
if (is_null($filenames))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$filesize = 0;
|
||||
|
||||
foreach ($filenames as $filename)
|
||||
{
|
||||
$filesize += @filesize($filename);
|
||||
}
|
||||
|
||||
$allFiles[] = [
|
||||
'id' => $id,
|
||||
'filenames' => $filenames,
|
||||
'size' => $filesize,
|
||||
'backupstart' => $backupTS,
|
||||
'day' => $backupDay,
|
||||
'logname' => $logName,
|
||||
];
|
||||
}
|
||||
|
||||
return $allFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the actual removal.
|
||||
*
|
||||
* @param array $removeBackupIDs The backup IDs which will have their files removed
|
||||
* @param array $filesToRemove The flat list of files to remove
|
||||
* @param array $removeLogPaths The flat list of log paths to remove
|
||||
*
|
||||
* @return bool True if we are done, false to come back in the next step of the engine
|
||||
* @throws Exception
|
||||
* @since 9.3.1
|
||||
*/
|
||||
protected function processRemovals(array &$removeBackupIDs, array &$filesToRemove, array &$removeLogPaths): bool
|
||||
{
|
||||
$timer = Factory::getTimer();
|
||||
|
||||
// Update the statistics record with the removed remote files
|
||||
if (!empty($removeBackupIDs))
|
||||
{
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Applying %s quotas: updating backup records',
|
||||
$this->quotaType
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
while (!empty($removeBackupIDs) && $timer->getTimeLeft() > 0)
|
||||
{
|
||||
$id = array_shift($removeBackupIDs);
|
||||
$data = ['filesexist' => '0'];
|
||||
|
||||
Platform::getInstance()->set_or_update_statistics($id, $data);
|
||||
}
|
||||
|
||||
// Check if I have enough time
|
||||
if ($timer->getTimeLeft() <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply quotas upon backup records
|
||||
if (!empty($filesToRemove) > 0)
|
||||
{
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Applying %s quotas: removing backup archives',
|
||||
$this->quotaType
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
while (!empty($filesToRemove) && $timer->getTimeLeft() > 0)
|
||||
{
|
||||
$file = array_shift($filesToRemove);
|
||||
|
||||
if (@Platform::getInstance()->unlink($file))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Factory::getLog()->warning(
|
||||
sprintf(
|
||||
'Failed to remove old backup file %s',
|
||||
$file
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if I have enough time
|
||||
if ($timer->getTimeLeft() <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply quotas to log files
|
||||
if (!empty($removeLogPaths))
|
||||
{
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Applying %s quotas: removing obsolete log files',
|
||||
$this->quotaType
|
||||
)
|
||||
);
|
||||
Factory::getLog()->debug('Removing obsolete log files');
|
||||
}
|
||||
|
||||
while (!empty($removeLogPaths) && $timer->getTimeLeft() > 0)
|
||||
{
|
||||
$logPath = array_shift($removeLogPaths);
|
||||
|
||||
if (@Platform::getInstance()->unlink($logPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Failed to remove old log file %s',
|
||||
$file
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if I have enough time
|
||||
if ($timer->getTimeLeft() <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
* @subpackage
|
||||
*
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain\Finalizer;
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
|
||||
/**
|
||||
* Sends an email to the site administrators on backup completion
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
*/
|
||||
final class MailAdministrators extends AbstractFinalizer
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
$this->setStep('Processing emails to administrators');
|
||||
$this->setSubstep('');
|
||||
|
||||
$platform = Platform::getInstance();
|
||||
|
||||
// Skip email for back-end backups
|
||||
if ($platform->get_backup_origin() == 'backend')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is the feature enabled?
|
||||
if ($platform->get_platform_configuration_option('frontend_email_on_finish', 0) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Factory::getLog()->debug("Preparing to send e-mail to administrators");
|
||||
|
||||
$email = trim($platform->get_platform_configuration_option('frontend_email_address', ''));
|
||||
|
||||
if (!empty($email))
|
||||
{
|
||||
Factory::getLog()->debug("Using pre-defined list of emails");
|
||||
|
||||
$emails = explode(',', $email);
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->debug("Fetching list of site administrator emails");
|
||||
|
||||
$emails = $platform->get_administrator_emails();
|
||||
}
|
||||
|
||||
if (empty($emails))
|
||||
{
|
||||
Factory::getLog()->debug("No email recipients found! Skipping email.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Factory::getLog()->debug("Creating email subject and body");
|
||||
|
||||
// Get the statistics
|
||||
$statistics = Factory::getStatistics();
|
||||
$profileNumber = $platform->get_active_profile();
|
||||
$profileName = $platform->get_profile_name($profileNumber);
|
||||
$statsRecord = $statistics->getRecord();
|
||||
$partsCount = max(1, $statsRecord['multipart']);
|
||||
$allFilenames = $this->getPartFilenames($statsRecord);
|
||||
$filesList = implode(
|
||||
"\n", array_map(function ($file) {
|
||||
return "\t" . $file;
|
||||
}, $allFilenames)
|
||||
);
|
||||
$totalSize = (int) ($statsRecord['total_size'] ?? 0);
|
||||
|
||||
// Get the approximate part sizes and create a list of files and sizes
|
||||
$configuration = Factory::getConfiguration();
|
||||
$partSize = $configuration->get('engine.archiver.common.part_size', 0);
|
||||
$lastPartSize = $totalSize - (($partsCount - 1) * $partSize);
|
||||
$partSizes = array_fill(0, $partsCount - 1, $partSize);
|
||||
$partSizes[] = $lastPartSize;
|
||||
$filesAndSizesList = implode(
|
||||
"\n",
|
||||
array_map(
|
||||
function ($file, $size) {
|
||||
return sprintf(
|
||||
"\t%s (approx. %s)",
|
||||
$file,
|
||||
$this->formatByteSize($size)
|
||||
);
|
||||
},
|
||||
$allFilenames,
|
||||
$partSizes
|
||||
)
|
||||
);
|
||||
|
||||
// Determine the upload to remote storage status
|
||||
$remoteStatus = '';
|
||||
$failedUpdate = false;
|
||||
$postProcEngine = Factory::getConfiguration()->get('akeeba.advanced.postproc_engine');
|
||||
|
||||
if (!empty($postProcEngine) && ($postProcEngine != 'none'))
|
||||
{
|
||||
$remoteStatus = $platform->translate('COM_AKEEBA_EMAIL_POSTPROCESSING_SUCCESS');
|
||||
|
||||
if (empty($statsRecord['remote_filename']))
|
||||
{
|
||||
$failedUpdate = true;
|
||||
$remoteStatus = $platform->translate('COM_AKEEBA_EMAIL_POSTPROCESSING_FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
// Did the user ask to be emailed only on failed uploads but the upload has succeeded?
|
||||
if (
|
||||
!$failedUpdate
|
||||
&& ($platform->get_platform_configuration_option('frontend_email_when', 'always') == 'failedupload')
|
||||
)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fetch user's preferences
|
||||
$subject = trim($platform->get_platform_configuration_option('frontend_email_subject', ''));
|
||||
$body = trim($platform->get_platform_configuration_option('frontend_email_body', ''));
|
||||
|
||||
// Get a default subject or post-process a manually defined subject
|
||||
$subject = empty($subject)
|
||||
? $platform->translate('COM_AKEEBA_COMMON_EMAIL_SUBJECT_OK')
|
||||
: Factory::getFilesystemTools()->replace_archive_name_variables($subject);
|
||||
|
||||
// Do we need a default body?
|
||||
if (empty($body))
|
||||
{
|
||||
$body = $platform->translate('COM_AKEEBA_COMMON_EMAIL_BODY_OK');
|
||||
$body .= "\n\n";
|
||||
$body .= sprintf(
|
||||
$platform->translate('COM_AKEEBA_COMMON_EMAIL_BODY_INFO'),
|
||||
$profileNumber,
|
||||
$partsCount
|
||||
);
|
||||
$body .= "\n\n";
|
||||
$body .= $filesAndSizesList;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Post-process the body
|
||||
$body = Factory::getFilesystemTools()->replace_archive_name_variables($body);
|
||||
$body = str_replace('[PROFILENUMBER]', $profileNumber, $body);
|
||||
$body = str_replace('[PROFILENAME]', $profileName, $body);
|
||||
$body = str_replace('[PARTCOUNT]', $partsCount, $body);
|
||||
$body = str_replace('[FILELIST]', $filesList, $body);
|
||||
$body = str_replace('[FILESIZESLIST]', $filesAndSizesList, $body);
|
||||
$body = str_replace('[REMOTESTATUS]', $remoteStatus, $body);
|
||||
$body = str_replace('[TOTALSIZE]', $this->formatByteSize($totalSize), $body);
|
||||
}
|
||||
|
||||
// Post-process the subject (support the [REMOTESTATUS] variable)
|
||||
$subject = str_replace('[REMOTESTATUS]', $remoteStatus, $subject);
|
||||
|
||||
// Sometimes $body contains literal \n instead of newlines
|
||||
$body = str_replace('\\n', "\n", $body);
|
||||
|
||||
foreach ($emails as $email)
|
||||
{
|
||||
Factory::getLog()->debug("Sending email to $email");
|
||||
try
|
||||
{
|
||||
$platform->send_email($email, $subject, $body);
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
// Don't cry if we cannot send an email; just log it as a warning
|
||||
Factory::getLog()->warning(
|
||||
sprintf(
|
||||
'Cannot send email to ‘%s’. Error message: “%s”',
|
||||
$email,
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of files' base names for the given backup statistics record
|
||||
*
|
||||
* @param array $statsRecord The statistics record
|
||||
*
|
||||
* @return array List of file names
|
||||
*
|
||||
* @since 9.3.1
|
||||
*/
|
||||
private function getPartFilenames(array $statsRecord): array
|
||||
{
|
||||
$baseFile = basename(
|
||||
$statsRecord['absolute_path'] ?? $statsRecord['archivename'] ?? $statsRecord['remote_filename'] ?? ''
|
||||
);
|
||||
|
||||
if (empty($baseFile))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$partsCount = max($statsRecord['multipart'] ?? 1, 1);
|
||||
|
||||
if ($partsCount === 1)
|
||||
{
|
||||
return [$baseFile];
|
||||
}
|
||||
|
||||
$ret = [];
|
||||
$extension = substr($baseFile, strrpos($baseFile, '.'));
|
||||
$bareName = basename($baseFile, $extension);
|
||||
|
||||
for ($i = 1; $i < $partsCount; $i++)
|
||||
{
|
||||
$ret[] = $bareName . substr($extension, 0, 2) . sprintf('%02d', $i);
|
||||
}
|
||||
|
||||
$ret[] = $baseFile;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number of bytes in human-readable format
|
||||
*
|
||||
* @param int|float $size The size in bytes to format, e.g. 8254862
|
||||
*
|
||||
* @return string The human-readable representation of the byte size, e.g. "7.87 Mb"
|
||||
* @since 9.3.1
|
||||
*/
|
||||
private function formatByteSize($size): string
|
||||
{
|
||||
$unit = ['b', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
|
||||
|
||||
return @round($size / 1024 ** ($i = floor(log($size, 1024))), 2) . ' ' . $unit[$i];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
* @subpackage
|
||||
*
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain\Finalizer;
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
|
||||
/**
|
||||
* Keeps a maximum number of "obsolete" records
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
*/
|
||||
final class ObsoleteRecordsQuotas extends AbstractFinalizer
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
$this->setStep('Applying quota limit on obsolete backup records');
|
||||
$this->setSubstep('');
|
||||
$registry = Factory::getConfiguration();
|
||||
$limit = $registry->get('akeeba.quota.obsolete_quota', 0);
|
||||
$limit = (int) $limit;
|
||||
|
||||
if ($limit <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$platform = Platform::getInstance();
|
||||
$statsTable = $platform->tableNameStats;
|
||||
$db = Factory::getDatabase($platform->get_platform_database_options());
|
||||
$query =
|
||||
$db->getQuery(true)
|
||||
->select([
|
||||
$db->qn('id'),
|
||||
$db->qn('tag'),
|
||||
$db->qn('backupid'),
|
||||
$db->qn('absolute_path'),
|
||||
])
|
||||
->from($db->qn($statsTable))
|
||||
->where($db->qn('profile_id') . ' = ' . $db->q($platform->get_active_profile()))
|
||||
->where($db->qn('status') . ' = ' . $db->q('complete'))
|
||||
->where($db->qn('filesexist') . '=' . $db->q('0'))
|
||||
->where(
|
||||
'(' .
|
||||
$db->qn('remote_filename') . '=' . $db->q('') . ' OR ' .
|
||||
$db->qn('remote_filename') . ' IS NULL'
|
||||
. ')'
|
||||
)
|
||||
->order($db->qn('id') . ' DESC');
|
||||
|
||||
$db->setQuery($query, $limit, 100000);
|
||||
$records = $db->loadAssocList();
|
||||
|
||||
if (empty($records))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$array = [];
|
||||
|
||||
// Delete backup-specific log files if they exist and add the IDs of the records to delete in the $array
|
||||
foreach ($records as $stat)
|
||||
{
|
||||
$array[] = $stat['id'];
|
||||
|
||||
// We can't delete logs if there is no backup ID in the record
|
||||
if (!isset($stat['backupid']) || empty($stat['backupid']))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$logFileName = 'akeeba.' . $stat['tag'] . '.' . $stat['backupid'] . '.log.php';
|
||||
$logPath = dirname($stat['absolute_path']) . '/' . $logFileName;
|
||||
|
||||
if (@file_exists($logPath))
|
||||
{
|
||||
@unlink($logPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transitional period: the log file akeeba.tag.log.php may not exist but the akeeba.tag.log does. This
|
||||
* addresses this transition.
|
||||
*/
|
||||
$logPath = dirname($stat['absolute_path']) . '/' . substr($logFileName, 0, -4);
|
||||
|
||||
if (@file_exists($logPath))
|
||||
{
|
||||
@unlink($logPath);
|
||||
}
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
|
||||
foreach ($array as $id)
|
||||
{
|
||||
$ids[] = $db->q($id);
|
||||
}
|
||||
|
||||
$ids = implode(',', $ids);
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->delete($db->qn($statsTable))
|
||||
->where($db->qn('id') . " IN ($ids)");
|
||||
$db->setQuery($query);
|
||||
$db->query();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
* @subpackage
|
||||
*
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain\Finalizer;
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Akeeba\Engine\Postproc\Base;
|
||||
use Akeeba\Engine\Postproc\PostProcInterface;
|
||||
use Exception;
|
||||
use Akeeba\Engine\Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Performs any necessary post-processing (remote file uploading) still pending.
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
*
|
||||
*/
|
||||
final class PostProcessing extends AbstractFinalizer
|
||||
{
|
||||
/** @var array A list of all backup parts to process */
|
||||
private $backupParts = [];
|
||||
|
||||
/** @var int The backup part we are currently processing */
|
||||
private $backupPartsIndex = -1;
|
||||
|
||||
/** @var int How many finalisation substeps I have already done */
|
||||
private $subStepsDone = 0;
|
||||
|
||||
/** @var int How many finalisation substeps I have in total */
|
||||
private $subStepsTotal = 0;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
$this->setStep('Post-processing');
|
||||
$this->setSubstep('');
|
||||
|
||||
// Do not run if the archive engine doesn't produce archives
|
||||
$configuration = Factory::getConfiguration();
|
||||
$engineName = $configuration->get('akeeba.advanced.postproc_engine');
|
||||
|
||||
Factory::getLog()->debug("Loading post-processing engine object ($engineName)");
|
||||
|
||||
$postProcEngine = Factory::getPostprocEngine($engineName);
|
||||
|
||||
if (!is_object($postProcEngine) || !($postProcEngine instanceof Base))
|
||||
{
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Post-processing engine “%s” not found.',
|
||||
$engineName
|
||||
)
|
||||
);
|
||||
Factory::getLog()->debug("The post-processing engine has either been removed or you are trying to use a profile created with the Professional version of the backup software in the Core version which doesn't have this post-processing engine.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Initialize the archive part list if required
|
||||
if (empty($this->backupParts))
|
||||
{
|
||||
$ret = $this->initialiseBackupParts($postProcEngine);
|
||||
|
||||
if ($ret !== null)
|
||||
{
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we don't accidentally break the step when not required to do so
|
||||
$configuration->set('volatile.breakflag', false);
|
||||
|
||||
// Do we have a filename from the previous run of the post-proc engine?
|
||||
$filename = $configuration->get('volatile.postproc.filename', null);
|
||||
|
||||
if (empty($filename))
|
||||
{
|
||||
$filename = $this->backupParts[$this->backupPartsIndex];
|
||||
Factory::getLog()->info('Beginning post processing file ' . $filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->info('Continuing post processing file ' . $filename);
|
||||
}
|
||||
|
||||
$this->setStep('Post-processing');
|
||||
$this->setSubstep(basename($filename));
|
||||
$timer = Factory::getTimer();
|
||||
$startTime = $timer->getRunningTime();
|
||||
$processingException = null;
|
||||
|
||||
try
|
||||
{
|
||||
$finishedProcessing = $postProcEngine->processPart($filename);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
$finishedProcessing = false;
|
||||
$processingException = $e;
|
||||
}
|
||||
|
||||
if (!is_null($processingException))
|
||||
{
|
||||
Factory::getLog()->warning('Failed to process file ' . $filename);
|
||||
Factory::getLog()->warning('Error received from the post-processing engine:');
|
||||
$this->logErrorsFromException($processingException, LogLevel::WARNING);
|
||||
}
|
||||
elseif ($finishedProcessing === true)
|
||||
{
|
||||
// The post-processing of this file ended successfully
|
||||
Factory::getLog()->info('Finished post-processing file ' . $filename);
|
||||
$configuration->set('volatile.postproc.filename', null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// More work required
|
||||
Factory::getLog()->info('More post-processing steps required for file ' . $filename);
|
||||
$configuration->set('volatile.postproc.filename', $filename);
|
||||
|
||||
// Do we need to break the step?
|
||||
$endTime = $timer->getRunningTime();
|
||||
$stepTime = $endTime - $startTime;
|
||||
$timeLeft = $timer->getTimeLeft();
|
||||
|
||||
// By default, we assume that we have enough time to run yet another step
|
||||
$configuration->set('volatile.breakflag', false);
|
||||
|
||||
/**
|
||||
* However, if the last step took longer than the time we already have left on the timer we can predict
|
||||
* that we are running out of time, therefore we need to break the step.
|
||||
*/
|
||||
if ($timeLeft < $stepTime)
|
||||
{
|
||||
$configuration->set('volatile.breakflag', true);
|
||||
}
|
||||
}
|
||||
|
||||
// Should we delete the file afterwards?
|
||||
$canAndShouldDeleteFileAfterwards =
|
||||
$configuration->get('engine.postproc.common.delete_after', false)
|
||||
&& $postProcEngine->isFileDeletionAfterProcessingAdvisable();
|
||||
|
||||
if ($canAndShouldDeleteFileAfterwards && $finishedProcessing)
|
||||
{
|
||||
Factory::getLog()->debug('Deleting already processed file ' . $filename);
|
||||
Platform::getInstance()->unlink($filename);
|
||||
}
|
||||
elseif ($canAndShouldDeleteFileAfterwards && !$finishedProcessing)
|
||||
{
|
||||
Factory::getLog()->debug('Not removing the non-processed file ' . $filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
Factory::getLog()->debug('Not removing processed file ' . $filename);
|
||||
}
|
||||
|
||||
if ($finishedProcessing === true)
|
||||
{
|
||||
// Move the index forward if the part finished processing
|
||||
$this->backupPartsIndex++;
|
||||
|
||||
// Mark substep done
|
||||
$this->subStepsDone++;
|
||||
|
||||
// Break step after processing?
|
||||
if (
|
||||
$postProcEngine->recommendsBreakAfter()
|
||||
&& !Factory::getConfiguration()->get('akeeba.tuning.nobreak.finalization', 0)
|
||||
)
|
||||
{
|
||||
$configuration->set('volatile.breakflag', true);
|
||||
}
|
||||
|
||||
// If we just finished processing the first archive part, save its remote path in the statistics.
|
||||
if (($this->subStepsDone == 1) || ($this->subStepsTotal == 0))
|
||||
{
|
||||
$this->updateStatistics($postProcEngine, $engineName);
|
||||
}
|
||||
|
||||
// Are we past the end of the array (i.e. we're finished)?
|
||||
if ($this->backupPartsIndex >= count($this->backupParts))
|
||||
{
|
||||
Factory::getLog()->info('Post-processing has finished for all files');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_null($processingException))
|
||||
{
|
||||
// If the post-processing failed, make sure we don't process anything else
|
||||
$this->backupPartsIndex = count($this->backupParts);
|
||||
Factory::getLog()->warning('Post-processing interrupted -- no more files will be transferred');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Indicate we're not done yet
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the backup record upon post-processing the first part.
|
||||
*
|
||||
* @param PostProcInterface $postProcEngine The post-processing engine we're using
|
||||
* @param string $engineName The name of the post-processing engine we're using
|
||||
*
|
||||
* @throws Exception
|
||||
* @since 9.3.1
|
||||
*/
|
||||
public function updateStatistics(PostProcInterface $postProcEngine, string $engineName): void
|
||||
{
|
||||
if (empty($postProcEngine->getRemotePath()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$configuration = Factory::getConfiguration();
|
||||
$statistics = Factory::getStatistics();
|
||||
$remote_filename = $engineName . '://';
|
||||
$remote_filename .= $postProcEngine->getRemotePath();
|
||||
$data = [
|
||||
'remote_filename' => $remote_filename,
|
||||
];
|
||||
$remove_after = $configuration->get('engine.postproc.common.delete_after', false);
|
||||
|
||||
if ($remove_after)
|
||||
{
|
||||
$data['filesexist'] = 0;
|
||||
}
|
||||
|
||||
$statistics->setStatistics($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the backup parts information
|
||||
*
|
||||
* @param PostProcInterface $postProcEngine The post-processing engine we're using
|
||||
*
|
||||
* @return bool|null
|
||||
*
|
||||
* @since 9.3.1
|
||||
*/
|
||||
private function initialiseBackupParts(PostProcInterface $postProcEngine): ?bool
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
Factory::getLog()->info('Initializing post-processing engine');
|
||||
|
||||
// Initialize the flag for multistep post-processing of parts
|
||||
$configuration->set('volatile.postproc.filename', null);
|
||||
$configuration->set('volatile.postproc.directory', null);
|
||||
|
||||
// Populate array w/ absolute names of backup parts
|
||||
$statistics = Factory::getStatistics();
|
||||
$stat = $statistics->getRecord();
|
||||
$this->backupParts = Factory::getStatistics()->get_all_filenames($stat, false);
|
||||
|
||||
if (is_null($this->backupParts))
|
||||
{
|
||||
// No archive produced, or they are all already post-processed
|
||||
Factory::getLog()->info('No archive files found to post-process');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Factory::getLog()->debug(count($this->backupParts) . ' files to process found');
|
||||
|
||||
$this->subStepsTotal = count($this->backupParts);
|
||||
$this->subStepsDone = 0;
|
||||
|
||||
$this->backupPartsIndex = 0;
|
||||
|
||||
// If we have an empty array, do not run
|
||||
if (empty($this->backupParts))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Break step before processing?
|
||||
if (
|
||||
$postProcEngine->recommendsBreakBefore()
|
||||
&& !$configuration->get(
|
||||
'akeeba.tuning.nobreak.finalization', 0
|
||||
)
|
||||
)
|
||||
{
|
||||
Factory::getLog()->debug('Breaking step before post-processing run');
|
||||
$configuration->set('volatile.breakflag', true);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
* @subpackage
|
||||
*
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain\Finalizer;
|
||||
|
||||
use Akeeba\Engine\Core\Domain\Finalization;
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
|
||||
class RemoteQuotas extends AbstractQuotaManagement
|
||||
{
|
||||
/** @inheritDoc */
|
||||
public function __construct(Finalization $finalizationPart)
|
||||
{
|
||||
$this->quotaType = 'remote';
|
||||
|
||||
parent::__construct($finalizationPart);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function __invoke()
|
||||
{
|
||||
// Make sure we are enabled
|
||||
$configuration = Factory::getConfiguration();
|
||||
$enableRemote = $configuration->get('akeeba.quota.remote', 0);
|
||||
|
||||
if (!$enableRemote)
|
||||
{
|
||||
Factory::getLog()->debug('Count quotas are not enabled; no remote quotas will be processed.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::__invoke();
|
||||
}
|
||||
|
||||
|
||||
/** @inheritDoc */
|
||||
protected function getAllRecords(): array
|
||||
{
|
||||
$configuration = Factory::getConfiguration();
|
||||
$useLatest = $configuration->get('akeeba.quota.remote_latest', '1') == 1;
|
||||
|
||||
// Get all records with a remote filename and filter out the current record and frozen records
|
||||
$allRecords = array_filter(
|
||||
Platform::getInstance()->get_valid_remote_records() ?: [],
|
||||
function (array $stat) use ($useLatest): bool {
|
||||
// Exclude frozen records from quota management
|
||||
if (isset($stat['frozen']) && $stat['frozen'])
|
||||
{
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Excluding frozen backup id %d from %s quota management',
|
||||
$stat['id'],
|
||||
$this->quotaType
|
||||
)
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exclude the current record from the remote quota management
|
||||
return $useLatest ? true : ($stat['id'] != $this->latestBackupId);
|
||||
}
|
||||
);
|
||||
|
||||
// Convert stat records to entries used in quota management
|
||||
return array_map(
|
||||
function (array $stat): array {
|
||||
$remoteFilenames = $this->getRemoteFiles($stat['remote_filename'], $stat['multipart']);
|
||||
|
||||
try
|
||||
{
|
||||
$backupStart = new DateTime($stat['backupstart']);
|
||||
$backupTS = $backupStart->format('U');
|
||||
$backupDay = $backupStart->format('d');
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
$backupTS = 0;
|
||||
$backupDay = 0;
|
||||
}
|
||||
|
||||
// Get the log file name
|
||||
$tag = $stat['tag'] ?? 'backend';
|
||||
$backupId = $stat['backupid'] ?? '';
|
||||
$logName = '';
|
||||
|
||||
if (!empty($backupId))
|
||||
{
|
||||
$logName = 'akeeba.' . $tag . '.' . $backupId . '.log.php';
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $stat['id'],
|
||||
'filenames' => $remoteFilenames,
|
||||
'size' => $stat['total_size'],
|
||||
'backupstart' => $backupTS,
|
||||
'day' => $backupDay,
|
||||
'logname' => $logName,
|
||||
];
|
||||
},
|
||||
$allRecords
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the actual removal.
|
||||
*
|
||||
* @param array $removeBackupIDs The backup IDs which will have their files removed
|
||||
* @param array $filesToRemove The flat list of files to remove
|
||||
* @param array $removeLogPaths The flat list of log paths to remove
|
||||
*
|
||||
* @return bool True if we are done, false to come back in the next step of the engine
|
||||
* @throws Exception
|
||||
* @since 9.3.1
|
||||
*/
|
||||
protected function processRemovals(array &$removeBackupIDs, array &$filesToRemove, array &$removeLogPaths): bool
|
||||
{
|
||||
$timer = Factory::getTimer();
|
||||
|
||||
// Update the statistics record with the removed remote files
|
||||
if (!empty($removeBackupIDs))
|
||||
{
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Applying %s quotas: updating backup records',
|
||||
$this->quotaType
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
while (!empty($removeBackupIDs) && $timer->getTimeLeft() > 0)
|
||||
{
|
||||
$id = array_shift($removeBackupIDs);
|
||||
$data = ['remote_filename' => ''];
|
||||
|
||||
Platform::getInstance()->set_or_update_statistics($id, $data);
|
||||
}
|
||||
|
||||
// Check if I have enough time
|
||||
if ($timer->getTimeLeft() <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply quotas upon backup records
|
||||
if (!empty($filesToRemove) > 0)
|
||||
{
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Applying %s quotas: removing backup archives',
|
||||
$this->quotaType
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
while (!empty($filesToRemove) && $timer->getTimeLeft() > 0)
|
||||
{
|
||||
$filename = array_shift($filesToRemove);
|
||||
[$engineName, $path] = explode('://', $filename);
|
||||
$engine = Factory::getPostprocEngine($engineName);
|
||||
|
||||
if (!$engine->supportsDelete())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Removing remotely stored file %s',
|
||||
$filename
|
||||
)
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
$engine->delete($path);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
Factory::getLog()->debug(
|
||||
sprintf(
|
||||
'Could not remove remotely stored file. Error: %s',
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if I have enough time
|
||||
if ($timer->getTimeLeft() <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full paths to all remote backup parts
|
||||
*
|
||||
* @param string $filename The full filename of the last part stored in the database
|
||||
* @param int $multipart How many parts does this archive consist of?
|
||||
*
|
||||
* @return array A list of the full paths of all remotely stored backup archive parts
|
||||
* @since 9.3.1
|
||||
*/
|
||||
private function getRemoteFiles(string $filename, int $multipart): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$extension = substr($filename, -3);
|
||||
$base = substr($filename, 0, -4);
|
||||
$extensionPrefix = substr($extension, 0, 1);
|
||||
$result[] = $filename;
|
||||
|
||||
if ($multipart <= 1)
|
||||
{
|
||||
return $result;
|
||||
}
|
||||
|
||||
for ($i = 1; $i < $multipart; $i++)
|
||||
{
|
||||
$newExt = $extensionPrefix . sprintf('%02u', $i);
|
||||
$result[] = $base . '.' . $newExt;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
* @subpackage
|
||||
*
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain\Finalizer;
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Removes temporary files.
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
*
|
||||
*/
|
||||
final class RemoveTemporaryFiles extends AbstractFinalizer
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
$this->setStep('Removing temporary files');
|
||||
$this->setSubstep('');
|
||||
Factory::getLog()->debug("Removing temporary files");
|
||||
Factory::getTempFiles()->deleteTempFiles();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
* @subpackage
|
||||
*
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain\Finalizer;
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
|
||||
/**
|
||||
* Updates the file sizes in the statistics records
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
*
|
||||
*/
|
||||
final class UpdateFileSizes extends AbstractFinalizer
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
$this->setStep('Updating file sizes');
|
||||
$this->setSubstep('');
|
||||
Factory::getLog()->debug("Updating statistics with file sizes");
|
||||
|
||||
// Fetch the stats record
|
||||
$statistics = Factory::getStatistics();
|
||||
$configuration = Factory::getConfiguration();
|
||||
$record = $statistics->getRecord();
|
||||
$filenames = $statistics->get_all_filenames($record) ?: [];
|
||||
$filesize = 0.0;
|
||||
|
||||
// Calculate file sizes of files remaining on the server
|
||||
foreach ($filenames as $file)
|
||||
{
|
||||
$filesize += ((@filesize($file)) ?: 0) * 1.0;
|
||||
}
|
||||
|
||||
// Get the part size in volatile storage, set from the immediate part uploading effected by the
|
||||
// "Process each part immediately" option, and add it to the total file size
|
||||
$config = $configuration;
|
||||
$postProcImmediately = $config->get('engine.postproc.common.after_part', 0, false);
|
||||
$deleteAfter = $config->get('engine.postproc.common.delete_after', 0, false);
|
||||
$postProcEngine = $config->get('akeeba.advanced.postproc_engine', 'none');
|
||||
|
||||
if ($postProcImmediately && $deleteAfter && ($postProcEngine != 'none'))
|
||||
{
|
||||
$filesize += $configuration->get('volatile.engine.archiver.totalsize', 0) ?: 0;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'total_size' => $filesize,
|
||||
];
|
||||
|
||||
Factory::getLog()->debug("Total size of backup archive (in bytes): $filesize");
|
||||
|
||||
$statistics->setStatistics($data);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
* @subpackage
|
||||
*
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain\Finalizer;
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Updates the backup statistics record
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
*/
|
||||
final class UpdateStatistics extends AbstractFinalizer
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
$this->setStep('Updating backup record information');
|
||||
$this->setSubstep('');
|
||||
|
||||
Factory::getLog()->debug('Updating statistics');
|
||||
|
||||
// We finished normally. Fetch the stats record
|
||||
$statistics = Factory::getStatistics();
|
||||
$registry = Factory::getConfiguration();
|
||||
$data = [
|
||||
'backupend' => Platform::getInstance()->get_timestamp_database(),
|
||||
'status' => 'complete',
|
||||
'multipart' => $registry->get('volatile.statistics.multipart', 0),
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$result = $statistics->setStatistics($data);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
$result = false;
|
||||
}
|
||||
|
||||
if ($result === false)
|
||||
{
|
||||
// Most likely a "MySQL has gone away" issue...
|
||||
$configuration = Factory::getConfiguration();
|
||||
$configuration->set('volatile.breakflag', true);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* We could have handled it in $data above. However, if the schema has not been updated this function will
|
||||
* continue failing infinitely, causing the backup to never end.
|
||||
*/
|
||||
$statistics->updateInStep(false);
|
||||
|
||||
$stat = (object) $statistics->getRecord();
|
||||
Platform::getInstance()->remove_duplicate_backup_records($stat->archivename);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
/**
|
||||
* Akeeba Engine
|
||||
*
|
||||
* @package akeebaengine
|
||||
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
* @subpackage
|
||||
*
|
||||
* @copyright A copyright
|
||||
* @license A "Slug" license name e.g. GPL2
|
||||
*/
|
||||
|
||||
namespace Akeeba\Engine\Core\Domain\Finalizer;
|
||||
|
||||
use Akeeba\Engine\Factory;
|
||||
use Akeeba\Engine\Platform;
|
||||
use Exception;
|
||||
use Akeeba\Engine\Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Uploads Kickstart using the post-processing engine
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @package Akeeba\Engine\Core\Domain\Finalizer
|
||||
*/
|
||||
final class UploadKickstart extends AbstractFinalizer
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
$this->setStep('Post-processing Kickstart');
|
||||
$this->setSubstep('');
|
||||
|
||||
$configuration = Factory::getConfiguration();
|
||||
|
||||
// Do not run if we are not told to upload Kickstart
|
||||
$uploadKickstart = $configuration->get('akeeba.advanced.uploadkickstart', 0);
|
||||
|
||||
if (!$uploadKickstart)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$engineName = $configuration->get('akeeba.advanced.postproc_engine');
|
||||
Factory::getLog()->debug("Loading post-processing engine object ($engineName)");
|
||||
$postProcEngine = Factory::getPostprocEngine($engineName);
|
||||
|
||||
// Set $filename to kickstart's source file
|
||||
$filename = Platform::getInstance()->get_installer_images_path() . '/kickstart.txt';
|
||||
|
||||
// Post-process the file
|
||||
$this->setSubstep('kickstart.php');
|
||||
|
||||
if (!@file_exists($filename) || !is_file($filename))
|
||||
{
|
||||
Factory::getLog()->warning(
|
||||
sprintf(
|
||||
'Failed to upload kickstart.php. Missing file %s',
|
||||
$filename
|
||||
)
|
||||
);
|
||||
|
||||
// Indicate we're done.
|
||||
return true;
|
||||
}
|
||||
|
||||
$exception = null;
|
||||
$finishedProcessing = false;
|
||||
|
||||
try
|
||||
{
|
||||
$finishedProcessing = $postProcEngine->processPart($filename, 'kickstart.php');
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
if (!is_null($exception))
|
||||
{
|
||||
Factory::getLog()->warning('Failed to upload kickstart.php');
|
||||
Factory::getLog()->warning('Error received from the post-processing engine:');
|
||||
$this->logErrorsFromException($exception, LogLevel::WARNING);
|
||||
}
|
||||
elseif ($finishedProcessing === true)
|
||||
{
|
||||
// The post-processing of this file ended successfully
|
||||
Factory::getLog()->info('Finished uploading kickstart.php');
|
||||
$configuration->set('volatile.postproc.filename', null);
|
||||
}
|
||||
|
||||
// Indicate we're done
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
final 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]';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
final 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
@@ -0,0 +1,442 @@
|
||||
<?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 FilterBase[] 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;
|
||||
}
|
||||
|
||||
public function filterDatabaseRowContent(string $root, string $tableAbstract, array &$row): void
|
||||
{
|
||||
foreach ($this->filters as $filter)
|
||||
{
|
||||
$filter->filterDatabaseRowContent($root, $tableAbstract, $row);
|
||||
}
|
||||
}
|
||||
|
||||
public function canFilterDatabaseRowContent(): bool
|
||||
{
|
||||
return array_reduce($this->filters, function (bool $carry, FilterBase $filter) {
|
||||
return $carry || $filter->canFilterDatabaseRowContent;
|
||||
}, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,869 @@
|
||||
<?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 akeebaBackupErrorHandler is registered as an error handler
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $registeredErrorHandler = false;
|
||||
|
||||
/**
|
||||
* Set to true when deadOnTimeout is registered as a shutdown function
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $registeredShutdownCallback = false;
|
||||
|
||||
/**
|
||||
* Cached copy of the response array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $array_cache = null;
|
||||
|
||||
/**
|
||||
* A unique backup ID which allows us to run multiple parallel backups using the same backup origin (tag)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $backup_id = '';
|
||||
|
||||
/**
|
||||
* The active domain's class name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $class = '';
|
||||
|
||||
/**
|
||||
* The current domain's name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $domain = '';
|
||||
|
||||
/**
|
||||
* The list of remaining steps
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $domain_chain = [];
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
}
|
||||
|
||||
public function _onSerialize()
|
||||
{
|
||||
parent::_onSerialize();
|
||||
|
||||
$this->array_cache = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obsolete method.
|
||||
*
|
||||
* @deprecated 7.0
|
||||
*/
|
||||
public function resetWarnings()
|
||||
{
|
||||
Factory::getLog()->debug('DEPRECATED: Akeeba Engine consumers must remove calls to resetWarnings()');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('ini_set'))
|
||||
{
|
||||
@ini_set('max_execution_time', 844000);
|
||||
}
|
||||
|
||||
if (function_exists('set_time_limit'))
|
||||
{
|
||||
set_time_limit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply a large memory limit (1Gb) if required
|
||||
if ($registry->get('akeeba.tuning.setmemlimit', 0))
|
||||
{
|
||||
if (function_exists('ini_set'))
|
||||
{
|
||||
ini_set('memory_limit', '17179869184');
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
$loggable = false;
|
||||
$type = '';
|
||||
$logMode = 'debug';
|
||||
|
||||
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:
|
||||
$loggable = true;
|
||||
$type = 'WARNING';
|
||||
$logMode = defined('AKEEBADEBUG') ? 'warning' : 'debug';
|
||||
|
||||
break;
|
||||
|
||||
case E_USER_WARNING:
|
||||
$loggable = defined('AKEEBADEBUG');
|
||||
$type = 'User Warning';
|
||||
break;
|
||||
|
||||
case E_NOTICE:
|
||||
$loggable = defined('AKEEBADEBUG');
|
||||
$type = 'Notice';
|
||||
break;
|
||||
|
||||
case E_USER_NOTICE:
|
||||
$loggable = defined('AKEEBADEBUG');
|
||||
$type = 'User Notice';
|
||||
break;
|
||||
|
||||
case E_DEPRECATED:
|
||||
$loggable = defined('AKEEBADEBUG');
|
||||
$type = 'Deprecated';
|
||||
break;
|
||||
|
||||
case E_USER_DEPRECATED:
|
||||
$loggable = defined('AKEEBADEBUG');
|
||||
$type = 'User Deprecated';
|
||||
break;
|
||||
|
||||
case E_STRICT:
|
||||
$loggable = defined('AKEEBADEBUG');
|
||||
$type = 'Strict Notice';
|
||||
break;
|
||||
|
||||
default:
|
||||
// These are E_DEPRECATED, E_STRICT etc. Let PHP handle them
|
||||
return false;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($loggable)
|
||||
{
|
||||
Factory::getLog()->{$logMode}("PHP $type (not an error; you can ignore) on line $errline in file $errfile:");
|
||||
Factory::getLog()->{$logMode}($errstr);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
205
administrator/components/com_akeebabackup/engine/Core/Timer.php
Normal file
205
administrator/components/com_akeebabackup/engine/Core/Timer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
1586
administrator/components/com_akeebabackup/engine/Driver/Base.php
Normal file
1586
administrator/components/com_akeebabackup/engine/Driver/Base.php
Normal file
File diff suppressed because it is too large
Load Diff
1054
administrator/components/com_akeebabackup/engine/Driver/Mysql.php
Normal file
1054
administrator/components/com_akeebabackup/engine/Driver/Mysql.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,586 @@
|
||||
<?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 Akeeba\Engine\FixMySQLHostname;
|
||||
use mysqli_result;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* MySQL Improved (mysqli) database driver for Akeeba Engine
|
||||
*
|
||||
* Based on Joomla! Platform 11.2
|
||||
*/
|
||||
class Mysqli extends Mysql
|
||||
{
|
||||
use FixMySQLHostname;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
protected $ssl = [];
|
||||
|
||||
/** @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 = '`';
|
||||
|
||||
$options['ssl'] = $options['ssl'] ?? [];
|
||||
$options['ssl'] = is_array($options['ssl']) ? $options['ssl'] : [];
|
||||
|
||||
$options['ssl']['enable'] = ($options['ssl']['enable'] ?? $options['dbencryption'] ?? false) ?: false;
|
||||
$options['ssl']['cipher'] = ($options['ssl']['cipher'] ?? $options['dbsslcipher'] ?? null) ?: null;
|
||||
$options['ssl']['ca'] = ($options['ssl']['ca'] ?? $options['dbsslca'] ?? null) ?: null;
|
||||
$options['ssl']['capath'] = ($options['ssl']['capath'] ?? $options['dbsslcapath'] ?? null) ?: null;
|
||||
$options['ssl']['key'] = ($options['ssl']['key'] ?? $options['dbsslkey'] ?? null) ?: null;
|
||||
$options['ssl']['cert'] = ($options['ssl']['cert'] ?? $options['dbsslcert'] ?? null) ?: null;
|
||||
$options['ssl']['verify_server_cert'] = ($options['ssl']['verify_server_cert'] ?? $options['dbsslverifyservercert'] ?? false) ?: false;
|
||||
|
||||
// Figure out if a port is included in the host name
|
||||
$this->fixHostnamePortSocket($options['host'], $options['port'], $options['socket']);
|
||||
|
||||
// Set the information
|
||||
$this->host = $options['host'] ?? 'localhost';
|
||||
$this->user = $options['user'] ?? '';
|
||||
$this->password = $options['password'] ?? '';
|
||||
$this->port = $options['port'] ?? '';
|
||||
$this->socket = $options['socket'] ?? '';
|
||||
$this->_database = $options['database'] ?? '';
|
||||
$this->selectDatabase = $options['select'] ?? true;
|
||||
$this->ssl = $options['ssl'] ?? [];
|
||||
|
||||
// Finalize initialization. Also opens the connection.
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
{
|
||||
$mariadb = stripos($this->connection->server_info, 'mariadb') !== false;
|
||||
$client_version = mysqli_get_client_info();
|
||||
$server_version = $this->getVersion();
|
||||
|
||||
if (version_compare($server_version, '5.5.3', '<'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($mariadb && version_compare($server_version, '10.0.0', '<'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strpos($client_version, 'mysqlnd') !== false)
|
||||
{
|
||||
$client_version = preg_replace('/^\D+([\d.]+).*/', '$1', $client_version);
|
||||
|
||||
return version_compare($client_version, '5.0.9', '>=');
|
||||
}
|
||||
|
||||
return version_compare($client_version, '5.5.3', '>=');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// Let's prepare a connection
|
||||
$this->connection = mysqli_init();
|
||||
|
||||
$connectionFlags = 0;
|
||||
|
||||
// For SSL/TLS connection encryption.
|
||||
if ($this->ssl !== [] && $this->ssl['enable'] === true)
|
||||
{
|
||||
$connectionFlags = $connectionFlags | MYSQLI_CLIENT_SSL;
|
||||
|
||||
// Verify server certificate is only available in PHP 5.6.16+. See https://www.php.net/ChangeLog-5.php#5.6.16
|
||||
if (isset($this->ssl['verify_server_cert']))
|
||||
{
|
||||
// New constants in PHP 5.6.16+. See https://www.php.net/ChangeLog-5.php#5.6.16
|
||||
if ($this->ssl['verify_server_cert'] === true && defined('MYSQLI_CLIENT_SSL_VERIFY_SERVER_CERT'))
|
||||
{
|
||||
$connectionFlags = $connectionFlags | MYSQLI_CLIENT_SSL_VERIFY_SERVER_CERT;
|
||||
}
|
||||
elseif ($this->ssl['verify_server_cert'] === false && defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT'))
|
||||
{
|
||||
$connectionFlags = $connectionFlags | MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
|
||||
}
|
||||
elseif (defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT'))
|
||||
{
|
||||
$this->connection->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, $this->ssl['verify_server_cert']);
|
||||
}
|
||||
}
|
||||
|
||||
// Add SSL/TLS options only if changed.
|
||||
$this->connection->ssl_set(
|
||||
($this->ssl['key'] ?? null) ?: null,
|
||||
($this->ssl['cert'] ?? null) ?: null,
|
||||
($this->ssl['ca'] ?? null) ?: null,
|
||||
($this->ssl['capath'] ?? null) ?: null,
|
||||
($this->ssl['cipher'] ?? null) ?: null
|
||||
);
|
||||
}
|
||||
|
||||
// Attempt to connect to the server, use error suppression to silence warnings and allow us to throw an Exception separately.
|
||||
try
|
||||
{
|
||||
$connected = @$this->connection->real_connect(
|
||||
$this->host,
|
||||
$this->user,
|
||||
$this->password ?: null,
|
||||
null,
|
||||
$this->port ?: 3306,
|
||||
$this->socket ?: null,
|
||||
$connectionFlags
|
||||
);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$connected = false;
|
||||
}
|
||||
|
||||
// connect to the server
|
||||
if (!$connected)
|
||||
{
|
||||
$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 = 0;
|
||||
$this->errorMsg = '';
|
||||
|
||||
if ($this->connection)
|
||||
{
|
||||
$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);
|
||||
}
|
||||
}
|
||||
373
administrator/components/com_akeebabackup/engine/Driver/None.php
Normal file
373
administrator/components/com_akeebabackup/engine/Driver/None.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,774 @@
|
||||
<?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 Akeeba\Engine\FixMySQLHostname;
|
||||
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
|
||||
{
|
||||
use FixMySQLHostname;
|
||||
|
||||
/**
|
||||
* The default cipher suite for TLS connections.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $defaultCipherSuite = [
|
||||
'AES128-GCM-SHA256',
|
||||
'AES256-GCM-SHA384',
|
||||
'AES128-CBC-SHA256',
|
||||
'AES256-CBC-SHA384',
|
||||
'DES-CBC3-SHA',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
protected $ssl = [];
|
||||
|
||||
/** @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 = '`';
|
||||
|
||||
$options['ssl'] = $options['ssl'] ?? [];
|
||||
$options['ssl'] = is_array($options['ssl']) ? $options['ssl'] : [];
|
||||
|
||||
$options['ssl']['enable'] = ($options['ssl']['enable'] ?? $options['dbencryption'] ?? false) ?: false;
|
||||
$options['ssl']['cipher'] = ($options['ssl']['cipher'] ?? $options['dbsslcipher'] ?? null) ?: null;
|
||||
$options['ssl']['ca'] = ($options['ssl']['ca'] ?? $options['dbsslca'] ?? null) ?: null;
|
||||
$options['ssl']['capath'] = ($options['ssl']['capath'] ?? $options['dbsslcapath'] ?? null) ?: null;
|
||||
$options['ssl']['key'] = ($options['ssl']['key'] ?? $options['dbsslkey'] ?? null) ?: null;
|
||||
$options['ssl']['cert'] = ($options['ssl']['cert'] ?? $options['dbsslcert'] ?? null) ?: null;
|
||||
$options['ssl']['verify_server_cert'] = ($options['ssl']['verify_server_cert'] ?? $options['dbsslverifyservercert'] ?? false) ?: false;
|
||||
|
||||
// Figure out if a port is included in the host name
|
||||
$this->fixHostnamePortSocket($options['host'], $options['port'], $options['socket']);
|
||||
|
||||
// Open the connection
|
||||
$this->host = $options['host'] ?? 'localhost';
|
||||
$this->user = $options['user'] ?? '';
|
||||
$this->password = $options['password'] ?? '';
|
||||
$this->port = $options['port'] ?? '';
|
||||
$this->socket = $options['socket'] ?? '';
|
||||
$this->_database = $options['database'] ?? '';
|
||||
$this->selectDatabase = $options['select'] ?? true;
|
||||
$this->ssl = $options['ssl'] ?? [];
|
||||
|
||||
$this->charset = $options['charset'] ?? 'utf8mb4';
|
||||
$this->driverOptions = $options['driverOptions'] ?? [];
|
||||
$this->tablePrefix = $options['prefix'] ?? '';
|
||||
$this->connection = $options['connection'] ?? null;
|
||||
$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()
|
||||
{
|
||||
$version = $this->connection->getAttribute(\PDO::ATTR_SERVER_VERSION);
|
||||
|
||||
if (stripos($version, 'mariadb') !== false)
|
||||
{
|
||||
// MariaDB: Strip off any leading '5.5.5-', if present
|
||||
return preg_replace('/^5\.5\.5-/', '', $version);
|
||||
}
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the database engine supports UTF-8 character encoding.
|
||||
*
|
||||
* @return boolean True if supported.
|
||||
*/
|
||||
public function hasUTF()
|
||||
{
|
||||
$serverVersion = $this->getVersion();
|
||||
$mariadb = stripos($serverVersion, 'mariadb') !== false;
|
||||
|
||||
// At this point we know the client supports utf8mb4. Now we must check if the server supports utf8mb4 as well.
|
||||
$utf8mb4 = version_compare($serverVersion, '5.5.3', '>=');
|
||||
|
||||
if ($mariadb && version_compare($serverVersion, '10.0.0', '<'))
|
||||
{
|
||||
$utf8mb4 = false;
|
||||
}
|
||||
|
||||
return $utf8mb4;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// For SSL/TLS connection encryption.
|
||||
if ($this->ssl !== [] && $this->ssl['enable'] === true)
|
||||
{
|
||||
$sslContextIsNull = true;
|
||||
|
||||
// If customised, add cipher suite, ca file path, ca path, private key file path and certificate file path to PDO driver options.
|
||||
foreach (['cipher', 'ca', 'capath', 'key', 'cert'] as $key => $value)
|
||||
{
|
||||
if ($this->ssl[$value] !== null)
|
||||
{
|
||||
$this->driverOptions[constant('\PDO::MYSQL_ATTR_SSL_' . strtoupper($value))] = $this->ssl[$value];
|
||||
|
||||
$sslContextIsNull = false;
|
||||
}
|
||||
}
|
||||
|
||||
// PDO, if no cipher, ca, capath, cert and key are set, can't start TLS one-way connection, set a common ciphers suite to force it.
|
||||
if ($sslContextIsNull === true)
|
||||
{
|
||||
$this->driverOptions[\PDO::MYSQL_ATTR_SSL_CIPHER] = implode(':', static::$defaultCipherSuite);
|
||||
}
|
||||
|
||||
// If customised, for capable systems (PHP 7.0.14+ and 7.1.4+) verify certificate chain and Common Name to driver options.
|
||||
if ($this->ssl['verify_server_cert'] !== null && defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT'))
|
||||
{
|
||||
$this->driverOptions[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->ssl['verify_server_cert'];
|
||||
}
|
||||
}
|
||||
|
||||
// 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
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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) . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?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 Exception;
|
||||
|
||||
class QueryException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
1018
administrator/components/com_akeebabackup/engine/Dump/Base.php
Normal file
1018
administrator/components/com_akeebabackup/engine/Dump/Base.php
Normal file
File diff suppressed because it is too large
Load Diff
142
administrator/components/com_akeebabackup/engine/Dump/Native.php
Normal file
142
administrator/components/com_akeebabackup/engine/Dump/Native.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?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 = $this->_parametersArray['driver'] ?? 'mysql';
|
||||
$prefix = $this->_parametersArray['prefix'] ?? '';
|
||||
|
||||
if (($driver == 'mysql') && !function_exists('mysql_connect'))
|
||||
{
|
||||
$driver = 'mysqli';
|
||||
}
|
||||
|
||||
$options = [
|
||||
'driver' => $driver,
|
||||
'host' => $this->_parametersArray['host'] ?? '',
|
||||
'port' => $this->_parametersArray['port'] ?? '',
|
||||
'user' => $this->_parametersArray['user'] ?? ($this->_parametersArray['username'] ?? ''),
|
||||
'password' => $this->_parametersArray['password'] ?? '',
|
||||
'database' => $this->_parametersArray['database'] ?? '',
|
||||
'prefix' => is_null($prefix) ? '' : $prefix,
|
||||
];
|
||||
|
||||
$options['ssl'] = $this->_parametersArray['ssl'] ?? [];
|
||||
$options['ssl'] = is_array($options['ssl']) ? $options['ssl'] : [];
|
||||
|
||||
$options['ssl']['enable'] = (bool) ($options['ssl']['enable'] ?? $this->_parametersArray['dbencryption'] ?? false);
|
||||
$options['ssl']['cipher'] = ($options['ssl']['cipher'] ?? $this->_parametersArray['dbsslcipher'] ?? null) ?: null;
|
||||
$options['ssl']['ca'] = ($options['ssl']['ca'] ?? $this->_parametersArray['dbsslca'] ?? null) ?: null;
|
||||
$options['ssl']['capath'] = ($options['ssl']['capath'] ?? $this->_parametersArray['dbsslcapath'] ?? null) ?: null;
|
||||
$options['ssl']['key'] = ($options['ssl']['key'] ?? $this->_parametersArray['dbsslkey'] ?? null) ?: null;
|
||||
$options['ssl']['cert'] = ($options['ssl']['cert'] ?? $this->_parametersArray['dbsslcert'] ?? null) ?: null;
|
||||
$options['ssl']['verify_server_cert'] = (bool) (($options['ssl']['verify_server_cert'] ?? $this->_parametersArray['dbsslverifyservercert'] ?? false) ?: false);
|
||||
|
||||
}
|
||||
|
||||
$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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
931
administrator/components/com_akeebabackup/engine/Factory.php
Normal file
931
administrator/components/com_akeebabackup/engine/Factory.php
Normal file
@@ -0,0 +1,931 @@
|
||||
<?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\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 = [];
|
||||
|
||||
/**
|
||||
* The class to use for push messages
|
||||
*
|
||||
* @since 9.3.1
|
||||
* @var string
|
||||
*/
|
||||
private static $pushClassName = 'Util\\PushMessages';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
'pushClassname' => static::$pushClassName,
|
||||
];
|
||||
|
||||
// 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::$pushClassName = $engineInfo['pushClassname'] ?? 'Utils\\PushMessages';
|
||||
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 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(self::$pushClassName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the push notifications helper class to use with this factory
|
||||
*
|
||||
* @param string $className The classname to use
|
||||
*
|
||||
* @since 9.3.1
|
||||
*/
|
||||
public static function setPushClass(string $className)
|
||||
{
|
||||
self::$pushClassName = $className;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
elseif (class_exists($class_name))
|
||||
{
|
||||
static::$objectList[$class_name] = new $class_name;
|
||||
}
|
||||
|
||||
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");
|
||||
697
administrator/components/com_akeebabackup/engine/Filter/Base.php
Normal file
697
administrator/components/com_akeebabackup/engine/Filter/Base.php
Normal file
@@ -0,0 +1,697 @@
|
||||
<?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 overridden 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 [];
|
||||
}
|
||||
|
||||
public $canFilterDatabaseRowContent = false;
|
||||
|
||||
public function filterDatabaseRowContent(string $root, string $tableAbstract, array &$row): void
|
||||
{
|
||||
// No operation
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
/**
|
||||
* Extra Directories inclusion filter
|
||||
*/
|
||||
class Extradirs extends Base
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->object = 'dir';
|
||||
$this->subtype = 'inclusion';
|
||||
$this->method = 'direct';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
/**
|
||||
* Multiple Database inclusion filter
|
||||
*/
|
||||
class Multidb extends Base
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->object = 'db';
|
||||
$this->subtype = 'inclusion';
|
||||
$this->method = 'direct';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@@ -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 based on regular expressions
|
||||
*/
|
||||
class Regexdirectories extends Base
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->object = 'dir';
|
||||
$this->subtype = 'all';
|
||||
$this->method = 'regex';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@@ -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 based on regular expressions
|
||||
*/
|
||||
class Regexfiles extends Base
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->object = 'file';
|
||||
$this->subtype = 'all';
|
||||
$this->method = 'regex';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@@ -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 based on regular expressions
|
||||
*/
|
||||
class Regexskipdirs extends Base
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->object = 'dir';
|
||||
$this->subtype = 'children';
|
||||
$this->method = 'regex';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@@ -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 based on regular expressions
|
||||
*/
|
||||
class Regexskipfiles extends Base
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->object = 'dir';
|
||||
$this->subtype = 'content';
|
||||
$this->method = 'regex';
|
||||
|
||||
if (empty($this->filter_name))
|
||||
{
|
||||
$this->filter_name = strtolower(basename(__FILE__, '.php'));
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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",
|
||||
"showon": "core.filters.dateconditional.enabled:1"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<?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();
|
||||
|
||||
trait FixMySQLHostname
|
||||
{
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @since 9.2.3
|
||||
*/
|
||||
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;
|
||||
|
||||
// If the hostname looks like a *NIX filename we need to treat it as a socket.
|
||||
if (preg_match('#^/([^/]*/)?[^/]#', $host))
|
||||
{
|
||||
$socket = $host;
|
||||
$host = null;
|
||||
}
|
||||
|
||||
// Special case: Windows named pipe (\\.\something\or\another), with or without parentheses.
|
||||
$isNamedPipe = false;
|
||||
|
||||
if (preg_match("#^\(?\\\\\\\\\.\\\\#", $host))
|
||||
{
|
||||
$isNamedPipe = true;
|
||||
$socket = $host;
|
||||
$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;
|
||||
|
||||
if ($host === 'localhost')
|
||||
{
|
||||
$port = null;
|
||||
}
|
||||
// UNIX socket URI, e.g. 'unix:/path/to/unix/socket.sock'
|
||||
elseif (preg_match('/^unix:(?P<socket>[^:]+)$/', $host, $matches))
|
||||
{
|
||||
$host = null;
|
||||
$socket = $matches['socket'];
|
||||
$port = null;
|
||||
}
|
||||
// It's an IPv4 address with or without port
|
||||
elseif (preg_match('/^(?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>.+))?$/', $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 = '127.0.0.1';
|
||||
$port = $matches['port'];
|
||||
}
|
||||
// ... else we assume normal (naked) IPv6 address, so host and port stay as they are or default
|
||||
|
||||
// If there is both a valid port and a valid socket we will choose the socket instead
|
||||
if (is_numeric($port) && !empty($socket))
|
||||
{
|
||||
$port = null;
|
||||
}
|
||||
|
||||
// Get the port number or socket name
|
||||
if (is_numeric($port))
|
||||
{
|
||||
$port = (int) $port;
|
||||
$socket = '';
|
||||
}
|
||||
elseif (is_string($port) && empty($socket))
|
||||
{
|
||||
$socket = $port;
|
||||
$port = null;
|
||||
}
|
||||
|
||||
// If there is a socket the hostname must be null
|
||||
if (!empty($socket))
|
||||
{
|
||||
$host = null;
|
||||
}
|
||||
|
||||
// If there is a socket the port must be null
|
||||
if (!empty($socket))
|
||||
{
|
||||
$port = null;
|
||||
}
|
||||
|
||||
// If there is a numeric port and the hostname is 'localhost' convert to 127.0.0.1
|
||||
if (is_numeric($port) && ($host === 'localhost'))
|
||||
{
|
||||
$host = '127.0.0.1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Special case: MySQL sockets on Windows need to be enclosed with parentheses and have \\.\ in front.
|
||||
*
|
||||
* @see https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-shell-connection-socket.html
|
||||
* @see https://www.php.net/manual/en/mysqli.quickstart.connections.php
|
||||
*/
|
||||
if (!empty($socket) && $isNamedPipe)
|
||||
{
|
||||
$host = '.';
|
||||
|
||||
/**
|
||||
* Remove any existing parentheses, otherwise URL-decode the socket (in case it was given in the correct
|
||||
* percent encoded format).
|
||||
*/
|
||||
if (substr($socket, 0, 1) === '(' && substr($socket, -1) === ')')
|
||||
{
|
||||
$socket = substr($socket, 1, -1);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
$socket = rawurldecode($socket);
|
||||
}
|
||||
|
||||
// If the socket doesn't already start with \\.\ add it
|
||||
if (substr($socket, 0, 4) !== '\\\\.\\')
|
||||
{
|
||||
$socket = '\\\\.\\' . $socket;
|
||||
}
|
||||
|
||||
$socket = '(' . $socket . ')';
|
||||
}
|
||||
|
||||
// Finally, if it's a persistent connection we have to prefix the hostname with 'p:'
|
||||
$host = ($isPersistent && $host !== null) ? "p:$host" : $host;
|
||||
}
|
||||
|
||||
}
|
||||
350
administrator/components/com_akeebabackup/engine/Platform.php
Normal file
350
administrator/components/com_akeebabackup/engine/Platform.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
1126
administrator/components/com_akeebabackup/engine/Platform/Base.php
Normal file
1126
administrator/components/com_akeebabackup/engine/Platform/Base.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 = '');
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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.';
|
||||
}
|
||||
@@ -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.';
|
||||
}
|
||||
@@ -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.';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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.';
|
||||
}
|
||||
@@ -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.';
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user