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