Current File : /home/pacjaorg/www/nsa/administrator/components/com_akeebabackup/engine/Factory.php |
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Engine;
defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Core\Database;
use Akeeba\Engine\Core\Filters;
use Akeeba\Engine\Core\Kettenrad;
use Akeeba\Engine\Core\Timer;
use Akeeba\Engine\Driver\Base;
use Akeeba\Engine\Postproc\PostProcInterface;
use Akeeba\Engine\Util\ConfigurationCheck;
use Akeeba\Engine\Util\CRC32;
use Akeeba\Engine\Util\Encrypt;
use Akeeba\Engine\Util\EngineParameters;
use Akeeba\Engine\Util\FactoryStorage;
use Akeeba\Engine\Util\FileLister;
use Akeeba\Engine\Util\FileSystem;
use Akeeba\Engine\Util\Logger;
use Akeeba\Engine\Util\PushMessages;
use Akeeba\Engine\Util\RandomValue;
use Akeeba\Engine\Util\SecureSettings;
use Akeeba\Engine\Util\Statistics;
use Akeeba\Engine\Util\TemporaryFiles;
use Exception;
use RuntimeException;
// Try to kill errors display
if (function_exists('ini_set') && !defined('AKEEBADEBUG'))
{
ini_set('display_errors', false);
}
// Make sure the class autoloader is loaded
require_once __DIR__ . '/Autoloader.php';
/**
* The Akeeba Engine Factory class
*
* This class is responsible for instantiating all Akeeba Engine classes
*/
abstract class Factory
{
/**
* The absolute path to Akeeba Engine's installation
*
* @var string
*/
private static $root;
/**
* Partial class names of the loaded engines e.g. 'archiver' => 'Archiver\\Jpa'. Survives serialization.
*
* @var array
*/
private static $engineClassnames = [];
/**
* A list of instantiated objects which will persist after serialisation / unserialisation
*
* @var array
*/
private static $objectList = [];
/**
* A list of instantiated objects which will NOT persist after serialisation / unserialisation
*
* @var array
*/
private static $temporaryObjectList = [];
/**
* 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");