Файловый менеджер - Редактировать - /home/wuectly/www/03cbe/Core.tar
Назад
04.quota.json 0000604 00000004436 15245604442 0007023 0 ustar 00 { "_group": { "description": "COM_AKEEBA_CONFIG_HEADER_QUOTA" }, "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" }, "akeeba.quota.remotely.maxage.enable": { "default": "0", "type": "none", "protected": "1" }, "akeeba.quota.remotely.enable_count_quota": { "default": "0", "type": "none", "protected": "1" }, "akeeba.quota.remotely.enable_size_quota": { "default": "0", "type": "none", "protected": "1" } } 05.tuning.json 0000604 00000006434 15245604442 0007177 0 ustar 00 { "_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": "bool", "title": "COM_AKEEBA_CONFIG_ADVANCED_SETTIMELIMIT_LABEL", "description": "COM_AKEEBA_CONFIG_ADVANCED_SETTIMELIMIT_DESC" }, "akeeba.tuning.setmemlimit": { "default": "1", "type": "bool", "title": "COM_AKEEBA_CONFIG_ADVANCED_SETMEMLIMIT_LABEL", "description": "COM_AKEEBA_CONFIG_ADVANCED_SETMEMLIMIT_DESC" } } Database.php 0000604 00000007112 15245604442 0006764 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ namespace Akeeba\Engine\Core; defined('AKEEBAENGINE') || die(); use Akeeba\Engine\Driver\Base as DriverBase; use Akeeba\Engine\Driver\Mysqli; use Akeeba\Engine\Platform; use Akeeba\Engine\Util\HashTrait; /** * A utility class to return a database connection object */ class Database { use HashTrait; 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 = self::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 = self::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 = self::md5(serialize($options)); if (!isset(self::$instances[$signature])) { return; } unset(self::$instances[$signature]); } } scripting.json 0000604 00000007351 15245604442 0007451 0 ustar 00 { "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" } Timer.php 0000604 00000014251 15245604442 0006342 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ 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); } } Domain/Db.php 0000604 00000025254 15245604442 0007023 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ 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', []); // Get the (possibly updated) database table name prefix $this->database_config['prefix'] = $this->dump_engine->getPrefix(); // 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(); } } /** * 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'] = ''; } } } } Domain/Pack.php 0000604 00000106550 15245604442 0007353 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ namespace Akeeba\Engine\Core\Domain; defined('AKEEBAENGINE') || die(); use Akeeba\Engine\Archiver\Base as BaseArchiverClass; use Akeeba\Engine\Base\Exceptions\ErrorException; use Akeeba\Engine\Base\Exceptions\WarningException; use Akeeba\Engine\Base\Part; use Akeeba\Engine\Configuration; use Akeeba\Engine\Factory; use Akeeba\Engine\Platform; use Exception; use Akeeba\Engine\Psr\Log\LogLevel; use RuntimeException; /* Windows system detection */ if (!defined('_AKEEBA_IS_WINDOWS')) { $isWindows = DIRECTORY_SEPARATOR == '\\'; if (function_exists('php_uname')) { $isWindows = stristr(php_uname(), 'windows'); } define('_AKEEBA_IS_WINDOWS', $isWindows); } /** * Packing engine. Takes care of putting gathered files (the file list) into * an archive. */ final class Pack extends Part { /** @var array Directories left to be scanned */ private $directory_list; /** @var array Files left to be put into the archive */ private $file_list; /** * Have we finished scanning all subdirectories of the current directory? * * @var boolean */ private $done_subdir_scanning = false; /** * Have we finished scanning all files of the current directory? * * @var boolean */ private $done_file_scanning = true; /** * Is the current directory completely excluded? * * @var boolean */ private $excluded_folder = false; /** * Are the current directory's subdirectories excluded? * * @var boolean */ private $excluded_subdirectories = false; /** * Are the current directory's files excluded? * * @var boolean */ private $excluded_files = false; /** @var string Path to add to scanned files */ private $path_prefix; /** @var string Path to remove from scanned files */ private $remove_path_prefix; /** @var array An array of root directories to scan */ private $root_definitions = []; /** @var integer How many files have been processed in the current step */ private $processed_files_counter; /** @var string Current directory being scanned */ private $current_directory; /** @var integer|null The position in the file list scanning */ private $getFiles_position = null; /** @var integer|null The position in the folder list scanning */ private $getFolders_position = null; /** @var string Current root directory being processed */ private $root = '[SITEROOT]'; /** @var integer Total root directories to scan, used in percentage calculation */ private $total_roots = 0; /** @var integer Total files to process */ private $total_files = 0; /** @var integer Total files already processed */ private $done_files = 0; /** @var integer Total folders to process */ private $total_folders = 0; /** @var integer Total folders already processed */ private $done_folders = 0; /** * Public constructor of the class * * @return void */ public function __construct() { parent::__construct(); Factory::getLog()->debug(__CLASS__ . " :: new instance"); } /** * Immediate post-processing of a part file that's just been completed. * * This method returns false when no post-processing was possible, or if it failed. * * The method returns true when it's post-processed a part file, even if such post-processing has only been partial. * * @param BaseArchiverClass $archiver The archiver engine * @param Configuration $configuration Reference to the Factory configuration object * * @return bool True to indicate our caller should return immediately. */ public static function postProcessDonePartFile(BaseArchiverClass $archiver, Configuration $configuration) { // Get configuration parameters $postprocEngine = Factory::getPostprocEngine(); $allowImmediatePostProcessing = $configuration->get('engine.postproc.common.after_part', 0); $filename = $configuration->get('volatile.postproc.filename', null); $shouldDeleteProcessedPart = $configuration->get('engine.postproc.common.delete_after', false); $shouldAbortOnFailed = $configuration->get('engine.postproc.common.abort_on_fail', false); $engineCanDeleteFiles = $postprocEngine->isFileDeletionAfterProcessingAdvisable(); // Is the immediate post-processing disabled? Return false. if (!$allowImmediatePostProcessing) { return false; } // Are we continuing the post-processing of a previous file? if (!empty($filename)) { Factory::getLog()->info(sprintf("Continuing immediate post-processing of part file %s", basename($filename))); } // Do we have a NEW file to post-process? if (empty($filename) && !empty($archiver->finishedPart)) { $filename = array_shift($archiver->finishedPart); if (!empty($filename)) { Factory::getLog()->info(sprintf("Starting immediate post-processing of part file %s", basename($filename))); } } // Not continuing post-processing and no new file to process? Return false and let the caller carry on. if (empty($filename)) { return false; } // Store the file to post-process in volatile storage $configuration->set('volatile.postproc.filename', $filename); // Try to post-process the file $timer = Factory::getTimer(); $startTime = $timer->getRunningTime(); try { $postProcessingResult = $postprocEngine->processPart($filename); $endTime = $timer->getRunningTime(); $stepTime = $endTime - $startTime; $notEnoughTimeLeft = $timer->getTimeLeft() < $stepTime; /** * FINISHED POST-PROCESSING THE FILE */ if ($postProcessingResult === true) { Factory::getLog()->info('Successfully processed file ' . basename($filename)); // Indicate the file has finished post-processing $configuration->set('volatile.postproc.filename', null); // Add this part's size to the volatile storage variable holding the total size of the backup set $volatileTotalSize = $configuration->get('volatile.engine.archiver.totalsize', 0); $volatileTotalSize += (int) @filesize($filename); $configuration->set('volatile.engine.archiver.totalsize', $volatileTotalSize); // If the engine recommends breaking the step after post-processing let's set the break flag if ($postprocEngine->recommendsBreakAfter()) { $configuration->set('volatile.breakflag', true); } // If I don't need to delete the post-processed part just return if (!$shouldDeleteProcessedPart) { return true; } if (!$engineCanDeleteFiles) { return true; } Factory::getLog()->debug(sprintf("Deleting post-processed file %s", basename($filename))); Platform::getInstance()->unlink($filename); return true; } /** * MORE WORK REQUIRED */ Factory::getLog()->info(sprintf("More post-processing steps required for file %s", basename($filename))); /** * If the time left is not at least as much as the previous post-processing step took us we break the step. * This prevents a time-out trying to post-process another chunk of the file. */ if ($notEnoughTimeLeft) { $configuration->set('volatile.breakflag', true); } } /** * POST-PROCESSING FAILED */ catch (Exception $e) { // Indicate no further processing is possible on this file. $configuration->set('volatile.postproc.filename', null); Factory::getLog()->warning('Failed to process file ' . basename($filename)); Factory::getLog()->warning('Error received from the post-processing engine:'); self::logErrorsFromException($e, $shouldAbortOnFailed ? LogLevel::ERROR : LogLevel::WARNING); // Fail the backup if we're configured to do so if ($shouldAbortOnFailed) { throw new ErrorException(sprintf('Failed to process backup archive file %s', basename($filename))); } return false; } // No further processing necessary return true; } /** * Implements the getProgress() percentage calculation based on how many * roots we have fully backed up and how much of the current root we * have backed up. */ public function getProgress() { if (empty($this->total_roots)) { return 0; } // Get the overall percentage (based on databases fully dumped so far) $remaining_steps = count($this->root_definitions); $remaining_steps++; $overall = 1 - ($remaining_steps / $this->total_roots); // How much is this step worth? $this_max = 1 / $this->total_roots; // Get the percentage done of the current root. Hey, the calculation *is* dodgy, I know it! $local = 0; if ($this->total_files > 0) { $local += 0.05 * $this->done_files / $this->total_files; } if ($this->total_folders > 0) { $local += 0.95 * $this->done_folders / $this->total_folders; } $percentage = $overall + $local * $this_max; if ($percentage < 0) { $percentage = 0; } if ($percentage > 1) { $percentage = 1; } return $percentage; } /** * Implements the _prepare() abstract method * * @return void */ protected function _prepare() { Factory::getLog()->debug(__CLASS__ . " :: Starting _prepare()"); $registry = Factory::getConfiguration(); // Get a list of directories to include Factory::getLog()->debug(__CLASS__ . " :: Getting directory inclusion filters"); $filters = Factory::getFilters(); $this->root_definitions = $filters->getInclusions('dir'); $this->total_roots = count($this->root_definitions); // Add the mapping text file if there are external directories defined! if (count($this->root_definitions) > 1) { // The site's root is the last directory to be backed up. Um, no, // this is not what we need $temp = array_pop($this->root_definitions); array_unshift($this->root_definitions, $temp); // We add a README.txt file in our virtual directory... Factory::getLog()->debug("Creating README.txt in the EFF virtual folder"); $virtualContents = <<<ENDVCONTENT This directory contains directories above the web site's root you chose to include in the backup set. This file helps you figure out which directory in the backup set corresponds to which directory in the original site's structure. You'll have to restore these files manually! ENDVCONTENT; $counter = 0; $vdir = trim($registry->get('akeeba.advanced.virtual_folder'), '/') . '/'; $effini = ['eff' => []]; $rootsToPack = array_filter( $this->root_definitions, function ($data) { return $data[0] != '[SITEROOT]'; } ); foreach ($rootsToPack as $dir) { $counter++; $test = trim($dir[1]); if ($test == '/') { $counter--; continue; } $virtualContents .= $dir[1] . "\tis the backup of\t" . $dir[0] . "\n"; $effini['eff'][$dir[0]] = $vdir . $dir[1]; } $effini = json_encode($effini, JSON_PRETTY_PRINT); // Add the file to our archive $archiver = Factory::getArchiverEngine(); if ($counter >= 1) { $archiver->addFileVirtual('README.txt', $registry->get('akeeba.advanced.virtual_folder'), $virtualContents); $archiver->addFileVirtual('eff.json', $this->installerSettings->installerroot, $effini); } else { Factory::getLog()->debug("README.txt was not created; all EFF directories are being backed up to the archive's root"); } } // Find the site's root element and shift it into the directory list $dir_definition = array_shift($this->root_definitions); $count = 0; $max_dir_count = count($this->root_definitions); while (!is_null($dir_definition[1]) && ($count < $max_dir_count)) { $count++; array_push($this->root_definitions, $dir_definition); $dir_definition = array_shift($this->root_definitions); } // Settling with whatever we have, let's put it to use, shall we? $this->remove_path_prefix = $dir_definition[0]; // Remove absolute path to directory when storing the file if (is_null($dir_definition[1])) { $this->path_prefix = ''; // No added path for main site if (empty($dir_definition[0])) { $this->root = '[SITEROOT]'; } else { $this->root = $dir_definition[0]; } } else { $dir_definition[1] = trim($dir_definition[1]); if (empty($dir_definition[1]) || $dir_definition[1] == '/') { $this->path_prefix = ''; } else { $this->path_prefix = $registry->get('akeeba.advanced.virtual_folder') . '/' . $dir_definition[1]; } $this->root = $dir_definition[0]; } // Translate the root into an absolute path $stock_dirs = Platform::getInstance()->get_stock_directories(); $absolute_dir = substr($this->root, 0); if (!empty($stock_dirs)) { foreach ($stock_dirs as $key => $replacement) { $absolute_dir = str_replace($key, $replacement, $absolute_dir); } } $this->directory_list[] = $absolute_dir; $this->remove_path_prefix = $absolute_dir; $registry = Factory::getConfiguration(); $registry->set('volatile.filesystem.current_root', $absolute_dir); $this->done_subdir_scanning = true; $this->done_file_scanning = true; $this->total_files = 0; $this->done_files = 0; $this->total_folders = 0; $this->done_folders = 0; $this->setState(self::STATE_PREPARED); Factory::getLog()->debug(__CLASS__ . " :: prepared"); } // ============================================================================================ // PRIVATE METHODS // ============================================================================================ /** * @return void * @throws Exception */ protected function _run() { if ($this->getState() == self::STATE_POSTRUN) { Factory::getLog()->debug(__CLASS__ . " :: Already finished"); $this->setStep("-"); $this->setSubstep(""); return; } // If I'm done scanning files and subdirectories and there are no more files to pack get the next // directory. This block is triggered in the first step in a new root. if (empty($this->file_list) && $this->done_subdir_scanning && $this->done_file_scanning) { $this->progressMarkFolderDone(); if (!$this->getNextDirectory()) { if ($this->getNextRoot()) { if (!$this->getNextDirectory()) { return; } } else { return; } } } /** * Automated tests override * * If the file .akeeba_engine_automated_tests_error file is present in the site's root I will throw an error. */ [$root, $translated_root, $dir] = $this->getCleanDirectoryComponents(); if (@file_exists($translated_root . '/.akeeba_engine_automated_tests_error')) { throw new RuntimeException("Akeeba Engine automated tests: I am throwing an error because the file .akeeba_engine_automated_tests_error is present in the site's root folder."); } // If I'm not done scanning for files and the file list is empty then scan for more files if (!$this->done_file_scanning && empty($this->file_list)) { $this->scanFiles(); } // If I have files left, pack them elseif (!empty($this->file_list)) { $this->pack_files(); } // If I'm not done scanning subdirectories, go ahead and scan some more of them elseif (!$this->done_subdir_scanning) { $this->scanSubdirs(); } /** * If we have excluded contained files or subdirectories BUT NOT the entire folder itself AND there are * no files in this directory THEN add an empty directory to the archive. **/ elseif ( ($this->excluded_files || $this->excluded_subdirectories) && !$this->excluded_folder && empty($this->file_list) ) { Factory::getLog()->info("Empty directory " . $this->current_directory . ' (files and directories are filtered)'); $archiver = Factory::getArchiverEngine(); if ($this->current_directory != $this->remove_path_prefix) { $archiver->addFile($this->current_directory, $this->remove_path_prefix, $this->path_prefix); } } } /** * Implements the _finalize() abstract method * */ protected function _finalize() { Factory::getLog()->info("Finalizing archive"); $archive = Factory::getArchiverEngine(); $archive->finalize(); Factory::getLog()->debug("Archive is finalized"); $this->setState(self::STATE_FINISHED); } /** * Gets the next directory to scan from the stack. It also applies folder * filters (directory exclusion, subdirectory exclusion, file exclusion), * updating the operation toggle properties of the class. * * @return boolean True if we found a directory, false if the directory * stack is empty. It also returns true if the folder is * filtered (we are told to skip it) */ protected function getNextDirectory() { // Reset the file / folder scanning positions $this->getFiles_position = null; $this->getFolders_position = null; $this->done_file_scanning = false; $this->done_subdir_scanning = false; $this->excluded_folder = false; $this->excluded_subdirectories = false; $this->excluded_files = false; if ((is_array($this->directory_list) || $this->directory_list instanceof \Countable ? count($this->directory_list) : 0) == 0) { // No directories left to scan return false; } else { // Get and remove the last entry from the $directory_list array $this->current_directory = array_pop($this->directory_list); $this->setStep($this->current_directory); $this->processed_files_counter = 0; } [$root, $translated_root, $dir] = $this->getCleanDirectoryComponents(); // Get a filters instance $filters = Factory::getFilters(); // Apply DEF (directory exclusion filters) // Note: the !empty($dir) prevents the site's root from being filtered out if ($filters->isFiltered($dir, $root, 'dir', 'all') && !empty($dir)) { Factory::getLog()->info("Skipping directory " . $this->current_directory); $this->done_subdir_scanning = true; $this->done_file_scanning = true; $this->excluded_folder = true; return true; } // Apply Skip Contained Directories Filters if ($filters->isFiltered($dir, $root, 'dir', 'children')) { $this->excluded_subdirectories = true; Factory::getLog()->info("Skipping subdirectories of directory " . $this->current_directory); $this->done_subdir_scanning = true; } // Apply Skipfiles if ($filters->isFiltered($dir, $root, 'dir', 'content')) { $this->excluded_files = true; Factory::getLog()->info("Skipping files of directory " . $this->current_directory); $this->done_file_scanning = true; // When the files of a folder are skipped we will have to add some // files anyway if they are present. These are files used to // prevent direct access to the folder. // Try to find and include .htaccess and index.htm(l) files // # Fix 2.4: Do not add DIRECTORY_SEPARATOR if we are on the site's root and it's an empty string $ds = ($this->current_directory == '') || ($this->current_directory == '/') ? '' : DIRECTORY_SEPARATOR; $checkForTheseFiles = [ $this->current_directory . $ds . '.htaccess', $this->current_directory . $ds . 'web.config', $this->current_directory . $ds . 'index.html', $this->current_directory . $ds . 'index.htm', $this->current_directory . $ds . 'robots.txt', ]; $this->processed_files_counter = 0; foreach ($checkForTheseFiles as $fileName) { if (@file_exists($fileName)) { // Fix 3.3 - We have to also put them through other filters, ahem! if (!$filters->isFiltered($fileName, $root, 'file', 'all')) { $this->file_list[] = $fileName; $this->processed_files_counter++; } } } } return true; } /** * Try to add some files from the $file_list into the archive * * @return boolean True if there were files packed, false otherwise * (empty filelist or fatal error) */ protected function pack_files() { // Get a reference to the archiver and the timer classes $archiver = Factory::getArchiverEngine(); $timer = Factory::getTimer(); $configuration = Factory::getConfiguration(); // Check whether we need to immediately post-processing a done part if (self::postProcessDonePartFile($archiver, $configuration)) { return true; } // If the archiver has work to do, make sure it finished up before continuing if ($configuration->get('volatile.engine.archiver.processingfile', false)) { Factory::getLog()->debug("Continuing file packing from previous step"); $archiver->addFile('', '', ''); // If that was the last step for packing this file, mark a file done if (!$configuration->get('volatile.engine.archiver.processingfile', false)) { $this->progressMarkFileDone(); } } // Did it finish, or does it have more work to do? if ($configuration->get('volatile.engine.archiver.processingfile', false)) { // More work to do. Let's just tell our parent that we finished up successfully. return true; } // Normal file backup loop; we keep on processing the file list, packing files as we go. if ((is_array($this->file_list) || $this->file_list instanceof \Countable ? count($this->file_list) : 0) == 0) { // No files left to pack. Return true and let the engine loop $this->progressMarkFolderDone(); return true; } else { Factory::getLog()->debug("Packing files"); $packedSize = 0; $numberOfFiles = 0; [$usec, $sec] = explode(" ", microtime()); $opStartTime = ((float) $usec + (float) $sec); $largeFileThreshold = Factory::getConfiguration()->get('engine.scan.common.largefile', 10485760); while (((is_array($this->file_list) || $this->file_list instanceof \Countable ? count($this->file_list) : 0) > 0)) { $file = @array_shift($this->file_list); $size = 0; if (file_exists($file)) { $size = @filesize($file); } // Anticipatory file size algorithm if (($numberOfFiles > 0) && ($size > $largeFileThreshold)) { if (!Factory::getConfiguration()->get('akeeba.tuning.nobreak.beforelargefile', 0)) { // If the file is bigger than the big file threshold, break the step // to avoid potential timeouts $this->setBreakFlag(); Factory::getLog()->info("Breaking step _before_ large file: " . $file . " - size: " . $size); // Push the file back to the list. array_unshift($this->file_list, $file); // Return true and let the engine loop return true; } } // Proactive potential timeout detection // Rough estimation of packing speed in bytes per second [$usec, $sec] = explode(" ", microtime()); $opEndTime = ((float) $usec + (float) $sec); if (($opEndTime - $opStartTime) == 0) { $_packSpeed = 0; } else { $_packSpeed = $packedSize / ($opEndTime - $opStartTime); } // Estimate required time to pack next file. If it's the first file of this operation, // do not impose any limitations. $_reqTime = ($_packSpeed - 0.01) <= 0 ? 0 : $size / $_packSpeed; // Do we have enough time? if ($timer->getTimeLeft() < $_reqTime) { if (!Factory::getConfiguration()->get('akeeba.tuning.nobreak.proactive', 0)) { array_unshift($this->file_list, $file); Factory::getLog()->info("Proactive step break - file: " . $file . " - size: " . $size . " - req. time " . sprintf('%2.2f', $_reqTime)); $this->setBreakFlag(); return true; } } $packedSize += $size; $numberOfFiles++; $archiver->addFile($file, $this->remove_path_prefix, $this->path_prefix); // If no more processing steps are required, mark a done file if (!$configuration->get('volatile.engine.archiver.processingfile', false)) { $this->progressMarkFileDone(); } // If this was the first file packed and we've already gone past // the large file size threshold break the step. Continuing with // more operations after packing such a big file is increasing // the risk to hit a timeout. if (($packedSize > $largeFileThreshold) && ($numberOfFiles == 1)) { if (!Factory::getConfiguration()->get('akeeba.tuning.nobreak.afterlargefile', 0)) { Factory::getLog()->info("Breaking step *after* large file: " . $file . " - size: " . $size); $this->setBreakFlag(); return true; } } // If we have to continue processing the file, break the file packing loop forcibly if ($configuration->get('volatile.engine.archiver.processingfile', false)) { return true; } } // True if we have more files, false if we're done packing return ((is_array($this->file_list) || $this->file_list instanceof \Countable ? count($this->file_list) : 0) > 0); } } protected function progressAddFile() { $this->total_files++; } protected function progressMarkFileDone() { $this->done_files++; } protected function progressAddFolder() { $this->total_folders++; } protected function progressMarkFolderDone() { $this->done_folders++; } /** * Returns the site root, the translated site root and the translated current directory * * @return array */ protected function getCleanDirectoryComponents() { $fsUtils = Factory::getFilesystemTools(); // Break directory components if (Factory::getConfiguration()->get('akeeba.platform.override_root', 0)) { $siteroot = Factory::getConfiguration()->get('akeeba.platform.newroot', '[SITEROOT]'); } else { $siteroot = '[SITEROOT]'; } $root = $this->root; if ($this->root == $siteroot) { $translated_root = $fsUtils->translateStockDirs($siteroot, true); } else { $translated_root = $this->remove_path_prefix; } $dir = $fsUtils->TrimTrailingSlash($this->current_directory); if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { $translated_root = $fsUtils->TranslateWinPath($translated_root); $dir = $fsUtils->TranslateWinPath($dir); } if (substr($dir, 0, strlen($translated_root)) == $translated_root) { $dir = substr($dir, strlen($translated_root)); } elseif (in_array(substr($translated_root, -1), ['/', '\\'])) { $new_translated_root = rtrim($translated_root, '/\\'); if (substr($dir, 0, strlen($new_translated_root)) == $new_translated_root) { $dir = substr($dir, strlen($new_translated_root)); } } if (substr($dir, 0, 1) == '/') { $dir = substr($dir, 1); } return [$root, $translated_root, $dir]; } /** * Steps the subdirectory scanning of the current directory * * @return boolean True on success, false on fatal error */ protected function scanSubdirs() { $engine = Factory::getScanEngine(); $subdirectories = false; [$root, $translated_root, $dir] = $this->getCleanDirectoryComponents(); // Get a filters instance $filters = Factory::getFilters(); if (is_null($this->getFolders_position)) { Factory::getLog()->info("Scanning directories of " . $this->current_directory); } else { Factory::getLog()->info("Resuming scanning directories of " . $this->current_directory); } // Get subdirectories $exception = null; try { $subdirectories = $engine->getFolders($this->current_directory, $this->getFolders_position); } catch (WarningException $e) { Factory::getLog()->warning($e->getMessage()); $subdirectories = false; } catch (Exception $e) { $exception = $e; } // If the list contains "too many" items, please break this step! if (Factory::getConfiguration()->get('volatile.breakflag', false)) { // Log the step break decision, for debugging reasons Factory::getLog()->info("Large directory " . $this->current_directory . " while scanning for subdirectories; I will resume scanning in next step."); // Return immediately, marking that we are not done yet! return true; } // Error control if (!is_null($exception)) { throw $exception; } // Start adding the subdirectories if (!empty($subdirectories) && is_array($subdirectories)) { $dereferenceSymlinks = Factory::getConfiguration()->get('engine.archiver.common.dereference_symlinks'); // If we have to treat symlinks as real directories just add everything if ($dereferenceSymlinks) { // Treat symlinks to directories as actual directories foreach ($subdirectories as $subdirectory) { $this->directory_list[] = $subdirectory; $this->progressAddFolder(); } } // If we are told not to dereference symlinks we'll need to check each subdirectory thoroughly else { // Treat symlinks to directories as simple symlink files (ONLY WORKS WITH CERTAIN ARCHIVERS!) foreach ($subdirectories as $subdirectory) { if (is_link($subdirectory)) { // Symlink detected; apply directory filters to it if (empty($dir)) { $dirSlash = $dir; } else { $dirSlash = $dir . '/'; } $check = $dirSlash . basename($subdirectory); Factory::getLog()->debug("Directory symlink detected: $check"); if (_AKEEBA_IS_WINDOWS) { $check = Factory::getFilesystemTools()->TranslateWinPath($check); } // Do I need this? $dir contains a path relative to the root anyway... $check = ltrim(str_replace($translated_root, '', $check), '/'); // Check for excluded symlinks (note that they are excluded as DIRECTORIES in the GUI) if ($filters->isFiltered($check, $root, 'dir', 'all')) { Factory::getLog()->info("Skipping directory symlink " . $check); } else { Factory::getLog()->debug('Adding folder symlink: ' . $check); $this->file_list[] = $subdirectory; $this->progressAddFile(); } } else { $this->directory_list[] = $subdirectory; $this->progressAddFolder(); } } } } // If the scanner nullified the next position to scan, we're done // scanning for subdirectories if (is_null($this->getFolders_position)) { $this->done_subdir_scanning = true; } return true; } /** * Steps the files scanning of the current directory * * @return void True on success, false on fatal error * @throws Exception */ protected function scanFiles() { $engine = Factory::getScanEngine(); $fileList = false; [$root, $translated_root, $dir] = $this->getCleanDirectoryComponents(); // Get a filters instance $filters = Factory::getFilters(); if (is_null($this->getFiles_position)) { Factory::getLog()->info("Scanning files of " . $this->current_directory); $this->processed_files_counter = 0; } else { Factory::getLog()->info("Resuming scanning files of " . $this->current_directory); } // Get file listing $exception = null; try { $fileList = $engine->getFiles($this->current_directory, $this->getFiles_position); } catch (WarningException $e) { Factory::getLog()->warning($e->getMessage()); $fileList = false; } catch (Exception $e) { $exception = $e; } // If the list contains "too many" items, please break this step! if (Factory::getConfiguration()->get('volatile.breakflag', false)) { // Log the step break decision, for debugging reasons Factory::getLog()->info("Large directory " . $this->current_directory . " while scanning for files; I will resume scanning in next step."); // Return immediately, marking that we are not done yet! return; } // Error control if (!is_null($exception)) { throw $exception; } // Do I have an unreadable directory? if ($fileList === false) { Factory::getLog()->warning('Unreadable directory ' . $this->current_directory); $this->done_file_scanning = true; } // Directory was readable, process the file list elseif (is_array($fileList) && !empty($fileList)) { // Add required trailing slash to $dir if (!empty($dir)) { $dir .= '/'; } // Scan all directory entries foreach ($fileList as $fileName) { $check = $dir . basename($fileName); if (_AKEEBA_IS_WINDOWS) { $check = Factory::getFilesystemTools()->TranslateWinPath($check); } // Do I need this? $dir contains a path relative to the root anyway... $check = ltrim(str_replace($translated_root, '', $check), '/'); $byFilter = ''; $skipThisFile = $filters->isFilteredExtended($check, $root, 'file', 'all', $byFilter); if ($skipThisFile) { Factory::getLog()->info("Skipping file $fileName (filter: $byFilter)"); } else { $this->file_list[] = $fileName; $this->processed_files_counter++; $this->progressAddFile(); } } } // If the scanner engine nullified the next position we are done // scanning for files if (is_null($this->getFiles_position)) { $this->done_file_scanning = true; } // If the directory was genuinely empty we will have to add an empty // directory entry in the archive, otherwise this directory will never // be restored. if ($this->done_file_scanning && ($this->processed_files_counter == 0)) { Factory::getLog()->info("Empty directory " . $this->current_directory); $archiver = Factory::getArchiverEngine(); if ($this->current_directory != $this->remove_path_prefix) { $archiver->addFile($this->current_directory, $this->remove_path_prefix, $this->path_prefix); } unset($archiver); } return; } /** * Try to determine the next root folder to scan * * @return boolean True if there was a new root to scan */ protected function getNextRoot() { // We have finished with our directory list. Hmm... Do we have extra directories? if (count($this->root_definitions) > 0) { Factory::getLog()->debug("More off-site directories detected"); $registry = Factory::getConfiguration(); $dir_definition = array_shift($this->root_definitions); $this->remove_path_prefix = $dir_definition[0]; // Remove absolute path to directory when storing the file if (is_null($dir_definition[1])) { $this->path_prefix = ''; // No added path for main site } else { $dir_definition[1] = trim($dir_definition[1]); if (empty($dir_definition[1]) || $dir_definition[1] == '/') { $this->path_prefix = ''; } else { $this->path_prefix = $registry->get('akeeba.advanced.virtual_folder') . '/' . $dir_definition[1]; } } $this->done_scanning = false; // Make sure we process this file list! $this->root = $dir_definition[0]; // Translate the root into an absolute path $stock_dirs = Platform::getInstance()->get_stock_directories(); $absolute_dir = substr($this->root, 0); if (!empty($stock_dirs)) { foreach ($stock_dirs as $key => $replacement) { $absolute_dir = str_replace($key, $replacement, $absolute_dir); } } $this->directory_list[] = $absolute_dir; $this->remove_path_prefix = $absolute_dir; $registry->set('volatile.filesystem.current_root', $absolute_dir); $this->total_files = 0; $this->done_files = 0; $this->total_folders = 0; $this->done_folders = 0; Factory::getLog()->info("Including new off-site directory to " . $dir_definition[1]); return true; } else // Nope, we are completely done! { $this->setState(self::STATE_POSTRUN); return false; } } } Domain/Init.php 0000604 00000035070 15245604442 0007376 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ 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]'; } } Domain/Installer.php 0000604 00000015041 15245604442 0010424 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ namespace Akeeba\Engine\Core\Domain; defined('AKEEBAENGINE') || die(); use Akeeba\Engine\Base\Part; use Akeeba\Engine\Factory; use Akeeba\Engine\Platform; use Akeeba\Engine\Util\HashTrait; /** * Installer deployment */ final class Installer extends Part { use HashTrait; /** @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, ]; $platform = Platform::getInstance(); try { $extraData = $platform->get_extra_info(); } catch (\Exception $e) { // Not all platform classes implement get_extra_info(), therefore Platform will throw an Exception $extraData = []; } $data = array_merge($data, $extraData); $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 = self::md5($password . $salt) . ':' . $salt; $ret = "<?php\n"; $ret .= "define('AKEEBA_PASSHASH', '" . $passhash . "');\n"; return $ret; } } Domain/Finalizer/RemoveTemporaryFiles.php 0000604 00000002702 15245604442 0014535 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ /** * @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; } } Domain/Finalizer/FinalizerInterface.php 0000604 00000003171 15245604442 0014157 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ /** * @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(); } Domain/Finalizer/AbstractQuotaManagement.php 0000604 00000033735 15245604442 0015176 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ /** * @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 = $def['absolute_path']; 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); } } Domain/Finalizer/RemoteQuotas.php 0000604 00000016136 15245604442 0013050 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ /** * @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'; $this->configKeys = [ 'maxAgeEnable' => 'akeeba.quota.remotely.maxage.enable', 'maxAgeDays' => 'akeeba.quota.remotely.maxage.maxdays', 'maxAgeKeep' => 'akeeba.quota.remotely.maxage.keepday', 'countEnable' => 'akeeba.quota.remotely.enable_count_quota', 'countValue' => 'akeeba.quota.remotely.count_quota', 'sizeEnable' => 'akeeba.quota.remotely.enable_size_quota', 'sizeValue' => 'akeeba.quota.remotely.size_quota', ]; parent::__construct($finalizationPart); } /** @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, 'absolute_path' => $stat['absolute_path'], ]; }, $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; } // 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', $logPath ) ); } // 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; } } Domain/Finalizer/LocalQuotas.php 0000604 00000012313 15245604442 0012640 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ /** * @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, 'absolute_path' => $stat['absolute_path'], ]; } 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', $logPath ) ); } // Check if I have enough time if ($timer->getTimeLeft() <= 0) { return false; } return true; } } Domain/Finalizer/ObsoleteRecordsQuotas.php 0000604 00000007310 15245604442 0014705 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ /** * @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 = (method_exists($db, 'createQuery') ? $db->createQuery() : $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 = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true)) ->delete($db->qn($statsTable)) ->where($db->qn('id') . " IN ($ids)"); $db->setQuery($query); $db->query(); return true; } } Domain/Finalizer/UpdateFileSizes.php 0000604 00000005156 15245604442 0013460 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ /** * @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; } } Domain/Finalizer/AbstractFinalizer.php 0000604 00000004772 15245604442 0014032 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ /** * @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); } } Domain/Finalizer/MailAdministrators.php 0000604 00000021610 15245604442 0014217 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ /** * @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"); $emails = $this->getEmailAddresses(); 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(); $db = Factory::getDatabase(); $query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))->select('1'); $dummy = $db->setQuery($query)->loadResult(); $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]; } /** * Get the addresses to send emails to. * * @return array * @since 9.4.9 */ private function getEmailAddresses(): array { $platform = Platform::getInstance(); $configuration = Factory::getConfiguration(); // Check if we have a list of emails saved inside the profile $email = trim($configuration->get('akeeba.basic.email_recipients', '') ?? ''); if (!empty($email)) { Factory::getLog()->debug("Using list of emails stored inside profile configuration"); $list = array_map( function ($x) { return trim ($x ?? ''); }, explode(',', $email) ); $list = array_filter( $list, function ($x) { return !empty($x); } ); if (!empty($list)) { return $list; } } $email = trim($platform->get_platform_configuration_option('frontend_email_address', '') ?? ''); if (!empty($email)) { Factory::getLog()->debug("Using pre-defined list of emails"); $list = array_map( function ($x) { return trim ($x ?? ''); }, explode(',', $email) ); $list = array_filter( $list, function ($x) { return !empty($x); } ); if (!empty($list)) { return $list; } } Factory::getLog()->debug("Fetching list of site administrator emails"); return $platform->get_administrator_emails(); } } Domain/Finalizer/UpdateStatistics.php 0000604 00000004661 15245604442 0013715 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ /** * @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; } } Domain/Finalizer/PostProcessing.php 0000604 00000023331 15245604442 0013375 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ /** * @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\Base\Exceptions\ErrorException; 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'); $shouldAbortOnFailed = $configuration->get('engine.postproc.common.abort_on_fail', false); 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, $shouldAbortOnFailed ? LogLevel::ERROR : LogLevel::WARNING); if ($shouldAbortOnFailed) { throw new ErrorException(sprintf('Failed to process backup archive file %s', basename($filename))); } } 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; } } Domain/Finalizer/UploadKickstart.php 0000604 00000006001 15245604442 0013512 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ /** * @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; } } Domain/Finalization.php 0000604 00000017171 15245604442 0011124 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ 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(''); } } } Kettenrad.php 0000604 00000061166 15245604442 0007212 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ 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); try { Factory::getPush()->message($pushSubject, $pushDetails); } catch (Throwable $e) { Factory::getLog()->notice(sprintf("Sending push notification failed: %s", $e->getMessage())); } } /** * 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()); try { Factory::getPush()->message($pushSubject, $pushDetails); } catch (Throwable $e) { Factory::getLog()->notice(sprintf("Sending push notification failed: %s", $e->getMessage())); } } /** * 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; } 02.advanced.json 0000604 00000002341 15245604442 0007426 0 ustar 00 { "_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" } } Filters.php 0000604 00000025162 15245604442 0006675 0 ustar 00 <?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License version 3, or later * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program. If not, see * <https://www.gnu.org/licenses/>. */ 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 = []; } } 01.basic.json 0000604 00000002711 15245604442 0006742 0 ustar 00 { "_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" } }
| ver. 1.4 |
Github
|
.
| PHP 8.1.34 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка