Your IP : 216.73.217.68


Current Path : /home/w/u/e/wuectly/www/03cbe/
Upload File :
Current File : /home/w/u/e/wuectly/www/03cbe/BackupEngine.tar

Platform.php000060400000020161152456704520007047 0ustar00<?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;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Platform\Base;
use Akeeba\Engine\Platform\PlatformInterface;
use DirectoryIterator;
use Exception;

/**
 * Platform abstraction. Manages the loading of platform connector objects and delegates calls to itself the them.
 *
 * @property string $tableNameProfiles The name of the table where backup profiles are stored
 * @property string $tableNameStats    The name of the table where backup records are stored
 *
 * @since    3.4
 */
class Platform
{
	/** @var Base|null The currently loaded platform connector object instance */
	protected static $platformConnectorInstance = null;

	/** @var array A list of additional directories where platform classes can be found */
	protected static $knownPlatformsDirectories = [];

	/** @var Platform The currently loaded object instance of this class. WARNING: This is NOT the platform connector! */
	protected static $instance = null;

	/**
	 * Public class constructor
	 *
	 * @param   string  $platform  Optional; platform name. Leave blank to auto-detect.
	 *
	 * @throws  Exception  When the platform cannot be loaded
	 */
	public function __construct($platform = null)
	{
		if (empty($platform) || is_null($platform))
		{
			$platform = static::detectPlatform();
		}

		if (empty($platform))
		{
			throw new Exception('Can not find a suitable Akeeba Engine platform for your site');
		}

		static::$platformConnectorInstance = static::loadPlatform($platform);

		if (!is_object(static::$platformConnectorInstance))
		{
			throw new Exception("Can not load Akeeba Engine platform $platform");
		}
	}

	/**
	 * Implements the Singleton pattern for this class
	 *
	 * @staticvar Platform $instance The static object instance
	 *
	 * @param   string  $platform  Optional; platform name. Autodetect if blank.
	 *
	 * @return  PlatformInterface
	 */
	public static function &getInstance($platform = null)
	{
		if (!is_object(static::$instance))
		{
			static::$instance = new Platform($platform);
		}

		return static::$instance;
	}

	/**
	 * Get a list of all directories where platform classes can be found
	 *
	 * @return  array
	 */
	public static function getPlatformDirectories()
	{
		$defaultPath = [];

		if (is_object(static::$platformConnectorInstance))
		{
			$defaultPath[] = __DIR__ . '/Platform/' . static::$platformConnectorInstance->platformName;
		}

		return array_merge(
			static::$knownPlatformsDirectories,
			$defaultPath
		);
	}

	/**
	 * Lists available platforms
	 *
	 * @staticvar   array   $platforms   Static cache of the available platforms
	 *
	 * @return  array  The list of available platforms
	 */
	static public function listPlatforms()
	{
		if (empty(static::$knownPlatformsDirectories))
		{
			$di = new DirectoryIterator(__DIR__ . '/Platform');

			/** @var DirectoryIterator $file */
			foreach ($di as $file)
			{
				if (!$file->isDir())
				{
					continue;
				}

				if ($file->isDot())
				{
					continue;
				}

				if ($file->getExtension() !== 'php')
				{
					continue;
				}

				$shortName = $file->getFilename();
				$bareName  = basename($shortName, '.php');

				/**
				 * We never have dots in our filenames but some hosts will rename files similar to  foo.1.php when their
				 * broken security scanners detect a false positive. This is our defence against that.
				 */
				if (strpos($bareName, '.') !== false)
				{
					continue;
				}

				static::$knownPlatformsDirectories[$shortName] = $file->getRealPath();
			}
		}

		return static::$knownPlatformsDirectories;
	}

	/**
	 * Add a platform to the list of known platforms
	 *
	 * @param   string  $slug               Short name of the platform
	 * @param   string  $platformDirectory  The path where you can find it
	 *
	 * @return  void
	 */
	public static function addPlatform($slug, $platformDirectory)
	{
		if (empty(static::$knownPlatformsDirectories))
		{
			static::listPlatforms();

			static::$knownPlatformsDirectories[$slug] = $platformDirectory;
		}
	}

	/**
	 * Auto-detect the suitable platform for this site
	 *
	 * @return  string
	 *
	 * @throws  Exception  When no platform is detected
	 */
	protected static function detectPlatform()
	{
		$platforms = static::listPlatforms();

		if (empty($platforms))
		{
			throw new Exception('No Akeeba Engine platform class found');
		}

		$bestPlatform = (object) [
			'name'     => null,
			'priority' => 0,
		];

		foreach ($platforms as $platform => $path)
		{
			$o = static::loadPlatform($platform, $path);

			if (is_null($o))
			{
				continue;
			}

			if ($o->isThisPlatform())
			{
				if ($o->priority > $bestPlatform->priority)
				{
					$bestPlatform->priority = $o->priority;
					$bestPlatform->name     = $platform;
				}
			}
		}

		return $bestPlatform->name;
	}

	/**
	 * Load a given platform and return the platform object
	 *
	 * @param   string  $platform  Platform name
	 * @param   string  $path      The path to laod the platform from (optional)
	 *
	 * @return  Base
	 */
	protected static function &loadPlatform($platform, $path = null)
	{
		if (empty($path))
		{
			if (isset(static::$knownPlatformsDirectories[$platform]))
			{
				$path = static::$knownPlatformsDirectories[$platform];
			}
		}

		if (empty($path))
		{
			$path = dirname(__FILE__) . '/' . $platform;
		}

		$classFile = $path . '/Platform.php';
		$className = '\\Akeeba\\Engine\\Platform\\' . ucfirst($platform);

		$null = null;

		if (!file_exists($classFile))
		{
			return $null;
		}

		require_once($classFile);

		if (!class_exists($className, false))
		{
			return $null;
		}

		$o = new $className;

		return $o;
	}

	/**
	 * Magic method to proxy all calls to the loaded platform object
	 *
	 * @param   string  $name       The name of the method to call
	 * @param   array   $arguments  The arguments to pass
	 *
	 * @return  mixed  The result of the method being called
	 *
	 * @throws  Exception  When the platform isn't loaded or an non-existent method is called
	 */
	public function __call($name, array $arguments)
	{
		if (is_null(static::$platformConnectorInstance))
		{
			throw new Exception('Akeeba Engine platform is not loaded');
		}

		if (method_exists(static::$platformConnectorInstance, $name))
		{
			return static::$platformConnectorInstance->$name(...$arguments);
		}
		else
		{
			throw new Exception('Method ' . $name . ' not found in Akeeba Platform');
		}
	}

	/**
	 * Magic getter for the properties of the loaded platform
	 *
	 * @param   string  $name  The name of the property to get
	 *
	 * @return  mixed  The value of the property
	 */
	public function __get($name)
	{
		if (!isset(static::$platformConnectorInstance->$name) || !property_exists(static::$platformConnectorInstance, $name))
		{
			static::$platformConnectorInstance->$name = null;
			user_error(__CLASS__ . ' does not support property ' . $name, E_NOTICE);
		}

		return static::$platformConnectorInstance->$name;
	}

	/**
	 * Magic setter for the properties of the loaded platform
	 *
	 * @param   string  $name   The name of the property to set
	 * @param   mixed   $value  The value of the property to set
	 */
	public function __set($name, $value)
	{
		if (isset(static::$platformConnectorInstance->$name) || property_exists(static::$platformConnectorInstance, $name))
		{
			static::$platformConnectorInstance->$name = $value;
		}
		else
		{
			static::$platformConnectorInstance->$name = null;
			user_error(__CLASS__ . ' does not support property ' . $name, E_NOTICE);
		}
	}
}
Driver/None.php000060400000021534152456704520007422 0ustar00<?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\Driver;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Driver\Query\Base as QueryBase;

/**
 * Dummy driver class for flat-file CMS
 */
#[\AllowDynamicProperties]
class None extends Base
{
	public static $dbtech = 'none';
	/**
	 * The name of the database driver.
	 *
	 * @var    string
	 * @since  1.0
	 */
	public $name = 'none';

	public function __construct(array $options)
	{
		$this->driverType = 'none';

		parent::__construct($options);
	}

	/**
	 * Test to see if this db driver is available
	 *
	 * @return  boolean  True on success, false otherwise.
	 *
	 * @since   1.0
	 */
	public static function isSupported()
	{
		return true;
	}

	public function open()
	{
		return $this;
	}

	/**
	 * Closes the database connection
	 */
	public function close()
	{
		return;
	}

	/**
	 * Determines if the connection to the server is active.
	 *
	 * @return  boolean  True if connected to the database engine.
	 */
	public function connected()
	{
		return true;
	}

	/**
	 * Drops a table from the database.
	 *
	 * @param   string   $table     The name of the database table to drop.
	 * @param   boolean  $ifExists  Optionally specify that the table must exist before it is dropped.
	 *
	 * @return  Base  Returns this object to support chaining.
	 */
	public function dropTable($table, $ifExists = true)
	{
		return $this;
	}

	/**
	 * Method to escape a string for usage in an SQL statement.
	 *
	 * @param   string   $text   The string to be escaped.
	 * @param   boolean  $extra  Optional parameter to provide extra escaping.
	 *
	 * @return  string   The escaped string.
	 */
	public function escape($text, $extra = false)
	{
		return '';
	}

	/**
	 * Method to fetch a row from the result set cursor as an associative array.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  mixed  Either the next row from the result set or false if there are no more rows.
	 */
	public function fetchAssoc($cursor = null)
	{
		return false;
	}

	/**
	 * Method to free up the memory used for the result set.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  void
	 */
	public function freeResult($cursor = null)
	{
		return;
	}

	/**
	 * Get the number of affected rows for the previous executed SQL statement.
	 *
	 * @return  integer  The number of affected rows.
	 */
	public function getAffectedRows()
	{
		return 0;
	}

	/**
	 * Method to get the database collation in use by sampling a text field of a table in the database.
	 *
	 * @return  mixed  The collation in use by the database or boolean false if not supported.
	 */
	public function getCollation()
	{
		return false;
	}

	/**
	 * Get the number of returned rows for the previous executed SQL statement.
	 *
	 * @param   resource  $cursor  An optional database cursor resource to extract the row count from.
	 *
	 * @return  integer   The number of returned rows.
	 */
	public function getNumRows($cursor = null)
	{
		return 0;
	}

	/**
	 * Get the current query object or a new QueryBase object.
	 *
	 * @param   boolean  $new  False to return the current query object, True to return a new QueryBase object.
	 *
	 * @return  QueryBase  The current query object or a new object extending the QueryBase class.
	 */
	public function getQuery($new = false)
	{
		return $this->sql;
	}

	public function createQuery()
	{
		return $this->sql;
	}

	/**
	 * Retrieves field information about the given tables.
	 *
	 * @param   string   $table     The name of the database table.
	 * @param   boolean  $typeOnly  True (default) to only return field types.
	 *
	 * @return  array  An array of fields by table.
	 */
	public function getTableColumns($table, $typeOnly = true)
	{
		return [];
	}

	/**
	 * Shows the table CREATE statement that creates the given tables.
	 *
	 * @param   mixed  $tables  A table name or a list of table names.
	 *
	 * @return  array  A list of the create SQL for the tables.
	 */
	public function getTableCreate($tables)
	{
		return [];
	}

	/**
	 * Retrieves field information about the given tables.
	 *
	 * @param   mixed  $tables  A table name or a list of table names.
	 *
	 * @return  array  An array of keys for the table(s).
	 */
	public function getTableKeys($tables)
	{
		return [];
	}

	/**
	 * Method to get an array of all tables in the database.
	 *
	 * @return  array  An array of all the tables in the database.
	 */
	public function getTableList()
	{
		return [];
	}

	/**
	 * Returns an array with the names of tables, views, procedures, functions and triggers
	 * in the database. The table names are the keys of the tables, whereas the value is
	 * the type of each element: table, view, merge, temp, procedure, function or trigger.
	 * Note that merge are MRG_MYISAM tables and temp is non-permanent data table, usually
	 * set up as temporary, black hole or federated tables. These two types should never,
	 * ever, have their data dumped in the SQL dump file.
	 *
	 * @param   bool  $abstract  Return or normal names? Defaults to true (names)
	 *
	 * @return  array
	 */
	public function getTables($abstract = true)
	{
		return [];
	}

	/**
	 * Get the version of the database connector
	 *
	 * @return  string  The database connector version.
	 */
	public function getVersion()
	{
		return '0.0.0';
	}

	/**
	 * Method to get the auto-incremented value from the last INSERT statement.
	 *
	 * @return  integer  The value of the auto-increment field from the last inserted row.
	 */
	public function insertid()
	{
		return 0;
	}

	/**
	 * Locks a table in the database.
	 *
	 * @param   string  $tableNameName  The name of the table to unlock.
	 *
	 * @return  Base  Returns this object to support chaining.
	 */
	public function lockTable($tableNameName)
	{
		return $this;
	}

	/**
	 * Execute the SQL statement.
	 *
	 * @return  mixed  A database cursor resource on success, boolean false on failure.
	 */
	public function query()
	{
		return false;
	}

	/**
	 * Renames a table in the database.
	 *
	 * @param   string  $oldTable  The name of the table to be renamed
	 * @param   string  $newTable  The new name for the table.
	 * @param   string  $backup    Table prefix
	 * @param   string  $prefix    For the table - used to rename constraints in non-mysql databases
	 *
	 * @return  Base  Returns this object to support chaining.
	 */
	public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
	{
		return $this;
	}

	/**
	 * Select a database for use.
	 *
	 * @param   string  $database  The name of the database to select for use.
	 *
	 * @return  boolean  True if the database was successfully selected.
	 */
	public function select($database)
	{
		return true;
	}

	/**
	 * Set the connection to use UTF-8 character encoding.
	 *
	 * @return  boolean  True on success.
	 */
	public function setUTF()
	{
		return true;
	}

	/**
	 * Method to commit a transaction.
	 *
	 * @return  void
	 */
	public function transactionCommit()
	{
		return;
	}

	/**
	 * Method to roll back a transaction.
	 *
	 * @return  void
	 */
	public function transactionRollback()
	{
		return;
	}

	/**
	 * Method to initialize a transaction.
	 *
	 * @return  void
	 */
	public function transactionStart()
	{
		return;
	}

	/**
	 * Unlocks tables in the database.
	 *
	 * @return  Base  Returns this object to support chaining.
	 */
	public function unlockTables()
	{
		return $this;
	}

	/**
	 * Method to fetch a row from the result set cursor as an array.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  mixed  Either the next row from the result set or false if there are no more rows.
	 */
	protected function fetchArray($cursor = null)
	{
		return false;
	}

	/**
	 * Method to fetch a row from the result set cursor as an object.
	 *
	 * @param   mixed   $cursor  The optional result set cursor from which to fetch the row.
	 * @param   string  $class   The class name to use for the returned row object.
	 *
	 * @return  mixed   Either the next row from the result set or false if there are no more rows.
	 */
	protected function fetchObject($cursor = null, $class = 'stdClass')
	{
		return false;
	}
}
Driver/Sqlite.php000060400000047176152456704520007776 0ustar00<?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\Driver;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Driver\Query\Base as QueryBase;
use Akeeba\Engine\Driver\Query\Limitable;
use Akeeba\Engine\Driver\Query\Preparable;
use PDO;
use PDOException;
use PDOStatement;
use RuntimeException;
use SQLite3;

/**
 * SQLite database driver supporting PDO based connections
 *
 * @see    http://php.net/manual/en/ref.pdo-sqlite.php
 * @since  1.0
 */
#[\AllowDynamicProperties]
class Sqlite extends Base
{

	public static $dbtech = 'sqlite';

	/**
	 * The name of the database driver.
	 *
	 * @var    string
	 * @since  1.0
	 */
	public $name = 'sqlite';

	/** @var PDOStatement The database connection cursor from the last query. */
	protected $cursor;

	/** @var array   Contains the current query execution status */
	protected $executed = false;

	/**
	 * The character(s) used to quote SQL statement names such as table names or field names,
	 * etc. The child classes should define this as necessary.  If a single character string the
	 * same character is used for both sides of the quoted name, else the first character will be
	 * used for the opening quote and the second for the closing quote.
	 *
	 * @var    string
	 * @since  1.0
	 */
	protected $nameQuote = '`';

	/** @var resource   The prepared statement. */
	protected $prepared;

	/** @var bool Are we in the process of reconnecting to the database server? */
	private $isReconnecting = false;

	public function __construct(array $options)
	{
		$this->driverType = 'sqlite';

		parent::__construct($options);

		if (!is_object($this->connection))
		{
			$this->open();
		}
	}

	/**
	 * Test to see if the PDO ODBC connector is available.
	 *
	 * @return  boolean  True on success, false otherwise.
	 *
	 * @since   1.0
	 */
	public static function isSupported()
	{
		return class_exists('\\PDO') && in_array('sqlite', PDO::getAvailableDrivers());
	}

	/**
	 * Destructor.
	 *
	 * @since   1.0
	 */
	public function __destruct()
	{
		$this->freeResult();
		unset($this->connection);
	}

	public function close()
	{
		$return = false;

		if (is_object($this->cursor))
		{
			$this->cursor->closeCursor();
		}

		$this->connection = null;

		return $return;
	}

	/**
	 * Determines if the connection to the server is active.
	 *
	 * @return  boolean  True if connected to the database engine.
	 */
	public function connected()
	{
		return !empty($this->connection);
	}

	/**
	 * Disconnects the database.
	 *
	 * @return  void
	 *
	 * @since   1.0
	 */
	public function disconnect()
	{
		$this->freeResult();
		unset($this->connection);
	}

	/**
	 * Drops a table from the database.
	 *
	 * @param   string   $table     The name of the database table to drop.
	 * @param   boolean  $ifExists  Optionally specify that the table must exist before it is dropped.
	 *
	 * @return  Sqlite  Returns this object to support chaining.
	 *
	 * @since   1.0
	 */
	public function dropTable($table, $ifExists = true)
	{
		$this->open();

		$query = $this->getQuery(true);

		$this->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $query->quoteName($table));

		$this->execute();

		return $this;
	}

	/**
	 * Method to escape a string for usage in an SQLite statement.
	 *
	 * Note: Using query objects with bound variables is preferable to the below.
	 *
	 * @param   string   $text   The string to be escaped.
	 * @param   boolean  $extra  Unused optional parameter to provide extra escaping.
	 *
	 * @return  string  The escaped string.
	 *
	 * @since   1.0
	 */
	public function escape($text, $extra = false)
	{
		if (is_int($text) || is_float($text))
		{
			return $text;
		}

		if (is_null($text))
		{
			return 'NULL';
		}

		return SQLite3::escapeString($text);
	}

	public function fetchAssoc($cursor = null)
	{
		if (!empty($cursor) && $cursor instanceof PDOStatement)
		{
			return $cursor->fetch(PDO::FETCH_ASSOC);
		}

		if ($this->prepared instanceof PDOStatement)
		{
			return $this->prepared->fetch(PDO::FETCH_ASSOC);
		}
	}

	public function freeResult($cursor = null)
	{
		$this->executed = false;

		if ($cursor instanceof PDOStatement)
		{
			$cursor->closeCursor();
			$cursor = null;
		}

		if ($this->prepared instanceof PDOStatement)
		{
			$this->prepared->closeCursor();
			$this->prepared = null;
		}
	}

	public function getAffectedRows()
	{
		$this->open();

		if ($this->prepared instanceof PDOStatement)
		{
			return $this->prepared->rowCount();
		}
		else
		{
			return 0;
		}
	}

	/**
	 * Method to get the database collation in use by sampling a text field of a table in the database.
	 *
	 * @return  mixed  The collation in use by the database or boolean false if not supported.
	 *
	 * @since   1.0
	 */
	public function getCollation()
	{
		return $this->charset;
	}

	public function getNumRows($cursor = null)
	{
		$this->open();

		if ($cursor instanceof PDOStatement)
		{
			return $cursor->rowCount();
		}
		elseif ($this->prepared instanceof PDOStatement)
		{
			return $this->prepared->rowCount();
		}
		else
		{
			return 0;
		}
	}

	/**
	 * Retrieve a PDO database connection attribute
	 * http://www.php.net/manual/en/pdo.getattribute.php
	 *
	 * Usage: $db->getOption(PDO::ATTR_CASE);
	 *
	 * @param   mixed  $key  One of the PDO::ATTR_* Constants
	 *
	 * @return  mixed
	 *
	 * @since   1.0
	 */
	public function getOption($key)
	{
		$this->open();

		return $this->connection->getAttribute($key);
	}

	/**
	 * Get the current query object or a new Query object.
	 * We have to override the parent method since it will always return a PDO query, while we have a
	 * specialized class for SQLite
	 *
	 * @param   boolean  $new  False to return the current query object, True to return a new Query object.
	 *
	 * @return  QueryBase  The current query object or a new object extending the Query class.
	 *
	 * @throws  RuntimeException
	 */
	public function getQuery($new = false)
	{
		if ($new)
		{
			return new Query\Sqlite($this);
		}

		return $this->sql;
	}

	public function createQuery()
	{
		return new Query\Sqlite($this);
	}

	/**
	 * Retrieves field information about a given table.
	 *
	 * @param   string   $table     The name of the database table.
	 * @param   boolean  $typeOnly  True to only return field types.
	 *
	 * @return  array  An array of fields for the database table.
	 *
	 * @throws  RuntimeException
	 * @since   1.0
	 */
	public function getTableColumns($table, $typeOnly = true)
	{
		$this->open();

		$columns = [];
		$query   = $this->getQuery(true);

		$fieldCasing = $this->getOption(PDO::ATTR_CASE);

		$this->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER);

		$table = strtoupper($table);

		$query->setQuery('pragma table_info(' . $table . ')');

		$this->setQuery($query);
		$fields = $this->loadObjectList();

		if ($typeOnly)
		{
			foreach ($fields as $field)
			{
				$columns[$field->NAME] = $field->TYPE;
			}
		}
		else
		{
			foreach ($fields as $field)
			{
				// Do some dirty translation to MySQL output.
				$columns[$field->NAME] = (object) [
					'Field'   => $field->NAME,
					'Type'    => $field->TYPE,
					'Null'    => ($field->NOTNULL == '1' ? 'NO' : 'YES'),
					'Default' => $field->DFLT_VALUE,
					'Key'     => ($field->PK == '1' ? 'PRI' : ''),
				];
			}
		}

		$this->setOption(PDO::ATTR_CASE, $fieldCasing);

		return $columns;
	}

	/**
	 * Shows the table CREATE statement that creates the given tables.
	 *
	 * Note: Doesn't appear to have support in SQLite
	 *
	 * @param   mixed  $tables  A table name or a list of table names.
	 *
	 * @return  array  A list of the create SQL for the tables.
	 *
	 * @throws  RuntimeException
	 * @since   1.0
	 */
	public function getTableCreate($tables)
	{
		$this->open();

		// Sanitize input to an array and iterate over the list.
		$tables = (array) $tables;

		return $tables;
	}

	/**
	 * Get the details list of keys for a table.
	 *
	 * @param   string  $tables  The name of the table.
	 *
	 * @return  array  An array of the column specification for the table.
	 *
	 * @throws  RuntimeException
	 * @since   1.0
	 */
	public function getTableKeys($tables)
	{
		$this->open();

		$keys  = [];
		$query = $this->getQuery(true);

		$fieldCasing = $this->getOption(PDO::ATTR_CASE);

		$this->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER);

		$tables = strtoupper($tables);
		$query->setQuery('pragma table_info( ' . $tables . ')');

		// $query->bind(':tableName', $table);

		$this->setQuery($query);
		$rows = $this->loadObjectList();

		foreach ($rows as $column)
		{
			if ($column->PK == 1)
			{
				$keys[$column->NAME] = $column;
			}
		}

		$this->setOption(PDO::ATTR_CASE, $fieldCasing);

		return $keys;
	}

	/**
	 * Method to get an array of all tables in the database (schema).
	 *
	 * @return  array   An array of all the tables in the database.
	 *
	 * @throws  RuntimeException
	 * @since   1.0
	 */
	public function getTableList()
	{
		$this->open();

		/* @type  Query\Sqlite $query */
		$query = $this->getQuery(true);

		$type = 'table';

		$query->select('name');
		$query->from('sqlite_master');
		$query->where('type = :type');
		$query->bind(':type', $type);
		$query->order('name');

		$this->setQuery($query);

		$tables = $this->loadColumn();

		return $tables;
	}

	/**
	 * There's no point on return "a list of tables" inside a SQLite database: we are simple going to
	 * copy the whole database file in the new location
	 *
	 * @param   bool  $abstract
	 *
	 * @return array
	 */
	public function getTables($abstract = true)
	{
		return [];
	}

	/**
	 * Get the version of the database connector.
	 *
	 * @return  string  The database connector version.
	 *
	 * @since   1.0
	 */
	public function getVersion()
	{
		$this->open();

		$this->setQuery("SELECT sqlite_version()");

		return $this->loadResult();
	}

	public function insertid()
	{
		$this->open();

		// Error suppress this to prevent PDO warning us that the driver doesn't support this operation.
		return @$this->connection->lastInsertId();
	}

	/**
	 * Locks a table in the database.
	 *
	 * @param   string  $tableName  The name of the table to unlock.
	 *
	 * @return  Sqlite Returns this object to support chaining.
	 *
	 * @throws  RuntimeException
	 * @since   1.0
	 */
	public function lockTable($tableName)
	{
		return $this;
	}

	public function open()
	{
		if ($this->connected())
		{
			return;
		}
		else
		{
			$this->close();
		}

		if (isset($this->options['version']) && $this->options['version'] == 2)
		{
			$format = 'sqlite2:#DBNAME#';
		}
		else
		{
			$format = 'sqlite:#DBNAME#';
		}

		$replace = ['#DBNAME#'];
		$with    = [$this->options['database']];

		// Create the connection string:
		$connectionString = str_replace($replace, $with, $format);

		try
		{
			$this->connection = new PDO(
				$connectionString,
				$this->options['user'],
				$this->options['password']
			);
		}
		catch (PDOException $e)
		{
			throw new RuntimeException('Could not connect to PDO' . ': ' . $e->getMessage(), 2, $e);
		}
	}

	public function query()
	{
		$this->open();

		if (!is_object($this->connection))
		{
			throw new RuntimeException($this->errorMsg, $this->errorNum);
		}

		// Take a local copy so that we don't modify the original query and cause issues later
		$sql = $this->replacePrefix((string) $this->sql);

		if ($this->limit > 0 || $this->offset > 0)
		{
			$sql .= ' LIMIT ' . $this->limit;

			if ($this->offset > 0)
			{
				$sql .= ' OFFSET ' . $this->offset;
			}
		}

		// Increment the query counter.
		$this->count++;

		// If debugging is enabled then let's log the query.
		if ($this->debug)
		{
			// Add the query to the object queue.
			$this->log[] = $sql;
		}

		// Reset the error values.
		$this->errorNum = 0;
		$this->errorMsg = '';

		// Execute the query.
		$this->executed = false;

		if ($this->prepared instanceof PDOStatement)
		{
			// Bind the variables:
			if ($this->sql instanceof Preparable)
			{
				$bounded =& $this->sql->getBounded();

				foreach ($bounded as $key => $obj)
				{
					$this->prepared->bindParam($key, $obj->value, $obj->dataType, $obj->length, $obj->driverOptions);
				}
			}

			$this->executed = $this->prepared->execute();
		}

		// If an error occurred handle it.
		if (!$this->executed)
		{
			// Get the error number and message before we execute any more queries.
			$errorNum = (int) $this->connection->errorCode();
			$errorMsg = (string) 'SQL: ' . implode(", ", $this->connection->errorInfo());

			// Check if the server was disconnected.
			if (!$this->connected() && !$this->isReconnecting)
			{
				$this->isReconnecting = true;

				try
				{
					// Attempt to reconnect.
					$this->connection = null;
					$this->open();
				}
				catch (RuntimeException $e)
					// If connect fails, ignore that exception and throw the normal exception.
				{
					// Get the error number and message.
					$this->errorNum = (int) $this->connection->errorCode();
					$this->errorMsg = (string) 'SQL: ' . implode(", ", $this->connection->errorInfo());

					// Throw the normal query exception.
					throw new RuntimeException($this->errorMsg, $this->errorNum);
				}

				// Since we were able to reconnect, run the query again.
				$result               = $this->query();
				$this->isReconnecting = false;

				return $result;
			}
			else
				// The server was not disconnected.
			{
				// Get the error number and message from before we tried to reconnect.
				$this->errorNum = $errorNum;
				$this->errorMsg = $errorMsg;

				// Throw the normal query exception.
				throw new RuntimeException($this->errorMsg, $this->errorNum);
			}
		}

		return $this->prepared;
	}

	/**
	 * Renames a table in the database.
	 *
	 * @param   string  $oldTable  The name of the table to be renamed
	 * @param   string  $newTable  The new name for the table.
	 * @param   string  $backup    Not used by Sqlite.
	 * @param   string  $prefix    Not used by Sqlite.
	 *
	 * @return  Sqlite Returns this object to support chaining.
	 *
	 * @throws  RuntimeException
	 * @since   1.0
	 */
	public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
	{
		$this->setQuery('ALTER TABLE ' . $oldTable . ' RENAME TO ' . $newTable)->execute();

		return $this;
	}

	/**
	 * Select a database for use.
	 *
	 * @param   string  $database  The name of the database to select for use.
	 *
	 * @return  boolean  True if the database was successfully selected.
	 *
	 * @throws  RuntimeException
	 * @since   1.0
	 */
	public function select($database)
	{
		$this->open();

		$this->_database = $database;

		return true;
	}

	/**
	 * Sets an attribute on the PDO database handle.
	 * http://www.php.net/manual/en/pdo.setattribute.php
	 *
	 * Usage: $db->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER);
	 *
	 * @param   integer  $key    One of the PDO::ATTR_* Constants
	 * @param   mixed    $value  One of the associated PDO Constants
	 *                           related to the particular attribute
	 *                           key.
	 *
	 * @return boolean
	 *
	 * @since  1.0
	 */
	public function setOption($key, $value)
	{
		$this->open();

		return $this->connection->setAttribute($key, $value);
	}

	/**
	 * Sets the SQL statement string for later execution.
	 *
	 * @param   mixed    $query          The SQL statement to set either as a JDatabaseQuery object or a string.
	 * @param   integer  $offset         The affected row offset to set.
	 * @param   integer  $limit          The maximum affected rows to set.
	 * @param   array    $driverOptions  The optional PDO driver options
	 *
	 * @return  Base  This object to support method chaining.
	 *
	 * @since   1.0
	 */
	public function setQuery($query, $offset = null, $limit = null, $driverOptions = [])
	{
		$this->open();

		$this->freeResult();

		if (is_string($query))
		{
			// Allows taking advantage of bound variables in a direct query:
			$query = $this->getQuery(true)->setQuery($query);
		}

		if ($query instanceof Limitable && !is_null($offset) && !is_null($limit))
		{
			$query->setLimit($limit, $offset);
		}

		$sql = $this->replacePrefix((string) $query);

		$this->prepared = $this->connection->prepare($sql, $driverOptions);

		// Store reference to the DatabaseQuery instance:
		parent::setQuery($query, $offset, $limit);

		return $this;
	}

	/**
	 * Set the connection to use UTF-8 character encoding.
	 *
	 * Returns false automatically for the Oracle driver since
	 * you can only set the character set when the connection
	 * is created.
	 *
	 * @return  boolean  True on success.
	 *
	 * @since   1.0
	 */
	public function setUTF()
	{
		$this->open();

		return false;
	}

	/**
	 * Method to commit a transaction.
	 *
	 * @param   boolean  $toSavepoint  If true, commit to the last savepoint.
	 *
	 * @return  void
	 *
	 * @throws  RuntimeException
	 * @since   1.0
	 */
	public function transactionCommit($toSavepoint = false)
	{
		$this->open();

		if (!$toSavepoint || $this->transactionDepth <= 1)
		{
			$this->open();

			if (!$toSavepoint || $this->transactionDepth == 1)
			{
				$this->connection->commit();
			}

			$this->transactionDepth--;
		}
		else
		{
			$this->transactionDepth--;
		}
	}

	/**
	 * Method to roll back a transaction.
	 *
	 * @param   boolean  $toSavepoint  If true, rollback to the last savepoint.
	 *
	 * @return  void
	 *
	 * @throws  RuntimeException
	 * @since   1.0
	 */
	public function transactionRollback($toSavepoint = false)
	{
		$this->connected();

		if (!$toSavepoint || $this->transactionDepth <= 1)
		{
			$this->open();

			if (!$toSavepoint || $this->transactionDepth == 1)
			{
				$this->connection->rollBack();
			}

			$this->transactionDepth--;
		}
		else
		{
			$savepoint = 'SP_' . ($this->transactionDepth - 1);
			$this->setQuery('ROLLBACK TO ' . $this->quoteName($savepoint));

			if ($this->execute())
			{
				$this->transactionDepth--;
			}
		}
	}

	/**
	 * Method to initialize a transaction.
	 *
	 * @param   boolean  $asSavepoint  If true and a transaction is already active, a savepoint will be created.
	 *
	 * @return  void
	 *
	 * @throws  RuntimeException
	 * @since   1.0
	 */
	public function transactionStart($asSavepoint = false)
	{
		$this->connected();

		if (!$asSavepoint || !$this->transactionDepth)
		{
			$this->open();

			if (!$asSavepoint || !$this->transactionDepth)
			{
				$this->connection->beginTransaction();
			}

			$this->transactionDepth++;
		}
		else
		{
			$savepoint = 'SP_' . $this->transactionDepth;
			$this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint));

			if ($this->execute())
			{
				$this->transactionDepth++;
			}
		}
	}

	/**
	 * Unlocks tables in the database.
	 *
	 * @return  Sqlite Returns this object to support chaining.
	 *
	 * @throws  RuntimeException
	 * @since   1.0
	 */
	public function unlockTables()
	{
		return $this;
	}

	protected function fetchArray($cursor = null)
	{
		if (!empty($cursor) && $cursor instanceof PDOStatement)
		{
			return $cursor->fetch(PDO::FETCH_NUM);
		}

		if ($this->prepared instanceof PDOStatement)
		{
			return $this->prepared->fetch(PDO::FETCH_NUM);
		}
	}

	protected function fetchObject($cursor = null, $class = 'stdClass')
	{
		if (!empty($cursor) && $cursor instanceof PDOStatement)
		{
			return $cursor->fetchObject($class);
		}

		if ($this->prepared instanceof PDOStatement)
		{
			return $this->prepared->fetchObject($class);
		}
	}
}
Driver/Mysql.php000060400000063445152456704520007637 0ustar00<?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\Driver;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Driver\Query\Mysql as QueryMysql;
use Akeeba\Engine\Factory;
use Exception;
use RuntimeException;

/**
 * MySQL classic driver for Akeeba Engine
 *
 * Based on Joomla! Platform 11.2
 */
#[\AllowDynamicProperties]
class Mysql extends Base
{

	/**
	 * The name of the database driver.
	 *
	 * @var    string
	 * @since  11.1
	 */
	public $name = 'mysql';

	/**
	 * Hostname
	 *
	 * @var   string
	 */
	protected $host;

	/**
	 * The character(s) used to quote SQL statement names such as table names or field names,
	 * etc. The child classes should define this as necessary.  If a single character string the
	 * same character is used for both sides of the quoted name, else the first character will be
	 * used for the opening quote and the second for the closing quote.
	 *
	 * @var    string
	 * @since  11.1
	 */
	protected $nameQuote = '`';

	/**
	 * The null or zero representation of a timestamp for the database driver.  This should be
	 * defined in child classes to hold the appropriate value for the engine.
	 *
	 * @var    string
	 * @since  11.1
	 */
	protected $nullDate = '0000-00-00 00:00:00';

	/**
	 * Password
	 *
	 * @var   string
	 */
	protected $password;

	/**
	 * Should I select a database?
	 *
	 * @var   bool
	 */
	protected $selectDatabase;

	/**
	 * Username
	 *
	 * @var   string
	 */
	protected $user;

	/** @var bool Are we in the process of reconnecting to the database server? */
	private $isReconnecting = false;

	/** @var array|null A cache of the tables contained in the currently connected database */
	public $tablesCache = null;

	/**
	 * Database object constructor
	 *
	 * @param   array  $options  List of options used to configure the connection
	 */
	public function __construct($options)
	{
		$this->driverType = 'mysql';

		// Init
		$this->nameQuote = '`';

		$host     = array_key_exists('host', $options) ? $options['host'] : 'localhost';
		$port     = array_key_exists('port', $options) ? $options['port'] : '';
		$user     = array_key_exists('user', $options) ? $options['user'] : '';
		$password = array_key_exists('password', $options) ? $options['password'] : '';
		$database = array_key_exists('database', $options) ? $options['database'] : '';
		$prefix   = array_key_exists('prefix', $options) ? $options['prefix'] : '';
		$select   = array_key_exists('select', $options) ? $options['select'] : true;

		if (!empty($port))
		{
			$host .= ':' . $port;
		}

		// finalize initialization
		parent::__construct($options);

		// Avoid overwriting connection info if they're already set
		if (is_null($this->host))
		{
			$this->host = $host;
		}

		if (is_null($this->user))
		{
			$this->user = $user;
		}

		if (is_null($this->password))
		{
			$this->password = $password;
		}

		if (is_null($this->_database))
		{
			$this->_database = $database;
		}

		if (is_null($this->selectDatabase))
		{
			$this->selectDatabase = $select;
		}

		// Open the connection
		if (!is_resource($this->connection) || is_null($this->connection))
		{
			$this->open();
		}
	}

	/**
	 * Test to see if the MySQL connector is available.
	 *
	 * @return  boolean  True on success, false otherwise.
	 *
	 * @since   12.1
	 */
	public static function isSupported()
	{
		return (function_exists('mysql_connect'));
	}

	/**
	 * Test to see if the MySQL connector is available.
	 *
	 * @return  boolean  True on success, false otherwise.
	 */
	public static function test()
	{
		return (function_exists('mysql_connect'));
	}

	public function close()
	{
		$return = false;
		if (is_resource($this->cursor))
		{
			mysql_free_result($this->cursor);
		}
		if (is_resource($this->connection) || (!is_null($this->connection) && !is_bool($this->connection)))
		{
			$return = mysql_close($this->connection);
		}
		$this->connection = null;

		return $return;
	}

	/**
	 * Determines if the connection to the server is active.
	 *
	 * @return  boolean  True if connected to the database engine.
	 */
	public function connected()
	{
		if (is_resource($this->connection))
		{
			return @mysql_ping($this->connection);
		}

		return false;
	}

	/**
	 * Drops a table from the database.
	 *
	 * @param   string   $table     The name of the database table to drop.
	 * @param   boolean  $ifExists  Optionally specify that the table must exist before it is dropped.
	 *
	 * @return  Mysql  Returns this object to support chaining.
	 */
	public function dropTable($table, $ifExists = true)
	{
		$query = $this->getQuery(true);

		$this->setQuery('DROP TABLE ' . ($ifExists ? 'IF EXISTS ' : '') . $query->quoteName($table));

		$this->query();

		return $this;
	}

	/**
	 * Method to escape a string for usage in an SQL statement.
	 *
	 * @param   string   $text   The string to be escaped.
	 * @param   boolean  $extra  Optional parameter to provide extra escaping.
	 *
	 * @return  string  The escaped string.
	 */
	public function escape($text, $extra = false)
	{
		if (is_null($text))
		{
			return 'NULL';
		}

		$result = @mysql_real_escape_string($text, $this->getConnection());

		if ($result === false)
		{
			// Attempt to reconnect.
			try
			{
				$this->connection = null;
				$this->open();

				$result = @mysql_real_escape_string($text, $this->getConnection());
			}
			catch (RuntimeException $e)
			{
				$result = $this->unsafe_escape($text);
			}
		}

		if ($extra)
		{
			$result = addcslashes($result, '%_');
		}

		return $result;
	}

	/**
	 * Method to fetch a row from the result set cursor as an associative array.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  mixed  Either the next row from the result set or false if there are no more rows.
	 */
	public function fetchAssoc($cursor = null)
	{
		return mysql_fetch_assoc($cursor ?: $this->cursor);
	}

	/**
	 * Method to free up the memory used for the result set.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  void
	 */
	public function freeResult($cursor = null)
	{
		mysql_free_result($cursor ?: $this->cursor);
	}

	/**
	 * Get the number of affected rows for the previous executed SQL statement.
	 *
	 * @return  integer  The number of affected rows.
	 */
	public function getAffectedRows()
	{
		return mysql_affected_rows($this->connection);
	}

	/**
	 * Method to get the database collation in use by sampling a text field of a table in the database.
	 *
	 * @return  mixed  The collation in use by the database (string) or boolean false if not supported.
	 */
	public function getCollation()
	{
		$this->setQuery('SHOW FULL COLUMNS FROM #__ak_stats');
		$array = $this->loadAssocList();

		return $array['2']['Collation'];
	}

	/**
	 * Get the number of returned rows for the previous executed SQL statement.
	 *
	 * @param   resource  $cursor  An optional database cursor resource to extract the row count from.
	 *
	 * @return  integer   The number of returned rows.
	 */
	public function getNumRows($cursor = null)
	{
		return mysql_num_rows($cursor ?: $this->cursor);
	}

	/**
	 * Get the current or query, or new JDatabaseQuery object.
	 *
	 * @param   boolean  $new  False to return the last query set, True to return a new JDatabaseQuery object.
	 *
	 * @return  mixed  The current value of the internal SQL variable or a new JDatabaseQuery object.
	 */
	public function getQuery($new = false)
	{
		if ($new)
		{
			return new QueryMysql($this);
		}
		else
		{
			return $this->sql;
		}
	}

	public function createQuery()
	{
		return new QueryMysql($this);
	}

	/**
	 * Retrieves field information about a given table.
	 *
	 * @param   string   $table     The name of the database table.
	 * @param   boolean  $typeOnly  True to only return field types.
	 *
	 * @return  array  An array of fields for the database table.
	 */
	public function getTableColumns($table, $typeOnly = true)
	{
		$result = [];

		// Set the query to get the table fields statement.
		$this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($this->escape($table)));
		$fields = $this->loadObjectList();

		// If we only want the type as the value add just that to the list.
		if ($typeOnly)
		{
			foreach ($fields as $field)
			{
				$result[$field->Field] = preg_replace("/[(0-9)]/", '', $field->Type);
			}
		}
		// If we want the whole field data object add that to the list.
		else
		{
			foreach ($fields as $field)
			{
				$result[$field->Field] = $field;
			}
		}

		return $result;
	}

	/**
	 * Shows the table CREATE statement that creates the given tables.
	 *
	 * @param   mixed  $tables  A table name or a list of table names.
	 *
	 * @return  array  A list of the create SQL for the tables.
	 */
	public function getTableCreate($tables)
	{
		// Initialise variables.
		$result = [];

		// Sanitize input to an array and iterate over the list.
		$tables = (array) $tables;
		foreach ($tables as $table)
		{
			// Set the query to get the table CREATE statement.
			$this->setQuery('SHOW CREATE table ' . $this->quoteName($this->escape($table)));
			$row = $this->loadRow();

			// Populate the result array based on the create statements.
			$result[$table] = $row[1];
		}

		return $result;
	}

	/**
	 * Get the details list of keys for a table.
	 *
	 * @param   string  $tables  The name of the table.
	 *
	 * @return  array  An array of the column specification for the table.
	 */
	public function getTableKeys($tables)
	{
		// Get the details columns information.
		$this->setQuery('SHOW KEYS FROM ' . $this->quoteName($tables));
		$keys = $this->loadObjectList();

		return $keys;
	}

	/**
	 * Method to get an array of all tables in the database.
	 *
	 * @return  array  An array of all the tables in the database.
	 */
	public function getTableList()
	{
		// Set the query to get the tables statement.
		$this->setQuery('SHOW TABLES');
		$tables = $this->loadColumn();

		return $tables;
	}

	/**
	 * Returns an array with the names of tables, views, procedures, functions and triggers
	 * in the database. The table names are the keys of the tables, whereas the value is
	 * the type of each element: table, view, merge, temp, procedure, function or trigger.
	 * Note that merge are MRG_MYISAM tables and temp is non-permanent data table, usually
	 * set up as temporary, black hole or federated tables. These two types should never,
	 * ever, have their data dumped in the SQL dump file.
	 *
	 * @param   bool  $abstract  Return abstract or normal names? Defaults to true (abstract names)
	 *
	 * @return array
	 */
	public function getTables($abstract = true)
	{
		if (!empty($this->tablesCache[$this->_database]))
		{
			return $this->tablesCache[$this->_database];
		}

		$sql = "SHOW TABLES";
		$this->setQuery($sql);
		$all_tables = $this->loadColumn();

		if (!empty($all_tables))
		{
			// Start by adding tables and views to the list
			foreach ($all_tables as $table_name)
			{
				if ($abstract)
				{
					$table_name = $this->getAbstract($table_name);
				}
				$this->tablesCache[$this->_database][$table_name] = 'table';
			}

			// Loop all metadatas
			foreach ($all_tables as $table_metadata)
			{
				$table_name     = $table_metadata;
				$table_abstract = $this->getAbstract($table_metadata);
				$type           = 'table';

				if ($abstract)
				{
					$table_metadata = $table_abstract;
				}

				$create = $this->get_create($table_abstract, $table_name, $type);
				// Scan for the table engine.
				$engine = null; // So that we detect VIEWs correctly

				if ($type == 'table')
				{
					$engine      = 'MyISAM'; // So that even with MySQL 4 hosts we don't screw this up
					$engine_keys = ['ENGINE=', 'TYPE='];
					foreach ($engine_keys as $engine_key)
					{
						$start_pos = strrpos($create, $engine_key);
						if ($start_pos !== false)
						{
							// Advance the start position just after the position of the ENGINE keyword
							$start_pos += strlen($engine_key);
							// Try to locate the space after the engine type
							$end_pos = stripos($create, ' ', $start_pos);
							if ($end_pos === false)
							{
								// Uh... maybe it ends with ENGINE=EngineType;
								$end_pos = stripos($create, ';');
							}
							if ($end_pos !== '')
							{
								// Grab the string
								$engine = substr($create, $start_pos, $end_pos - $start_pos);
							}
						}
					}
					$engine = strtoupper($engine);
				}

				switch ($engine)
				{
					// Views -- FIX: They are detected based on their CREATE STATEMENT
					case null:
						$this->tablesCache[$this->_database][$table_metadata] = 'view';
						break;

					// Merge tables
					case 'MRG_MYISAM':
						$this->tablesCache[$this->_database][$table_metadata] = 'merge';
						break;

					// Tables whose data we do not back up (memory, federated and can-have-no-data tables)
					case 'MEMORY':
					case 'EXAMPLE':
					case 'BLACKHOLE':
					case 'FEDERATED':
						$this->tablesCache[$this->_database][$table_metadata] = 'temp';
						break;

					// Normal tables
					default:
						break;
				} // switch
			} // foreach
		} // if !empty

		// If we have MySQL > 5.0 add the list of stored procedures, stored functions
		// and triggers
		$registry        = Factory::getConfiguration();
		$enable_entities = $registry->get('engine.dump.native.advanced_entitites', true);
		if ($enable_entities)
		{
			// 1. Stored procedures
			$sql = "SHOW PROCEDURE STATUS WHERE " . $this->quoteName('Db') . "=" . $this->quote($this->_database);
			$this->setQuery($sql);

			try
			{
				$all_entries = $this->loadAssocList();
			}
			catch (Exception $e)
			{
				$all_entries = [];
			}

			if (is_array($all_entries) || $all_entries instanceof \Countable ? count($all_entries) : 0)
			{
				foreach ($all_entries as $entry)
				{
					$table_name = $entry['Name'];
					if ($abstract)
					{
						$table_name = $this->getAbstract($table_name);
					}
					$this->tablesCache[$this->_database][$table_name] = 'procedure';
				}
			}

			// 2. Stored functions
			$sql = "SHOW FUNCTION STATUS WHERE " . $this->quoteName('Db') . "=" . $this->quote($this->_database);
			$this->setQuery($sql);

			try
			{
				$all_entries = $this->loadColumn(1);
			}
			catch (Exception $e)
			{
				$all_entries = [];
			}

			// If we have filters, make sure the tables pass the filtering
			if (is_array($all_entries))
			{
				if (count($all_entries))
				{
					foreach ($all_entries as $table_name)
					{
						if ($abstract)
						{
							$table_name = $this->getAbstract($table_name);
						}
						$this->tablesCache[$this->_database][$table_name] = 'function';
					}
				}
			}

			// 3. Triggers
			$sql = "SHOW TRIGGERS";
			$this->setQuery($sql);

			try
			{
				$all_entries = $this->loadColumn();
			}
			catch (Exception $e)
			{
				$all_entries = [];
			}

			// If we have filters, make sure the tables pass the filtering
			if (is_array($all_entries))
			{
				if (count($all_entries))
				{
					foreach ($all_entries as $table_name)
					{
						if ($abstract)
						{
							$table_name = $this->getAbstract($table_name);
						}
						$this->tablesCache[$this->_database][$table_name] = 'trigger';
					}
				}
			}

		}

		return $this->tablesCache[$this->_database];
	}

	/**
	 * Get the version of the database connector.
	 *
	 * @return  string  The database connector version.
	 */
	public function getVersion()
	{
		return mysql_get_server_info($this->connection);
	}

	/**
	 * Determines if the database engine supports UTF-8 character encoding.
	 *
	 * @return  boolean  True if supported.
	 */
	public function hasUTF()
	{
		$verParts = explode('.', $this->getVersion());

		return ($verParts[0] == 5 || ($verParts[0] == 4 && $verParts[1] == 1 && (int) $verParts[2] >= 2));
	}

	/**
	 * Method to get the auto-incremented value from the last INSERT statement.
	 *
	 * @return  integer  The value of the auto-increment field from the last inserted row.
	 */
	public function insertid()
	{
		return mysql_insert_id($this->connection);
	}

	/**
	 * Locks a table in the database.
	 *
	 * @param   string  $tableName  The name of the table to unlock.
	 *
	 * @return  Mysql  Returns this object to support chaining.
	 */
	public function lockTable($tableName)
	{
		$this->setQuery('LOCK TABLES ' . $this->quoteName($tableName) . ' WRITE')->query();

		return $this;
	}

	public function open()
	{
		if ($this->connected())
		{
			return;
		}
		else
		{
			$this->close();
		}

		// perform a number of fatality checks, then return gracefully
		if (!function_exists('mysql_connect'))
		{
			$this->errorNum = 1;
			$this->errorMsg = 'The MySQL adapter "mysql" is not available.';

			return;
		}

		if (!($this->connection = @mysql_connect($this->host, $this->user, $this->password, true)))
		{
			$this->errorNum = 2;
			$this->errorMsg = 'Could not connect to MySQL';

			return;
		}

		// Set sql_mode to non_strict mode
		mysql_query("SET @@SESSION.sql_mode = '';", $this->connection);

		// If auto-select is enabled select the given database.
		if ($this->selectDatabase && !empty($this->_database))
		{
			if (!$this->select($this->_database))
			{
				$this->errorNum = 3;
				$this->errorMsg = "Cannot select database {$this->_database}";

				return;
			}
		}

		$this->setUTF();
	}

	/**
	 * Execute the SQL statement.
	 *
	 * @return  mixed  A database cursor resource on success, boolean false on failure.
	 */
	public function query()
	{
		if (!is_resource($this->connection))
		{
			throw new RuntimeException($this->errorMsg, $this->errorNum);
		}

		// Take a local copy so that we don't modify the original query and cause issues later
		$query = $this->replacePrefix((string) $this->sql);
		if ($this->limit > 0 || $this->offset > 0)
		{
			$query .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
		}

		// Increment the query counter.
		$this->count++;

		// If debugging is enabled then let's log the query.
		if ($this->debug)
		{
			// Add the query to the object queue.
			$this->log[] = $query;
		}

		// Reset the error values.
		$this->errorNum = 0;
		$this->errorMsg = '';

		// Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost.
		$this->cursor = @mysql_query($query, $this->connection);

		// If an error occurred handle it.
		if (!$this->cursor)
		{
			// Check if the server was disconnected.
			if (!$this->connected() && !$this->isReconnecting)
			{
				$this->isReconnecting = true;

				try
				{
					// Attempt to reconnect.
					$this->connection = null;
					$this->open();
				}
					// If connect fails, ignore that exception and throw the normal exception.
				catch (RuntimeException $e)
				{
					// Get the error number and message.
					$this->errorNum = (int) mysql_errno($this->connection);
					$this->errorMsg = (string) mysql_error($this->connection) . ' SQL=' . $query;

					// Throw the normal query exception.
					throw new RuntimeException($this->errorMsg, $this->errorNum);
				}

				// Since we were able to reconnect, run the query again.
				$result               = $this->query();
				$this->isReconnecting = false;

				return $result;
			}
			// The server was not disconnected.
			else
			{
				// Get the error number and message.
				$this->errorNum = (int) mysql_errno($this->connection);
				$this->errorMsg = (string) mysql_error($this->connection) . ' SQL=' . $query;

				// Throw the normal query exception.
				if ($this->errorNum != 0)
				{
					throw new RuntimeException($this->errorMsg, $this->errorNum);
				}
			}
		}

		return $this->cursor;
	}

	/**
	 * Renames a table in the database.
	 *
	 * @param   string  $oldTable  The name of the table to be renamed
	 * @param   string  $newTable  The new name for the table.
	 * @param   string  $backup    Not used by MySQL.
	 * @param   string  $prefix    Not used by MySQL.
	 *
	 * @return  Mysql  Returns this object to support chaining.
	 */
	public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
	{
		$this->setQuery('RENAME TABLE ' . $oldTable . ' TO ' . $newTable)->query();

		return $this;
	}

	/**
	 * Select a database for use.
	 *
	 * @param   string  $database  The name of the database to select for use.
	 *
	 * @return  boolean  True if the database was successfully selected.
	 */
	public function select($database)
	{
		if (!$database)
		{
			return false;
		}

		if (!mysql_select_db($database, $this->connection))
		{
			throw new RuntimeException('Could not connect to database');
		}

		return true;
	}

	/**
	 * Set the connection to use UTF-8 character encoding.
	 *
	 * @return  boolean  True on success.
	 */
	public function setUTF()
	{
		$result = false;

		if ($this->supportsUtf8mb4())
		{
			$result = @mysql_set_charset('utf8mb4', $this->connection);
		}

		if (!$result)
		{
			$result = @mysql_set_charset('utf8', $this->connection);
		}

		return $result;
	}

	/**
	 * Method to commit a transaction.
	 *
	 * @return  void
	 */
	public function transactionCommit()
	{
		$this->setQuery('COMMIT');
		$this->execute();
	}

	/**
	 * Method to roll back a transaction.
	 *
	 * @return  void
	 */
	public function transactionRollback()
	{
		$this->setQuery('ROLLBACK');
		$this->execute();
	}

	/**
	 * Method to initialize a transaction.
	 *
	 * @return  void
	 */
	public function transactionStart()
	{
		$this->setQuery('START TRANSACTION');
		$this->execute();
	}

	/**
	 * Unlocks tables in the database.
	 *
	 * @return  Mysql  Returns this object to support chaining.
	 *
	 * @throws  Exception
	 * @since   11.4
	 */
	public function unlockTables()
	{
		$this->setQuery('UNLOCK TABLES')->execute();

		return $this;
	}

	/**
	 * Method to fetch a row from the result set cursor as an array.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  mixed  Either the next row from the result set or false if there are no more rows.
	 */
	protected function fetchArray($cursor = null)
	{
		return mysql_fetch_row($cursor ?: $this->cursor);
	}

	/**
	 * Method to fetch a row from the result set cursor as an object.
	 *
	 * @param   mixed   $cursor  The optional result set cursor from which to fetch the row.
	 * @param   string  $class   The class name to use for the returned row object.
	 *
	 * @return  mixed   Either the next row from the result set or false if there are no more rows.
	 */
	protected function fetchObject($cursor = null, $class = 'stdClass')
	{
		return mysql_fetch_object($cursor ?: $this->cursor, $class);
	}

	/**
	 * Gets the CREATE TABLE command for a given table/view
	 *
	 * @param   string  $table_abstract  The abstracted name of the entity
	 * @param   string  $table_name      The name of the table
	 * @param   string  $type            The type of the entity to scan. If it's found to differ, the correct type is
	 *                                   returned.
	 *
	 * @return string The CREATE command, w/out newlines
	 */
	protected function get_create($table_abstract, $table_name, &$type)
	{
		$sql = "SHOW CREATE TABLE `$table_abstract`";
		$this->setQuery($sql);
		$temp      = $this->loadRowList();
		$table_sql = $temp[0][1];
		unset($temp);

		// Smart table type detection
		if (in_array($type, ['table', 'merge', 'view']))
		{
			// Check for CREATE VIEW
			$pattern = '/^CREATE(.*) VIEW (.*)/i';
			$result  = preg_match($pattern, $table_sql);
			if ($result === 1)
			{
				// This is a view.
				$type = 'view';
			}
			else
			{
				// This is a table.
				$type = 'table';
			}

			// Is it a VIEW but we don't have SHOW VIEW privileges?
			if (empty($table_sql))
			{
				$type = 'view';
			}
		}

		$table_sql = str_replace($table_name, $table_abstract, $table_sql);

		// Replace newlines with spaces
		$table_sql = str_replace("\n", " ", $table_sql) . ";\n";
		$table_sql = str_replace("\r", " ", $table_sql);
		$table_sql = str_replace("\t", " ", $table_sql);

		// Post-process CREATE VIEW
		if ($type == 'view')
		{
			$pos_view = strpos($table_sql, ' VIEW ');

			if ($pos_view > 7)
			{
				// Only post process if there are view properties between the CREATE and VIEW keywords
				$propstring = substr($table_sql, 7, $pos_view - 7); // Properties string
				// Fetch the ALGORITHM={UNDEFINED | MERGE | TEMPTABLE} keyword
				$algostring = '';
				$algo_start = strpos($propstring, 'ALGORITHM=');
				if ($algo_start !== false)
				{
					$algo_end   = strpos($propstring, ' ', $algo_start);
					$algostring = substr($propstring, $algo_start, $algo_end - $algo_start + 1);
				}
				// Create our modified create statement
				$table_sql = 'CREATE OR REPLACE ' . $algostring . substr($table_sql, $pos_view);
			}
		}

		return $table_sql;
	}

	/**
	 * Does this database server support UTF-8 four byte (utf8mb4) collation?
	 *
	 * libmysql supports utf8mb4 since 5.5.3 (same version as the MySQL server). mysqlnd supports utf8mb4 since 5.0.9.
	 *
	 * This method's code is based on WordPress' wpdb::has_cap() method
	 *
	 * @return  bool
	 */
	protected function supportsUtf8mb4()
	{
		$client_version = mysql_get_client_info();

		if (strpos($client_version, 'mysqlnd') !== false)
		{
			$client_version = preg_replace('/^\D+([\d.]+).*/', '$1', $client_version);

			return version_compare($client_version, '5.0.9', '>=');
		}
		else
		{
			return version_compare($client_version, '5.5.3', '>=');
		}
	}

	protected function unsafe_escape($string)
	{
		if (function_exists('mb_ereg_replace'))
		{
			return mb_ereg_replace('[\x00\x0A\x0D\x1A\x22\x27\x5C]', '\\\0', $string);
		}

		return preg_replace('~[\x00\x0A\x0D\x1A\x22\x27\x5C]~u', '\\\$0', $string);
	}
}
Driver/Mysqli.php000060400000036424152456704520010005 0ustar00<?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\Driver;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Driver\Query\Mysqli as QueryMysqli;
use Akeeba\Engine\FixMySQLHostname;
use mysqli_result;
use RuntimeException;

/**
 * MySQL Improved (mysqli) database driver for Akeeba Engine
 *
 * Based on Joomla! Platform 11.2
 */
#[\AllowDynamicProperties]
class Mysqli extends Mysql
{
	use FixMySQLHostname;

	/**
	 * The name of the database driver.
	 *
	 * @var    string
	 * @since  11.1
	 */
	public $name = 'mysqli';

	/** @var \mysqli|null The db connection resource */
	protected $connection = '';

	/** @var mysqli_result|null The database connection cursor from the last query. */
	protected $cursor;

	protected $port;

	protected $socket;

	protected $ssl = [];

	/** @var bool Are we in the process of reconnecting to the database server? */
	private $isReconnecting = false;

	/**
	 * Database object constructor
	 *
	 * @param   array  $options  List of options used to configure the connection
	 */
	public function __construct($options)
	{
		$this->driverType = 'mysql';

		// Init
		$this->nameQuote = '`';

		$options['ssl'] = $options['ssl'] ?? [];
		$options['ssl'] = is_array($options['ssl']) ? $options['ssl'] : [];

		$options['ssl']['enable']             = ($options['ssl']['enable'] ?? $options['dbencryption'] ?? false) ?: false;
		$options['ssl']['cipher']             = ($options['ssl']['cipher'] ?? $options['dbsslcipher'] ?? null) ?: null;
		$options['ssl']['ca']                 = ($options['ssl']['ca'] ?? $options['dbsslca'] ?? null) ?: null;
		$options['ssl']['capath']             = ($options['ssl']['capath'] ?? $options['dbsslcapath'] ?? null) ?: null;
		$options['ssl']['key']                = ($options['ssl']['key'] ?? $options['dbsslkey'] ?? null) ?: null;
		$options['ssl']['cert']               = ($options['ssl']['cert'] ?? $options['dbsslcert'] ?? null) ?: null;
		$options['ssl']['verify_server_cert'] = ($options['ssl']['verify_server_cert'] ?? $options['dbsslverifyservercert'] ?? false) ?: false;

		// Figure out if a port is included in the host name
		$this->fixHostnamePortSocket($options['host'], $options['port'], $options['socket']);

		// Set the information
		$this->host           = $options['host'] ?? 'localhost';
		$this->user           = $options['user'] ?? '';
		$this->password       = $options['password'] ?? '';
		$this->port           = $options['port'] ?? '';
		$this->socket         = $options['socket'] ?? '';
		$this->_database      = $options['database'] ?? '';
		$this->selectDatabase = $options['select'] ?? true;
		$this->ssl            = $options['ssl'] ?? [];

		// Finalize initialization. Also opens the connection.
		parent::__construct($options);
	}

	/**
	 * Test to see if the MySQL connector is available.
	 *
	 * @return  boolean  True on success, false otherwise.
	 */
	public static function isSupported()
	{
		return (function_exists('mysqli_connect'));
	}

	public function close()
	{
		$return = false;

		if (is_object($this->cursor) && ($this->cursor instanceof mysqli_result))
		{
			try
			{
				@$this->cursor->free();
			}
			catch (\Throwable $e)
			{
			}

			$this->cursor = null;
		}

		if (is_object($this->connection) && ($this->connection instanceof \mysqli))
		{
			try
			{
				$return = @$this->connection->close();
			}
			catch (\Throwable $e)
			{
				$return = false;
			}
		}

		$this->connection = null;

		return $return;
	}

	/**
	 * Determines if the connection to the server is active.
	 *
	 * @return  boolean  True if connected to the database engine.
	 */
	public function connected()
	{
		if (!is_object($this->connection))
		{
			return false;
		}

		// mysqli_ping is deprecated since PHP 8.4.
		if (version_compare(PHP_VERSION, '8.4.0', '<'))
		{
			try
			{
				return @mysqli_ping($this->connection);
			}
			catch (\Throwable $e)
			{
				return false;
			}
		}

		try
		{
			$cursor = @mysqli_query($this->connection, 'SELECT 1');

			if (!$cursor)
			{
				return false;
			}

			mysqli_free_result($cursor);

			return true;
		}
		catch (\Throwable $e)
		{
			return false;
		}
	}

	/**
	 * Method to escape a string for usage in an SQL statement.
	 *
	 * @param   string   $text   The string to be escaped.
	 * @param   boolean  $extra  Optional parameter to provide extra escaping.
	 *
	 * @return  string  The escaped string.
	 */
	public function escape($text, $extra = false)
	{
		if (is_null($text))
		{
			return 'NULL';
		}

		$result = @mysqli_real_escape_string($this->getConnection(), $text);

		if ($result === false)
		{
			// Attempt to reconnect.
			try
			{
				$this->connection = null;
				$this->open();

				$result = @mysqli_real_escape_string($this->getConnection(), $text);;
			}
			catch (RuntimeException $e)
			{
				$result = $this->unsafe_escape($text);
			}
		}

		if ($extra)
		{
			$result = addcslashes($result, '%_');
		}

		return $result;
	}

	/**
	 * Method to fetch a row from the result set cursor as an associative array.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  mixed  Either the next row from the result set or false if there are no more rows.
	 */
	public function fetchAssoc($cursor = null)
	{
		return mysqli_fetch_assoc($cursor ?: $this->cursor);
	}

	/**
	 * Method to free up the memory used for the result set.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  void
	 */
	public function freeResult($cursor = null)
	{
		mysqli_free_result($cursor ?: $this->cursor);
	}

	/**
	 * Get the number of affected rows for the previous executed SQL statement.
	 *
	 * @return  integer  The number of affected rows.
	 */
	public function getAffectedRows()
	{
		return mysqli_affected_rows($this->connection);
	}

	/**
	 * Get the number of returned rows for the previous executed SQL statement.
	 *
	 * @param   mysqli_result  $cursor  An optional database cursor resource to extract the row count from.
	 *
	 * @return  integer   The number of returned rows.
	 */
	public function getNumRows($cursor = null)
	{
		return mysqli_num_rows($cursor ?: $this->cursor);
	}

	/**
	 * Get the current or query, or new JDatabaseQuery object.
	 *
	 * @param   boolean  $new  False to return the last query set, True to return a new JDatabaseQuery object.
	 *
	 * @return  mixed  The current value of the internal SQL variable or a new JDatabaseQuery object.
	 */
	public function getQuery($new = false)
	{
		if ($new)
		{
			return new QueryMysqli($this);
		}
		else
		{
			return $this->sql;
		}
	}

	public function createQuery()
	{
		return new QueryMysqli($this);
	}

	/**
	 * Get the version of the database connector.
	 *
	 * @return  string  The database connector version.
	 */
	public function getVersion()
	{
		return mysqli_get_server_info($this->connection);
	}

	/**
	 * Determines if the database engine supports UTF-8 character encoding.
	 *
	 * @return  boolean  True if supported.
	 */
	public function hasUTF()
	{
		$mariadb = stripos($this->connection->server_info, 'mariadb') !== false;
		$client_version = mysqli_get_client_info();
		$server_version = $this->getVersion();

		if (version_compare($server_version, '5.5.3', '<'))
		{
			return false;
		}

		if ($mariadb && version_compare($server_version, '10.0.0', '<'))
		{
			return false;
		}

		if (strpos($client_version, 'mysqlnd') !== false)
		{
			$client_version = preg_replace('/^\D+([\d.]+).*/', '$1', $client_version);

			return version_compare($client_version, '5.0.9', '>=');
		}

		return version_compare($client_version, '5.5.3', '>=');
	}

	/**
	 * Method to get the auto-incremented value from the last INSERT statement.
	 *
	 * @return  integer  The value of the auto-increment field from the last inserted row.
	 */
	public function insertid()
	{
		return mysqli_insert_id($this->connection);
	}

	public function open()
	{
		if ($this->connected())
		{
			return;
		}
		else
		{
			$this->close();
		}

		// perform a number of fatality checks, then return gracefully
		if (!function_exists('mysqli_connect'))
		{
			$this->errorNum = 1;
			$this->errorMsg = 'The MySQL adapter "mysqli" is not available.';

			return;
		}

		// Let's prepare a connection
		$this->connection = mysqli_init();

		$connectionFlags = 0;

		// For SSL/TLS connection encryption.
		if ($this->ssl !== [] && $this->ssl['enable'] === true)
		{
			$connectionFlags = $connectionFlags | MYSQLI_CLIENT_SSL;

			// Verify server certificate is only available in PHP 5.6.16+. See https://www.php.net/ChangeLog-5.php#5.6.16
			if (isset($this->ssl['verify_server_cert']))
			{
				// New constants in PHP 5.6.16+. See https://www.php.net/ChangeLog-5.php#5.6.16
				if ($this->ssl['verify_server_cert'] === true && defined('MYSQLI_CLIENT_SSL_VERIFY_SERVER_CERT'))
				{
					$connectionFlags = $connectionFlags | MYSQLI_CLIENT_SSL_VERIFY_SERVER_CERT;
				}
				elseif ($this->ssl['verify_server_cert'] === false && defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT'))
				{
					$connectionFlags = $connectionFlags | MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
				}
				elseif (defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT'))
				{
					$this->connection->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, $this->ssl['verify_server_cert']);
				}
			}

			// Add SSL/TLS options only if changed.
			$this->connection->ssl_set(
				($this->ssl['key'] ?? null) ?: null,
				($this->ssl['cert'] ?? null) ?: null,
				($this->ssl['ca'] ?? null) ?: null,
				($this->ssl['capath'] ?? null) ?: null,
				($this->ssl['cipher'] ?? null) ?: null
			);
		}

		// Attempt to connect to the server, use error suppression to silence warnings and allow us to throw an Exception separately.
		try
		{
			$connected = @$this->connection->real_connect(
				$this->host,
				$this->user,
				$this->password ?: null,
				null,
				$this->port ?: 3306,
				$this->socket ?: null,
				$connectionFlags
			);
		}
		catch (\Throwable $e)
		{
			$connected = false;
		}

		// connect to the server
		if (!$connected)
		{
			$this->errorNum = 2;
			$this->errorMsg = 'Could not connect to MySQL';

			return;
		}

		// Set sql_mode to non_strict mode
		mysqli_query($this->connection, "SET @@SESSION.sql_mode = '';");

		if ($this->selectDatabase && !empty($this->_database))
		{
			if (!$this->select($this->_database))
			{
				$this->errorNum = 3;
				$this->errorMsg = "Cannot select database {$this->_database}";

				return;
			}
		}

		$this->setUTF();
	}

	/**
	 * Execute the SQL statement.
	 *
	 * @return  mixed  A database cursor resource on success, boolean false on failure.
	 */
	public function query()
	{
		$this->open();

		if (!is_object($this->connection))
		{
			throw new RuntimeException($this->errorMsg, $this->errorNum);
		}

		// Take a local copy so that we don't modify the original query and cause issues later
		$query = $this->replacePrefix((string) $this->sql);
		if ($this->limit > 0 || $this->offset > 0)
		{
			$query .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
		}

		// Increment the query counter.
		$this->count++;

		// If debugging is enabled then let's log the query.
		if ($this->debug)
		{
			// Add the query to the object queue.
			$this->log[] = $query;
		}

		// Reset the error values.
		$this->errorNum = 0;
		$this->errorMsg = '';

		// Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost.
		$this->cursor = @mysqli_query($this->connection, $query);

		// If an error occurred handle it.
		if (!$this->cursor)
		{
			$this->errorNum = 0;
			$this->errorMsg = '';

			if ($this->connection)
			{
				$this->errorNum = (int) @mysqli_errno($this->connection);
				$this->errorMsg = (string) @mysqli_error($this->connection) . ' SQL=' . $query;
			}

			// Check if the server was disconnected.
			if (!$this->connected() && !$this->isReconnecting)
			{
				$this->isReconnecting = true;

				try
				{
					// Attempt to reconnect.
					$this->connection = null;
					$this->open();
				}
					// If connect fails, ignore that exception and throw the normal exception.
				catch (RuntimeException $e)
				{
					throw new RuntimeException($this->errorMsg, $this->errorNum);
				}

				// Since we were able to reconnect, run the query again.
				$result               = $this->query();
				$this->isReconnecting = false;

				return $result;
			}
			// The server was not disconnected.
			elseif ($this->errorNum != 0)
			{
				throw new RuntimeException($this->errorMsg, $this->errorNum);
			}
		}

		return $this->cursor;
	}

	/**
	 * Select a database for use.
	 *
	 * @param   string  $database  The name of the database to select for use.
	 *
	 * @return  boolean  True if the database was successfully selected.
	 */
	public function select($database)
	{
		if (!$database)
		{
			return false;
		}

		if (!mysqli_select_db($this->connection, $database))
		{
			return false;
		}

		return true;
	}

	/**
	 * Set the connection to use UTF-8 character encoding.
	 *
	 * @return  boolean  True on success.
	 */
	public function setUTF()
	{
		$result = false;

		if ($this->supportsUtf8mb4())
		{
			$result = @mysqli_set_charset($this->connection, 'utf8mb4');
		}

		if (!$result)
		{
			$result = @mysqli_set_charset($this->connection, 'utf8');
		}

		return $result;

	}

	/**
	 * Does this database server support UTF-8 four byte (utf8mb4) collation?
	 *
	 * libmysql supports utf8mb4 since 5.5.3 (same version as the MySQL server). mysqlnd supports utf8mb4 since 5.0.9.
	 *
	 * This method's code is based on WordPress' wpdb::has_cap() method
	 *
	 * @return  bool
	 */
	public function supportsUtf8mb4()
	{
		$client_version = mysqli_get_client_info();

		if (strpos($client_version, 'mysqlnd') !== false)
		{
			$client_version = preg_replace('/^\D+([\d.]+).*/', '$1', $client_version);

			return version_compare($client_version, '5.0.9', '>=');
		}
		else
		{
			return version_compare($client_version, '5.5.3', '>=');
		}
	}

	/**
	 * Method to fetch a row from the result set cursor as an array.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  mixed  Either the next row from the result set or false if there are no more rows.
	 */
	protected function fetchArray($cursor = null)
	{
		return mysqli_fetch_row($cursor ?: $this->cursor);
	}

	/**
	 * Method to fetch a row from the result set cursor as an object.
	 *
	 * @param   mixed   $cursor  The optional result set cursor from which to fetch the row.
	 * @param   string  $class   The class name to use for the returned row object.
	 *
	 * @return  mixed   Either the next row from the result set or false if there are no more rows.
	 */
	protected function fetchObject($cursor = null, $class = 'stdClass')
	{
		return mysqli_fetch_object($cursor ?: $this->cursor, $class);
	}
}
Driver/QueryException.php000060400000001657152456704520011513 0ustar00<?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\Driver;

defined('AKEEBAENGINE') || die();

use Exception;

class QueryException extends Exception
{
}
Driver/Query/Base.php000060400000113371152456704520010503 0ustar00<?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\Driver\Query;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Driver\Base as DriverBase;
use Akeeba\Engine\Driver\Query\Element as QueryElement;
use Akeeba\Engine\Driver\Query\Limitable as QueryLimitable;
use Akeeba\Engine\Driver\QueryException;

/**
 * Query Building Class.
 *
 * Based on Joomla! Platform 11.3
 */
abstract class Base
{
	/**
	 * @var    DriverBase  The database connection resource.
	 */
	protected $db = null;

	/**
	 * @var    string  The SQL query (if a direct query string was provided).
	 */
	protected $sql = null;

	/**
	 * @var    string  The query type.
	 */
	protected $type = '';

	/**
	 * @var    QueryElement  The query element for a generic query (type = null).
	 */
	protected $element = null;

	/**
	 * @var    QueryElement  The select element.
	 */
	protected $select = null;

	/**
	 * @var    QueryElement  The delete element.
	 */
	protected $delete = null;

	/**
	 * @var    QueryElement  The update element.
	 */
	protected $update = null;

	/**
	 * @var    QueryElement  The insert element.
	 */
	protected $insert = null;

	/**
	 * @var    QueryElement  The from element.
	 */
	protected $from = null;

	/**
	 * @var    QueryElement  The join element.
	 */
	protected $join = null;

	/**
	 * @var    QueryElement  The set element.
	 */
	protected $set = null;

	/**
	 * @var    QueryElement  The where element.
	 */
	protected $where = null;

	/**
	 * @var    QueryElement  The group by element.
	 */
	protected $group = null;

	/**
	 * @var    QueryElement  The having element.
	 */
	protected $having = null;

	/**
	 * @var    QueryElement  The column list for an INSERT statement.
	 */
	protected $columns = null;

	/**
	 * @var    QueryElement  The values list for an INSERT statement.
	 */
	protected $values = null;

	/**
	 * @var    QueryElement  The order element.
	 */
	protected $order = null;

	/**
	 * @var   object  The auto increment insert field element.
	 */
	protected $autoIncrementField = null;

	/**
	 * @var    QueryElement  The call element.
	 */
	protected $call = null;

	/**
	 * @var    QueryElement  The exec element.
	 */
	protected $exec = null;

	/**
	 * @var    QueryElement  The union element.
	 */
	protected $union = null;

	/**
	 * @var    QueryElement  The unionAll element.
	 */
	protected $unionAll = null;

	/**
	 * Class constructor.
	 *
	 * @param   DriverBase  $db  The database connector resource.
	 */
	public function __construct(DriverBase $db = null)
	{
		$this->db = $db;
	}

	/**
	 * Magic method to provide method alias support for quote() and quoteName().
	 *
	 * @param   string  $method  The called method.
	 * @param   array   $args    The array of arguments passed to the method.
	 *
	 * @return  string  The aliased method's return value or null.
	 */
	public function __call($method, $args)
	{
		if (empty($args))
		{
			return null;
		}

		switch ($method)
		{
			case 'q':
				return $this->quote($args[0], $args[1] ?? true);
				break;

			case 'qn':
				return $this->quoteName($args[0]);
				break;

			case 'e':
				return $this->escape($args[0], $args[1] ?? false);
				break;
		}

		return null;
	}

	/**
	 * Magic function to convert the query to a string.
	 *
	 * @return  string    The completed query.
	 */
	public function __toString()
	{
		$query = '';

		if ($this->sql)
		{
			return $this->sql;
		}

		switch ($this->type)
		{
			case 'element':
				$query .= (string) $this->element;
				break;

			case 'select':
				$query .= (string) $this->select;
				$query .= (string) $this->from;

				if ($this->join)
				{
					// Special case for joins
					foreach ($this->join as $join)
					{
						$query .= (string) $join;
					}
				}

				if ($this->where)
				{
					$query .= (string) $this->where;
				}

				if ($this->group)
				{
					$query .= (string) $this->group;
				}

				if ($this->having)
				{
					$query .= (string) $this->having;
				}

				if ($this->order)
				{
					$query .= (string) $this->order;
				}

				break;

			case 'union':
				$query .= (string) $this->union;
				break;

			case 'unionAll':
				$query .= (string) $this->unionAll;
				break;

			case 'delete':
				$query .= (string) $this->delete;
				$query .= (string) $this->from;

				if ($this->join)
				{
					// Special case for joins
					foreach ($this->join as $join)
					{
						$query .= (string) $join;
					}
				}

				if ($this->where)
				{
					$query .= (string) $this->where;
				}

				break;

			case 'update':
				$query .= (string) $this->update;

				if ($this->join)
				{
					// Special case for joins
					foreach ($this->join as $join)
					{
						$query .= (string) $join;
					}
				}

				$query .= (string) $this->set;

				if ($this->where)
				{
					$query .= (string) $this->where;
				}

				break;

			case 'insert':
				$query .= (string) $this->insert;

				// Set method
				if ($this->set)
				{
					$query .= (string) $this->set;
				}
				// Columns-Values method
				elseif ($this->values)
				{
					if ($this->columns)
					{
						$query .= (string) $this->columns;
					}

					$elements = $this->values->getElements();

					if (!($elements[0] instanceof $this))
					{
						$query .= ' VALUES ';
					}

					$query .= (string) $this->values;
				}

				break;

			case 'call':
				$query .= (string) $this->call;
				break;

			case 'exec':
				$query .= (string) $this->exec;
				break;
		}

		if ($this instanceof QueryLimitable)
		{
			$query = $this->processLimit($query, $this->limit, $this->offset);
		}

		return $query;
	}

	/**
	 * Magic function to get protected variable value
	 *
	 * @param   string  $name  The name of the variable.
	 *
	 * @return  mixed
	 */
	public function __get($name)
	{
		return $this->$name ?? null;
	}

	/**
	 * Add a single column, or array of columns to the CALL clause of the query.
	 *
	 * Note that you must not mix insert, update, delete and select method calls when building a query.
	 * The call method can, however, be called multiple times in the same query.
	 *
	 * Usage:
	 * $query->call('a.*')->call('b.id');
	 * $query->call(array('a.*', 'b.id'));
	 *
	 * @param   mixed  $columns  A string or an array of field names.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function call($columns)
	{
		$this->type = 'call';

		if (is_null($this->call))
		{
			$this->call = new QueryElement('CALL', $columns);
		}
		else
		{
			$this->call->append($columns);
		}

		return $this;
	}

	/**
	 * Casts a value to a char.
	 *
	 * Ensure that the value is properly quoted before passing to the method.
	 *
	 * Usage:
	 * $query->select($query->castAsChar('a'));
	 *
	 * @param   string  $value  The value to cast as a char.
	 *
	 * @return  string  Returns the cast value.
	 */
	public function castAsChar($value)
	{
		return $value;
	}

	/**
	 * Gets the number of characters in a string.
	 *
	 * Note, use 'length' to find the number of bytes in a string.
	 *
	 * Usage:
	 * $query->select($query->charLength('a'));
	 *
	 * @param   string  $field      A value.
	 * @param   string  $operator   Comparison operator between charLength integer value and $condition
	 * @param   string  $condition  Integer value to compare charLength with.
	 *
	 * @return  string  The required char length call.
	 */
	public function charLength($field, $operator = null, $condition = null)
	{
		return 'CHAR_LENGTH(' . $field . ')' . (isset($operator) && isset($condition) ? ' ' . $operator . ' ' . $condition : '');
	}

	/**
	 * Clear data from the query or a specific clause of the query.
	 *
	 * @param   string  $clause  Optionally, the name of the clause to clear, or nothing to clear the whole query.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function clear($clause = null)
	{
		$this->sql = null;

		switch ($clause)
		{
			case 'select':
				$this->select = null;
				$this->type   = null;
				break;

			case 'delete':
				$this->delete = null;
				$this->type   = null;
				break;

			case 'update':
				$this->update = null;
				$this->type   = null;
				break;

			case 'insert':
				$this->insert             = null;
				$this->type               = null;
				$this->autoIncrementField = null;
				break;

			case 'from':
				$this->from = null;
				break;

			case 'join':
				$this->join = null;
				break;

			case 'set':
				$this->set = null;
				break;

			case 'where':
				$this->where = null;
				break;

			case 'group':
				$this->group = null;
				break;

			case 'having':
				$this->having = null;
				break;

			case 'order':
				$this->order = null;
				break;

			case 'columns':
				$this->columns = null;
				break;

			case 'values':
				$this->values = null;
				break;

			case 'exec':
				$this->exec = null;
				$this->type = null;
				break;

			case 'call':
				$this->call = null;
				$this->type = null;
				break;

			case 'limit':
				$this->offset = 0;
				$this->limit  = 0;
				break;

			case 'union':
				$this->union = null;
				break;

			case 'unionAll':
				$this->unionAll = null;
				break;

			default:
				$this->type               = null;
				$this->select             = null;
				$this->delete             = null;
				$this->update             = null;
				$this->insert             = null;
				$this->from               = null;
				$this->join               = null;
				$this->set                = null;
				$this->where              = null;
				$this->group              = null;
				$this->having             = null;
				$this->order              = null;
				$this->columns            = null;
				$this->values             = null;
				$this->autoIncrementField = null;
				$this->exec               = null;
				$this->call               = null;
				$this->union              = null;
				$this->unionAll           = null;
				$this->offset             = 0;
				$this->limit              = 0;
				break;
		}

		return $this;
	}

	/**
	 * Adds a column, or array of column names that would be used for an INSERT INTO statement.
	 *
	 * @param   mixed  $columns  A column name, or array of column names.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function columns($columns)
	{
		if (is_null($this->columns))
		{
			$this->columns = new QueryElement('()', $columns);
		}
		else
		{
			$this->columns->append($columns);
		}

		return $this;
	}

	/**
	 * Concatenates an array of column names or values.
	 *
	 * Usage:
	 * $query->select($query->concatenate(array('a', 'b')));
	 *
	 * @param   array   $values     An array of values to concatenate.
	 * @param   string  $separator  As separator to place between each value.
	 *
	 * @return  string  The concatenated values.
	 */
	public function concatenate($values, $separator = null)
	{
		if ($separator)
		{
			return 'CONCATENATE(' . implode(' || ' . $this->quote($separator) . ' || ', $values) . ')';
		}
		else
		{
			return 'CONCATENATE(' . implode(' || ', $values) . ')';
		}
	}

	/**
	 * Gets the current date and time.
	 *
	 * Usage:
	 * $query->where('published_up < '.$query->currentTimestamp());
	 *
	 * @return  string
	 */
	public function currentTimestamp()
	{
		return 'CURRENT_TIMESTAMP()';
	}

	/**
	 * Returns a PHP date() function compliant date format for the database driver.
	 *
	 * This method is provided for use where the query object is passed to a function for modification.
	 * If you have direct access to the database object, it is recommended you use the getDateFormat method directly.
	 *
	 * @return  string  The format string.
	 */
	public function dateFormat()
	{
		if (!($this->db instanceof DriverBase))
		{
			throw new QueryException('Invalid database object');
		}

		return $this->db->getDateFormat();
	}

	/**
	 * Creates a formatted dump of the query for debugging purposes.
	 *
	 * Usage:
	 * echo $query->dump();
	 *
	 * @return  string
	 */
	public function dump()
	{
		return '<pre class="AkeebaEngineQuery">' . str_replace('#__', $this->db->getPrefix(), $this) . '</pre>';
	}

	/**
	 * Add a table name to the DELETE clause of the query.
	 *
	 * Note that you must not mix insert, update, delete and select method calls when building a query.
	 *
	 * Usage:
	 * $query->delete('#__a')->where('id = 1');
	 *
	 * @param   string  $table  The name of the table to delete from.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function delete($table = null)
	{
		$this->type   = 'delete';
		$this->delete = new QueryElement('DELETE', null);

		if (!empty($table))
		{
			$this->from($table);
		}

		return $this;
	}

	/**
	 * Method to escape a string for usage in an SQL statement.
	 *
	 * This method is provided for use where the query object is passed to a function for modification.
	 * If you have direct access to the database object, it is recommended you use the escape method directly.
	 *
	 * Note that 'e' is an alias for this method as it is in DriverBase.
	 *
	 * @param   string   $text   The string to be escaped.
	 * @param   boolean  $extra  Optional parameter to provide extra escaping.
	 *
	 * @return  string  The escaped string.
	 */
	public function escape($text, $extra = false)
	{
		if (!($this->db instanceof DriverBase))
		{
			throw new QueryException('Invalid database object');
		}

		return $this->db->escape($text, $extra);
	}

	/**
	 * Add a single column, or array of columns to the EXEC clause of the query.
	 *
	 * Note that you must not mix insert, update, delete and select method calls when building a query.
	 * The exec method can, however, be called multiple times in the same query.
	 *
	 * Usage:
	 * $query->exec('a.*')->exec('b.id');
	 * $query->exec(array('a.*', 'b.id'));
	 *
	 * @param   mixed  $columns  A string or an array of field names.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function exec($columns)
	{
		$this->type = 'exec';

		if (is_null($this->exec))
		{
			$this->exec = new QueryElement('EXEC', $columns);
		}
		else
		{
			$this->exec->append($columns);
		}

		return $this;
	}

	/**
	 * Add a table to the FROM clause of the query.
	 *
	 * Note that while an array of tables can be provided, it is recommended you use explicit joins.
	 *
	 * Usage:
	 * $query->select('*')->from('#__a');
	 *
	 * @param   mixed   $tables         A string or array of table names.
	 *                                  This can be a Base object (or a child of it) when used
	 *                                  as a subquery in FROM clause along with a value for $subQueryAlias.
	 * @param   string  $subQueryAlias  Alias used when $tables is a Base.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function from($tables, $subQueryAlias = null)
	{
		if (is_null($this->from))
		{
			if ($tables instanceof $this)
			{
				if (is_null($subQueryAlias))
				{
					throw new QueryException('Null subquery defined');
				}

				$tables = '( ' . (string) $tables . ' ) AS ' . $this->quoteName($subQueryAlias);
			}

			$this->from = new QueryElement('FROM', $tables);
		}
		else
		{
			$this->from->append($tables);
		}

		return $this;
	}

	/**
	 * Used to get a string to extract year from date column.
	 *
	 * Usage:
	 * $query->select($query->year($query->quoteName('dateColumn')));
	 *
	 * @param   string  $date  Date column containing year to be extracted.
	 *
	 * @return  string  Returns string to extract year from a date.
	 */
	public function year($date)
	{
		return 'YEAR(' . $date . ')';
	}

	/**
	 * Used to get a string to extract month from date column.
	 *
	 * Usage:
	 * $query->select($query->month($query->quoteName('dateColumn')));
	 *
	 * @param   string  $date  Date column containing month to be extracted.
	 *
	 * @return  string  Returns string to extract month from a date.
	 */
	public function month($date)
	{
		return 'MONTH(' . $date . ')';
	}

	/**
	 * Used to get a string to extract day from date column.
	 *
	 * Usage:
	 * $query->select($query->day($query->quoteName('dateColumn')));
	 *
	 * @param   string  $date  Date column containing day to be extracted.
	 *
	 * @return  string  Returns string to extract day from a date.
	 */
	public function day($date)
	{
		return 'DAY(' . $date . ')';
	}

	/**
	 * Used to get a string to extract hour from date column.
	 *
	 * Usage:
	 * $query->select($query->hour($query->quoteName('dateColumn')));
	 *
	 * @param   string  $date  Date column containing hour to be extracted.
	 *
	 * @return  string  Returns string to extract hour from a date.
	 */
	public function hour($date)
	{
		return 'HOUR(' . $date . ')';
	}

	/**
	 * Used to get a string to extract minute from date column.
	 *
	 * Usage:
	 * $query->select($query->minute($query->quoteName('dateColumn')));
	 *
	 * @param   string  $date  Date column containing minute to be extracted.
	 *
	 * @return  string  Returns string to extract minute from a date.
	 */
	public function minute($date)
	{
		return 'MINUTE(' . $date . ')';
	}

	/**
	 * Used to get a string to extract seconds from date column.
	 *
	 * Usage:
	 * $query->select($query->second($query->quoteName('dateColumn')));
	 *
	 * @param   string  $date  Date column containing second to be extracted.
	 *
	 * @return  string  Returns string to extract second from a date.
	 */
	public function second($date)
	{
		return 'SECOND(' . $date . ')';
	}

	/**
	 * Add a grouping column to the GROUP clause of the query.
	 *
	 * Usage:
	 * $query->group('id');
	 *
	 * @param   mixed  $columns  A string or array of ordering columns.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function group($columns)
	{
		if (is_null($this->group))
		{
			$this->group = new QueryElement('GROUP BY', $columns);
		}
		else
		{
			$this->group->append($columns);
		}

		return $this;
	}

	/**
	 * A conditions to the HAVING clause of the query.
	 *
	 * Usage:
	 * $query->group('id')->having('COUNT(id) > 5');
	 *
	 * @param   mixed   $conditions  A string or array of columns.
	 * @param   string  $glue        The glue by which to join the conditions. Defaults to AND.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function having($conditions, $glue = 'AND')
	{
		if (is_null($this->having))
		{
			$glue         = strtoupper($glue);
			$this->having = new QueryElement('HAVING', $conditions, " $glue ");
		}
		else
		{
			$this->having->append($conditions);
		}

		return $this;
	}

	/**
	 * Add an INNER JOIN clause to the query.
	 *
	 * Usage:
	 * $query->innerJoin('b ON b.id = a.id')->innerJoin('c ON c.id = b.id');
	 *
	 * @param   string  $condition  The join condition.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function innerJoin($condition)
	{
		$this->join('INNER', $condition);

		return $this;
	}

	/**
	 * Add a table name to the INSERT clause of the query.
	 *
	 * Note that you must not mix insert, update, delete and select method calls when building a query.
	 *
	 * Usage:
	 * $query->insert('#__a')->set('id = 1');
	 * $query->insert('#__a')->columns('id, title')->values('1,2')->values('3,4');
	 * $query->insert('#__a')->columns('id, title')->values(array('1,2', '3,4'));
	 *
	 * @param   mixed    $table           The name of the table to insert data into.
	 * @param   boolean  $incrementField  The name of the field to auto increment.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function insert($table, $incrementField = false)
	{
		$this->type               = 'insert';
		$this->insert             = new QueryElement('INSERT INTO', $table);
		$this->autoIncrementField = $incrementField;

		return $this;
	}

	/**
	 * Add a JOIN clause to the query.
	 *
	 * Usage:
	 * $query->join('INNER', 'b ON b.id = a.id);
	 *
	 * @param   string  $type        The type of join. This string is prepended to the JOIN keyword.
	 * @param   string  $conditions  A string or array of conditions.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function join($type, $conditions)
	{
		if (is_null($this->join))
		{
			$this->join = [];
		}
		$this->join[] = new QueryElement(strtoupper($type) . ' JOIN', $conditions);

		return $this;
	}

	/**
	 * Add a LEFT JOIN clause to the query.
	 *
	 * Usage:
	 * $query->leftJoin('b ON b.id = a.id')->leftJoin('c ON c.id = b.id');
	 *
	 * @param   string  $condition  The join condition.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function leftJoin($condition)
	{
		$this->join('LEFT', $condition);

		return $this;
	}

	/**
	 * Get the length of a string in bytes.
	 *
	 * Note, use 'charLength' to find the number of characters in a string.
	 *
	 * Usage:
	 * query->where($query->length('a').' > 3');
	 *
	 * @param   string  $value  The string to measure.
	 *
	 * @return  int
	 */
	public function length($value)
	{
		return 'LENGTH(' . $value . ')';
	}

	/**
	 * Get the null or zero representation of a timestamp for the database driver.
	 *
	 * This method is provided for use where the query object is passed to a function for modification.
	 * If you have direct access to the database object, it is recommended you use the nullDate method directly.
	 *
	 * Usage:
	 * $query->where('modified_date <> '.$query->nullDate());
	 *
	 * @param   boolean  $quoted  Optionally wraps the null date in database quotes (true by default).
	 *
	 * @return  string  Null or zero representation of a timestamp.
	 */
	public function nullDate($quoted = true)
	{
		if (!($this->db instanceof DriverBase))
		{
			throw new QueryException('Invalid database object');
		}

		$result = $this->db->getNullDate();

		if ($quoted)
		{
			return $this->db->quote($result);
		}

		return $result;
	}

	/**
	 * Add a ordering column to the ORDER clause of the query.
	 *
	 * Usage:
	 * $query->order('foo')->order('bar');
	 * $query->order(array('foo','bar'));
	 *
	 * @param   mixed  $columns  A string or array of ordering columns.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function order($columns)
	{
		if (is_null($this->order))
		{
			$this->order = new QueryElement('ORDER BY', $columns);
		}
		else
		{
			$this->order->append($columns);
		}

		return $this;
	}

	/**
	 * Add an OUTER JOIN clause to the query.
	 *
	 * Usage:
	 * $query->outerJoin('b ON b.id = a.id')->outerJoin('c ON c.id = b.id');
	 *
	 * @param   string  $condition  The join condition.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function outerJoin($condition)
	{
		$this->join('OUTER', $condition);

		return $this;
	}

	/**
	 * Method to quote and optionally escape a string to database requirements for insertion into the database.
	 *
	 * This method is provided for use where the query object is passed to a function for modification.
	 * If you have direct access to the database object, it is recommended you use the quote method directly.
	 *
	 * Note that 'q' is an alias for this method as it is in DriverBase.
	 *
	 * Usage:
	 * $query->quote('fulltext');
	 * $query->q('fulltext');
	 * $query->q(array('option', 'fulltext'));
	 *
	 * @param   mixed    $text    A string or an array of strings to quote.
	 * @param   boolean  $escape  True to escape the string, false to leave it unchanged.
	 *
	 * @return  string  The quoted input string.
	 *
	 * @throws  QueryException if the internal db property is not a valid object.
	 */
	public function quote($text, $escape = true)
	{
		if (!($this->db instanceof DriverBase))
		{
			throw new QueryException('Invalid database object');
		}

		return $this->db->quote($text, $escape);
	}

	/**
	 * Wrap an SQL statement identifier name such as column, table or database names in quotes to prevent injection
	 * risks and reserved word conflicts.
	 *
	 * This method is provided for use where the query object is passed to a function for modification.
	 * If you have direct access to the database object, it is recommended you use the quoteName method directly.
	 *
	 * Note that 'qn' is an alias for this method as it is in DriverBase.
	 *
	 * Usage:
	 * $query->quoteName('#__a');
	 * $query->qn('#__a');
	 *
	 * @param   mixed  $name  The identifier name to wrap in quotes, or an array of identifier names to wrap in quotes.
	 *                        Each type supports dot-notation name.
	 * @param   mixed  $as    The AS query part associated to $name. It can be string or array, in latter case it has
	 *                        to be same length of $name; if is null there will not be any AS part for string or array
	 *                        element.
	 *
	 * @return  mixed  The quote wrapped name, same type of $name.
	 *
	 * @throws  QueryException if the internal db property is not a valid object.
	 */
	public function quoteName($name, $as = null)
	{
		if (!($this->db instanceof DriverBase))
		{
			throw new QueryException('Invalid database object');
		}

		return $this->db->quoteName($name, $as);
	}

	/**
	 * Add a RIGHT JOIN clause to the query.
	 *
	 * Usage:
	 * $query->rightJoin('b ON b.id = a.id')->rightJoin('c ON c.id = b.id');
	 *
	 * @param   string  $condition  The join condition.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function rightJoin($condition)
	{
		$this->join('RIGHT', $condition);

		return $this;
	}

	/**
	 * Add a single column, or array of columns to the SELECT clause of the query.
	 *
	 * Note that you must not mix insert, update, delete and select method calls when building a query.
	 * The select method can, however, be called multiple times in the same query.
	 *
	 * Usage:
	 * $query->select('a.*')->select('b.id');
	 * $query->select(array('a.*', 'b.id'));
	 *
	 * @param   mixed  $columns  A string or an array of field names.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function select($columns)
	{
		$this->type = 'select';

		if (is_null($this->select))
		{
			$this->select = new QueryElement('SELECT', $columns);
		}
		else
		{
			$this->select->append($columns);
		}

		return $this;
	}

	/**
	 * Add a single condition string, or an array of strings to the SET clause of the query.
	 *
	 * Usage:
	 * $query->set('a = 1')->set('b = 2');
	 * $query->set(array('a = 1', 'b = 2');
	 *
	 * @param   mixed   $conditions  A string or array of string conditions.
	 * @param   string  $glue        The glue by which to join the condition strings. Defaults to ,.
	 *                               Note that the glue is set on first use and cannot be changed.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function set($conditions, $glue = ',')
	{
		if (is_null($this->set))
		{
			$glue      = strtoupper($glue);
			$this->set = new QueryElement('SET', $conditions, "\n\t$glue ");
		}
		else
		{
			$this->set->append($conditions);
		}

		return $this;
	}

	/**
	 * Allows a direct query to be provided to the database
	 * driver's setQuery() method, but still allow queries
	 * to have bounded variables.
	 *
	 * Usage:
	 * $query->setQuery('select * from #__users');
	 *
	 * @param   mixed  $query  An SQL Query
	 *
	 * @return  QueryElement  Returns this object to allow chaining.
	 */
	public function setQuery($query)
	{
		$this->sql = $query;

		return $this;
	}

	/**
	 * Add a table name to the UPDATE clause of the query.
	 *
	 * Note that you must not mix insert, update, delete and select method calls when building a query.
	 *
	 * Usage:
	 * $query->update('#__foo')->set(...);
	 *
	 * @param   string  $table  A table to update.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function update($table)
	{
		$this->type   = 'update';
		$this->update = new QueryElement('UPDATE', $table);

		return $this;
	}

	/**
	 * Adds a tuple, or array of tuples that would be used as values for an INSERT INTO statement.
	 *
	 * Usage:
	 * $query->values('1,2,3')->values('4,5,6');
	 * $query->values(array('1,2,3', '4,5,6'));
	 *
	 * @param   string  $values  A single tuple, or array of tuples.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function values($values)
	{
		if (is_null($this->values))
		{
			$this->values = new QueryElement('()', $values, '),(');
		}
		else
		{
			$this->values->append($values);
		}

		return $this;
	}

	/**
	 * Add a single condition, or an array of conditions to the WHERE clause of the query.
	 *
	 * Usage:
	 * $query->where('a = 1')->where('b = 2');
	 * $query->where(array('a = 1', 'b = 2'));
	 *
	 * @param   mixed   $conditions  A string or array of where conditions.
	 * @param   string  $glue        The glue by which to join the conditions. Defaults to AND.
	 *                               Note that the glue is set on first use and cannot be changed.
	 *
	 * @return  Base  Returns this object to allow chaining.
	 */
	public function where($conditions, $glue = 'AND')
	{
		if (is_null($this->where))
		{
			$glue        = strtoupper($glue);
			$this->where = new QueryElement('WHERE', $conditions, " $glue ");
		}
		else
		{
			$this->where->append($conditions);
		}

		return $this;
	}

	/**
	 * Method to provide deep copy support to nested objects and
	 * arrays when cloning.
	 *
	 * @return  void
	 */
	public function __clone()
	{
		foreach ($this as $k => $v)
		{
			if ($k === 'db')
			{
				continue;
			}

			if (is_object($v) || is_array($v))
			{
				$this->$k = unserialize(serialize($v));
			}
		}
	}

	/**
	 * Add a query to UNION with the current query.
	 * Multiple unions each require separate statements and create an array of unions.
	 *
	 * Usage:
	 * $query->union('SELECT name FROM  #__foo')
	 * $query->union('SELECT name FROM  #__foo','distinct')
	 * $query->union(array('SELECT name FROM  #__foo','SELECT name FROM  #__bar'))
	 *
	 * @param   mixed    $query     The Base object or string to union.
	 * @param   boolean  $distinct  True to only return distinct rows from the union.
	 * @param   string   $glue      The glue by which to join the conditions.
	 *
	 * @return  mixed    The Base object on success or boolean false on failure.
	 */
	public function union($query, $distinct = false, $glue = '')
	{
		// Clear any ORDER BY clause in UNION query
		// See http://dev.mysql.com/doc/refman/5.0/en/union.html
		if (!is_null($this->order))
		{
			$this->clear('order');
		}

		// Set up the DISTINCT flag, the name with parentheses, and the glue.
		if ($distinct)
		{
			$name = 'UNION DISTINCT ()';
			$glue = ')' . PHP_EOL . 'UNION DISTINCT (';
		}
		else
		{
			$glue = ')' . PHP_EOL . 'UNION (';
			$name = 'UNION ()';
		}

		// Get the QueryElement if it does not exist
		if (is_null($this->union))
		{
			$this->union = new QueryElement($name, $query, "$glue");
		}
		// Otherwise append the second UNION.
		else
		{
			$glue = '';
			$this->union->append($query);
		}

		return $this;
	}

	/**
	 * Add a query to UNION DISTINCT with the current query. Simply a proxy to Union with the Distinct clause.
	 *
	 * Usage:
	 * $query->unionDistinct('SELECT name FROM  #__foo')
	 *
	 * @param   mixed   $query  The Base object or string to union.
	 * @param   string  $glue   The glue by which to join the conditions.
	 *
	 * @return  mixed   The Base object on success or boolean false on failure.
	 */
	public function unionDistinct($query, $glue = '')
	{
		$distinct = true;

		// Apply the distinct flag to the union.
		return $this->union($query, $distinct, $glue);
	}

	/**
	 * Find and replace sprintf-like tokens in a format string.
	 * Each token takes one of the following forms:
	 *     %%       - A literal percent character.
	 *     %[t]     - Where [t] is a type specifier.
	 *     %[n]$[x] - Where [n] is an argument specifier and [t] is a type specifier.
	 *
	 * Types:
	 * a - Numeric: Replacement text is coerced to a numeric type but not quoted or escaped.
	 * e - Escape: Replacement text is passed to $this->escape().
	 * E - Escape (extra): Replacement text is passed to $this->escape() with true as the second argument.
	 * n - Name Quote: Replacement text is passed to $this->quoteName().
	 * q - Quote: Replacement text is passed to $this->quote().
	 * Q - Quote (no escape): Replacement text is passed to $this->quote() with false as the second argument.
	 * r - Raw: Replacement text is used as-is. (Be careful)
	 *
	 * Date Types:
	 * - Replacement text automatically quoted (use uppercase for Name Quote).
	 * - Replacement text should be a string in date format or name of a date column.
	 * y/Y - Year
	 * m/M - Month
	 * d/D - Day
	 * h/H - Hour
	 * i/I - Minute
	 * s/S - Second
	 *
	 * Invariable Types:
	 * - Takes no argument.
	 * - Argument index not incremented.
	 * t - Replacement text is the result of $this->currentTimestamp().
	 * z - Replacement text is the result of $this->nullDate(false).
	 * Z - Replacement text is the result of $this->nullDate(true).
	 *
	 * Usage:
	 * $query->format('SELECT %1$n FROM %2$n WHERE %3$n = %4$a', 'foo', '#__foo', 'bar', 1);
	 * Returns: SELECT `foo` FROM `#__foo` WHERE `bar` = 1
	 *
	 * Notes:
	 * The argument specifier is optional but recommended for clarity.
	 * The argument index used for unspecified tokens is incremented only when used.
	 *
	 * @param   string  $format  The formatting string.
	 *
	 * @return  string  Returns a string produced according to the formatting string.
	 */
	public function format($format)
	{
		$query = $this;
		$args  = array_slice(func_get_args(), 1);
		array_unshift($args, null);

		$i    = 1;
		$func = function ($match) use ($query, $args, &$i) {
			if (isset($match[6]) && $match[6] == '%')
			{
				return '%';
			}

			// No argument required, do not increment the argument index.
			switch ($match[5])
			{
				case 't':
					return $query->currentTimestamp();
					break;

				case 'z':
					return $query->nullDate(false);
					break;

				case 'Z':
					return $query->nullDate(true);
					break;
			}

			// Increment the argument index only if argument specifier not provided.
			$index = is_numeric($match[4]) ? (int) $match[4] : $i++;

			if (!$index || !isset($args[$index]))
			{
				$replacement = '';
			}
			else
			{
				$replacement = $args[$index];
			}

			switch ($match[5])
			{
				case 'a':
					return 0 + $replacement;
					break;

				case 'e':
					return $query->escape($replacement);
					break;

				case 'E':
					return $query->escape($replacement, true);
					break;

				case 'n':
					return $query->quoteName($replacement);
					break;

				case 'q':
					return $query->quote($replacement);
					break;

				case 'Q':
					return $query->quote($replacement, false);
					break;

				case 'r':
					return $replacement;
					break;

				// Dates
				case 'y':
					return $query->year($query->quote($replacement));
					break;

				case 'Y':
					return $query->year($query->quoteName($replacement));
					break;

				case 'm':
					return $query->month($query->quote($replacement));
					break;

				case 'M':
					return $query->month($query->quoteName($replacement));
					break;

				case 'd':
					return $query->day($query->quote($replacement));
					break;

				case 'D':
					return $query->day($query->quoteName($replacement));
					break;

				case 'h':
					return $query->hour($query->quote($replacement));
					break;

				case 'H':
					return $query->hour($query->quoteName($replacement));
					break;

				case 'i':
					return $query->minute($query->quote($replacement));
					break;

				case 'I':
					return $query->minute($query->quoteName($replacement));
					break;

				case 's':
					return $query->second($query->quote($replacement));
					break;

				case 'S':
					return $query->second($query->quoteName($replacement));
					break;
			}

			return '';
		};

		/**
		 * Regexp to find an replace all tokens.
		 * Matched fields:
		 * 0: Full token
		 * 1: Everything following '%'
		 * 2: Everything following '%' unless '%'
		 * 3: Argument specifier and '$'
		 * 4: Argument specifier
		 * 5: Type specifier
		 * 6: '%' if full token is '%%'
		 */

		return preg_replace_callback('#%(((([\d]+)\$)?([aeEnqQryYmMdDhHiIsStzZ]))|(%))#', $func, $format);
	}

	/**
	 * Add to the current date and time.
	 * Usage:
	 * $query->select($query->dateAdd());
	 * Prefixing the interval with a - (negative sign) will cause subtraction to be used.
	 * Note: Not all drivers support all units.
	 *
	 * @param   string  $date      The SQL-formatted date to add to. May be a date or datetime string.
	 * @param   string  $interval  The string representation of the appropriate number of units
	 * @param   string  $datePart  The part of the date to perform the addition on
	 *
	 * @return  string  The string with the appropriate sql for addition of dates
	 *
	 * @see http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_date-add
	 */
	public function dateAdd($date, $interval, $datePart)
	{
		return trim("DATE_ADD('" . $date . "', INTERVAL " . $interval . ' ' . $datePart . ')');
	}

	/**
	 * Add a query to UNION ALL with the current query.
	 * Multiple unions each require separate statements and create an array of unions.
	 *
	 * Usage:
	 * $query->union('SELECT name FROM  #__foo')
	 * $query->union('SELECT name FROM  #__foo','distinct')
	 * $query->union(array('SELECT name FROM  #__foo','SELECT name FROM  #__bar'))
	 *
	 * @param   mixed    $query     The Base object or string to union.
	 * @param   boolean  $distinct  True to only return distinct rows from the union.
	 * @param   string   $glue      The glue by which to join the conditions.
	 *
	 * @return  mixed    The Base object on success or boolean false on failure.
	 */
	public function unionAll($query, $distinct = false, $glue = '')
	{
		$glue = ')' . PHP_EOL . 'UNION ALL (';
		$name = 'UNION ALL ()';

		// Get the QueryElement if it does not exist
		if (is_null($this->unionAll))
		{
			$this->unionAll = new QueryElement($name, $query, "$glue");
		}

		// Otherwise append the second UNION.
		else
		{
			$glue = '';
			$this->unionAll->append($query);
		}

		return $this;
	}
}
Driver/Query/Mysqli.php000060400000005644152456704520011112 0ustar00<?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\Driver\Query;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Driver\Query\Base as BaseQuery;

/**
 * Query Building Class.
 *
 * Based on Joomla! Platform 11.3
 */
class Mysqli extends Base implements Limitable
{
	/**
	 * @var    integer  The offset for the result set.
	 */
	protected $offset;

	/**
	 * @var    integer  The limit for the result set.
	 */
	protected $limit;

	/**
	 * Method to modify a query already in string format with the needed
	 * additions to make the query limited to a particular number of
	 * results, or start at a particular offset.
	 *
	 * @param   string   $query   The query in string format
	 * @param   integer  $limit   The limit for the result set
	 * @param   integer  $offset  The offset for the result set
	 *
	 * @return string
	 */
	public function processLimit($query, $limit, $offset = 0)
	{
		if ($limit > 0 || $offset > 0)
		{
			$query .= ' LIMIT ' . $offset . ', ' . $limit;
		}

		return $query;
	}

	/**
	 * Sets the offset and limit for the result set, if the database driver supports it.
	 *
	 * Usage:
	 * $query->setLimit(100, 0); (retrieve 100 rows, starting at first record)
	 * $query->setLimit(50, 50); (retrieve 50 rows, starting at 50th record)
	 *
	 * @param   integer  $limit   The limit for the result set
	 * @param   integer  $offset  The offset for the result set
	 *
	 * @return  BaseQuery  Returns this object to allow chaining.
	 */
	public function setLimit($limit = 0, $offset = 0)
	{
		$this->limit  = (int) $limit;
		$this->offset = (int) $offset;

		return $this;
	}

	/**
	 * Concatenates an array of column names or values.
	 *
	 * @param   array   $values     An array of values to concatenate.
	 * @param   string  $separator  As separator to place between each value.
	 *
	 * @return  string  The concatenated values.
	 */
	public function concatenate($values, $separator = null)
	{
		if ($separator)
		{
			$concat_string = 'CONCAT_WS(' . $this->quote($separator);

			foreach ($values as $value)
			{
				$concat_string .= ', ' . $value;
			}

			return $concat_string . ')';
		}
		else
		{
			return 'CONCAT(' . implode(',', $values) . ')';
		}
	}
}
Driver/Query/Pdomysql.php000060400000001767152456704520011446 0ustar00<?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\Driver\Query;

defined('AKEEBAENGINE') || die();

/**
 * Query Building Class.
 *
 * Based on Joomla! Platform 11.3
 */
class Pdomysql extends Mysqli implements Limitable
{
}
Driver/Query/Limitable.php000060400000004427152456704520011534 0ustar00<?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\Driver\Query;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Driver\Query\Base as BaseQuery;

/**
 * Query Limitable Interface.
 * Adds bind/unbind methods as well as a getBounded() method
 * to retrieve the stored bounded variables on demand prior to
 * query execution.
 *
 * Based on Joomla! Platform 11.2
 */
interface Limitable
{
	/**
	 * Method to modify a query already in string format with the needed
	 * additions to make the query limited to a particular number of
	 * results, or start at a particular offset. This method is used
	 * automatically by the __toString() method if it detects that the
	 * query implements the Limitable interface.
	 *
	 * @param   string   $query   The query in string format
	 * @param   integer  $limit   The limit for the result set
	 * @param   integer  $offset  The offset for the result set
	 *
	 * @return  string
	 *
	 * @since   12.1
	 */
	public function processLimit($query, $limit, $offset = 0);

	/**
	 * Sets the offset and limit for the result set, if the database driver supports it.
	 *
	 * Usage:
	 * $query->setLimit(100, 0); (retrieve 100 rows, starting at first record)
	 * $query->setLimit(50, 50); (retrieve 50 rows, starting at 50th record)
	 *
	 * @param   integer  $limit   The limit for the result set
	 * @param   integer  $offset  The offset for the result set
	 *
	 * @return  BaseQuery  Returns this object to allow chaining.
	 *
	 * @since   12.1
	 */
	public function setLimit($limit = 0, $offset = 0);
}
Driver/Query/Mysql.php000060400000001737152456704520010740 0ustar00<?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\Driver\Query;

defined('AKEEBAENGINE') || die();

/**
 * Query Building Class.
 *
 * Based on Joomla! Platform 11.3
 */
class Mysql extends Mysqli
{
}
Driver/Query/Element.php000060400000005235152456704520011221 0ustar00<?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\Driver\Query;

defined('AKEEBAENGINE') || die();

/**
 * Query Element Class.
 *
 * Based on Joomla! Platform 11.3
 */
class Element
{
	/**
	 * @var    string  The name of the element.
	 */
	protected $name = null;

	/**
	 * @var    array  An array of elements.
	 */
	protected $elements = null;

	/**
	 * @var    string  Glue piece.
	 */
	protected $glue = null;

	/**
	 * Constructor.
	 *
	 * @param   string  $name      The name of the element.
	 * @param   mixed   $elements  String or array.
	 * @param   string  $glue      The glue for elements.
	 */
	public function __construct($name, $elements, $glue = ',')
	{
		$this->elements = [];
		$this->name     = $name;
		$this->glue     = $glue;

		$this->append($elements);
	}

	/**
	 * Magic function to convert the query element to a string.
	 *
	 * @return  string
	 */
	public function __toString()
	{
		if (substr($this->name, -2) == '()')
		{
			return PHP_EOL . substr($this->name, 0, -2) . '(' . implode($this->glue, $this->elements) . ')';
		}
		else
		{
			return PHP_EOL . $this->name . ' ' . implode($this->glue, $this->elements);
		}
	}

	/**
	 * Appends element parts to the internal list.
	 *
	 * @param   mixed  $elements  String or array.
	 *
	 * @return  void
	 */
	public function append($elements)
	{
		if (is_array($elements))
		{
			$this->elements = array_merge($this->elements, $elements);
		}
		else
		{
			$this->elements = array_merge($this->elements, [$elements]);
		}
	}

	/**
	 * Gets the elements of this element.
	 *
	 * @return  string
	 */
	public function getElements()
	{
		return $this->elements;
	}

	/**
	 * Method to provide deep copy support to nested objects and arrays
	 * when cloning.
	 *
	 * @return  void
	 */
	public function __clone()
	{
		foreach ($this as $k => $v)
		{
			if (is_object($v) || is_array($v))
			{
				$this->{$k} = unserialize(serialize($v));
			}
		}
	}
}
Driver/Query/Sqlite.php000060400000014741152456704520011073 0ustar00<?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\Driver\Query;

defined('AKEEBAENGINE') || die();

use PDO;
use stdClass;

/**
 * SQLite Query Building Class.
 *
 * @since  1.0
 */
class Sqlite extends Base implements Preparable, Limitable
{
	/**
	 * The limit for the result set.
	 *
	 * @var    integer
	 * @since  1.0
	 */
	protected $limit;

	/**
	 * The offset for the result set.
	 *
	 * @var    integer
	 * @since  1.0
	 */
	protected $offset;

	/**
	 * Holds key / value pair of bound objects.
	 *
	 * @var    mixed
	 * @since  1.0
	 */
	protected $bounded = [];

	/**
	 * Method to add a variable to an internal array that will be bound to a prepared SQL statement before query execution. Also
	 * removes a variable that has been bounded from the internal bounded array when the passed in value is null.
	 *
	 * @param   string|integer   $key            The key that will be used in your SQL query to reference the value. Usually of
	 *                                           the form ':key', but can also be an integer.
	 * @param   mixed           &$value          The value that will be bound. The value is passed by reference to support output
	 *                                           parameters such as those possible with stored procedures.
	 * @param   integer          $dataType       Constant corresponding to a SQL datatype.
	 * @param   integer          $length         The length of the variable. Usually required for OUTPUT parameters.
	 * @param   array            $driverOptions  Optional driver options to be used.
	 *
	 * @return  Sqlite  Returns this object to allow chaining.
	 *
	 * @since   1.0
	 */
	public function bind($key = null, &$value = null, $dataType = PDO::PARAM_STR, $length = 0, $driverOptions = [])
	{
		// Case 1: Empty Key (reset $bounded array)
		if (empty($key))
		{
			$this->bounded = [];

			return $this;
		}

		// Case 2: Key Provided, null value (unset key from $bounded array)
		if (is_null($value))
		{
			if (isset($this->bounded[$key]))
			{
				unset($this->bounded[$key]);
			}

			return $this;
		}

		$obj = new stdClass;

		$obj->value         = &$value;
		$obj->dataType      = $dataType;
		$obj->length        = $length;
		$obj->driverOptions = $driverOptions;

		// Case 3: Simply add the Key/Value into the bounded array
		$this->bounded[$key] = $obj;

		return $this;
	}

	/**
	 * Retrieves the bound parameters array when key is null and returns it by reference. If a key is provided then that item is
	 * returned.
	 *
	 * @param   mixed  $key  The bounded variable key to retrieve.
	 *
	 * @return  mixed
	 *
	 * @since   1.0
	 */
	public function &getBounded($key = null)
	{
		if (empty($key))
		{
			return $this->bounded;
		}
		else
		{
			if (isset($this->bounded[$key]))
			{
				return $this->bounded[$key];
			}
		}
	}

	/**
	 * Gets the number of characters in a string.
	 *
	 * Note, use 'length' to find the number of bytes in a string.
	 *
	 * Usage:
	 * $query->select($query->charLength('a'));
	 *
	 * @param   string  $field      A value.
	 * @param   string  $operator   Comparison operator between charLength integer value and $condition
	 * @param   string  $condition  Integer value to compare charLength with.
	 *
	 * @return  string  The required char length call.
	 *
	 * @since   1.1.0
	 */
	public function charLength($field, $operator = null, $condition = null)
	{
		return 'length(' . $field . ')' . (isset($operator) && isset($condition) ? ' ' . $operator . ' ' . $condition : '');
	}

	/**
	 * Clear data from the query or a specific clause of the query.
	 *
	 * @param   string  $clause  Optionally, the name of the clause to clear, or nothing to clear the whole query.
	 *
	 * @return  Sqlite  Returns this object to allow chaining.
	 *
	 * @since   1.0
	 */
	public function clear($clause = null)
	{
		switch ($clause)
		{
			case null:
				$this->bounded = [];
				break;
		}

		return parent::clear($clause);
	}

	/**
	 * Concatenates an array of column names or values.
	 *
	 * Usage:
	 * $query->select($query->concatenate(array('a', 'b')));
	 *
	 * @param   array   $values     An array of values to concatenate.
	 * @param   string  $separator  As separator to place between each value.
	 *
	 * @return  string  The concatenated values.
	 *
	 * @since   1.1.0
	 */
	public function concatenate($values, $separator = null)
	{
		if ($separator)
		{
			return implode(' || ' . $this->quote($separator) . ' || ', $values);
		}
		else
		{
			return implode(' || ', $values);
		}
	}

	/**
	 * Method to modify a query already in string format with the needed
	 * additions to make the query limited to a particular number of
	 * results, or start at a particular offset. This method is used
	 * automatically by the __toString() method if it detects that the
	 * query implements the LimitableInterface.
	 *
	 * @param   string   $query   The query in string format
	 * @param   integer  $limit   The limit for the result set
	 * @param   integer  $offset  The offset for the result set
	 *
	 * @return  string
	 *
	 * @since   1.0
	 */
	public function processLimit($query, $limit, $offset = 0)
	{
		if ($limit > 0 || $offset > 0)
		{
			$query .= ' LIMIT ' . $offset . ', ' . $limit;
		}

		return $query;
	}

	/**
	 * Sets the offset and limit for the result set, if the database driver supports it.
	 *
	 * Usage:
	 * $query->setLimit(100, 0); (retrieve 100 rows, starting at first record)
	 * $query->setLimit(50, 50); (retrieve 50 rows, starting at 50th record)
	 *
	 * @param   integer  $limit   The limit for the result set
	 * @param   integer  $offset  The offset for the result set
	 *
	 * @return  Sqlite  Returns this object to allow chaining.
	 *
	 * @since   1.0
	 */
	public function setLimit($limit = 0, $offset = 0)
	{
		$this->limit  = (int) $limit;
		$this->offset = (int) $offset;

		return $this;
	}
}
Driver/Query/Preparable.php000060400000005067152456704520011710 0ustar00<?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\Driver\Query;

defined('AKEEBAENGINE') || die();

use PDO;

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

	/**
	 * Retrieves the bound parameters array when key is null and returns it by reference. If a key is provided then that item is
	 * returned.
	 *
	 * @param   mixed  $key  The bounded variable key to retrieve.
	 *
	 * @return  mixed
	 *
	 * @since   1.0
	 */
	public function &getBounded($key = null);
}
Driver/Base.php000060400000115134152456704520007375 0ustar00<?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\Driver;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Driver\Query\Base as QueryBase;
use RuntimeException;

/**
 * Database driver superclass. Used as the base of all Akeeba Engine database drivers.
 * Strongly based on Joomla Platform's JDatabase class.
 *
 * @method qn(string $name, string $as = null)  Alias for quoteName
 * @method q(string $text, bool $escape = true)  Alias for quote
 */
#[\AllowDynamicProperties]
abstract class Base
{
	/** @var    string  The minimum supported database version. */
	protected static $dbMinimum;

	/** @var    array  JDatabaseDriver instances container. */
	protected static $instances = [];

	/** @var string The name of the database driver. */
	public $name;

	/** @var string The name of the database. */
	protected $_database;

	/** @var resource The db connection resource */
	protected $connection = '';

	/** @var    integer  The number of SQL statements executed by the database driver. */
	protected $count = 0;

	/** @var resource The database connection cursor from the last query. */
	protected $cursor;

	/** @var    boolean  The database driver debugging state. */
	protected $debug = false;

	/** @var string Driver type. This should always be mysql as we don't support anything else anymore. */
	protected $driverType = '';

	/** @var string The db server's error string */
	protected $errorMsg = '';

	/** @var int The db server's error number */
	protected $errorNum = 0;

	/** @var int Query's limit */
	protected $limit = 0;

	/** @var    array  The log of executed SQL statements by the database driver. */
	protected $log = [];

	/** @var string Quote for named objects */
	protected $nameQuote = '';

	/** @var string  The null or zero representation of a timestamp for the database driver. */
	protected $nullDate;

	/** @var int Query's offset */
	protected $offset = 0;

	/** @var    array  Passed in upon instantiation and saved. */
	protected $options;

	/** @var mixed The SQL query string */
	protected $sql = '';

	/** @var string The prefix used in the database, if any */
	protected $tablePrefix = '';

	/** @var bool Support for UTF-8 */
	protected $utf = true;

	/**
	 * Database object constructor
	 *
	 * @param   array  $options  List of options used to configure the connection
	 */
	public function __construct($options)
	{
		$prefix     = array_key_exists('prefix', $options) ? $options['prefix'] : '';
		$database   = array_key_exists('database', $options) ? $options['database'] : '';
		$connection = array_key_exists('connection', $options) ? $options['connection'] : null;

		$this->tablePrefix = $prefix;
		$this->_database   = $database;
		$this->connection  = $connection;
		$this->errorNum    = 0;
		$this->count       = 0;
		$this->log         = [];
		$this->options     = $options;
	}

	/**
	 * Is this driver class supported on this server? Child classes are supposed to override this and perform a
	 * compatibility check.
	 *
	 * @return  bool  True if the driver class is supported on the server
	 */
	public static function isSupported()
	{
		return false;
	}

	/**
	 * Splits a string of multiple queries into an array of individual queries.
	 *
	 * @param   string  $query  Input SQL string with which to split into individual queries.
	 *
	 * @return  array  The queries from the input string separated into an array.
	 */
	public static function splitSql($query)
	{
		$start   = 0;
		$open    = false;
		$char    = '';
		$end     = strlen($query);
		$queries = [];

		for ($i = 0; $i < $end; $i++)
		{
			$current = substr($query, $i, 1);
			if (($current == '"' || $current == '\''))
			{
				$n = 2;

				while (substr($query, $i - $n + 1, 1) == '\\' && $n < $i)
				{
					$n++;
				}

				if ($n % 2 == 0)
				{
					if ($open)
					{
						if ($current == $char)
						{
							$open = false;
							$char = '';
						}
					}
					else
					{
						$open = true;
						$char = $current;
					}
				}
			}

			if (($current == ';' && !$open) || $i == $end - 1)
			{
				$queries[] = substr($query, $start, ($i - $start + 1));
				$start     = $i + 1;
			}
		}

		return $queries;
	}

	/**
	 * Is this driver supported under the current system configuration?
	 *
	 * @return bool
	 */
	public static function test()
	{
		return self::isSupported();
	}

	/**
	 * Magic method to provide method alias support for quote() and quoteName().
	 *
	 * @param   string  $method  The called method.
	 * @param   array   $args    The array of arguments passed to the method.
	 *
	 * @return  string  The aliased method's return value or null.
	 */
	public function __call($method, $args)
	{
		if (empty($args))
		{
			return null;
		}

		switch ($method)
		{
			case 'q':
				return $this->quote($args[0], $args[1] ?? true);
				break;
			case 'nq':
			case 'qn':
				return $this->quoteName($args[0]);
				break;
		}

		return null;
	}

	/**
	 * Database object destructor
	 *
	 * @return bool
	 */
	public function __destruct()
	{
		return $this->close();
	}

	public function __wakeup()
	{
		$this->open();
	}

	/**
	 * By default, when the object is shutting down, the connection is closed
	 */
	public function _onSerialize()
	{
		$this->close();
	}

	/**
	 * Alter database's character set, obtaining query string from protected member.
	 *
	 * @param   string  $dbName  The database name that will be altered
	 *
	 * @return  string  The query that alter the database query string
	 *
	 * @throws  RuntimeException
	 */
	public function alterDbCharacterSet($dbName)
	{
		if (is_null($dbName))
		{
			throw new RuntimeException('Database name must not be null.');
		}

		$this->setQuery($this->getAlterDbCharacterSet($dbName));

		return $this->execute();
	}

	/**
	 * Closes the database connection
	 */
	abstract public function close();

	/**
	 * Determines if the connection to the server is active.
	 *
	 * @return  boolean  True if connected to the database engine.
	 */
	abstract public function connected();

	/**
	 * Create a new database using information from $options object, obtaining query string
	 * from protected member.
	 *
	 * @param   object   $options         Object used to pass user and database name to database driver.
	 *                                    This object must have "db_name" and "db_user" set.
	 * @param   boolean  $utf             True if the database supports the UTF-8 character set.
	 *
	 * @return  string  The query that creates database
	 *
	 * @throws  RuntimeException
	 */
	public function createDatabase($options, $utf = true)
	{
		if (is_null($options))
		{
			throw new RuntimeException('$options object must not be null.');
		}
		elseif (empty($options->db_name))
		{
			throw new RuntimeException('$options object must have db_name set.');
		}
		elseif (empty($options->db_user))
		{
			throw new RuntimeException('$options object must have db_user set.');
		}

		$this->setQuery($this->getCreateDatabaseQuery($options, $utf));

		return $this->execute();
	}

	/**
	 * Drops a table from the database.
	 *
	 * @param   string   $table     The name of the database table to drop.
	 * @param   boolean  $ifExists  Optionally specify that the table must exist before it is dropped.
	 *
	 * @return  Base  Returns this object to support chaining.
	 */
	public abstract function dropTable($table, $ifExists = true);

	/**
	 * Method to escape a string for usage in an SQL statement.
	 *
	 * @param   string   $text   The string to be escaped.
	 * @param   boolean  $extra  Optional parameter to provide extra escaping.
	 *
	 * @return  string   The escaped string.
	 */
	abstract public function escape($text, $extra = false);

	/**
	 * An alias for query()
	 *
	 * @return  mixed  A database cursor resource on success, boolean false on failure.
	 */
	public function execute()
	{
		return $this->query();
	}

	/**
	 * Method to fetch a row from the result set cursor as an associative array.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  mixed  Either the next row from the result set or false if there are no more rows.
	 */
	abstract public function fetchAssoc($cursor = null);

	/**
	 * Method to free up the memory used for the result set.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  void
	 */
	abstract public function freeResult($cursor = null);

	/**
	 * Returns the abstracted name of a database object
	 *
	 * @param   string  $tableName
	 *
	 * @return  string
	 */
	public function getAbstract($tableName)
	{
		$prefix = $this->getPrefix();

		// Don't return abstract names for non-CMS tables
		if (is_null($prefix))
		{
			return $tableName;
		}

		switch ($prefix)
		{
			case '':
				// This is more of a hack; it assumes all tables are CMS tables if the prefix is empty.
				return '#__' . $tableName;
				break;

			default:
				// Normal behaviour for 99% of sites
				$tableAbstract = $tableName;
				if (!empty($prefix))
				{
					if (substr($tableName, 0, strlen($prefix)) == $prefix)
					{
						$tableAbstract = '#__' . substr($tableName, strlen($prefix));
					}
					else
					{
						$tableAbstract = $tableName;
					}
				}

				return $tableAbstract;
				break;
		}
	}

	/**
	 * Get the number of affected rows for the previous executed SQL statement.
	 *
	 * @return  integer  The number of affected rows.
	 */
	abstract public function getAffectedRows();

	/**
	 * Method to get the database collation in use by sampling a text field of a table in the database.
	 *
	 * @return  mixed  The collation in use by the database or boolean false if not supported.
	 */
	abstract public function getCollation();

	/**
	 * Method that provides access to the underlying database connection.
	 *
	 * @return  resource  The underlying database connection resource.
	 */
	public function getConnection()
	{
		return $this->connection;
	}

	/**
	 * Inherits the connection of another database driver. Useful for cloning
	 * the CMS database connection into an Akeeba Engine database driver.
	 *
	 * @param   resource  $connection
	 */
	public function setConnection($connection)
	{
		$this->connection = $connection;
	}

	/**
	 * Get the total number of SQL statements executed by the database driver.
	 *
	 * @return  integer
	 *
	 * @since   11.1
	 */
	public function getCount()
	{
		return $this->count;
	}

	/**
	 * Returns a PHP date() function compliant date format for the database driver.
	 *
	 * @return  string  The format string.
	 */
	public function getDateFormat()
	{
		return 'Y-m-d H:i:s';
	}

	/**
	 * Return the database driver type, e.g. "mysql" for all drivers which can talk to MySQL
	 *
	 * @return string
	 */
	public function getDriverType()
	{
		return $this->driverType;
	}

	/**
	 * Get the error message
	 *
	 * @return string The error message for the most recent query
	 */
	public function getErrorMsg($escaped = false)
	{
		if ($escaped)
		{
			return addslashes($this->errorMsg);
		}
		else
		{
			return $this->errorMsg;
		}
	}

	/**
	 * Get the error number
	 *
	 * @return int The error number for the most recent query
	 */
	public function getErrorNum()
	{
		return $this->errorNum;
	}

	/**
	 * Method to escape a string for usage in an SQL statement.
	 *
	 * @param   string   $text   The string to be escaped.
	 * @param   boolean  $extra  Optional parameter to provide extra escaping.
	 *
	 * @return  string  The escaped string.
	 */
	public function getEscaped($text, $extra = false)
	{
		return $this->escape($text, $extra);
	}

	/**
	 * Get the database driver SQL statement log.
	 *
	 * @return  array  SQL statements executed by the database driver.
	 *
	 * @since   11.1
	 */
	public function getLog()
	{
		return $this->log;
	}

	/**
	 * Get the minimum supported database version.
	 *
	 * @return  string  The minimum version number for the database driver.
	 *
	 * @since   12.1
	 */
	public function getMinimum()
	{
		return static::$dbMinimum;
	}

	/**
	 * Get the null or zero representation of a timestamp for the database driver.
	 *
	 * @return  string  Null or zero representation of a timestamp.
	 */
	public function getNullDate()
	{
		return $this->nullDate;
	}

	/**
	 * Get the number of returned rows for the previous executed SQL statement.
	 *
	 * @param   resource  $cursor  An optional database cursor resource to extract the row count from.
	 *
	 * @return  integer   The number of returned rows.
	 */
	abstract public function getNumRows($cursor = null);

	/**
	 * Get the database table prefix
	 *
	 * @return string The database prefix
	 */
	public function getPrefix()
	{
		return $this->tablePrefix;
	}

	/**
	 * Get the current query object or a new QueryBase object.
	 *
	 * @param   boolean  $new  False to return the current query object, True to return a new QueryBase object.
	 *
	 * @return  QueryBase  The current query object or a new object extending the QueryBase class.
	 */
	abstract public function getQuery($new = false);

	/**
	 * Create a new QueryBase object.
	 *
	 * @return  QueryBase  The current query object or a new object extending the QueryBase class.
	 */
	abstract public function createQuery();

	/**
	 * Retrieves field information about the given tables.
	 *
	 * @param   string   $table     The name of the database table.
	 * @param   boolean  $typeOnly  True (default) to only return field types.
	 *
	 * @return  array  An array of fields by table.
	 */
	abstract public function getTableColumns($table, $typeOnly = true);

	/**
	 * Shows the table CREATE statement that creates the given tables.
	 *
	 * @param   mixed  $tables  A table name or a list of table names.
	 *
	 * @return  array  A list of the create SQL for the tables.
	 */
	abstract public function getTableCreate($tables);

	/**
	 * Retrieves field information about the given tables.
	 *
	 * @param   mixed    $tables    A table name or a list of table names.
	 * @param   boolean  $typeOnly  True to only return field types.
	 *
	 * @return  array  An array of fields by table.
	 */
	public function getTableFields($tables, $typeOnly = true)
	{
		$results = [];

		$tables = (array) $tables;

		foreach ($tables as $table)
		{
			$results[$table] = $this->getTableColumns($table, $typeOnly);
		}

		return $results;
	}

	/**
	 * Retrieves field information about the given tables.
	 *
	 * @param   mixed  $tables  A table name or a list of table names.
	 *
	 * @return  array  An array of keys for the table(s).
	 */
	abstract public function getTableKeys($tables);

	/**
	 * Method to get an array of all tables in the database.
	 *
	 * @return  array  An array of all the tables in the database.
	 */
	abstract public function getTableList();

	/**
	 * Returns an array with the names of tables, views, procedures, functions and triggers
	 * in the database. The table names are the keys of the tables, whereas the value is
	 * the type of each element: table, view, merge, temp, procedure, function or trigger.
	 * Note that merge are MRG_MYISAM tables and temp is non-permanent data table, usually
	 * set up as temporary, black hole or federated tables. These two types should never,
	 * ever, have their data dumped in the SQL dump file.
	 *
	 * @param   bool  $abstract  Return abstract or normal names? Defaults to true (abstract names)
	 *
	 * @return  array
	 */
	abstract public function getTables($abstract = true);

	/**
	 * Determine whether or not the database engine supports UTF-8 character encoding.
	 *
	 * @return  boolean  True if the database engine supports UTF-8 character encoding.
	 */
	public function getUTFSupport()
	{
		return $this->utf;
	}

	/**
	 * Get the version of the database connector
	 *
	 * @return  string  The database connector version.
	 */
	abstract public function getVersion();

	/**
	 * Determines if the database engine supports UTF-8 character encoding.
	 *
	 * @return  boolean  True if supported.
	 */
	public function hasUTF()
	{
		return $this->utf;
	}

	/**
	 * Determine whether or not the database engine supports UTF-8 character encoding.
	 *
	 * @return  boolean  True if the database engine supports UTF-8 character encoding.
	 */
	public function hasUTFSupport()
	{
		return $this->utf;
	}

	/**
	 * Inserts a row into a table based on an object's properties.
	 *
	 * @param   string  $table   The name of the database table to insert into.
	 * @param   object &$object  A reference to an object whose public properties match the table fields.
	 * @param   string  $key     The name of the primary key. If provided the object property is updated.
	 *
	 * @return  boolean    True on success.
	 */
	public function insertObject($table, &$object, $key = null)
	{
		$fields = [];
		$values = [];

		// Iterate over the object variables to build the query fields and values.
		foreach (get_object_vars($object) as $k => $v)
		{
			// Only process non-null scalars.
			if (is_array($v) || is_object($v) || ($v === null))
			{
				continue;
			}

			// Ignore any internal fields.
			if ($k[0] == '_')
			{
				continue;
			}

			// Prepare and sanitize the fields and values for the database query.
			$fields[] = $this->quoteName($k);
			$values[] = $this->quote($v);
		}

		// Create the base insert statement.
		$query = $this->getQuery(true)
			->insert($this->quoteName($table))
			->columns($fields)
			->values(implode(',', $values));

		// Set the query and execute the insert.
		$this->setQuery($query);
		if (!$this->execute())
		{
			return false;
		}

		// Update the primary key if it exists.
		$id = $this->insertid();
		if ($key && $id && is_string($key))
		{
			$object->$key = $id;
		}

		return true;
	}

	/**
	 * Method to get the auto-incremented value from the last INSERT statement.
	 *
	 * @return  integer  The value of the auto-increment field from the last inserted row.
	 */
	abstract public function insertid();

	/**
	 * Method to check whether the installed database version is supported by the database driver
	 *
	 * @return  boolean  True if the database version is supported
	 *
	 * @since   12.1
	 */
	public function isMinimumVersion()
	{
		return version_compare($this->getVersion(), static::$dbMinimum) >= 0;
	}

	/**
	 * Method to get the first row of the result set from the database query as an associative array
	 * of ['field_name' => 'row_value'].
	 *
	 * @return  mixed  The return value or null if the query failed.
	 */
	public function loadAssoc()
	{
		$ret = null;

		// Execute the query and get the result set cursor.
		if (!($cursor = $this->execute()))
		{
			return null;
		}

		// Get the first row from the result set as an associative array.
		if ($array = $this->fetchAssoc($cursor))
		{
			$ret = $array;
		}

		// Free up system resources and return.
		$this->freeResult($cursor);

		return $ret;
	}

	/**
	 * Method to get an array of the result set rows from the database query where each row is an associative array
	 * of ['field_name' => 'row_value'].  The array of rows can optionally be keyed by a field name, but defaults to
	 * a sequential numeric array.
	 *
	 * NOTE: Chosing to key the result array by a non-unique field name can result in unwanted
	 * behavior and should be avoided.
	 *
	 * @param   string  $key     The name of a field on which to key the result array.
	 * @param   string  $column  An optional column name. Instead of the whole row, only this column value will be in
	 *                           the result array.
	 *
	 * @return  mixed   The return value or null if the query failed.
	 */
	public function loadAssocList($key = null, $column = null)
	{
		$array = [];

		// Execute the query and get the result set cursor.
		if (!($cursor = $this->execute()))
		{
			return null;
		}

		// Get all of the rows from the result set.
		while ($row = $this->fetchAssoc($cursor))
		{
			$value = ($column) ? ($row[$column] ?? $row) : $row;
			if ($key)
			{
				$array[$row[$key]] = $value;
			}
			else
			{
				$array[] = $value;
			}
		}

		// Free up system resources and return.
		$this->freeResult($cursor);

		return $array;
	}

	/**
	 * Method to get an array of values from the <var>$offset</var> field in each row of the result set from
	 * the database query.
	 *
	 * @param   integer  $offset  The row offset to use to build the result array.
	 *
	 * @return  mixed    The return value or null if the query failed.
	 */
	public function loadColumn($offset = 0)
	{
		$array = [];

		// Execute the query and get the result set cursor.
		if (!($cursor = $this->execute()))
		{
			return null;
		}

		// Get all of the rows from the result set as arrays.
		while ($row = $this->fetchArray($cursor))
		{
			$array[] = $row[$offset];
		}

		// Free up system resources and return.
		$this->freeResult($cursor);

		return $array;
	}

	/**
	 * Method to get the next row in the result set from the database query as an object.
	 *
	 * @param   string  $class  The class name to use for the returned row object.
	 *
	 * @return  mixed   The result of the query as an array, false if there are no more rows.
	 */
	public function loadNextObject($class = 'stdClass')
	{
		// Execute the query and get the result set cursor.
		if (is_null($this->cursor))
		{
			if (!($this->cursor = $this->execute()))
			{
				return $this->errorNum ? null : false;
			}
		}

		// Get the next row from the result set as an object of type $class.
		if ($row = $this->fetchObject($this->cursor, $class))
		{
			return $row;
		}

		// Free up system resources and return.
		$this->freeResult($this->cursor);
		$this->cursor = null;

		return false;
	}

	/**
	 * Method to get the next row in the result set from the database query as an array.
	 *
	 * @return  mixed  The result of the query as an array, false if there are no more rows.
	 */
	public function loadNextRow()
	{
		// Execute the query and get the result set cursor.
		if (is_null($this->cursor))
		{
			if (!($this->cursor = $this->execute()))
			{
				return $this->errorNum ? null : false;
			}
		}

		// Get the next row from the result set as an object of type $class.
		if ($row = $this->fetchArray($this->cursor))
		{
			return $row;
		}

		// Free up system resources and return.
		$this->freeResult($this->cursor);
		$this->cursor = null;

		return false;
	}

	/**
	 * Method to get the first row of the result set from the database query as an object.
	 *
	 * @param   string  $class  The class name to use for the returned row object.
	 *
	 * @return  mixed   The return value or null if the query failed.
	 */
	public function loadObject($class = 'stdClass')
	{
		$ret = null;

		// Execute the query and get the result set cursor.
		if (!($cursor = $this->execute()))
		{
			return null;
		}

		// Get the first row from the result set as an object of type $class.
		if ($object = $this->fetchObject($cursor, $class))
		{
			$ret = $object;
		}

		// Free up system resources and return.
		$this->freeResult($cursor);

		return $ret;
	}

	/**
	 * Method to get an array of the result set rows from the database query where each row is an object.  The array
	 * of objects can optionally be keyed by a field name, but defaults to a sequential numeric array.
	 *
	 * NOTE: Choosing to key the result array by a non-unique field name can result in unwanted
	 * behavior and should be avoided.
	 *
	 * @param   string  $key    The name of a field on which to key the result array.
	 * @param   string  $class  The class name to use for the returned row objects.
	 *
	 * @return  mixed   The return value or null if the query failed.
	 */
	public function loadObjectList($key = '', $class = 'stdClass')
	{
		$array = [];

		// Execute the query and get the result set cursor.
		if (!($cursor = $this->execute()))
		{
			return null;
		}

		// Get all of the rows from the result set as objects of type $class.
		while ($row = $this->fetchObject($cursor, $class))
		{
			if ($key)
			{
				$array[$row->$key] = $row;
			}
			else
			{
				$array[] = $row;
			}
		}

		// Free up system resources and return.
		$this->freeResult($cursor);

		return $array;
	}

	/**
	 * Method to get the first field of the first row of the result set from the database query.
	 *
	 * @return  mixed  The return value or null if the query failed.
	 */
	public function loadResult()
	{
		$ret = null;

		// Execute the query and get the result set cursor.
		if (!($cursor = $this->execute()))
		{
			return null;
		}

		// Get the first row from the result set as an array.
		if ($row = $this->fetchArray($cursor))
		{
			$ret = $row[0];
		}

		// Free up system resources and return.
		$this->freeResult($cursor);

		return $ret;
	}

	/**
	 * Method to get an array of values from the <var>$offset</var> field in each row of the result set from
	 * the database query.
	 *
	 * @param   integer  $offset  The row offset to use to build the result array.
	 *
	 * @return  mixed    The return value or null if the query failed.
	 */
	public function loadResultArray($offset = 0)
	{
		return $this->loadColumn($offset);
	}

	/**
	 * Method to get the first row of the result set from the database query as an array.  Columns are indexed
	 * numerically so the first column in the result set would be accessible via <var>$row[0]</var>, etc.
	 *
	 * @return  mixed  The return value or null if the query failed.
	 */
	public function loadRow()
	{
		$ret = null;

		// Execute the query and get the result set cursor.
		if (!($cursor = $this->execute()))
		{
			return null;
		}

		// Get the first row from the result set as an array.
		if ($row = $this->fetchArray($cursor))
		{
			$ret = $row;
		}

		// Free up system resources and return.
		$this->freeResult($cursor);

		return $ret;
	}

	/**
	 * Method to get an array of the result set rows from the database query where each row is an array.  The array
	 * of objects can optionally be keyed by a field offset, but defaults to a sequential numeric array.
	 *
	 * NOTE: Choosing to key the result array by a non-unique field can result in unwanted
	 * behavior and should be avoided.
	 *
	 * @param   string  $key  The name of a field on which to key the result array.
	 *
	 * @return  mixed   The return value or null if the query failed.
	 */
	public function loadRowList($key = null)
	{
		$array = [];

		// Execute the query and get the result set cursor.
		if (!($cursor = $this->execute()))
		{
			return null;
		}

		// Get all of the rows from the result set as arrays.
		while ($row = $this->fetchArray($cursor))
		{
			if ($key !== null)
			{
				$array[$row[$key]] = $row;
			}
			else
			{
				$array[] = $row;
			}
		}

		// Free up system resources and return.
		$this->freeResult($cursor);

		return $array;
	}

	/**
	 * Locks a table in the database.
	 *
	 * @param   string  $tableNameName  The name of the table to unlock.
	 *
	 * @return  Base  Returns this object to support chaining.
	 */
	public abstract function lockTable($tableNameName);

	/**
	 * Wrap an SQL statement identifier name such as column, table or database names in quotes to prevent injection
	 * risks and reserved word conflicts.
	 *
	 * @param   string  $name  The identifier name to wrap in quotes.
	 *
	 * @return  string  The quote wrapped name.
	 */
	public function nameQuote($name)
	{
		return $this->quoteName($name);
	}

	/**
	 * Opens a database connection. It MUST be overriden by children classes
	 *
	 * @return Base
	 */
	public function open()
	{
		// Don't try to reconnect if we're already connected
		if (is_resource($this->connection) && !is_null($this->connection))
		{
			return $this;
		}

		// Determine utf-8 support
		$this->utf = $this->hasUTF();

		// Set charactersets (needed for MySQL 4.1.2+)
		if ($this->utf)
		{
			$this->setUTF();
		}

		// Select the current database
		$this->select($this->_database);

		return $this;
	}

	/**
	 * Execute the SQL statement.
	 *
	 * @return  mixed  A database cursor resource on success, boolean false on failure.
	 */
	abstract public function query();

	/**
	 * Method to quote and optionally escape a string to database requirements for insertion into the database.
	 *
	 * @param   string   $text    The string to quote.
	 * @param   boolean  $escape  True (default) to escape the string, false to leave it unchanged.
	 */
	public function quote($text, $escape = true)
	{
		return '\'' . ($escape ? $this->escape($text) : $text) . '\'';
	}

	/**
	 * Wrap an SQL statement identifier name such as column, table or database names in quotes to prevent injection
	 * risks and reserved word conflicts.
	 *
	 * @param   mixed  $name      The identifier name to wrap in quotes, or an array of identifier names to wrap in
	 *                            quotes. Each type supports dot-notation name.
	 * @param   mixed  $as        The AS query part associated to $name. It can be string or array, in latter case it
	 *                            has to be same length of $name; if is null there will not be any AS part for string
	 *                            or array element.
	 */
	public function quoteName($name, $as = null)
	{
		if (!is_array($name))
		{
			$quotedName = $this->quoteNameStr(explode('.', $name));

			$quotedAs = '';
			if (!is_null($as))
			{
				$as       = (array) $as;
				$quotedAs .= ' AS ' . $this->quoteNameStr($as);
			}

			return $quotedName . $quotedAs;
		}
		else
		{
			$fin = [];

			if (is_null($as))
			{
				foreach ($name as $str)
				{
					$fin[] = $this->quoteName($str);
				}
			}
			elseif (is_array($name) && (count($name) == (is_array($as) || $as instanceof \Countable ? count($as) : 0)))
			{
				$count = count($name);
				for ($i = 0; $i < $count; $i++)
				{
					$fin[] = $this->quoteName($name[$i], $as[$i]);
				}
			}

			return $fin;
		}
	}

	/**
	 * Renames a table in the database.
	 *
	 * @param   string  $oldTable  The name of the table to be renamed
	 * @param   string  $newTable  The new name for the table.
	 * @param   string  $backup    Table prefix
	 * @param   string  $prefix    For the table - used to rename constraints in non-mysql databases
	 *
	 * @return  Base  Returns this object to support chaining.
	 */
	public abstract function renameTable($oldTable, $newTable, $backup = null, $prefix = null);

	/**
	 * This function replaces a string identifier <var>$prefix</var> with the string held is the
	 * <var>tablePrefix</var> class variable.
	 *
	 * @param   string  $query   The SQL statement to prepare.
	 * @param   string  $prefix  The common table prefix.
	 *
	 * @return  string  The processed SQL statement.
	 */
	public function replacePrefix($query, $prefix = '#__')
	{
		$escaped   = false;
		$startPos  = 0;
		$quoteChar = '';
		$literal   = '';

		$query = trim($query);
		$n     = strlen($query);

		while ($startPos < $n)
		{
			$ip = strpos($query, $prefix, $startPos);
			if ($ip === false)
			{
				break;
			}

			$j = strpos($query, "'", $startPos);
			$k = strpos($query, '"', $startPos);
			if (($k !== false) && (($k < $j) || ($j === false)))
			{
				$quoteChar = '"';
				$j         = $k;
			}
			else
			{
				$quoteChar = "'";
			}

			if ($j === false)
			{
				$j = $n;
			}

			$literal  .= str_replace($prefix, $this->tablePrefix, substr($query, $startPos, $j - $startPos));
			$startPos = $j;

			$j = $startPos + 1;

			if ($j >= $n)
			{
				break;
			}

			// Quote comes first, find end of quote
			while (true)
			{
				$k       = strpos($query, $quoteChar, $j);
				$escaped = false;
				if ($k === false)
				{
					break;
				}
				$l = $k - 1;
				while ($l >= 0 && $query[$l] == '\\')
				{
					$l--;
					$escaped = !$escaped;
				}
				if ($escaped)
				{
					$j = $k + 1;
					continue;
				}
				break;
			}
			if ($k === false)
			{
				// Error in the query - no end quote; ignore it
				break;
			}
			$literal  .= substr($query, $startPos, $k - $startPos + 1);
			$startPos = $k + 1;
		}
		if ($startPos < $n)
		{
			$literal .= substr($query, $startPos, $n - $startPos);
		}

		return $literal;
	}

	/**
	 * Resets the error condition in the driver. Useful to reset the error state after handling a thrown exception.
	 *
	 * @return $this for chaining
	 */
	public function resetErrors()
	{
		$this->errorNum = 0;
		$this->errorMsg = '';

		return $this;
	}

	/**
	 * Select a database for use.
	 *
	 * @param   string  $database  The name of the database to select for use.
	 *
	 * @return  boolean  True if the database was successfully selected.
	 */
	abstract public function select($database);

	/**
	 * Sets the database debugging state for the driver.
	 *
	 * @param   boolean  $level  True to enable debugging.
	 *
	 * @return  boolean  The old debugging level.
	 */
	public function setDebug($level)
	{
		$previous    = $this->debug;
		$this->debug = (bool) $level;

		return $previous;
	}

	/**
	 * Sets the SQL statement string for later execution.
	 *
	 * @param   mixed    $query   The SQL statement to set either as a QueryBase object or a string.
	 * @param   integer  $offset  The affected row offset to set.
	 * @param   integer  $limit   The maximum affected rows to set.
	 *
	 * @return  self  This object to support method chaining.
	 */
	public function setQuery($query, $offset = 0, $limit = 0)
	{
		$this->sql    = $query;
		$this->limit  = (int) $limit;
		$this->offset = (int) $offset;

		return $this;
	}

	/**
	 * Set the connection to use UTF-8 character encoding.
	 *
	 * @return  boolean  True on success.
	 */
	abstract public function setUTF();

	/**
	 * Method to commit a transaction.
	 *
	 * @return  void
	 */
	abstract public function transactionCommit();

	/**
	 * Method to roll back a transaction.
	 *
	 * @return  void
	 */
	abstract public function transactionRollback();

	/**
	 * Method to initialize a transaction.
	 *
	 * @return  void
	 */
	abstract public function transactionStart();

	/**
	 * Method to truncate a table.
	 *
	 * @param   string  $table  The table to truncate
	 *
	 * @return  void
	 */
	public function truncateTable($table)
	{
		$this->setQuery('TRUNCATE TABLE ' . $this->quoteName($table));
		$this->query();
	}

	/**
	 * Unlocks tables in the database.
	 *
	 * @return  Base  Returns this object to support chaining.
	 */
	public abstract function unlockTables();

	/**
	 * Updates a row in a table based on an object's properties.
	 *
	 * @param   string   $table   The name of the database table to update.
	 * @param   object  &$object  A reference to an object whose public properties match the table fields.
	 * @param   string   $key     The name of the primary key.
	 * @param   boolean  $nulls   True to update null fields or false to ignore them.
	 *
	 * @return  boolean  True on success.
	 */
	public function updateObject($table, &$object, $key, $nulls = false)
	{
		$fields = [];
		$where  = [];

		if (is_string($key))
		{
			$key = [$key];
		}

		if (is_object($key))
		{
			$key = (array) $key;
		}

		// Create the base update statement.
		$statement = 'UPDATE ' . $this->quoteName($table) . ' SET %s WHERE %s';

		// Iterate over the object variables to build the query fields/value pairs.
		foreach (get_object_vars($object) as $k => $v)
		{
			// Only process scalars that are not internal fields.
			if (is_array($v) || is_object($v) || ($k[0] == '_'))
			{
				continue;
			}

			// Set the primary key to the WHERE clause instead of a field to update.
			if (in_array($k, $key))
			{
				$where[] = $this->quoteName($k) . '=' . $this->quote($v);
				continue;
			}

			// Prepare and sanitize the fields and values for the database query.
			if ($v === null)
			{
				// If the value is null and we want to update nulls then set it.
				if ($nulls)
				{
					$val = 'NULL';
				}
				// If the value is null and we do not want to update nulls then ignore this field.
				else
				{
					continue;
				}
			}
			// The field is not null so we prep it for update.
			else
			{
				$val = $this->quote($v);
			}

			// Add the field to be updated.
			$fields[] = $this->quoteName($k) . '=' . $val;
		}

		// We don't have any fields to update.
		if (empty($fields))
		{
			return true;
		}

		// Set the query and execute the update.
		$this->setQuery(sprintf($statement, implode(",", $fields), implode(' AND ', $where)));

		return $this->execute();
	}

	/**
	 * Method to fetch a row from the result set cursor as an array.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  mixed  Either the next row from the result set or false if there are no more rows.
	 */
	abstract protected function fetchArray($cursor = null);

	/**
	 * Method to fetch a row from the result set cursor as an object.
	 *
	 * @param   mixed   $cursor  The optional result set cursor from which to fetch the row.
	 * @param   string  $class   The class name to use for the returned row object.
	 *
	 * @return  mixed   Either the next row from the result set or false if there are no more rows.
	 */
	abstract protected function fetchObject($cursor = null, $class = 'stdClass');

	/**
	 * Return the query string to alter the database character set.
	 *
	 * @param   string  $dbName  The database name
	 *
	 * @return  string  The query that alter the database query string
	 */
	protected function getAlterDbCharacterSet($dbName)
	{
		$query = 'ALTER DATABASE ' . $this->quoteName($dbName) . ' CHARACTER SET `utf8`';

		return $query;
	}

	/**
	 * Return the query string to create new Database.
	 * Each database driver, other than MySQL, need to override this member to return correct string.
	 *
	 * @param   object   $options         Object used to pass user and database name to database driver.
	 *                                    This object must have "db_name" and "db_user" set.
	 * @param   boolean  $utf             True if the database supports the UTF-8 character set.
	 *
	 * @return  string  The query that creates database
	 */
	protected function getCreateDatabaseQuery($options, $utf)
	{
		if ($utf)
		{
			$query = 'CREATE DATABASE ' . $this->quoteName($options->db_name) . ' CHARACTER SET `utf8`';
		}
		else
		{
			$query = 'CREATE DATABASE ' . $this->quoteName($options->db_name);
		}

		return $query;
	}

	/**
	 * Gets the name of the database used by this connection.
	 *
	 * @return  string
	 */
	protected function getDatabase()
	{
		return $this->_database;
	}

	/**
	 * Quote strings coming from quoteName call.
	 *
	 * @param   array  $strArr  Array of strings coming from quoteName dot-explosion.
	 *
	 * @return  string  Dot-imploded string of quoted parts.
	 */
	protected function quoteNameStr($strArr)
	{
		$parts = [];
		$q     = $this->nameQuote;

		foreach ($strArr as $part)
		{
			if (is_null($part))
			{
				continue;
			}

			if (strlen($q) == 1)
			{
				$parts[] = $q . $part . $q;
			}
			else
			{
				$parts[] = $q[0] . $part . $q[1];
			}
		}

		return implode('.', $parts);
	}
}
Driver/Pdomysql.php000060400000044715152456704520010341 0ustar00<?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\Driver;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Driver\Query\Pdomysql as QueryPdomysql;
use Akeeba\Engine\FixMySQLHostname;
use Exception;
use PDO;
use PDOException;
use PDOStatement;
use ReflectionClass;
use RuntimeException;

/**
 * PDO MySQL database driver for Akeeba Engine
 *
 * Based on Joomla! Platform 12.1
 */
#[\AllowDynamicProperties]
class Pdomysql extends Mysql
{
	use FixMySQLHostname;

	/**
	 * The default cipher suite for TLS connections.
	 *
	 * @var    array
	 */
	protected static $defaultCipherSuite = [
		'AES128-GCM-SHA256',
		'AES256-GCM-SHA384',
		'AES128-CBC-SHA256',
		'AES256-CBC-SHA384',
		'DES-CBC3-SHA',
	];

	/**
	 * The name of the database driver.
	 *
	 * @var    string
	 */
	public $name = 'pdomysql';

	/** @var string Connection character set */
	protected $charset = 'utf8mb4';

	/** @var PDO The db connection resource */
	protected $connection = null;

	/** @var PDOStatement The database connection cursor from the last query. */
	protected $cursor;

	/** @var array Driver options for PDO */
	protected $driverOptions = [];

	protected $ssl = [];

	/** @var bool Are we in the process of reconnecting to the database server? */
	private $isReconnecting = false;

	/**
	 * Database object constructor
	 *
	 * @param   array  $options  List of options used to configure the connection
	 */
	public function __construct($options)
	{
		$this->driverType = 'mysql';

		// Init
		$this->nameQuote = '`';

		$options['ssl'] = $options['ssl'] ?? [];
		$options['ssl'] = is_array($options['ssl']) ? $options['ssl'] : [];

		$options['ssl']['enable']             =
			($options['ssl']['enable'] ?? $options['dbencryption'] ?? false) ?: false;
		$options['ssl']['cipher']             = ($options['ssl']['cipher'] ?? $options['dbsslcipher'] ?? null) ?: null;
		$options['ssl']['ca']                 = ($options['ssl']['ca'] ?? $options['dbsslca'] ?? null) ?: null;
		$options['ssl']['capath']             = ($options['ssl']['capath'] ?? $options['dbsslcapath'] ?? null) ?: null;
		$options['ssl']['key']                = ($options['ssl']['key'] ?? $options['dbsslkey'] ?? null) ?: null;
		$options['ssl']['cert']               = ($options['ssl']['cert'] ?? $options['dbsslcert'] ?? null) ?: null;
		$options['ssl']['verify_server_cert'] =
			($options['ssl']['verify_server_cert'] ?? $options['dbsslverifyservercert'] ?? false) ?: false;

		// Figure out if a port is included in the host name
		$this->fixHostnamePortSocket($options['host'], $options['port'], $options['socket']);

		// Open the connection
		$this->host           = $options['host'] ?? 'localhost';
		$this->user           = $options['user'] ?? '';
		$this->password       = $options['password'] ?? '';
		$this->port           = $options['port'] ?? '';
		$this->socket         = $options['socket'] ?? '';
		$this->_database      = $options['database'] ?? '';
		$this->selectDatabase = $options['select'] ?? true;
		$this->ssl            = $options['ssl'] ?? [];

		$this->charset       = $options['charset'] ?? 'utf8mb4';
		$this->driverOptions = $options['driverOptions'] ?? [];
		$this->tablePrefix   = $options['prefix'] ?? '';
		$this->connection    = $options['connection'] ?? null;
		$this->errorNum      = 0;
		$this->count         = 0;
		$this->log           = [];
		$this->options       = $options;

		if (!is_object($this->connection))
		{
			$this->open();
		}
	}

	/**
	 * Test to see if the MySQL connector is available.
	 *
	 * @return  boolean  True on success, false otherwise.
	 */
	public static function isSupported()
	{
		if (!defined('\PDO::ATTR_DRIVER_NAME'))
		{
			return false;
		}

		return in_array('mysql', PDO::getAvailableDrivers());
	}

	/**
	 * PDO does not support serialize
	 *
	 * @return  array
	 */
	public function __sleep()
	{
		$serializedProperties = [];

		$reflect = new ReflectionClass($this);

		// Get properties of the current class
		$properties = $reflect->getProperties();

		foreach ($properties as $property)
		{
			// Do not serialize properties that are \PDO
			if ($property->isStatic() == false && !($this->{$property->name} instanceof PDO))
			{
				array_push($serializedProperties, $property->name);
			}
		}

		return $serializedProperties;
	}

	/**
	 * Wake up after serialization
	 *
	 * @return  array
	 */
	public function __wakeup()
	{
		// Get connection back
		$this->__construct($this->options);
	}

	public function close()
	{
		$return = false;

		if (is_object($this->cursor))
		{
			try
			{
				$this->cursor->closeCursor();
			}
			catch (\Throwable $e)
			{
			}
		}

		$this->connection = null;

		return $return;
	}

	/**
	 * Determines if the connection to the server is active.
	 *
	 * @return  boolean  True if connected to the database engine.
	 */
	public function connected()
	{
		if (!is_object($this->connection))
		{
			return false;
		}

		try
		{
			/** @var PDOStatement $statement */
			$statement = $this->connection->prepare('SELECT 1');
			$executed  = $statement->execute();
			$ret       = 0;

			if ($executed)
			{
				$row = [0];

				if (!empty($statement) && $statement instanceof PDOStatement)
				{
					$row = $statement->fetch(PDO::FETCH_NUM);
				}

				$ret = $row[0];
			}

			$status = $ret == 1;

			$statement->closeCursor();
			$statement = null;
		}
		// If we catch an exception here, we must not be connected.
		catch (\Throwable $e)
		{
			$status = false;
		}

		return $status;
	}

	/**
	 * Method to escape a string for usage in an SQL statement.
	 *
	 * @param   string   $text   The string to be escaped.
	 * @param   boolean  $extra  Optional parameter to provide extra escaping.
	 *
	 * @return  string  The escaped string.
	 */
	public function escape($text, $extra = false)
	{
		if (is_int($text) || is_float($text))
		{
			return $text;
		}

		if (is_null($text))
		{
			return 'NULL';
		}

		$result = substr($this->connection->quote($text), 1, -1);

		if ($extra)
		{
			$result = addcslashes($result, '%_');
		}

		return $result;
	}

	/**
	 * Method to fetch a row from the result set cursor as an associative array.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  mixed  Either the next row from the result set or false if there are no more rows.
	 */
	public function fetchAssoc($cursor = null)
	{
		$ret = null;

		if (!empty($cursor) && $cursor instanceof PDOStatement)
		{
			$ret = $cursor->fetch(PDO::FETCH_ASSOC);
		}
		elseif ($this->cursor instanceof PDOStatement)
		{
			$ret = $this->cursor->fetch(PDO::FETCH_ASSOC);
		}

		return $ret;
	}

	/**
	 * Method to free up the memory used for the result set.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  void
	 */
	public function freeResult($cursor = null)
	{
		if ($cursor instanceof PDOStatement)
		{
			$cursor->closeCursor();
			$cursor = null;
		}

		if ($this->cursor instanceof PDOStatement)
		{
			$this->cursor->closeCursor();
			$this->cursor = null;
		}
	}

	/**
	 * Get the number of affected rows for the previous executed SQL statement.
	 *
	 * @return  integer  The number of affected rows.
	 */
	public function getAffectedRows()
	{
		if ($this->cursor instanceof PDOStatement)
		{
			return $this->cursor->rowCount();
		}

		return 0;
	}

	/**
	 * Get the number of returned rows for the previous executed SQL statement.
	 *
	 * @param   resource  $cursor  An optional database cursor resource to extract the row count from.
	 *
	 * @return  integer   The number of returned rows.
	 */
	public function getNumRows($cursor = null)
	{
		if ($cursor instanceof PDOStatement)
		{
			return $cursor->rowCount();
		}

		if ($this->cursor instanceof PDOStatement)
		{
			return $this->cursor->rowCount();
		}

		return 0;
	}

	/**
	 * Get the current or query, or new JDatabaseQuery object.
	 *
	 * @param   boolean  $new  False to return the last query set, True to return a new JDatabaseQuery object.
	 *
	 * @return  mixed  The current value of the internal SQL variable or a new JDatabaseQuery object.
	 */
	public function getQuery($new = false)
	{
		if ($new)
		{
			return new QueryPdomysql($this);
		}
		else
		{
			return $this->sql;
		}
	}

	public function createQuery()
	{
		return new QueryPdomysql($this);
	}

	/**
	 * Get the version of the database connector.
	 *
	 * @return  string  The database connector version.
	 */
	public function getVersion()
	{
		$version = $this->connection->getAttribute(\PDO::ATTR_SERVER_VERSION);

		if (stripos($version, 'mariadb') !== false)
		{
			// MariaDB: Strip off any leading '5.5.5-', if present
			return preg_replace('/^5\.5\.5-/', '', $version);
		}

		return $version;
	}

	/**
	 * Determines if the database engine supports UTF-8 character encoding.
	 *
	 * @return  boolean  True if supported.
	 */
	public function hasUTF()
	{
		$serverVersion = $this->getVersion();
		$mariadb       = stripos($serverVersion, 'mariadb') !== false;

		// At this point we know the client supports utf8mb4.  Now we must check if the server supports utf8mb4 as well.
		$utf8mb4 = version_compare($serverVersion, '5.5.3', '>=');

		if ($mariadb && version_compare($serverVersion, '10.0.0', '<'))
		{
			$utf8mb4 = false;
		}

		return $utf8mb4;
	}

	/**
	 * Method to get the auto-incremented value from the last INSERT statement.
	 *
	 * @return  integer  The value of the auto-increment field from the last inserted row.
	 */
	public function insertid()
	{
		// Error suppress this to prevent PDO warning us that the driver doesn't support this operation.
		return @$this->connection->lastInsertId();
	}

	/**
	 * Method to get the next row in the result set from the database query as an object.
	 *
	 * @param   string  $class  The class name to use for the returned row object.
	 *
	 * @return  mixed   The result of the query as an array, false if there are no more rows.
	 */
	public function loadNextObject($class = 'stdClass')
	{
		// Execute the query and get the result set cursor.
		if (!$this->cursor)
		{
			if (!($this->execute()))
			{
				return $this->errorNum ? null : false;
			}
		}

		// Get the next row from the result set as an object of type $class.
		if ($row = $this->fetchObject(null, $class))
		{
			return $row;
		}

		// Free up system resources and return.
		$this->freeResult();

		return false;
	}

	/**
	 * Method to get the next row in the result set from the database query as an array.
	 *
	 * @return  mixed  The result of the query as an array, false if there are no more rows.
	 */
	public function loadNextRow()
	{
		// Execute the query and get the result set cursor.
		if (!$this->cursor)
		{
			if (!($this->execute()))
			{
				return $this->errorNum ? null : false;
			}
		}

		// Get the next row from the result set as an object of type $class.
		if ($row = $this->fetchArray())
		{
			return $row;
		}

		// Free up system resources and return.
		$this->freeResult();

		return false;
	}

	public function open()
	{
		if ($this->connected())
		{
			return;
		}
		else
		{
			$this->close();
		}

		if (!isset($this->charset))
		{
			$this->charset = 'utf8mb4';
		}

		$this->port = $this->port ?: 3306;

		$format = 'mysql:host=#HOST#;port=#PORT#;dbname=#DBNAME#;charset=#CHARSET#';

		if ($this->socket)
		{
			$format = 'mysql:socket=#SOCKET#;dbname=#DBNAME#;charset=#CHARSET#';
		}

		$replace = ['#HOST#', '#PORT#', '#SOCKET#', '#DBNAME#', '#CHARSET#'];
		$with    = [$this->host, $this->port, $this->socket, $this->_database, $this->charset];

		// Create the connection string:
		$connectionString = str_replace($replace, $with, $format);

		// For SSL/TLS connection encryption.
		if ($this->ssl !== [] && $this->ssl['enable'] === true)
		{
			$sslContextIsNull = true;

			// If customised, add cipher suite, ca file path, ca path, private key file path and certificate file path to PDO driver options.
			foreach (['cipher', 'ca', 'capath', 'key', 'cert'] as $key => $value)
			{
				if ($this->ssl[$value] !== null)
				{
					$this->driverOptions[constant('\PDO::MYSQL_ATTR_SSL_' . strtoupper($value))] = $this->ssl[$value];

					$sslContextIsNull = false;
				}
			}

			// PDO, if no cipher, ca, capath, cert and key are set, can't start TLS one-way connection, set a common ciphers suite to force it.
			if ($sslContextIsNull === true)
			{
				$this->driverOptions[\PDO::MYSQL_ATTR_SSL_CIPHER] = implode(':', static::$defaultCipherSuite);
			}

			// If customised, for capable systems (PHP 7.0.14+ and 7.1.4+) verify certificate chain and Common Name to driver options.
			if ($this->ssl['verify_server_cert'] !== null && defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT'))
			{
				$this->driverOptions[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->ssl['verify_server_cert'];
			}
		}

		// connect to the server
		try
		{
			$this->connection = new PDO(
				$connectionString,
				$this->user,
				$this->password,
				$this->driverOptions
			);
		}
		catch (PDOException $e)
		{
			// If we tried connecting through utf8mb4 and we failed let's retry with regular utf8
			if ($this->charset == 'utf8mb4')
			{
				$this->charset = 'UTF8';
				$this->open();

				return;
			}

			$this->errorNum = 2;
			$this->errorMsg = 'Could not connect to MySQL via PDO: ' . $e->getMessage();

			return;
		}

		// Reset the SQL mode of the connection
		try
		{
			$this->connection->exec("SET @@SESSION.sql_mode = '';");
		}
			// Ignore any exceptions (incompatible MySQL versions)
		catch (Exception $e)
		{
		}

		$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
		$this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

		if ($this->selectDatabase && !empty($this->_database))
		{
			$this->select($this->_database);
		}

		$this->freeResult();
	}

	/**
	 * Execute the SQL statement.
	 *
	 * @return  mixed  A database cursor resource on success, boolean false on failure.
	 */
	public function query()
	{
		if (!is_object($this->connection))
		{
			$this->open();
		}

		$this->freeResult();

		// Take a local copy so that we don't modify the original query and cause issues later
		$query = $this->replacePrefix((string)$this->sql);

		if ($this->limit > 0 || $this->offset > 0)
		{
			$query .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
		}

		// Increment the query counter.
		$this->count++;

		// If debugging is enabled then let's log the query.
		if ($this->debug)
		{
			// Add the query to the object queue.
			$this->log[] = $query;
		}

		// Reset the error values.
		$this->errorNum = 0;
		$this->errorMsg = '';

		// Execute the query. Error suppression is used here to prevent warnings/notices that the connection has been lost.
		try
		{
			$this->cursor = $this->connection->query($query);
		}
		catch (Exception $e)
		{
		}

		// If an error occurred handle it.
		if (!$this->cursor)
		{
			$errorInfo      = $this->connection->errorInfo();
			$this->errorNum = $errorInfo[1];
			$this->errorMsg = $errorInfo[2] . ' SQL=' . $query;

			// Check if the server was disconnected.
			if (!$this->connected() && !$this->isReconnecting)
			{
				$this->isReconnecting = true;

				try
				{
					// Attempt to reconnect.
					$this->connection = null;
					$this->open();
				}
					// If connect fails, ignore that exception and throw the normal exception.
				catch (RuntimeException $e)
				{
					throw new RuntimeException($this->errorMsg, $this->errorNum);
				}

				// Since we were able to reconnect, run the query again.
				$result               = $this->query();
				$this->isReconnecting = false;

				return $result;
			}
			// The server was not disconnected.
			else
			{
				throw new RuntimeException($this->errorMsg, $this->errorNum);
			}
		}

		return $this->cursor;
	}

	/**
	 * Select a database for use.
	 *
	 * @param   string  $database  The name of the database to select for use.
	 *
	 * @return  boolean  True if the database was successfully selected.
	 */
	public function select($database)
	{
		try
		{
			$this->connection->exec('USE ' . $this->quoteName($database));
		}
		catch (Exception $e)
		{
			$errorInfo      = $this->connection->errorInfo();
			$this->errorNum = $errorInfo[1];
			$this->errorMsg = $errorInfo[2];

			return false;
		}

		return true;
	}

	/**
	 * Set the connection to use UTF-8 character encoding.
	 *
	 * @return  boolean  True on success.
	 */
	public function setUTF()
	{
		return true;
	}

	/**
	 * Method to commit a transaction.
	 *
	 * @return  void
	 */
	public function transactionCommit()
	{
		$this->connection->commit();
	}

	/**
	 * Method to roll back a transaction.
	 *
	 * @return  void
	 */
	public function transactionRollback()
	{
		$this->connection->rollBack();
	}

	/**
	 * Method to initialize a transaction.
	 *
	 * @return  void
	 */
	public function transactionStart()
	{
		$this->connection->beginTransaction();
	}

	/**
	 * Method to fetch a row from the result set cursor as an array.
	 *
	 * @param   mixed  $cursor  The optional result set cursor from which to fetch the row.
	 *
	 * @return  mixed  Either the next row from the result set or false if there are no more rows.
	 */
	protected function fetchArray($cursor = null)
	{
		$ret = null;

		if (!empty($cursor) && $cursor instanceof PDOStatement)
		{
			$ret = $cursor->fetch(PDO::FETCH_NUM);
		}
		elseif ($this->cursor instanceof PDOStatement)
		{
			$ret = $this->cursor->fetch(PDO::FETCH_NUM);
		}

		return $ret;
	}

	/**
	 * Method to fetch a row from the result set cursor as an object.
	 *
	 * @param   mixed   $cursor  The optional result set cursor from which to fetch the row.
	 * @param   string  $class   The class name to use for the returned row object.
	 *
	 * @return  mixed   Either the next row from the result set or false if there are no more rows.
	 */
	protected function fetchObject($cursor = null, $class = 'stdClass')
	{
		$ret = null;

		if (!empty($cursor) && $cursor instanceof PDOStatement)
		{
			$ret = $cursor->fetchObject($class);
		}
		elseif ($this->cursor instanceof PDOStatement)
		{
			$ret = $this->cursor->fetchObject($class);
		}

		return $ret;
	}
}
web.config000060400000001025152456704520006514 0ustar00<?xml version="1.0"?>
<!--
    This only works on IIS 7 or later. See https://www.iis.net/configreference/system.webserver/security/requestfiltering/fileextensions
-->
<configuration>
    <system.webServer>
        <security>
            <requestFiltering>
                <fileExtensions allowUnlisted="false" >
                    <clear />
                    <add fileExtension=".html" allowed="true"/>
                </fileExtensions>
            </requestFiltering>
        </security>
    </system.webServer>
</configuration>Platform/Exception/DecryptionException.php000060400000002612152456704520015005 0ustar00<?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\Platform\Exception;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Platform;
use Exception;
use RuntimeException;

/**
 * Thrown when the settings cannot be decrypted, e.g. when the server no longer has encyrption enabled or the key has
 * changed.
 */
class DecryptionException extends RuntimeException
{
	public function __construct($message = null, $code = 500, Exception $previous = null)
	{
		if (empty($message))
		{
			$message = Platform::getInstance()->translate('COM_AKEEBA_CONFIG_ERR_DECRYPTION');
		}

		parent::__construct($message, $code, $previous);
	}

}
Platform/PlatformInterface.php000060400000030332152456704520012455 0ustar00<?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\Platform;

defined('AKEEBAENGINE') || die();

use Exception;

/**
 * Interface PlatformInterface
 *
 * @property string $tableNameProfiles The name of the table where backup profiles are stored
 * @property string $tableNameStats    The name of the table where backup records are stored
 * @property array  $configOverrides   Configuration overrides
 */
interface PlatformInterface
{
	/**
	 * Returns an array with the directory/-ies in which the magic autoloader should look for platform overrides.
	 *
	 * @return  array
	 */
	public function getPlatformDirectories();

	/**
	 * Performs heuristics to determine if this platform object is the ideal
	 * candidate for the environment Akeeba Engine is running in.
	 *
	 * @return  bool
	 */
	public function isThisPlatform();

	/**
	 * Saves the current configuration to the database table
	 *
	 * @param   int  $profile_id      The profile where to save the configuration
	 *                                to, defaults to current profile
	 *
	 * @return  bool  True if everything was saved properly
	 */
	public function save_configuration($profile_id = null);

	/**
	 * Loads the current configuration off the database table
	 *
	 * @param   int  $profile_id  The profile where to read the configuration from, defaults to current profile
	 *
	 * @return  bool  True if everything was read properly
	 */
	public function load_configuration($profile_id = null);

	/**
	 * Returns an associative array of stock platform directories
	 *
	 * @return  array
	 */
	public function get_stock_directories();

	/**
	 * Returns the absolute path to the site's root
	 *
	 * @return  string
	 */
	public function get_site_root();

	/**
	 * Returns the absolute path to the installer images directory
	 *
	 * @return  string
	 */
	public function get_installer_images_path();

	/**
	 * Returns the active profile number
	 *
	 * @return  integer
	 */
	public function get_active_profile();

	/**
	 * Returns the selected profile's name. If no ID is specified, the current
	 * profile's name is returned.
	 *
	 * @param   integer|null  $id  The ID of the profile, skip for current profile
	 *
	 * @return  string
	 */
	public function get_profile_name($id = null);

	/**
	 * Returns the backup origin
	 *
	 * @return  string  Backup origin: backend|frontend
	 */
	public function get_backup_origin();

	/**
	 * Returns a timestamp formatted for the current site's database driver
	 *
	 * @param   string  $date  [optional] The timestamp to use. Omit to use current timestamp.
	 *
	 * @return  string
	 */
	public function get_timestamp_database($date = 'now');

	/**
	 * Returns the current timestamp, taking into account any TZ information,
	 * in the format specified by $format.
	 *
	 * @param   string  $format  Timestamp format string (standard PHP format string)
	 *
	 * @return  string
	 */
	public function get_local_timestamp($format);

	/**
	 * Returns the current host name
	 *
	 * @return  string
	 */
	public function get_host();

	/**
	 * Returns the current site's name
	 *
	 * @return  string
	 */
	public function get_site_name();

	/**
	 * Creates or updates the statistics record of the current backup attempt
	 *
	 * @param   int    $id    Backup record ID, use null for new record
	 * @param   array  $data  The data to store
	 *
	 * @return int|null The new record id, or null if this doesn't apply
	 *
	 * @throws Exception On database error
	 */
	public function set_or_update_statistics($id = null, $data = []);

	/**
	 * Loads and returns a backup statistics record as a hash array
	 *
	 * @param   int  $id  Backup record ID
	 *
	 * @return  array
	 */
	public function get_statistics($id);

	/**
	 * Completely removes a backup statistics record
	 *
	 * @param   int  $id  Backup record ID
	 *
	 * @return  bool  True on success
	 */
	public function delete_statistics($id);

	/**
	 * Returns a list of backup statistics records, respecting the pagination
	 *
	 * The $config array allows the following options to be set:
	 * limitstart    int        Offset in the recordset to start from
	 * limit        int        How many records to return at once
	 * filters        array    An array of filters to apply to the results. Alternatively you can just pass a profile
	 * ID to filter by that profile. order        array    Record ordering information (by and ordering)
	 *
	 * @param   array  $config  See above
	 *
	 * @return  array
	 */
	function &get_statistics_list($config = []);

	/**
	 * Return the total number of statistics records
	 *
	 * @param   array  $filters  An array of filters to apply to the results. Alternatively you can just pass a profile
	 *                           ID to filter by that profile.
	 *
	 * @return  integer
	 */
	function get_statistics_count($filters = null);

	/**
	 * Returns an array with the specifics of running backups
	 *
	 * @param   string  $tag  The backup type (e.g. backend)
	 *
	 * @return  array
	 */
	public function get_running_backups($tag = null);

	/**
	 * Multiple backup attempts can share the same backup file name. Only
	 * the last backup attempt's file is considered valid. Previous attempts
	 * have to be deemed "obsolete". This method returns a list of backup
	 * statistics ID's with "valid"-looking names. IT DOES NOT CHECK FOR THE
	 * EXISTENCE OF THE BACKUP FILE!
	 *
	 * @param   bool    $useprofile   If true, it will only return backup records of the current profile
	 * @param   array   $tagFilters   Which tags to include; leave blank for all. If the first item is "NOT", then all
	 *                                tags EXCEPT those listed will be included.
	 * @param   string  $ordering     Ordering of the records, default DESC (descending), use DESC or ASC
	 *
	 * @return  array    A list of ID's for records w/ "valid"-looking backup files
	 */
	public function &get_valid_backup_records($useprofile = false, $tagFilters = [], $ordering = 'DESC');

	/**
	 * Invalidates older records sharing the same $archivename
	 *
	 * @param   string  $archivename  The archive name
	 *
	 * @return  void
	 */
	public function remove_duplicate_backup_records($archivename);

	/**
	 * Marks the specified backup records as having no files
	 *
	 * @param   array  $ids  Array of backup record IDs to ivalidate
	 *
	 * @return  void
	 */
	public function invalidate_backup_records($ids);

	/**
	 * Gets a list of records with remotely stored files in the selected remote storage
	 * provider and profile.
	 *
	 * @param   int     $profile   (optional) The profile to use. Skip or use null for active profile.
	 * @param   string  $engine    (optional) The remote engine to looks for. Skip or use null for the active profile's
	 *                             engine.
	 *
	 * @return  array
	 */
	public function get_valid_remote_records($profile = null, $engine = null);

	/**
	 * Returns the filter data for the entire filter group collection
	 *
	 * @return  array
	 */
	public function &load_filters();

	/**
	 * Saves the nested filter data array $filter_data to the database
	 *
	 * @param   array  $filter_data  The filter data to save
	 *
	 * @return  bool  True on success
	 */
	public function save_filters(&$filter_data);

	/**
	 * Gets the best matching database driver class, according to CMS settings
	 *
	 * @param   bool  $use_platform     If set to false, it will forcibly try to assign one of the primitive type
	 *                                  (Mysql/Mysqli) and NEVER tell you to use an platform driver
	 *
	 * @return  string
	 */
	public function get_default_database_driver($use_platform = true);

	/**
	 * Returns a set of options to connect to the default database of the current CMS
	 *
	 * @return  array
	 */
	public function get_platform_database_options();

	/**
	 * Provides a platform-specific translation function
	 *
	 * @param   string  $key  The translation key
	 *
	 * @return  string
	 */
	public function translate($key);

	/**
	 * Populates global constants holding the Akeeba version
	 *
	 * @return  void
	 */
	public function load_version_defines();

	/**
	 * Returns the platform name and version
	 *
	 * @return  array  Contains the platform name and version
	 */
	public function getPlatformVersion();

	/**
	 * Logs platform-specific directories with LogLevel::INFO log level
	 *
	 * @return  string  If there are any extra notes / warnings on this platform
	 */
	public function log_platform_special_directories();

	/**
	 * Loads a platform-specific software configuration option. These are
	 * configuration values for the backup engine, but unlike the rest of the
	 * configuration options they are not stored in the backup profile.
	 * Instead, these are stored globally using a platform-specific method.
	 * So these are not configuration values for the platform itself.
	 *
	 * @param   string  $key      The configuration option to retrieve
	 * @param   mixed   $default  The default value to return if it's not defined
	 *
	 * @return  mixed
	 */
	public function get_platform_configuration_option($key, $default);

	/**
	 * Returns a list of emails to the administrators
	 *
	 * @return  array
	 */
	public function get_administrator_emails();

	/**
	 * Sends a very simple email using the platform's emailer facility
	 *
	 * @param   string  $to          Recipient address
	 * @param   string  $subject     Email subject line
	 * @param   string  $body        Email body (plain text)
	 * @param   string  $attachFile  (optional) Full path to the file being attached
	 *
	 * @return  bool  True on success
	 */
	public function send_email($to, $subject, $body, $attachFile = null);

	/**
	 * Deletes a file from the local server using direct file access or FTP
	 *
	 * @param   string  $file  The file to unlink
	 *
	 * @return  bool  True on success
	 */
	public function unlink($file);

	/**
	 * Moves a file around within the local server using direct file access or FTP
	 *
	 * @param   string  $from  Full path of the file to move
	 * @param   string  $to    Full path of where the file will be moved to
	 *
	 * @return  bool  True on success
	 */
	public function move($from, $to);

	/**
	 * Stores a flash (temporary) variable in the session.
	 *
	 * @param   string  $name   The name of the variable to store
	 * @param   string  $value  The value of the variable to store
	 *
	 * @return  void
	 */
	public function set_flash_variable($name, $value);

	/**
	 * Return the value of a flash (temporary) variable from the session and
	 * immediately removes it.
	 *
	 * @param   string  $name     The name of the flash variable
	 * @param   mixed   $default  Default value, if the variable is not defined
	 *
	 * @return  mixed  The value of the variable or $default if it's not set
	 */
	public function get_flash_variable($name, $default = null);

	/**
	 * Perform an immediate redirection to the defined URL
	 *
	 * @param   string  $url  The URL to redirect to
	 *
	 * @return  void
	 */
	public function redirect($url);

	/**
	 * Get the proxy configuration for this platform.
	 *
	 * @return  array{enabled: bool, host: string, port: int, user: string, pass: string}
	 * @since   9.0.7
	 */
	public function getProxySettings();

	/**
	 * Set the proxy configuration for this platform
	 *
	 * @param   false   $useProxy  Should I use a proxy at all?
	 * @param   string  $host      Proxy hostname or IP address
	 * @param   int     $port      Proxy port
	 * @param   string  $username  Proxy username. Optional. Leavel blank to turn off authentication.
	 * @param   string  $password  Proxy password.
	 *
	 * @return  void
	 * @since   9.0.7
	 */
	public function setProxySettings($useProxy = false, $host = '', $port = 8080, $username = '', $password = '');
}
Platform/Base.php000060400000072161152456704520007730 0ustar00<?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\Platform;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Driver\Mysqli;
use Akeeba\Engine\Driver\QueryException;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform\Exception\DecryptionException;
use Akeeba\Engine\Util\ProfileMigration;
use DateTime;
use DateTimeZone;
use Exception;
use RuntimeException;

abstract class Base implements PlatformInterface
{
	/** @var array Configuration overrides */
	public $configOverrides = [];

	/** @var bool Should I throw an exception when settings decryption fails? */
	public $decryptionException = false;

	/** @var string The name of the platform (same as the directory name) */
	public $platformName = null;

	/** @var int Priority of this platform. A lower number denotes higher priority. */
	public $priority = 50;

	/** @var string The name of the table where backup profiles are stored */
	public $tableNameProfiles = '#__ak_profiles';

	/** @var string The name of the table where backup records are stored */
	public $tableNameStats = '#__ak_stats';

	/** @var bool Have I initialised the proxy configuration settings? */
	protected $hasInitialisedProxySettings = false;

	/** @var bool Should I use a proxy? */
	protected $proxyEnabled = false;

	/** @var string Proxy hostname or IP address */
	protected $proxyHost = '';

	/** @var string Proxy password; only applied with a non-empty username */
	protected $proxyPass = '';

	/** @var int Proxy port */
	protected $proxyPort = 8080;

	/** @var string Proxy username; empty means no authentication */
	protected $proxyUser = '';

	/**
	 * Completely removes a backup statistics record
	 *
	 * @param   int  $id  Backup record ID
	 *
	 * @return bool True on success
	 */
	public function delete_statistics($id)
	{
		$db    = Factory::getDatabase($this->get_platform_database_options());
		$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->delete($db->qn($this->tableNameStats))
			->where($db->qn('id') . ' = ' . $db->q($id));
		$db->setQuery($query);

		$result = true;
		try
		{
			$db->query();
		}
		catch (Exception $exc)
		{
			$result = false;
		}

		return $result;
	}

	public function getPlatformDirectories()
	{
		return [dirname(__FILE__) . '/' . $this->platformName];
	}

	public function getPlatformVersion()
	{
		return [
			'name'    => 'Platform',
			'version' => 'unknown',
		];
	}

	/**
	 * @inheritdoc
	 */
	public final function getProxySettings()
	{
		if (!$this->hasInitialisedProxySettings)
		{
			$this->detectProxySettings();
		}

		return [
			'enabled' => $this->proxyEnabled ?? false,
			'host'    => $this->proxyHost ?: '',
			'port'    => $this->proxyPort ?: 8080,
			'user'    => $this->proxyUser ?: '',
			'pass'    => $this->proxyPass ?: '',
		];
	}

	public function get_active_profile()
	{
		return 1;
	}

	public function get_administrator_emails()
	{
		return [];
	}

	public function get_backup_origin()
	{
		return 'backend';
	}

	public function get_default_database_driver($use_platform = true)
	{
		return Mysqli::class;
	}

	public function get_host()
	{
		return '';
	}

	public function get_installer_images_path()
	{
		return '';
	}

	public function get_local_timestamp($format)
	{
		$dateNow = new DateTime('now', new DateTimeZone('UTC'));

		return $dateNow->format($format);
	}

	public function get_platform_configuration_option($key, $default)
	{
		return '';
	}

	public function get_platform_database_options()
	{
		return [];
	}

	public function get_profile_name($id = null)
	{
		return '';
	}

	/**
	 * Returns an array with the specifics of running backups
	 *
	 * @param   string  $tag
	 *
	 * @return  array   Array list of associative arrays
	 * @throws  QueryException
	 *
	 */
	public function get_running_backups($tag = null)
	{
		$db    = Factory::getDatabase($this->get_platform_database_options());
		$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->select('*')
			->from($db->qn($this->tableNameStats))
			->where($db->qn('status') . ' = ' . $db->q('run'))
			->where(' NOT ' . $db->qn('archivename') . ' = ' . $db->q(''));
		if (!empty($tag))
		{
			$query->where($db->qn('origin') . ' LIKE ' . $db->q($tag . '%'));
		}
		$db->setQuery($query);

		return $db->loadAssocList();
	}

	public function get_site_name()
	{
		return '';
	}

	public function get_site_root()
	{
		return '';
	}

	/**
	 * Loads and returns a backup statistics record as a hash array
	 *
	 * @param   int  $id  Backup record ID
	 *
	 * @return array
	 */
	public function get_statistics($id)
	{
		$db    = Factory::getDatabase($this->get_platform_database_options());
		$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->select('*')
			->from($db->qn($this->tableNameStats))
			->where($db->qn('id') . ' = ' . $db->q($id));
		$db->setQuery($query);

		return $db->loadAssoc();
	}

	/**
	 * Return the total number of statistics records
	 *
	 * @param   array  $filters  An array of filters to apply to the results. Alternatively you can just pass a profile
	 *                           ID to filter by that profile.
	 *
	 * @return int
	 */
	function get_statistics_count($filters = null)
	{
		$db = Factory::getDatabase($this->get_platform_database_options());

		$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true));

		if (!empty($filters))
		{
			if (is_array($filters))
			{
				if (!empty($filters))
				{
					// Parse the filters array
					foreach ($filters as $f)
					{
						$clause = $db->quoteName($f['field']);
						if (array_key_exists('operand', $f))
						{
							$clause .= ' ' . strtoupper($f['operand']) . ' ';
						}
						else
						{
							$clause .= ' = ';
						}
						if ($f['operand'] == 'BETWEEN')
						{
							$clause .= $db->q($f['value']) . ' AND ' . $db->q($f['value2']);
						}
						elseif ($f['operand'] == 'LIKE')
						{
							$clause .= '\'%' . $db->escape($f['value']) . '%\'';
						}
						else
						{
							$clause .= $db->q($f['value']);
						}
						$query->where($clause);
					}
				}
			}
			else
			{
				// Legacy mode: profile ID given
				$query->where($db->qn('profile_id') . ' = ' . $db->q($filters));
			}
		}

		$query->select('COUNT(*)')
			->from($db->quoteName($this->tableNameStats));
		$db->setQuery($query);

		return $db->loadResult();
	}

	/**
	 * Returns a list of backup statistics records, respecting the pagination
	 *
	 * The $config array allows the following options to be set:
	 * limitstart    int        Offset in the recordset to start from
	 * limit        int        How many records to return at once
	 * filters        array    An array of filters to apply to the results. Alternatively you can just pass a profile
	 * ID to filter by that profile. order        array    Record ordering information (by and ordering)
	 *
	 * @return array
	 */
	function &get_statistics_list($config = [])
	{
		$defaultConfiguration = [
			'limitstart' => 0,
			'limit'      => 0,
			'filters'    => [],
			'order'      => null,
		];
		$config               = (object) array_merge($defaultConfiguration, $config);

		$db = Factory::getDatabase($this->get_platform_database_options());

		$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true));

		if (!empty($config->filters))
		{
			if (is_array($config->filters))
			{
				if (!empty($config->filters))
				{
					// Parse the filters array
					foreach ($config->filters as $f)
					{
						$clause = $db->qn($f['field']);
						if (array_key_exists('operand', $f))
						{
							$clause .= ' ' . strtoupper($f['operand']) . ' ';
							if ($f['operand'] == 'BETWEEN')
							{
								$clause .= $db->q($f['value']) . ' AND ' . $db->q($f['value2']);
							}
							elseif ($f['operand'] == 'LIKE')
							{
								$clause .= '\'%' . $db->escape($f['value']) . '%\'';
							}
							else
							{
								$clause .= $db->q($f['value']);
							}
						}
						else
						{
							$clause .= ' = ' . $db->q($f['value']);
						}

						$query->where($clause);
					}
				}
			}
			else
			{
				// Legacy mode: profile ID given
				$query->where($db->qn('profile_id') . ' = ' . $db->q($config->filters));
			}
		}

		if (empty($config->order) || !is_array($config->order))
		{
			$config->order = [
				'by'    => 'id',
				'order' => 'DESC',
			];
		}

		$query->select('*')
			->from($db->qn($this->tableNameStats))
			->order($db->qn($config->order['by']) . " " . strtoupper($config->order['order']));

		$db->setQuery($query, $config->limitstart, $config->limit);

		$list = $db->loadAssocList();

		return $list;
	}

	public function get_stock_directories()
	{
		return [];
	}

	public function get_timestamp_database($date = 'now')
	{
		return '';
	}

	/**
	 * Multiple backup attempts can share the same backup file name. Only
	 * the last backup attempt's file is considered valid. Previous attempts
	 * have to be deemed "obsolete". This method returns a list of backup
	 * statistics ID's with "valid"-looking names. IT DOES NOT CHECK FOR THE
	 * EXISTENCE OF THE BACKUP FILE!
	 *
	 * @param   bool    $useprofile  If true, it will only return backup records of the current profile
	 * @param   array   $tagFilters  Which tags to include; leave blank for all. If the first item is "NOT", then all
	 *                               tags EXCEPT those listed will be included.     *
	 * @param   string  $ordering
	 *
	 * @return  array A list of ID's for records w/ "valid"-looking backup files
	 * @throws  QueryException
	 *
	 */
	public function &get_valid_backup_records($useprofile = false, $tagFilters = [], $ordering = 'DESC')
	{
		$db = Factory::getDatabase($this->get_platform_database_options());

		$query2 = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->select('MAX(' . $db->qn('id') . ') AS ' . $db->qn('id'))
			->from($db->qn($this->tableNameStats))
			->where($db->qn('status') . ' = ' . $db->q('complete'))
			->group($db->qn('absolute_path'));

		$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->select($db->qn('id'))
			->from($db->qn($this->tableNameStats))
			->where($db->qn('filesexist') . ' = ' . $db->q(1))
			->where($db->qn('id') . ' IN (' . $query2 . ')')
			->where('NOT ' . $db->qn('absolute_path') . ' = ' . $db->q(''))
			->order($db->qn('id') . ' ' . $ordering);

		if ($useprofile)
		{
			$profile_id = $this->get_active_profile();
			$query->where($db->qn('profile_id') . " = " . $db->q($profile_id));
		}

		if (!empty($tagFilters))
		{
			$operator = '';
			$first    = array_shift($tagFilters);
			if ($first == 'NOT')
			{
				$operator = 'NOT';
			}
			else
			{
				array_unshift($tagFilters, $first);
			}

			$quotedTags = [];
			foreach ($tagFilters as $tag)
			{
				$quotedTags[] = $db->q($tag);
			}
			$filter = implode(', ', $quotedTags);
			unset($quotedTags);
			$query->where($operator . ' ' . $db->quoteName('tag') . ' IN (' . $filter . ')');
		}

		$db->setQuery($query);
		$array = $db->loadColumn();

		return $array;
	}

	/**
	 * Gets a list of records with remotely stored files in the selected remote storage
	 * provider and profile.
	 *
	 * @param $profile int (optional) The profile to use. Skip or use null for active profile.
	 * @param $engine  string (optional) The remote engine to looks for. Skip or use null for the active profile's
	 *                 engine.
	 *
	 * @return array
	 */
	public function get_valid_remote_records($profile = null, $engine = null)
	{
		$config = Factory::getConfiguration();
		$result = [];

		if (is_null($profile))
		{
			$profile = $this->get_active_profile();
		}
		if (is_null($engine))
		{
			$engine = $config->get('akeeba.advanced.postproc_engine', '');
		}

		if (empty($engine))
		{
			return $result;
		}

		$db  = Factory::getDatabase($this->get_platform_database_options());
		$sql = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->select('*')
			->from($db->qn($this->tableNameStats))
			->where($db->qn('profile_id') . ' = ' . $db->q($profile))
			->where($db->qn('remote_filename') . ' LIKE ' . $db->q($engine . '://%'))
			->order($db->qn('id') . ' DESC');

		$db->setQuery($sql);

		return $db->loadAssocList();
	}

	/**
	 * Marks the specified backup records as having no files
	 *
	 * @param   array  $ids  Array of backup record IDs to ivalidate
	 */
	public function invalidate_backup_records($ids)
	{
		if (empty($ids))
		{
			return false;
		}
		$db   = Factory::getDatabase($this->get_platform_database_options());
		$temp = [];
		foreach ($ids as $id)
		{
			$temp[] = $db->q($id);
		}
		$list = implode(',', $temp);
		$sql  = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->update($db->qn($this->tableNameStats))
			->set($db->qn('filesexist') . ' = ' . $db->q('0'))
			->where($db->qn('id') . ' IN (' . $list . ')');;
		$db->setQuery($sql);

		try
		{
			$db->query();
		}
		catch (Exception $exc)
		{
			return false;
		}

		return true;
	}

	public function isThisPlatform()
	{
		return true;
	}

	/**
	 * Loads the current configuration off the database table
	 *
	 * @param   int   $profile_id  The profile where to read the configuration from, defaults to current profile
	 * @param   bool  $reset       Should I reset the Configuration object before loading the profile? Default: true.
	 *
	 * @return  bool  True if everything was read properly
	 */
	public function load_configuration($profile_id = null, $reset = true)
	{
		// Load the database class
		$db = Factory::getDatabase($this->get_platform_database_options());

		// Get the active profile number, if no profile was specified
		if (is_null($profile_id))
		{
			$profile_id = $this->get_active_profile();
		}

		// Initialize the registry
		$registry = Factory::getConfiguration();

		if ($reset)
		{
			$registry->reset();
		}

		// Is the database connected?
		if (!$db->connected())
		{
			return false;
		}

		try
		{
			// Load the INI format local configuration dump off the database
			$sql = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
				->select($db->qn('configuration'))
				->from($db->qn($this->tableNameProfiles))
				->where($db->qn('id') . ' = ' . $db->q($profile_id));

			$databaseData = $db->setQuery($sql)->loadResult();
		}
		catch (Exception $e)
		{
			$databaseData = null;
		}

		/**
		 * If the profile is not the default and we can't load anything let's switch back to the default profile.
		 *
		 * You will end up here when you have opened the application in two different browsers and Browser A is used to
		 * delete the active profile you were using with Browser B. If we were not to load the default profile Browser B
		 * would try to save the default configuration data to the deleted profile. However, since the profile does not
		 * exist in the database any more the load_configuration at the end of the following if-block would trigger the
		 * same code path, recursively, infinitely until you reached the maximum nesting level in PHP, run out of memory
		 * or hit the execution time limit.
		 */
		if ((empty($databaseData) || is_null($databaseData)) && ($profile_id != 1))
		{
			return $this->load_configuration(1);
		}

		if (empty($databaseData) || is_null($databaseData))
		{
			// No configuration was saved yet - store the defaults
			$saved = $this->save_configuration($profile_id);

			// If this is the case we probably don't have the necessary table. Throw an exception.
			if (!$saved)
			{
				throw new RuntimeException("Could not save data to backup profile #$profile_id", 500);
			}

			return $this->load_configuration($profile_id);
		}

		// Decrypt the data if required
		$secureSettings = Factory::getSecureSettings();
		$noData         = empty($databaseData);
		$signature      = ($noData || (strlen($databaseData) < 12)) ? '' : substr($databaseData, 0, 12);
		$parsedData     = [];

		/**
		 * Special case: profile data is encrypted but encryption is set to false. This means that the user has just
		 * asked for the encryption to be disabled. We have to NOT load the settings so that the application has the
		 * chance to decode the data and write the decoded data back to the database.
		 */

		if (!$secureSettings->supportsEncryption() && in_array($signature, ['###AES128###', '###CTR128###']))
		{
			$dataArray = ['volatile' => ['fake_decrypt_flag' => 1]];
		}
		else
		{
			$databaseData        = $secureSettings->decryptSettings($databaseData);
			$isMigrationRequired = false; // Do I have to migrate the data from INI to JSON
			$corruptedINI        = false; // Is the INI data corrupted?

			// Handle legacy, INI-encoded data
			if (ProfileMigration::looksLikeIni($databaseData))
			{
				$isMigrationRequired = true;
				$corruptedINI        = strpos($databaseData, '[akeeba]') === false;
				$databaseData        = ProfileMigration::convertINItoJSON($databaseData);
			}

			// Detect corrupt JSON data
			$corruptedJSON = strpos($databaseData, '"akeeba"') === false;

			// Did the decryption fail and we were asked to throw an exception?
			if ($this->decryptionException && !$noData)
			{
				// The decryption failed, it returned empty data
				if (!$isMigrationRequired && empty($databaseData))
				{
					throw new DecryptionException(
						$this->translate('COM_AKEEBA_CONFIG_ERR_DECRYPTION') .
						"\nAdditional info: Empty data after decryption."
					);
				}

				// We tried to migrate but the INI data is corrupt
				if ($isMigrationRequired && $corruptedINI)
				{
					throw new DecryptionException(
						$this->translate('COM_AKEEBA_CONFIG_ERR_DECRYPTION') .
						"\nAdditional info: old format INI data was corrupt and could not be migrated to JSON."
					);
				}

				// We tried to migrate but the resulting JSON data is corrupt
				if ($isMigrationRequired && $corruptedJSON)
				{
					throw new DecryptionException(
						$this->translate('COM_AKEEBA_CONFIG_ERR_DECRYPTION') .
						"\nAdditional info: JSON data was corrupt after migrating it from INI data."
					);
				}

				// We decrypted something but it does not look like JSON. Wrong encryption key?
				if ($corruptedJSON)
				{
					throw new DecryptionException(
						$this->translate('COM_AKEEBA_CONFIG_ERR_DECRYPTION') .
						"\nAdditional info: configuration JSON data was corrupt after decryption."
					);
				}
			}

			$dataArray = json_decode($databaseData, true);
		}

		unset($databaseData);

		if (!is_array($dataArray))
		{
			$dataArray = [];
		}

		foreach ($dataArray as $section => $row)
		{
			if ($section == 'volatile')
			{
				continue;
			}

			$row = $this->arrayToRegistryDefinitions($row);

			if (is_array($row) && !empty($row))
			{
				foreach ($row as $key => $value)
				{
					$parsedData["$section.$key"] = $value;
				}
			}
		}

		unset($dataArray);

		// Import the configuration array
		$protected_keys = $registry->getProtectedKeys();
		$registry->resetProtectedKeys();
		$registry->mergeArray($parsedData, false, false);

		// Old profiles have advanced.proc_engine instead of advanced.postproc_engine. Migrate them.
		$procEngine = $registry->get('akeeba.advanced.proc_engine', null);

		if (!empty($procEngine))
		{
			$registry->set('akeeba.advanced.postproc_engine', $procEngine);
			$registry->set('akeeba.advanced.proc_engine', null);
		}

		// Apply config overrides
		if (is_array($this->configOverrides) && !empty($this->configOverrides))
		{
			$registry->mergeArray($this->configOverrides, false, false);
		}

		$registry->setProtectedKeys($protected_keys);
		$registry->activeProfile = $profile_id;

		return true;
	}

	/**
	 * Returns the filter data for the entire filter group collection
	 *
	 * @return array
	 */
	public function &load_filters()
	{
		// Load the filter data from the database
		$profile_id = $this->get_active_profile();
		$db         = Factory::getDatabase($this->get_platform_database_options());

		// Load the INI format local configuration dump off the database
		$sql = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->select($db->qn('filters'))
			->from($db->qn($this->tableNameProfiles))
			->where($db->qn('id') . ' = ' . $db->q($profile_id));
		$db->setQuery($sql);
		$all_filter_data = $db->loadResult();

		if (is_null($all_filter_data) || empty($all_filter_data))
		{
			$all_filter_data = [];

			return $all_filter_data;
		}

		if (ProfileMigration::looksLikeSerialized($all_filter_data))
		{
			$all_filter_data = ProfileMigration::convertSerializedToJSON($all_filter_data);
		}

		$all_filter_data = json_decode($all_filter_data, true);

		// Catch unserialization errors
		if (empty($all_filter_data))
		{
			$all_filter_data = [];
		}

		return $all_filter_data;
	}

	public function load_version_defines()
	{
	}

	public function log_platform_special_directories()
	{
	}

	public function move($from, $to)
	{
		$result = @rename($from, $to);
		if (!$result)
		{
			$result = @copy($from, $to);
			if ($result)
			{
				$result = $this->unlink($from);
			}
		}

		return $result;
	}

	public function register_autoloader()
	{
	}

	/**
	 * Invalidates older records sharing the same $archivename
	 *
	 * @param   string  $archivename
	 */
	public function remove_duplicate_backup_records($archivename)
	{
		Factory::getLog()->debug("Removing any old records with $archivename filename");
		$db = Factory::getDatabase($this->get_platform_database_options());

		$query = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->select($db->qn('id'))
			->from($db->qn($this->tableNameStats))
			->where($db->qn('archivename') . ' = ' . $db->q($archivename))
			->order($db->qn('id') . ' DESC');

		$db->setQuery($query);
		$array = $db->loadColumn();

		Factory::getLog()->debug((is_array($array) || $array instanceof \Countable ? count($array) : 0) . " records found");

		// No records?! Quit.
		if (empty($array))
		{
			return;
		}
		// Only one record. Quit.
		if ((is_array($array) || $array instanceof \Countable ? count($array) : 0) == 1)
		{
			return;
		}

		// Shift the first (latest) element off the array
		$currentID = array_shift($array);

		// Invalidate older records
		$this->invalidate_backup_records($array);
	}

	/**
	 * Saves the current configuration to the database table
	 *
	 * @param   int  $profile_id  The profile where to save the configuration to, defaults to current profile
	 *
	 * @return    bool    True if everything was saved properly
	 */
	public function save_configuration($profile_id = null)
	{
		// Load the database class
		$db = Factory::getDatabase($this->get_platform_database_options());

		if (!$db->connected())
		{
			return false;
		}

		// Get the active profile number, if no profile was specified
		if (is_null($profile_id))
		{
			$profile_id = $this->get_active_profile();
		}

		// Get an INI format registry dump
		$registry     = Factory::getConfiguration();
		$dump_profile = $registry->exportAsJSON();

		// Encrypt the registry dump if required
		$secureSettings = Factory::getSecureSettings();
		$dump_profile   = $secureSettings->encryptSettings($dump_profile);

		// Does the record already exist?
		$sql = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->select('COUNT(*)')
			->from($db->qn($this->tableNameProfiles))
			->where($db->qn('id') . ' = ' . $db->q($profile_id));

		try
		{
			$count  = $db->setQuery($sql)->loadResult();
			$exists = ($count > 0);
		}
		catch (Exception $e)
		{
			$exists = true;
		}

		if ($exists)
		{
			$sql = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
				->update($db->qn($this->tableNameProfiles))
				->set($db->qn('configuration') . ' = ' . $db->q($dump_profile))
				->where($db->qn('id') . ' = ' . $db->q($profile_id));
		}
		else
		{
			$sql = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
				->insert($db->qn($this->tableNameProfiles))
				->columns([
					$db->qn('id'), $db->qn('description'), $db->qn('configuration'),
					$db->qn('filters'), $db->qn('quickicon'),
				])
				->values(
					$db->q(1) . ', ' .
					$db->q("Default backup profile") . ', ' .
					$db->q($dump_profile) . ', ' .
					$db->q('') . ', ' .
					$db->q(1)
				);
		}

		$db->setQuery($sql);

		try
		{
			$result = $db->query();
		}
		catch (Exception $exc)
		{
			return false;
		}

		return ($result == true);
	}

	/**
	 * Saves the nested filter data array $filter_data to the database
	 *
	 * @param   array  $filter_data  The filter data to save
	 *
	 * @return    bool    True on success
	 */
	public function save_filters(&$filter_data)
	{
		$profile_id = $this->get_active_profile();
		$db         = Factory::getDatabase($this->get_platform_database_options());

		$encodedFilterData = json_encode($filter_data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_FORCE_OBJECT | JSON_PRETTY_PRINT);

		$sql = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->update($db->qn($this->tableNameProfiles))
			->set($db->qn('filters') . '=' . $db->q($encodedFilterData))
			->where($db->qn('id') . ' = ' . $db->q($profile_id));

		try
		{
			$db->setQuery($sql)->query();
		}
		catch (Exception $exc)
		{
			return false;
		}

		return true;
	}

	public function send_email($to, $subject, $body, $attachFile = null)
	{
		return false;
	}

	/** @inheritdoc */
	public final function setProxySettings($useProxy = false, $host = '', $port = 8080, $username = '', $password = '')
	{
		$host = trim($host);
		$port = (int) $port;

		$this->hasInitialisedProxySettings = true;
		$this->proxyEnabled                = $useProxy && !empty($host) && ($port > 0) && ($port < 65536);
		$this->proxyHost                   = $host;
		$this->proxyPort                   = $port;
		$this->proxyUser                   = trim($username ?? '') ?: '';
		$this->proxyPass                   = trim($password ?? '') ?: '';
	}

	/**
	 * Creates or updates the statistics record of the current backup attempt
	 *
	 * @param   int    $id    Backup record ID, use null for new record
	 * @param   array  $data  The data to store
	 *
	 * @return int|null The new record id, or null if this doesn't apply
	 *
	 * @throws Exception On database error
	 */
	public function set_or_update_statistics($id = null, $data = [])
	{
		// No valid data?
		if (!is_array($data))
		{
			return null;
		}

		// No data at all?
		if (empty($data))
		{
			return null;
		}

		$db = Factory::getDatabase($this->get_platform_database_options());

		$tableFields = $db->getTableColumns($this->tableNameStats);
		$tableFields = array_keys($tableFields);

		if (is_null($id))
		{
			// Create a new record
			$sql_fields = [];
			$sql_values = '';

			foreach ($data as $key => $value)
			{
				if (!in_array($key, $tableFields))
				{
					continue;
				}

				$sql_fields[] = $db->qn($key);
				$sql_values   .= (!empty($sql_values) ? ',' : '') . $db->quote($value);
			}

			$sql = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
				->insert($db->quoteName($this->tableNameStats))
				->columns($sql_fields)
				->values($sql_values);

			$db->setQuery($sql);
			$db->query();

			return $db->insertid();
		}
		else
		{
			$sql_set = [];
			foreach ($data as $key => $value)
			{
				if ($key == 'id')
				{
					continue;
				}

				$sql_set[] = $db->qn($key) . '=' . $db->q($value);
			}
			$sql = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
				->update($db->qn($this->tableNameStats))
				->set($sql_set)
				->where($db->qn('id') . '=' . $db->q($id));
			$db->setQuery($sql);
			$db->query();

			return null;
		}
	}

	public function translate($key)
	{
		return '';
	}

	public function unlink($file)
	{
		return @unlink($file);
	}

	/**
	 * Flattens a hierarchical array to a set of registry keys.
	 *
	 * For example
	 * [ 'foo' => [ 'bar' => [ 'baz' => 1, 'bat' => 2 ] ] ]
	 * becomes
	 * [ 'foo.bar.baz' => 1, 'foo.bar.bat' => 2 ]
	 *
	 * @param   array   $array   The array to flatten
	 * @param   string  $prefix  The prefix to use (leave blank; it's used in recursive calls)
	 *
	 * @return  array  An array with flattened keys
	 *
	 * @since   6.4.1
	 */
	protected function arrayToRegistryDefinitions(array $array, $prefix = '')
	{
		$keys = [];

		foreach ($array as $k => $v)
		{
			if (is_array($v))
			{
				$keys = array_merge($keys, $this->arrayToRegistryDefinitions($v, $prefix . $k . "."));

				continue;
			}

			$keys[$prefix . $k] = $v;
		}

		return $keys;
	}

	/**
	 * Automatically-detect the proxy settings for this platform.
	 *
	 * Implement this method to detect the proxy settings. Use $this->setProxysettings() to apply them.
	 *
	 * This method is called by getProxySettings automatically. You do NOT need to call it yourself when initialising
	 * the platform.
	 *
	 * @return  void
	 * @since   9.0.7
	 */
	protected function detectProxySettings()
	{

	}
}
Scan/Base.php000060400000003136152456704520007024 0ustar00<?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\Scan;

defined('AKEEBAENGINE') || die();

abstract class Base
{
	/**
	 * Gets all the files of a given folder
	 *
	 * @param   string   $folder    The absolute path to the folder to scan for files
	 * @param   integer  $position  The position in the file list to seek to. Use null for the start of list.
	 *
	 * @return  array  A simple array of files
	 */
	abstract public function getFiles($folder, &$position);

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

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Base\Exceptions\WarningException;
use Akeeba\Engine\Factory;
use DirectoryIterator;
use Exception;
use RuntimeException;

/* Windows system detection */
if (!defined('_AKEEBA_IS_WINDOWS'))
{
	$isWindows = DIRECTORY_SEPARATOR == '\\';

	if (function_exists('php_uname'))
	{
		$isWindows = stristr(php_uname(), 'windows');
	}

	define('_AKEEBA_IS_WINDOWS', $isWindows);
}

/**
 * A filesystem scanner which uses opendir() and is smart enough to make large directories
 * be scanned inside a step of their own.
 *
 * The idea is that if it's not the first operation of this step and the number of contained
 * directories AND files is more than double the number of allowed files per fragment, we should
 * break the step immediately.
 *
 */
class Smart extends Base
{
	public function getFiles($folder, &$position)
	{
		$registry = Factory::getConfiguration();
		// Was the breakflag set BEFORE starting? -- This workaround is required due to PHP5 defaulting to assigning variables by reference
		$breakflag_before_process = $registry->get('volatile.breakflag', false);

		// Reset break flag before continuing
		$breakflag = false;

		// Initialize variables
		$arr   = [];
		$false = false;

		if (!@is_dir($folder) && !@is_dir($folder . '/'))
		{
			return $false;
		}

		$counter    = 0;
		$registry   = Factory::getConfiguration();
		$maxCounter = $registry->get('engine.scan.smart.large_dir_threshold', 100);

		$allowBreakflag = ($registry->get('volatile.operation_counter', 0) != 0) && !$breakflag_before_process;

		if (!@is_dir($folder))
		{
			throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP reports it as not a folder.');
		}

		if (!@is_readable($folder))
		{
			throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP reports it as not readable.');
		}

		try
		{
			$di = new DirectoryIterator($folder);
		}
		catch (Exception $e)
		{
			throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP\'s DirectoryIterator reports the path cannot be opened.', 0, $e);
		}

		if (!$di->valid())
		{
			throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP\'s DirectoryIterator could open the folder but immediately reports itself as not valid. If this happens your server is about to die.');
		}

		$ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR;

		/** @var DirectoryIterator $file */
		foreach ($di as $file)
		{
			if ($breakflag)
			{
				break;
			}

			/**
			 * If the directory entry is a link pointing somewhere outside the allowed directories per open_basedir we
			 * will get a RuntimeException (tested on PHP 5.3 onwards). Catching it lets us report the link as
			 * unreadable without suffering a PHP Fatal Error.
			 */
			try
			{
				$file->isLink();
			}
			catch (RuntimeException $e)
			{
				if (!in_array($di->getFilename(), ['.', '..']))
				{
					Factory::getLog()->warning(sprintf("Link %s is inaccessible. Check the open_basedir restrictions in your server's PHP configuration", $file->getPathname()));
				}

				continue;
			}

			if ($file->isDot())
			{
				continue;
			}

			if ($file->isDir())
			{
				continue;
			}

			$dir  = $folder . $ds . $file->getFilename();
			$data = $dir;

			if (_AKEEBA_IS_WINDOWS)
			{
				$data = Factory::getFilesystemTools()->TranslateWinPath($dir);
			}

			if ($data)
			{
				$arr[] = $data;
			}

			$counter++;

			if ($counter >= $maxCounter)
			{
				$breakflag = $allowBreakflag;
			}
		}

		// Save break flag status
		$registry->set('volatile.breakflag', $breakflag);

		return $arr;
	}

	public function getFolders($folder, &$position)
	{
		// Was the breakflag set BEFORE starting? -- This workaround is required due to PHP5 defaulting to assigning variables by reference
		$registry                 = Factory::getConfiguration();
		$breakflag_before_process = $registry->get('volatile.breakflag', false);

		// Reset break flag before continuing
		$breakflag = false;

		// Initialize variables
		$arr   = [];
		$false = false;

		if (!is_dir($folder) && !is_dir($folder . '/'))
		{
			throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP reports it as not a folder.');
		}

		if (!@is_readable($folder))
		{
			throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP reports it as not readable.');
		}

		$counter    = 0;
		$registry   = Factory::getConfiguration();
		$maxCounter = $registry->get('engine.scan.smart.large_dir_threshold', 100);

		$allowBreakflag = ($registry->get('volatile.operation_counter', 0) != 0) && !$breakflag_before_process;

		try
		{
			$di = new DirectoryIterator($folder);
		}
		catch (Exception $e)
		{
			throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP\'s DirectoryIterator reports the path cannot be opened.', 0, $e);
		}

		if (!$di->valid())
		{
			throw new WarningException('Cannot list contents of directory ' . $folder . ' -- PHP\'s DirectoryIterator could open the folder but immediately reports itself as not valid. If this happens your server is about to die.');
		}

		$ds = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR;

		/** @var DirectoryIterator $file */
		foreach ($di as $file)
		{
			if ($breakflag)
			{
				break;
			}

			/**
			 * If the directory entry is a link pointing somewhere outside the allowed directories per open_basedir we
			 * will get a RuntimeException (tested on PHP 5.3 onwards). Catching it lets us report the link as
			 * unreadable without suffering a PHP Fatal Error.
			 */
			try
			{
				$file->isLink();
			}
			catch (RuntimeException $e)
			{
				if (!in_array($di->getFilename(), ['.', '..']))
				{
					Factory::getLog()->warning(sprintf("Link %s is inaccessible. Check the open_basedir restrictions in your server's PHP configuration", $file->getPathname()));
				}

				continue;
			}

			if ($file->isDot())
			{
				continue;
			}

			if (!$file->isDir())
			{
				continue;
			}

			$dir  = $folder . $ds . $file->getFilename();
			$data = $dir;

			if (_AKEEBA_IS_WINDOWS)
			{
				$data = Factory::getFilesystemTools()->TranslateWinPath($dir);
			}

			if ($data)
			{
				$arr[] = $data;
			}

			$counter++;

			if ($counter >= $maxCounter)
			{
				$breakflag = $allowBreakflag;
			}
		}

		// Save break flag status
		$registry->set('volatile.breakflag', $breakflag);

		return $arr;
	}
}
Scan/smart.json000060400000001745152456704520007466 0ustar00{
    "_information": {
        "title": "COM_AKEEBA_CONFIG_ENGINE_SCAN_SMART_TITLE",
        "description": "COM_AKEEBA_CONFIG_ENGINE_SCAN_SMART_DESCRIPTION"
    },
    "engine.scan.smart.large_dir_threshold": {
        "default": "100",
        "type": "integer",
        "min": "0",
        "max": "500",
        "shortcuts": "20|50|100|200|300|400|500",
        "scale": "1",
        "uom": "",
        "title": "COM_AKEEBA_CONFIG_LARGEDIRTHRESHOLD_TITLE",
        "description": "COM_AKEEBA_CONFIG_LARGEDIRTHRESHOLD_DESCRIPTION"
    },
    "engine.scan.common.largefile": {
        "default": "10485760",
        "type": "integer",
        "min": "1048576",
        "max": "1048576000",
        "shortcuts": "1048576|2097152|5242880|10485760|15728640|20971520|26214400|31457280|41943040|52428800|78643200|104857600",
        "scale": "1048576",
        "uom": "MB",
        "title": "COM_AKEEBA_CONFIG_LARGEFILE_TITLE",
        "description": "COM_AKEEBA_CONFIG_LARGEFILE_DESCRIPTION"
    }
}Base/Exceptions/ErrorException.php000060400000002041152456704520013223 0ustar00<?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\Base\Exceptions;

defined('AKEEBAENGINE') || die();

use RuntimeException;

/**
 * An exception which leads to an error (and complete halt) in the backup process
 */
class ErrorException extends RuntimeException
{

}
Base/Exceptions/WarningException.php000060400000002020152456704520013534 0ustar00<?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\Base\Exceptions;

defined('AKEEBAENGINE') || die();

use RuntimeException;

/**
 * An exception which leads to a warning in the backup process
 */
class WarningException extends RuntimeException
{

}
Base/Part.php000060400000035320152456704520007046 0ustar00<?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\Base;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Factory;
use Exception;
use Akeeba\Engine\Psr\Log\LogLevel;
use Throwable;

/**
 * Base class for all Akeeba Engine parts.
 *
 * Parts are objects which perform a specific function during the backup process, e.g. backing up files or dumping
 * database contents. They have a fully defined and controlled lifecycle, from initialization to finalization. The
 * transition between lifecycle phases is handled by the `tick()` method which is essentially the only public interface
 * to interacting with an engine part.
 */
abstract class Part
{
	public const STATE_INIT = 0;
	public const STATE_PREPARED = 1;
	public const STATE_RUNNING = 2;
	public const STATE_POSTRUN = 3;
	public const STATE_FINISHED = 4;
	public const STATE_ERROR = 99;

	/**
	 * The current state of this part; see the constants at the top of this class
	 *
	 * @var int
	 */
	protected $currentState = self::STATE_INIT;

	/**
	 * The name of the engine part (a.k.a. Domain), used in return table
	 * generation.
	 *
	 * @var string
	 */
	protected $activeDomain = "";

	/**
	 * The step this engine part is in. Used verbatim in return table and
	 * should be set by the code in the _run() method.
	 *
	 * @var string
	 */
	protected $activeStep = "";

	/**
	 * A more detailed description of the step this engine part is in. Used
	 * verbatim in return table and should be set by the code in the _run()
	 * method.
	 *
	 * @var string
	 */
	protected $activeSubstep = "";

	/**
	 * Any configuration variables, in the form of an array.
	 *
	 * @var array
	 */
	protected $_parametersArray = [];

	/**
	 * The database root key
	 *
	 * @var  string
	 */
	protected $databaseRoot = [];

	/**
	 * Should we log the step nesting?
	 *
	 * @var  bool
	 */
	protected $nest_logging = false;

	/**
	 * Embedded installer preferences
	 *
	 * @var  object
	 */
	protected $installerSettings;

	/**
	 * How much milliseconds should we wait to reach the min exec time
	 *
	 * @var  int
	 */
	protected $waitTimeMsec = 0;

	/**
	 * Should I ignore the minimum execution time altogether?
	 *
	 * @var  bool
	 */
	protected $ignoreMinimumExecutionTime = false;

	/**
	 * The last exception thrown during the tick() method's execution.
	 *
	 * @var null|Exception
	 */
	protected $lastException = null;

	public function _onSerialize()
	{
		$this->lastException = null;
	}

	/**
	 * Public constructor
	 *
	 * @return  void
	 */
	public function __construct()
	{
		// Fetch the installer settings
		$this->installerSettings = (object) [
			'installerroot' => 'installation',
			'sqlroot'       => 'installation/sql',
			'databasesini'  => 1,
			'readme'        => 1,
			'extrainfo'     => 1,
			'password'      => 0,
		];

		$config               = Factory::getConfiguration();
		$installerKey         = $config->get('akeeba.advanced.embedded_installer');
		$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();

		// Fall back to default ANGIE installer if the selected installer is not found
		if (!array_key_exists($installerKey, $installerDescriptors))
		{
			$installerKey = 'angie';
		}

		if (array_key_exists($installerKey, $installerDescriptors))
		{
			$this->installerSettings = (object) $installerDescriptors[$installerKey];
		}
	}

	/**
	 * Nested logging of exceptions
	 *
	 * The message is logged using the specified log level. The detailed information of the Throwable and its trace are
	 * logged using the DEBUG level.
	 *
	 * If the Throwable is nested, its parents are logged recursively. This should create a thorough trace leading to
	 * the root cause of an error.
	 *
	 * @param   Exception|Throwable  $exception  The Exception or Throwable to log
	 * @param   string               $logLevel   The log level to use, default ERROR
	 */
	protected static function logErrorsFromException($exception, $logLevel = LogLevel::ERROR)
	{
		$logger = Factory::getLog();

		$logger->log($logLevel, $exception->getMessage());

		$logger->debug(sprintf('[%s] %s(%u) – #%u ‹%s›', get_class($exception), $exception->getFile(), $exception->getLine(), $exception->getCode(), $exception->getMessage()));

		foreach (explode("\n", $exception->getTraceAsString()) as $line)
		{
			$logger->debug(rtrim($line));
		}

		$previous = $exception->getPrevious();

		if (!is_null($previous))
		{
			self::logErrorsFromException($previous, $logLevel);
		}
	}

	/**
	 * The public interface to an engine part. This method takes care for
	 * calling the correct method in order to perform the initialisation -
	 * run - finalisation cycle of operation and return a proper response array.
	 *
	 * @param   int  $nesting
	 *
	 * @return  array  A response array
	 */
	public function tick($nesting = 0)
	{
		$configuration       = Factory::getConfiguration();
		$timer               = Factory::getTimer();
		$this->waitTimeMsec  = 0;
		$this->lastException = null;

		// Add a small wait based on the existence of a constant. Used in testing, to simulate slow servers.
		if (defined('AKEEBA_BACKUP_TESTING_STEP_THROTTLING'))
		{
			/** @noinspection PhpUndefinedConstantInspection */
			usleep((int) AKEEBA_BACKUP_TESTING_STEP_THROTTLING);
		}

		/**
		 * 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;
	}
}
Psr/Log/AbstractLogger.php000060400000011635152456704520011461 0ustar00<?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\Psr\Log;

/**
 * This file is part of a privately namespaced copy of PSR-3 version 1.
 *
 * You can find the original PSR-3 in https://www.php-fig.org/psr/psr-3/ and the original code in
 * https://github.com/php-fig/log
 *
 * The license of the original code can be found below.
 *
 * Copyright (c) 2012 PHP Framework Interoperability Group
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

defined('AKEEBAENGINE') || die();

/**
 * This is a simple Logger implementation that other Loggers can inherit from.
 *
 * It simply delegates all log-level-specific methods to the `log` method to
 * reduce boilerplate code that a simple Logger that does the same thing with
 * messages regardless of the error level has to implement.
 */
abstract class AbstractLogger implements LoggerInterface
{
	/**
	 * System is unusable.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function emergency($message, array $context = [])
	{
		$this->log(LogLevel::EMERGENCY, $message, $context);
	}

	/**
	 * Action must be taken immediately.
	 *
	 * Example: Entire website down, database unavailable, etc. This should
	 * trigger the SMS alerts and wake you up.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function alert($message, array $context = [])
	{
		$this->log(LogLevel::ALERT, $message, $context);
	}

	/**
	 * Critical conditions.
	 *
	 * Example: Application component unavailable, unexpected exception.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function critical($message, array $context = [])
	{
		$this->log(LogLevel::CRITICAL, $message, $context);
	}

	/**
	 * Runtime errors that do not require immediate action but should typically
	 * be logged and monitored.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function error($message, array $context = [])
	{
		$this->log(LogLevel::ERROR, $message, $context);
	}

	/**
	 * Exceptional occurrences that are not errors.
	 *
	 * Example: Use of deprecated APIs, poor use of an API, undesirable things
	 * that are not necessarily wrong.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function warning($message, array $context = [])
	{
		$this->log(LogLevel::WARNING, $message, $context);
	}

	/**
	 * Normal but significant events.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function notice($message, array $context = [])
	{
		$this->log(LogLevel::NOTICE, $message, $context);
	}

	/**
	 * Interesting events.
	 *
	 * Example: User logs in, SQL logs.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function info($message, array $context = [])
	{
		$this->log(LogLevel::INFO, $message, $context);
	}

	/**
	 * Detailed debug information.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function debug($message, array $context = [])
	{
		$this->log(LogLevel::DEBUG, $message, $context);
	}
}
Psr/Log/InvalidArgumentException.php000060400000004476152456704520013533 0ustar00<?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\Psr\Log;

/**
 * This file is part of a privately namespaced copy of PSR-3 version 1.
 *
 * You can find the original PSR-3 in https://www.php-fig.org/psr/psr-3/ and the original code in
 * https://github.com/php-fig/log
 *
 * The license of the original code can be found below.
 *
 * Copyright (c) 2012 PHP Framework Interoperability Group
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

defined('AKEEBAENGINE') || die();

class InvalidArgumentException extends \InvalidArgumentException
{
}
Psr/Log/LoggerTrait.php000060400000012126152456704520010775 0ustar00<?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\Psr\Log;

/**
 * This file is part of a privately namespaced copy of PSR-3 version 1.
 *
 * You can find the original PSR-3 in https://www.php-fig.org/psr/psr-3/ and the original code in
 * https://github.com/php-fig/log
 *
 * The license of the original code can be found below.
 *
 * Copyright (c) 2012 PHP Framework Interoperability Group
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

defined('AKEEBAENGINE') || die();

/**
 * This is a simple Logger trait that classes unable to extend AbstractLogger
 * (because they extend another class, etc) can include.
 *
 * It simply delegates all log-level-specific methods to the `log` method to
 * reduce boilerplate code that a simple Logger that does the same thing with
 * messages regardless of the error level has to implement.
 */
trait LoggerTrait
{
	/**
	 * System is unusable.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function emergency($message, array $context = [])
	{
		$this->log(LogLevel::EMERGENCY, $message, $context);
	}

	/**
	 * Action must be taken immediately.
	 *
	 * Example: Entire website down, database unavailable, etc. This should
	 * trigger the SMS alerts and wake you up.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function alert($message, array $context = [])
	{
		$this->log(LogLevel::ALERT, $message, $context);
	}

	/**
	 * Critical conditions.
	 *
	 * Example: Application component unavailable, unexpected exception.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function critical($message, array $context = [])
	{
		$this->log(LogLevel::CRITICAL, $message, $context);
	}

	/**
	 * Runtime errors that do not require immediate action but should typically
	 * be logged and monitored.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function error($message, array $context = [])
	{
		$this->error($message, $context);
	}

	/**
	 * Exceptional occurrences that are not errors.
	 *
	 * Example: Use of deprecated APIs, poor use of an API, undesirable things
	 * that are not necessarily wrong.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function warning($message, array $context = [])
	{
		$this->warning($message, $context);
	}

	/**
	 * Normal but significant events.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function notice($message, array $context = [])
	{
		$this->log(LogLevel::NOTICE, $message, $context);
	}

	/**
	 * Interesting events.
	 *
	 * Example: User logs in, SQL logs.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function info($message, array $context = [])
	{
		$this->info($message, $context);
	}

	/**
	 * Detailed debug information.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function debug($message, array $context = [])
	{
		$this->debug($message, $context);
	}

	/**
	 * Logs with an arbitrary level.
	 *
	 * @param   mixed   $level
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	abstract public function log($level, $message, array $context = []);
}
Psr/Log/LoggerAwareTrait.php000060400000005033152456704520011754 0ustar00<?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\Psr\Log;

/**
 * This file is part of a privately namespaced copy of PSR-3 version 1.
 *
 * You can find the original PSR-3 in https://www.php-fig.org/psr/psr-3/ and the original code in
 * https://github.com/php-fig/log
 *
 * The license of the original code can be found below.
 *
 * Copyright (c) 2012 PHP Framework Interoperability Group
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

defined('AKEEBAENGINE') || die();

/**
 * Basic Implementation of LoggerAwareInterface.
 */
trait LoggerAwareTrait
{
	/** @var LoggerInterface */
	protected $logger;

	/**
	 * Sets a logger.
	 *
	 * @param   LoggerInterface  $logger
	 */
	public function setLogger(LoggerInterface $logger)
	{
		$this->logger = $logger;
	}
}
Psr/Log/LoggerAwareInterface.php000060400000004760152456704520012577 0ustar00<?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\Psr\Log;

/**
 * This file is part of a privately namespaced copy of PSR-3 version 1.
 *
 * You can find the original PSR-3 in https://www.php-fig.org/psr/psr-3/ and the original code in
 * https://github.com/php-fig/log
 *
 * The license of the original code can be found below.
 *
 * Copyright (c) 2012 PHP Framework Interoperability Group
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

defined('AKEEBAENGINE') || die();

/**
 * Describes a logger-aware instance
 */
interface LoggerAwareInterface
{
	/**
	 * Sets a logger instance on the object
	 *
	 * @param   LoggerInterface  $logger
	 *
	 * @return null
	 */
	public function setLogger(LoggerInterface $logger);
}
Psr/Log/LogLevel.php000060400000004776152456704520010277 0ustar00<?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\Psr\Log;

/**
 * This file is part of a privately namespaced copy of PSR-3 version 1.
 *
 * You can find the original PSR-3 in https://www.php-fig.org/psr/psr-3/ and the original code in
 * https://github.com/php-fig/log
 *
 * The license of the original code can be found below.
 *
 * Copyright (c) 2012 PHP Framework Interoperability Group
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

defined('AKEEBAENGINE') || die();

/**
 * Describes log levels
 */
class LogLevel
{
	const EMERGENCY = 'emergency';
	const ALERT = 'alert';
	const CRITICAL = 'critical';
	const ERROR = 'error';
	const WARNING = 'warning';
	const NOTICE = 'notice';
	const INFO = 'info';
	const DEBUG = 'debug';
}
Psr/Log/LoggerInterface.php000060400000011661152456704520011615 0ustar00<?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\Psr\Log;

/**
 * This file is part of a privately namespaced copy of PSR-3 version 1.
 *
 * You can find the original PSR-3 in https://www.php-fig.org/psr/psr-3/ and the original code in
 * https://github.com/php-fig/log
 *
 * The license of the original code can be found below.
 *
 * Copyright (c) 2012 PHP Framework Interoperability Group
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

defined('AKEEBAENGINE') || die();

/**
 * Describes a logger instance
 *
 * The message MUST be a string or object implementing __toString().
 *
 * The message MAY contain placeholders in the form: {foo} where foo
 * will be replaced by the context data in key "foo".
 *
 * The context array can contain arbitrary data, the only assumption that
 * can be made by implementors is that if an Exception instance is given
 * to produce a stack trace, it MUST be in a key named "exception".
 *
 * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
 * for the full interface specification.
 */
interface LoggerInterface
{
	/**
	 * System is unusable.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function emergency($message, array $context = []);

	/**
	 * Action must be taken immediately.
	 *
	 * Example: Entire website down, database unavailable, etc. This should
	 * trigger the SMS alerts and wake you up.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function alert($message, array $context = []);

	/**
	 * Critical conditions.
	 *
	 * Example: Application component unavailable, unexpected exception.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function critical($message, array $context = []);

	/**
	 * Runtime errors that do not require immediate action but should typically
	 * be logged and monitored.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function error($message, array $context = []);

	/**
	 * Exceptional occurrences that are not errors.
	 *
	 * Example: Use of deprecated APIs, poor use of an API, undesirable things
	 * that are not necessarily wrong.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function warning($message, array $context = []);

	/**
	 * Normal but significant events.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function notice($message, array $context = []);

	/**
	 * Interesting events.
	 *
	 * Example: User logs in, SQL logs.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function info($message, array $context = []);

	/**
	 * Detailed debug information.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function debug($message, array $context = []);

	/**
	 * Logs with an arbitrary level.
	 *
	 * @param   mixed   $level
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function log($level, $message, array $context = []);
}
Psr/Log/NullLogger.php000060400000005502152456704520010624 0ustar00<?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\Psr\Log;

/**
 * This file is part of a privately namespaced copy of PSR-3 version 1.
 *
 * You can find the original PSR-3 in https://www.php-fig.org/psr/psr-3/ and the original code in
 * https://github.com/php-fig/log
 *
 * The license of the original code can be found below.
 *
 * Copyright (c) 2012 PHP Framework Interoperability Group
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

defined('AKEEBAENGINE') || die();

/**
 * This Logger can be used to avoid conditional log calls
 *
 * Logging should always be optional, and if no logger is provided to your
 * library creating a NullLogger instance to have something to throw logs at
 * is a good way to avoid littering your code with `if ($this->logger) { }`
 * blocks.
 */
class NullLogger extends AbstractLogger
{
	/**
	 * Logs with an arbitrary level.
	 *
	 * @param   mixed   $level
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return null
	 */
	public function log($level, $message, array $context = [])
	{
		// noop
	}
}
Postproc/Onedriveapp.php000060400000010171152456704520011350 0ustar00<?php
/**
 * Akeeba Engine
 *
 * @package   akeebaengine
 * @copyright Copyright (c)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\Postproc;

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Postproc\Connector\OneDriveApp as ConnectorOneDrive;
use Akeeba\Engine\Postproc\Exception\BadConfiguration;

class Onedriveapp extends Onedrivebusiness
{
	/**
	 * The name of the OAuth2 callback method in the parent window (the configuration page)
	 *
	 * @var   string
	 */
	protected $callbackMethod = 'akeeba_onedriveapp_oauth_callback';

	/**
	 * The key in Akeeba Engine's settings registry for this post-processing method
	 *
	 * @var   string
	 */
	protected $settingsKey = 'onedriveapp';

	public function __construct()
	{
		parent::__construct();

		// This OneDrive integration does not allow the getDrives method.
		array_pop($this->allowedCustomAPICallMethods);
	}

	protected function makeConnector()
	{
		// Retrieve engine configuration data
		$config = Factory::getConfiguration();

		$access_token  = trim($config->get('engine.postproc.' . $this->settingsKey . '.access_token', ''));
		$refresh_token = trim($config->get('engine.postproc.' . $this->settingsKey . '.refresh_token', ''));

		$this->isChunked  = $config->get('engine.postproc.' . $this->settingsKey . '.chunk_upload', true);
		$this->chunkSize  = $config->get('engine.postproc.' . $this->settingsKey . '.chunk_upload_size', 10) * 1024 * 1024;
		$defaultDirectory = rtrim($config->get('engine.postproc.' . $this->settingsKey . '.directory', ''), '/');
		$this->directory  = $config->get('volatile.postproc.directory', $defaultDirectory);

		// Sanity checks
		if (empty($refresh_token))
		{
			throw new BadConfiguration('You have not linked Akeeba Backup with your OneDrive account');
		}

		if (!function_exists('curl_init'))
		{
			throw new BadConfiguration('cURL is not enabled, please enable it in order to post-process your archives');
		}

		// Fix the directory name, if required
		$this->directory = empty($this->directory) ? '' : $this->directory;
		$this->directory = trim($this->directory);
		$this->directory = ltrim(Factory::getFilesystemTools()->TranslateWinPath($this->directory), '/');
		$this->directory = Factory::getFilesystemTools()->replace_archive_name_variables($this->directory);
		$config->set('volatile.postproc.directory', $this->directory);

		// Get Download ID
		$dlid = Platform::getInstance()->get_platform_configuration_option('update_dlid', '');

		if (empty($dlid))
		{
			throw new BadConfiguration('You must enter your Download ID in the application configuration before using the “Upload to OneDrive” feature.');
		}

		$connector = new ConnectorOneDrive($access_token, $refresh_token, $dlid);

		// Validate the tokens
		Factory::getLog()->debug(__METHOD__ . " - Validating the OneDrive tokens");
		$pingResult = $connector->ping();

		// Save new configuration if there was a refresh
		if ($pingResult['needs_refresh'])
		{
			Factory::getLog()->debug(__METHOD__ . " - OneDrive tokens were refreshed");
			$config->set('engine.postproc.' . $this->settingsKey . '.access_token', $pingResult['access_token'], false);
			$config->set('engine.postproc.' . $this->settingsKey . '.refresh_token', $pingResult['refresh_token'], false);

			$profile_id = Platform::getInstance()->get_active_profile();
			Platform::getInstance()->save_configuration($profile_id);
		}

		return $connector;
	}

	protected function getOAuth2HelperUrl()
	{
		return ConnectorOneDrive::helperUrl;
	}
}Postproc/onedrivebusiness.ini-disabled000060400000004663152456704520014231 0ustar00; Akeeba Upload to OneDrive for Business post processing engine
; Copyright (c)2006-2018 Nicholas K. Dionysopoulos / Akeeba Ltd
;
; Sorry, we had to cancel this feature.
;
; Microsoft requires us to register the app in the Azure Directory per
; https://dev.onedrive.com/app-registration-server.htm  Despite repeated attempts at following their instructions it
; seems to be impossible. There is no "midsize business" plan, the regular subscription doesn'tlet you create the
; "private site" required for development and both enterprise and developer accounts can't seem to be able to be
; purchased. Apparently the only way to make this work is having a US-based enterprise?! So, sorry, we won't waste any
; more time on this.
;

; Engine information
[_information]
title=COM_AKEEBA_CONFIG_ENGINE_POSTPROC_ONEDRIVEBUSINESS_TITLE
description=COM_AKEEBA_CONFIG_ENGINE_POSTPROC_ONEDRIVEBUSINESS_DESCRIPTION

; Post-process after generating each part?
[engine.postproc.common.after_part]
default=0
type=bool
title=COM_AKEEBA_CONFIG_POSTPROCPARTS_TITLE
description=COM_AKEEBA_CONFIG_POSTPROCPARTS_DESCRIPTION

; Delete from server after processing?
[engine.postproc.common.delete_after]
default=1
type=bool
title=COM_AKEEBA_CONFIG_DELETEAFTER_TITLE
description=COM_AKEEBA_CONFIG_DELETEAFTER_DESCRIPTION

; Enable chunk upload?
[engine.postproc.onedrivebusiness.chunk_upload]
default=1
type=bool
title=COM_AKEEBA_CONFIG_BOX_CHUNKUPLOAD_ENABLE

; Chunk size in megabytes
[engine.postproc.onedrivebusiness.chunk_upload_size]
default=10
type=integer
min=4
max=60
shortcuts="5|10|20|40|60"
scale=1
uom=MB
title=COM_AKEEBA_CONFIG_BOX_CHUNKUPLOAD_SIZE

; Open OAuth
[engine.postproc.onedrivebusiness.openoauth]
default=""
type=button
title=COM_AKEEBA_CONFIG_BOX_OPENOAUTH_TITLE
description=COM_AKEEBA_CONFIG_BOX_OPENOAUTH_DESC
hook=akconfig_onedrivebusiness_openoauth

; OneDrive Directory name
[engine.postproc.onedrivebusiness.directory]
default="/"
type=string
title=COM_AKEEBA_CONFIG_ONEDRIVEBUSINESS_DIRECTORY_TITLE
description=COM_AKEEBA_CONFIG_ONEDRIVEBUSINESS_DIRECTORY_DESCRIPTION

[engine.postproc.onedrivebusiness.service_id]
default = ""
type=string
title=COM_AKEEBA_CONFIG_ONEDRIVEBUSINESS_SERVICEID_TITLE
description=COM_AKEEBA_CONFIG_ONEDRIVEBUSINESS_SERVICEID_DESCRIPTION

[engine.postproc.onedrivebusiness.refresh_token]
default = ""
type=string
title=COM_AKEEBA_CONFIG_ONEDRIVEBUSINESS_REFRESHTOKEN_TITLE
description=COM_AKEEBA_CONFIG_ONEDRIVEBUSINESS_REFRESHTOKEN_DESCRIPTION
Postproc/Email.php000060400000005526152456704520010133 0ustar00<?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\Postproc;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Postproc\Exception\BadConfiguration;
use Awf\Text\Text;
use Joomla\CMS\Language\Text as JText;
use RuntimeException;

class Email extends Base
{
	public function processPart($localFilepath, $remoteBaseName = null)
	{
		// Retrieve engine configuration data
		$config  = Factory::getConfiguration();
		$address = trim($config->get('engine.postproc.email.address', ''));
		$subject = $config->get('engine.postproc.email.subject', '0');

		// Sanity checks
		if (empty($address))
		{
			throw new BadConfiguration('You have not set up a recipient\'s email address for the backup files');
		}

		// Send the file
		$basename = empty($remoteBaseName) ? basename($localFilepath) : $remoteBaseName;

		Factory::getLog()->info(sprintf("Preparing to email %s to %s", $basename, $address));

		if (empty($subject))
		{
			$subject = "You have a new backup part";

			if (class_exists('\Awf\Text\Text'))
			{
				$subject = Text::_('COM_AKEEBA_COMMON_EMAIL_DEAFULT_SUBJECT');

				if ($subject === 'COM_AKEEBA_COMMON_EMAIL_DEAFULT_SUBJECT')
				{
					$subject = JText::_('COM_AKEEBA_COMMON_EMAIL_DEAFULT_SUBJECT');
				}
			}
			elseif (class_exists('\Joomla\CMS\Language\Text'))
			{
				$subject = JText::_('COM_AKEEBA_COMMON_EMAIL_DEAFULT_SUBJECT');

				if ($subject === 'COM_AKEEBA_COMMON_EMAIL_DEAFULT_SUBJECT')
				{
					$subject = JText::_('COM_AKEEBA_COMMON_EMAIL_DEAFULT_SUBJECT');
				}
			}
		}

		$body = "Emailing $basename";

		Factory::getLog()->debug("Subject: $subject");
		Factory::getLog()->debug("Body: $body");

		$result = Platform::getInstance()->send_email($address, $subject, $body, $localFilepath);

		// Return the result
		if ($result !== true)
		{
			// An error occurred
			throw new RuntimeException($result);
		}

		// Return success
		Factory::getLog()->info("Email sent successfully");

		return true;
	}

	protected function makeConnector()
	{
		/**
		 * This method does not use a connector.
		 */
		return;
	}


}
Postproc/Base.php000060400000015200152456704520007744 0ustar00<?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\Postproc;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Platform;
use Akeeba\Engine\Postproc\Exception\BadConfiguration;
use Akeeba\Engine\Postproc\Exception\DeleteNotSupported;
use Akeeba\Engine\Postproc\Exception\DownloadToBrowserNotSupported;
use Akeeba\Engine\Postproc\Exception\DownloadToServerNotSupported;
use Akeeba\Engine\Postproc\Exception\OAuthNotSupported;
use Akeeba\Engine\Util\FileCloseAware;
use Exception;

/**
 * Akeeba Engine post-processing abstract class. Provides the default implementation of most of the PostProcInterface
 * methods.
 */
abstract class Base implements PostProcInterface
{
	use FileCloseAware;

	/**
	 * Should we break the step before post-processing?
	 *
	 * The only engine which does not require a step break before is the None engine.
	 *
	 * @var bool
	 */
	protected $recommendsBreakBefore = true;

	/**
	 * Should we break the step after post-processing?
	 *
	 * @var bool
	 */
	protected $recommendsBreakAfter = true;

	/**
	 * Does this engine processes the files in a way that makes deleting the originals safe?
	 *
	 * @var bool
	 */
	protected $advisesDeletionAfterProcessing = true;

	/**
	 * Does this engine support remote file deletes?
	 *
	 * @var bool
	 */
	protected $supportsDelete = false;

	/**
	 * Does this engine support downloads to files?
	 *
	 * @var bool
	 */
	protected $supportsDownloadToFile = false;

	/**
	 * Does this engine support downloads to browser?
	 *
	 * @var bool
	 */
	protected $supportsDownloadToBrowser = false;

	/**
	 * Does this engine push raw data to the browser when downloading a file?
	 *
	 * Set to true if raw data will be dumped to the browser when downloading the file to the browser. Set to false if
	 * a URL is returned instead.
	 *
	 * @var bool
	 */
	protected $inlineDownloadToBrowser = false;

	/**
	 * The remote absolute path to the file which was just processed. Leave null if the file is meant to
	 * be non-retrievable, i.e. sent to email or any other one way service.
	 *
	 * @var string
	 */
	protected $remotePath = null;

	/**
	 * Whitelist of method names you can call using customAPICall().
	 *
	 * @var array
	 */
	protected $allowedCustomAPICallMethods = ['oauthCallback'];

	/**
	 * The connector object for this post-processing engine
	 *
	 * @var object|null
	 */
	private $connector;

	public function delete($path)
	{
		throw new DeleteNotSupported();
	}

	public function downloadToFile($remotePath, $localFile, $fromOffset = null, $length = null)
	{
		throw new DownloadToServerNotSupported();
	}

	public function downloadToBrowser($remotePath)
	{
		throw new DownloadToBrowserNotSupported();
	}

	public final function customAPICall($method, $params = [])
	{
		if (!in_array($method, $this->allowedCustomAPICallMethods) || !method_exists($this, $method))
		{
			header('HTTP/1.0 501 Not Implemented');

			exit();
		}

		return call_user_func_array([$this, $method], [$params]);
	}

	public function oauthOpen($params = [])
	{
		$callback = $params['callbackURI'] . '&method=oauthCallback';

		$url = $this->getOAuth2HelperUrl();
		$url .= (strpos($url, '?') !== false) ? '&' : '?';
		$url .= 'callback=' . urlencode($callback);
		$url .= '&dlid=' . urlencode(Platform::getInstance()->get_platform_configuration_option('update_dlid', ''));

		Platform::getInstance()->redirect($url);
	}

	/**
	 * Fetches the authentication token from the OAuth helper script, after you've run the first step of the OAuth
	 * authentication process. Must be overridden in subclasses.
	 *
	 * @param   array  $params
	 *
	 * @return  void
	 *
	 * @throws  OAuthNotSupported
	 */
	public function oauthCallback(array $params)
	{
		throw new OAuthNotSupported();
	}

	public function recommendsBreakBefore()
	{
		return $this->recommendsBreakBefore;
	}

	public function recommendsBreakAfter()
	{
		return $this->recommendsBreakAfter;
	}

	public function isFileDeletionAfterProcessingAdvisable()
	{
		return $this->advisesDeletionAfterProcessing;
	}

	public function supportsDelete()
	{
		return $this->supportsDelete;
	}

	public function supportsDownloadToFile()
	{
		return $this->supportsDownloadToFile;
	}

	public function supportsDownloadToBrowser()
	{
		return $this->supportsDownloadToBrowser;
	}

	public function doesInlineDownloadToBrowser()
	{
		return $this->inlineDownloadToBrowser;
	}

	public function getRemotePath()
	{
		return $this->remotePath;
	}

	/**
	 * Returns the URL to the OAuth2 helper script. Used by the oauthOpen method. Must be overridden in subclasses.
	 *
	 * @return  string
	 *
	 * @throws  OAuthNotSupported
	 */
	protected function getOAuth2HelperUrl()
	{
		throw new OAuthNotSupported();
	}

	/**
	 * Returns an instance of the connector object.
	 *
	 * @param   bool  $forceNew  Should I force the creation of a new connector object?
	 *
	 * @return  object  The connector object
	 *
	 * @throws  BadConfiguration  If there is a configuration error which prevents creating a connector object.
	 * @throws  Exception
	 */
	final protected function getConnector($forceNew = false)
	{
		if ($forceNew)
		{
			$this->resetConnector();
		}

		if (empty($this->connector))
		{
			$this->connector = $this->makeConnector();
		}

		return $this->connector;
	}

	/**
	 * Resets the connector.
	 *
	 * If the connector requires any special handling upon destruction you must handle it in its __destruct method.
	 *
	 * @return  void
	 */
	final protected function resetConnector()
	{
		$this->connector = null;
	}

	/**
	 * Creates a new connector object based on the engine configuration stored in the backup profile.
	 *
	 * Do not use this method directly. Use getConnector() instead.
	 *
	 * @return  object  The connector object
	 *
	 * @throws  BadConfiguration  If there is a configuration error which prevents creating a connector object.
	 * @throws  Exception  Any other error when creating or initializing the connector object.
	 */
	protected abstract function makeConnector();
}
Postproc/PostProcInterface.php000060400000016022152456704520012467 0ustar00<?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\Postproc;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Postproc\Exception\DeleteNotSupported;
use Akeeba\Engine\Postproc\Exception\DownloadToBrowserNotSupported;
use Akeeba\Engine\Postproc\Exception\DownloadToServerNotSupported;
use Akeeba\Engine\Postproc\Exception\OAuthNotSupported;
use Akeeba\Engine\Postproc\Exception\RangeDownloadNotSupported;
use Exception;

interface PostProcInterface
{
	/**
	 * This function takes care of post-processing a file (typically a backup archive part file).
	 *
	 * If the process has ran to completion it returns true.
	 *
	 * If more work is required (the file has only been partially uploaded) it returns false.
	 *
	 * It the process has failed an Exception is thrown.
	 *
	 * @param   string       $localFilepath   Absolute path to the part we'll have to process
	 * @param   string|null  $remoteBaseName  Base name of the uploaded file, skip to use $absolute_filename's
	 *
	 * @return  bool  True on success, false if more work is required
	 *
	 * @throws  Exception  When an error occurred during post-processing
	 */
	public function processPart($localFilepath, $remoteBaseName = null);

	/**
	 * Deletes a remote file
	 *
	 * @param   string  $path  The absolute, remote storage path to the file we're deleting
	 *
	 * @return  void
	 *
	 * @throws  DeleteNotSupported  When this feature is not supported at all.
	 * @throws  Exception  When an engine error occurs
	 */
	public function delete($path);

	/**
	 * Downloads a remotely stored file back to the site's server. It can optionally do a range download. If range
	 * downloads are not supported we throw a RangeDownloadNotSupported exception. Any other type of Exception means
	 * that the download failed.
	 *
	 * @param   string    $remotePath  The path to the remote file
	 * @param   string    $localFile   The absolute path to the local file we're writing to
	 * @param   int|null  $fromOffset  The offset (in bytes) to start downloading from
	 * @param   int|null  $length      The amount of data (in bytes) to download
	 *
	 * @return  void
	 *
	 * @throws  DownloadToServerNotSupported  When this feature is not supported at all.
	 * @throws  RangeDownloadNotSupported  When range downloads are not supported.
	 * @throws  Exception  On failure.
	 */
	public function downloadToFile($remotePath, $localFile, $fromOffset = null, $length = null);

	/**
	 * Downloads a remotely stored file to the user's browser, without storing it on the site's web server first.
	 *
	 * If $this->inlineDownloadToBrowser is true the method outputs a byte stream to the browser and returns null.
	 *
	 * If $this->inlineDownloadToBrowser is false it returns a string containing a public download URL. The user's
	 * browser will be redirected to that URL.
	 *
	 * If this feature is not supported a DownloadToBrowserNotSupported exception will be thrown.
	 *
	 * Any other Exception indicates an error while trying to download to browser such as file not found, problem with
	 * the remote service etc.
	 *
	 * @param   string  $remotePath  The absolute, remote storage path to the file we want to download
	 *
	 * @return  string|null
	 *
	 * @throws  DownloadToBrowserNotSupported  When this feature is not supported at all.
	 * @throws  Exception  When an error occurs.
	 */
	public function downloadToBrowser($remotePath);

	/**
	 * A proxy which allows us to execute arbitrary methods in this engine. Used for AJAX calls, typically to update UI
	 * elements with information fetched from the remote storage service.
	 *
	 * For security reasons, only methods whitelisted in the $this->allowedCustomAPICallMethods array can be called.
	 *
	 * @param   string  $method  The method to call.
	 * @param   array   $params  Any parameters to send to the method, in array format
	 *
	 * @return  mixed  The return value of the method.
	 */
	public function customAPICall($method, $params = []);

	/**
	 * Opens an OAuth window (performs an HTTP redirection).
	 *
	 * @param   array  $params  Any parameters required to launch OAuth
	 *
	 * @return  void
	 *
	 * @throws  OAuthNotSupported  When not supported.
	 * @throws  Exception  When an error occurred.
	 */
	public function oauthOpen($params = []);

	/**
	 * Fetches the authentication token from the OAuth helper script, after you've run the first step of the OAuth
	 * authentication process. Must be overridden in subclasses.
	 *
	 * @param   array  $params
	 *
	 * @return  void
	 *
	 * @throws  OAuthNotSupported
	 */
	public function oauthCallback(array $params);

	/**
	 * Does the engine recommend doing a step break before post-processing backup archives with it?
	 *
	 * @return  bool
	 */
	public function recommendsBreakBefore();

	/**
	 * Does the engine recommend doing a step break after post-processing backup archives with it?
	 *
	 * @return  bool
	 */
	public function recommendsBreakAfter();

	/**
	 * Is it advisable to delete files successfully post-processed by this post-processing engine?
	 *
	 * Currently only the “None” method advises against deleting successfully post-processed files for the simple reason
	 * that it does absolutely nothing with the files. The only copy is still on the server.
	 *
	 * @return  bool
	 */
	public function isFileDeletionAfterProcessingAdvisable();

	/**
	 * Does this engine support deleting remotely stored files?
	 *
	 * Most engines support deletion. However, some engines such as “Send by email”, do not have a way to find files
	 * already processed and delete them. Or it may be that we are sending the file to a write-only storage service
	 * which does not support deletions.
	 *
	 * @return  bool
	 */
	public function supportsDelete();

	/**
	 * Does this engine support downloading backup archives back to the site's web server?
	 *
	 * @return  bool
	 */
	public function supportsDownloadToFile();

	/**
	 * Does this engine support downloading backup archives directly to the user's browser?
	 *
	 * @return  bool
	 */
	public function supportsDownloadToBrowser();

	/**
	 * Does this engine return a bytestream when asked to download backup archives directly to the user's browser?
	 *
	 * @return  bool
	 */
	public function doesInlineDownloadToBrowser();

	/**
	 * Returns the remote absolute path to the file which was just processed.
	 *
	 * @return  string
	 */
	public function getRemotePath();
}
Postproc/email.json000060400000002460152456704520010347 0ustar00{
    "_information": {
        "title": "COM_AKEEBA_CONFIG_ENGINE_POSTPROC_EMAIL_TITLE",
        "description": "COM_AKEEBA_CONFIG_ENGINE_POSTPROC_EMAIL_DESCRIPTION"
    },
    "engine.postproc.common.after_part": {
        "default": "0",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_POSTPROCPARTS_TITLE",
        "description": "COM_AKEEBA_CONFIG_POSTPROCPARTS_DESCRIPTION"
    },
    "engine.postproc.common.abort_on_fail": {
        "default": "0",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_POSTPROC_ABORT_ON_FAIL_TITLE",
        "description": "COM_AKEEBA_CONFIG_POSTPROC_ABORT_ON_FAIL_DESCRIPTION"
    },
    "engine.postproc.common.delete_after": {
        "default": "1",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_DELETEAFTER_TITLE",
        "description": "COM_AKEEBA_CONFIG_DELETEAFTER_DESCRIPTION"
    },
    "engine.postproc.email.address": {
        "default": "",
        "type": "string",
        "title": "COM_AKEEBA_CONFIG_PROCEMAIL_ADDRESS_TITLE",
        "description": "COM_AKEEBA_CONFIG_PROCEMAIL_ADDRESS_DESCRIPTION"
    },
    "engine.postproc.email.subject": {
        "default": "",
        "type": "string",
        "title": "COM_AKEEBA_CONFIG_PROCEMAIL_SUBJECT_TITLE",
        "description": "COM_AKEEBA_CONFIG_PROCEMAIL_SUBJECT_DESCRIPTION"
    }
}Postproc/None.php000060400000002617152456704520010001 0ustar00<?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\Postproc;

defined('AKEEBAENGINE') || die();

class None extends Base
{
	public function __construct()
	{
		// No point in breaking the step; we simply do nothing :)
		$this->recommendsBreakAfter           = false;
		$this->recommendsBreakBefore          = false;
		$this->advisesDeletionAfterProcessing = false;
	}

	public function processPart($localFilepath, $remoteBaseName = null)
	{
		// Really nothing to do!!
		return true;
	}

	protected function makeConnector()
	{
		// I have to return an object to satisfy the definition.
		return (object) [
			'foo' => 'bar',
		];
	}
}
Postproc/ProxyAware.php000060400000004675152456704520011211 0ustar00<?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\Postproc;

use Akeeba\Engine\Platform;

trait ProxyAware
{
	/**
	 * Apply the platform proxy configuration to the cURL resource.
	 *
	 * @param   resource  $ch  The cURL resource, returned by curl_init();
	 */
	protected function applyProxySettingsToCurl($ch)
	{
		if (defined('AKEEBA_NO_PROXY_AWARE'))
		{
			return;
		}

		$proxySettings = Platform::getInstance()->getProxySettings();

		if (!$proxySettings['enabled'])
		{
			return;
		}

		curl_setopt($ch, CURLOPT_PROXY, $proxySettings['host'] . ':' . $proxySettings['port']);

		if (empty($proxySettings['user']))
		{
			return;
		}

		curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxySettings['user'] . ':' . $proxySettings['pass']);
	}

	protected function getProxyStreamContext()
	{
		if (defined('AKEEBA_NO_PROXY_AWARE'))
		{
			return [];
		}

		$ret           = [];
		$proxySettings = Platform::getInstance()->getProxySettings();

		if (!$proxySettings['enabled'])
		{
			return $ret;
		}

		$ret['http'] = [
			'proxy'           => $proxySettings['host'] . ':' . $proxySettings['port'],
			'request_fulluri' => true,
		];
		$ret['ftp']  = [
			'proxy'           => $proxySettings['host'] . ':' . $proxySettings['port'],
			// So, request_fulluri isn't documented for the FTP transport but seems to be required...?!
			'request_fulluri' => true,
		];

		if (empty($proxySettings['user']))
		{
			return $ret;
		}

		$ret['http']['header'] = ['Proxy-Authorization: Basic ' . base64_encode($proxySettings['user'] . ':' . $proxySettings['pass'])];
		$ret['ftp']['header'] = ['Proxy-Authorization: Basic ' . base64_encode($proxySettings['user'] . ':' . $proxySettings['pass'])];

		return $ret;
	}
}Postproc/Exception/DownloadToBrowserNotSupported.php000060400000002277152456704520017047 0ustar00<?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\Postproc\Exception;

defined('AKEEBAENGINE') || die();

/**
 * Indicates that the post-processing engine does not support downloading remotely stored files to the user's browser.
 */
class DownloadToBrowserNotSupported extends EngineException
{
	protected $messagePrototype = 'The %s post-processing engine does not support downloading of backup archives to the browser.';
}
Postproc/Exception/OAuthNotSupported.php000060400000002344152456704520014444 0ustar00<?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\Postproc\Exception;

defined('AKEEBAENGINE') || die();

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

defined('AKEEBAENGINE') || die();

/**
 * Indicates that the post-processing engine does not support downloading remotely stored files to the server
 */
class DownloadToServerNotSupported extends EngineException
{
	protected $messagePrototype = 'The %s post-processing engine does not support downloading of backup archives to the server.';
}
Postproc/Exception/RangeDownloadNotSupported.php000060400000002226152456704520016147 0ustar00<?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\Postproc\Exception;

defined('AKEEBAENGINE') || die();

/**
 * Indicates that the post-processing engine does not support range downloads.
 */
class RangeDownloadNotSupported extends EngineException
{
	protected $messagePrototype = 'The %s post-processing engine does not support range downloads of backup archives to the server.';
}
Postproc/Exception/BadConfiguration.php000060400000002031152456704520014244 0ustar00<?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\Postproc\Exception;

defined('AKEEBAENGINE') || die();

use RuntimeException;

/**
 * Indicates an error with the post-processing engine's configuration
 */
class BadConfiguration extends RuntimeException
{
}
Postproc/Exception/EngineException.php000060400000005507152456704520014125 0ustar00<?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\Postproc\Exception;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Postproc\Base;
use Exception;
use RuntimeException;
use Throwable;

class EngineException extends RuntimeException
{
	protected $messagePrototype = 'The %s post-processing engine has experienced an unspecified error.';

	/**
	 * Construct the exception. If a message is not defined the default message for the exception will be used.
	 *
	 * @param   string               $message   [optional] The Exception message to throw.
	 * @param   int                  $code      [optional] The Exception code.
	 * @param   Exception|Throwable  $previous  [optional] The previous throwable used for the exception chaining.
	 */
	public function __construct($message = "", $code = 0, $previous = null)
	{
		if (empty($message))
		{
			$engineName = $this->getEngineKeyFromBacktrace();
			$message    = sprintf($this->messagePrototype, $engineName);
		}

		parent::__construct($message, $code, $previous);
	}

	/**
	 * Returns the engine name (class name without the namespace) from the PHP execution backtrace.
	 *
	 * @return mixed|string
	 */
	protected function getEngineKeyFromBacktrace()
	{
		// Make sure the backtrace is at least 3 levels deep
		$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 5);

		// We need to be at least two levels deep
		if (count($backtrace) < 2)
		{
			return 'current';
		}

		for ($i = 1; $i < count($backtrace); $i++)
		{
			// Get the fully qualified class
			$object = $backtrace[$i]['object'];

			// We need a backtrace element with an object attached.
			if (!is_object($object))
			{
				continue;
			}

			// If the object is not a Postproc\Base object go to the next entry.
			if (!($object instanceof Base))
			{
				continue;
			}

			// Get the bare class name
			$fqnClass  = $backtrace[$i]['class'];
			$parts     = explode('\\', $fqnClass);
			$bareClass = array_pop($parts);

			// Do not return the base object!
			if ($bareClass == 'Base')
			{
				continue;
			}

			return $bareClass;
		}

		return 'current';
	}
}
Postproc/Exception/DeleteNotSupported.php000060400000002211152456704520014617 0ustar00<?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\Postproc\Exception;

defined('AKEEBAENGINE') || die();

/**
 * Indicates that the post-processing engine does not support deleting remotely stored files.
 */
class DeleteNotSupported extends EngineException
{
	protected $messagePrototype = 'The %s post-processing engine does not support deletion of backup archives.';
}
Postproc/none.json000060400000000254152456704520010216 0ustar00{
    "_information": {
        "title": "COM_AKEEBA_CONFIG_ENGINE_POSTPROC_NONE_TITLE",
        "description": "COM_AKEEBA_CONFIG_ENGINE_POSTPROC_NONE_DESCRIPTION"
    }
}Postproc/onedriveapp.json000060400000004565152456704520011604 0ustar00{
    "_information": {
        "title": "COM_AKEEBA_CONFIG_ENGINE_POSTPROC_ONEDRIVEAPP_TITLE",
        "description": "COM_AKEEBA_CONFIG_ENGINE_POSTPROC_ONEDRIVEAPP_DESCRIPTION"
    },
    "engine.postproc.common.after_part": {
        "default": "0",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_POSTPROCPARTS_TITLE",
        "description": "COM_AKEEBA_CONFIG_POSTPROCPARTS_DESCRIPTION"
    },
    "engine.postproc.common.abort_on_fail": {
        "default": "0",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_POSTPROC_ABORT_ON_FAIL_TITLE",
        "description": "COM_AKEEBA_CONFIG_POSTPROC_ABORT_ON_FAIL_DESCRIPTION"
    },
    "engine.postproc.common.delete_after": {
        "default": "1",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_DELETEAFTER_TITLE",
        "description": "COM_AKEEBA_CONFIG_DELETEAFTER_DESCRIPTION"
    },
    "engine.postproc.onedriveapp.chunk_upload": {
        "default": "1",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_BOX_CHUNKUPLOAD_ENABLE"
    },
    "engine.postproc.onedriveapp.chunk_upload_size": {
        "default": "10",
        "type": "integer",
        "min": "4",
        "max": "60",
        "shortcuts": "5|10|20|40|60",
        "scale": "1",
        "uom": "MB",
        "title": "COM_AKEEBA_CONFIG_BOX_CHUNKUPLOAD_SIZE",
        "showon": "engine.postproc.onedriveapp.chunk_upload:1"
    },
    "engine.postproc.onedriveapp.openoauth": {
        "default": "",
        "type": "button",
        "title": "COM_AKEEBA_CONFIG_BOX_OPENOAUTH_TITLE",
        "description": "COM_AKEEBA_CONFIG_BOX_OPENOAUTH_DESC",
        "hook": "akconfig_onedriveapp_openoauth"
    },
    "engine.postproc.onedriveapp.directory": {
        "default": "\/",
        "type": "string",
        "title": "COM_AKEEBA_CONFIG_ONEDRIVEAPP_DIRECTORY_TITLE",
        "description": "COM_AKEEBA_CONFIG_ONEDRIVEAPP_DIRECTORY_DESCRIPTION"
    },
    "engine.postproc.onedriveapp.access_token": {
        "default": "",
        "type": "string",
        "title": "COM_AKEEBA_CONFIG_ONEDRIVE_ACCESSTOKEN_TITLE",
        "description": "COM_AKEEBA_CONFIG_ONEDRIVE_ACCESSTOKEN_DESCRIPTION"
    },
    "engine.postproc.onedriveapp.refresh_token": {
        "default": "",
        "type": "string",
        "title": "COM_AKEEBA_CONFIG_ONEDRIVE_REFRESHTOKEN_TITLE",
        "description": "COM_AKEEBA_CONFIG_ONEDRIVE_REFRESHTOKEN_DESCRIPTION"
    }
}Util/Log/WarningsLoggerInterface.php000060400000003420152456704520013471 0ustar00<?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\Util\Log;

defined('AKEEBAENGINE') || die();

interface WarningsLoggerInterface
{
	/**
	 * Returns an array with all warnings logged since the last time warnings were reset. The maximum number of warnings
	 * returned is controlled by setWarningsQueueSize().
	 *
	 * @return array
	 */
	public function getWarnings();

	/**
	 * Resets the warnings queue.
	 *
	 * @return void
	 */
	public function resetWarnings();

	/**
	 * A combination of getWarnings() and resetWarnings(). Returns the warnings and immediately resets the warnings
	 * queue.
	 *
	 * @return array
	 */
	public function getAndResetWarnings();

	/**
	 * Set the warnings queue size. A size of 0 means "no limit".
	 *
	 * @param   int  $queueSize  The size of the warnings queue (in number of warnings items)
	 *
	 * @return void
	 */
	public function setWarningsQueueSize($queueSize = 0);

	/**
	 * Returns the warnings queue size.
	 *
	 * @return int
	 */
	public function getWarningsQueueSize();
}
Util/Log/WarningsLoggerAware.php000060400000005562152456704520012641 0ustar00<?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\Util\Log;

defined('AKEEBAENGINE') || die();

trait WarningsLoggerAware
{
	/**
	 * The warnings in the current queue
	 *
	 * @var string[]
	 */
	private $warningsQueue = [];

	/**
	 * The maximum length of the warnings queue
	 *
	 * @var int
	 */
	private $warningsQueueSize = 0;

	/**
	 * A combination of getWarnings() and resetWarnings(). Returns the warnings and immediately resets the warnings
	 * queue.
	 *
	 * @return array
	 */
	final public function getAndResetWarnings()
	{
		$ret = $this->getWarnings();

		$this->resetWarnings();

		return $ret;
	}

	/**
	 * Returns an array with all warnings logged since the last time warnings were reset. The maximum number of warnings
	 * returned is controlled by setWarningsQueueSize().
	 *
	 * @return array
	 */
	final public function getWarnings()
	{
		return $this->warningsQueue;
	}

	/**
	 * Resets the warnings queue.
	 *
	 * @return void
	 */
	final public function resetWarnings()
	{
		$this->warningsQueue = [];
	}

	/**
	 * Returns the warnings queue size.
	 *
	 * @return int
	 */
	final public function getWarningsQueueSize()
	{
		return $this->warningsQueueSize;
	}

	/**
	 * Set the warnings queue size. A size of 0 means "no limit".
	 *
	 * @param   int  $queueSize  The size of the warnings queue (in number of warnings items)
	 *
	 * @return void
	 */
	final public function setWarningsQueueSize($queueSize = 0)
	{
		if (!is_numeric($queueSize) || empty($queueSize) || ($queueSize < 0))
		{
			$queueSize = 0;
		}

		$this->warningsQueueSize = $queueSize;
	}

	/**
	 * Adds a warning to the warnings queue.
	 *
	 * @param   string  $warning
	 */
	final protected function enqueueWarning($warning)
	{
		$this->warningsQueue[] = $warning;

		// If there is no queue size limit there's nothing else to be done.
		if ($this->warningsQueueSize <= 0)
		{
			return;
		}

		// If the queue size is exceeded remove as many of the earliest elements as required
		if (count($this->warningsQueue) > $this->warningsQueueSize)
		{
			$this->warningsQueueSize = array_slice($this->warningsQueue, -$this->warningsQueueSize);
		}
	}
}
Util/Log/LogInterface.php000060400000005556152456704520011276 0ustar00<?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\Util\Log;

defined('AKEEBAENGINE') || die();

/**
 * The interface for Akeeba Engine logger objects
 */
interface LogInterface
{
	/**
	 * Open a new log instance with the specified tag. If another log is already open it is closed before switching to
	 * the new log tag. If the tag is null use the default log defined in the logging system.
	 *
	 * @param   string|null  $tag        The log to open
	 * @param   string       $extension  The log file extension (default: .php, use empty string for .log files)
	 *
	 * @return void
	 */
	public function open($tag = null, $extension = '.php');

	/**
	 * Close the currently active log and set the current tag to null.
	 *
	 * @return  void
	 */
	public function close();

	/**
	 * Reset (remove entries) of the log with the specified tag.
	 *
	 * @param   string|null  $tag  The log to reset
	 *
	 * @return  void
	 */
	public function reset($tag = null);

	/**
	 * Add a message to the log
	 *
	 * @param   string  $level    One of the Akeeba\Engine\Psr\Log\LogLevel constants
	 * @param   string  $message  The message to log
	 * @param   array   $context  Currently not used. Left here for PSR-3 compatibility.
	 *
	 * @return  void
	 */
	public function log($level, $message, array $context = []);

	/**
	 * Temporarily pause log output. The log() method MUST respect this.
	 *
	 * @return  void
	 */
	public function pause();

	/**
	 * Resume the previously paused log output. The log() method MUST respect this.
	 *
	 * @return  void
	 */
	public function unpause();

	/**
	 * Returns the timestamp (in UNIX time long integer format) of the last log message written to the log with the
	 * specific tag. The timestamp MUST be read from the log itself, not from the logger object. It is used by the
	 * engine to find out the age of stalled backups which may have crashed.
	 *
	 * @param   string|null  $tag  The log tag for which the last timestamp is returned
	 *
	 * @return  int|null  The timestamp of the last log message, in UNIX time. NULL if we can't get the timestamp.
	 */
	public function getLastTimestamp($tag = null);
}
Util/EngineParameters.php000060400000063164152456704520011443 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use DirectoryIterator;
use LogicException;

/**
 * Unified engine parameters helper class. Deals with scripting, GUI configuration elements and information on engine
 * parts (filters, dump engines, scan engines, archivers, installers).
 */
class EngineParameters
{
	/**
	 * Holds the parsed scripting.json contents
	 *
	 * @var array
	 */
	public $scripting = null;

	/**
	 * The currently active scripting type
	 *
	 * @var string
	 */
	protected $activeType = null;

	/**
	 * Holds the known paths holding JSON definitions of engines, installers and configuration gui elements
	 *
	 * @var  array
	 */
	protected $enginePartPaths = [];

	/**
	 * Cache of the engines known to this object
	 *
	 * @var array
	 */
	protected $engine_list = [];

	/**
	 * Cache of the GUI configuration elements known to this object
	 *
	 * @var array
	 */
	protected $gui_list = [];

	/**
	 * Cache of the installers known to this object
	 *
	 * @var array
	 */
	protected $installer_list = [];

	/**
	 * Append a path to the end of the paths list for a specific section
	 *
	 * @param   string  $path     Absolute filesystem path to add
	 * @param   string  $section  The section to add it to (gui, engine, installer, filters)
	 *
	 * @return  void
	 */
	public function addPath(string $path, string $section = 'gui')
	{
		$path = Factory::getFilesystemTools()->TranslateWinPath($path);

		// If the array is empty, populate with the defaults
		if (!array_key_exists($section, $this->enginePartPaths))
		{
			$this->getEnginePartPaths($section);
		}

		// If the path doesn't already exist, add it
		if (!in_array($path, $this->enginePartPaths[$section]))
		{
			$this->enginePartPaths[$section][] = $path;
		}
	}

	/**
	 * Returns an array with domain keys and domain class names for the current
	 * backup type. The idea is that shifting this array walks through the backup
	 * process. When the array is empty, the backup is done.
	 *
	 * Each element of the array is an array with two keys: domain and class.
	 *
	 * @return  array
	 */
	public function getDomainChain(): array
	{
		$configuration = Factory::getConfiguration();
		$script        = $configuration->get('akeeba.basic.backup_type', 'full');

		$scripting = $this->loadScripting();
		$domains   = $scripting['domains'];
		$keys      = $scripting['scripts'][$script]['chain'];

		$result = [];

		foreach ($keys as $domain_key)
		{
			$result[] = [
				'domain' => $domains[$domain_key]['domain'],
				'class'  => $domains[$domain_key]['class'],
			];
		}

		return $result;
	}

	/**
	 * Get the paths for a specific section
	 *
	 * @param   string  $section  The section to get the path list for (engine, installer, gui, filter)
	 *
	 * @return  array
	 */
	public function getEnginePartPaths(string $section = 'gui')
	{
		// Create the key if it's not already present
		if (!array_key_exists($section, $this->enginePartPaths))
		{
			$this->enginePartPaths[$section] = [];
		}

		if (!empty($this->enginePartPaths[$section]))
		{
			return $this->enginePartPaths[$section];
		}

		// Add the defaults if the list is empty
		switch ($section)
		{
			case 'engine':
				$this->enginePartPaths[$section] = [
					Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot()),
				];
				break;

			case 'installer':
				$this->enginePartPaths[$section] = [
					Factory::getFilesystemTools()->TranslateWinPath(Platform::getInstance()->get_installer_images_path()),
				];
				break;

			case 'gui':
				// Add core GUI definitions
				$this->enginePartPaths[$section] = [
					Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Core'),
				];

				// Add platform GUI definition files
				$platform_paths = Platform::getInstance()->getPlatformDirectories();

				foreach ($platform_paths as $p)
				{
					$this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Config');

					$pro = defined('AKEEBA_PRO') && AKEEBA_PRO;
					$pro = defined('AKEEBABACKUP_PRO') ? (AKEEBABACKUP_PRO ? true : false) : $pro;

					if ($pro)
					{
						$this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Config/Pro');
					}
				}
				break;

			case 'filter':
				$this->enginePartPaths[$section] = [
					Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Platform/Filter/Stack'),
					Factory::getFilesystemTools()->TranslateWinPath(Factory::getAkeebaRoot() . '/Filter/Stack'),
				];

				$platform_paths = Platform::getInstance()->getPlatformDirectories();

				foreach ($platform_paths as $p)
				{
					$this->enginePartPaths[$section][] = Factory::getFilesystemTools()->TranslateWinPath($p . '/Filter/Stack');
				}

				break;

			default:
				throw new LogicException(sprintf('Can not get paths for engine section ‘%s’. No section by this name is known to Akeeba Engine.', $section));
		}

		return $this->enginePartPaths[$section];
	}

	/**
	 * Returns a hash list of Akeeba engines and their data. Each entry has the engine name as key and contains two
	 * arrays, under the 'information' and 'parameters' keys.
	 *
	 * @param   string  $engine_type  The engine type to return information for
	 *
	 * @return  array
	 */
	public function getEnginesList(string $engine_type): array
	{
		$engine_type = ucfirst($engine_type);

		// Try to serve cached data first
		if (isset($this->engine_list[$engine_type]))
		{
			return $this->engine_list[$engine_type];
		}

		// Find absolute path to normal and plugins directories
		$temp      = $this->getEnginePartPaths('engine');
		$path_list = [];

		foreach ($temp as $path)
		{
			$path_list[] = $path . '/' . $engine_type;
		}

		// Initialize the array where we store our data
		$this->engine_list[$engine_type] = [];

		// Loop for the paths where engines can be found
		foreach ($path_list as $path)
		{
			if (!@is_dir($path))
			{
				continue;
			}

			if (!@is_readable($path))
			{
				continue;
			}

			$di = new DirectoryIterator($path);

			/** @var DirectoryIterator $file */
			foreach ($di as $file)
			{
				if (!$file->isFile())
				{
					continue;
				}

				if ($file->getExtension() !== 'json')
				{
					continue;
				}

				$bare_name = ucfirst($file->getBasename('.json'));

				// 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
				if (preg_match('/[^A-Za-z0-9]/', $bare_name))
				{
					continue;
				}

				$information = [];
				$parameters  = [];

				$this->parseEngineJSON($file->getRealPath(), $information, $parameters);

				$this->engine_list[$engine_type][lcfirst($bare_name)] = [
					'information' => $information,
					'parameters'  => $parameters,
				];
			}
		}

		return $this->engine_list[$engine_type];
	}

	/**
	 * Parses the GUI JSON files and returns an array of groups and their data
	 *
	 * @return  array
	 */
	public function getGUIGroups(): array
	{
		// Try to serve cached data first
		if (!empty($this->gui_list) && is_array($this->gui_list))
		{
			if (count($this->gui_list) > 0)
			{
				return $this->gui_list;
			}
		}

		// Find absolute path to normal and plugins directories
		$path_list = $this->getEnginePartPaths('gui');

		// Initialize the array where we store our data
		$this->gui_list = [];

		// Loop for the paths where engines can be found
		foreach ($path_list as $path)
		{
			if (!@is_dir($path))
			{
				continue;
			}

			if (!@is_readable($path))
			{
				continue;
			}

			$allJSONFiles = [];
			$di           = new DirectoryIterator($path);

			/** @var DirectoryIterator $file */
			foreach ($di as $file)
			{
				if (!$file->isFile())
				{
					continue;
				}

				// PHP 5.3.5 and earlier do not support getExtension
				if ($file->getExtension() !== 'json')
				{
					continue;
				}

				$allJSONFiles[] = $file->getRealPath();
			}

			if (empty($allJSONFiles))
			{
				continue;
			}

			// Sort GUI files alphabetically
			asort($allJSONFiles);

			// Include each GUI def file
			foreach ($allJSONFiles as $filename)
			{
				$information = [];
				$parameters  = [];

				$this->parseInterfaceJSON($filename, $information, $parameters);

				// This effectively skips non-GUI JSONs (e.g. the scripting JSON)
				if (!empty($information['description']))
				{
					if (!isset($information['merge']))
					{
						$information['merge'] = 0;
					}

					$group_name = substr(basename($filename), 0, -5);

					$def = [
						'information' => $information,
						'parameters'  => $parameters,
					];

					if (!$information['merge'] || !isset($this->gui_list[$group_name]))
					{
						$this->gui_list[$group_name] = $def;
					}
					else
					{
						$this->gui_list[$group_name]['information'] = array_merge($this->gui_list[$group_name]['information'], $def['information']);
						$this->gui_list[$group_name]['parameters']  = array_merge($this->gui_list[$group_name]['parameters'], $def['parameters']);
					}
				}
			}
		}

		ksort($this->gui_list);

		// Push stack filter settings to the 03.filters section
		$path_list = $this->getEnginePartPaths('filter');

		// Loop for the paths where optional filters can be found
		foreach ($path_list as $path)
		{
			if (!@is_dir($path))
			{
				continue;
			}

			if (!@is_readable($path))
			{
				continue;
			}

			// Store JSON names in temp array because we'll sort based on filename (GUI order IS IMPORTANT!!)
			$allJSONFiles = [];

			$di = new DirectoryIterator($path);

			/** @var DirectoryIterator $file */
			foreach ($di as $file)
			{
				if (!$file->isFile())
				{
					continue;
				}

				// PHP 5.3.5 and earlier do not support getExtension
				if ($file->getExtension() !== 'json')
				{
					continue;
				}

				$allJSONFiles[] = $file->getRealPath();
			}

			if (empty($allJSONFiles))
			{
				continue;
			}

			// Sort filter files alphabetically
			asort($allJSONFiles);

			// Include each filter def file
			foreach ($allJSONFiles as $filename)
			{
				$information = [];
				$parameters  = [];

				$this->parseInterfaceJSON($filename, $information, $parameters);

				if (!array_key_exists('03.filters', $this->gui_list))
				{
					$this->gui_list['03.filters'] = ['parameters' => []];
				}

				if (!array_key_exists('parameters', $this->gui_list['03.filters']))
				{
					$this->gui_list['03.filters']['parameters'] = [];
				}

				if (!is_array($parameters))
				{
					$parameters = [];
				}

				$this->gui_list['03.filters']['parameters'] = array_merge($this->gui_list['03.filters']['parameters'], $parameters);
			}
		}

		/**
		 * Parse showon attributes
		 *
		 * The GUI list array format is like this:
		 * ```
		 * [
		 *    '01.sectionName' => [
		 *       'information' => [...],
		 *       'parameters' => [
		 *           'some.parameter.name' => [
		 *               'title' => 'something',
		 *               ...
		 *               'showon' => 'some.other.parameter.name:1'
		 *           ],
		 *           ...
		 *       ]
		 *    ],
		 *    ...
		 * ]
		 * ```
		 *
		 * We need to convert the shown of the parameters to JSON data which can be used by the `showon` JavaScript
		 * code. The assumption only made here is that all parameter keys are turned into `INPUT` elements with an
		 * attribute `name="var[some.parameter.name]"`. This is how the GUI code in all backup applications our company
		 * makes works.
		 *
		 * For backup applications which do not have showon support (they lack the JavaScript code) this does not matter
		 * and neither does it break anything. All parameters are shown all the time like we have been doing for years.
		 */
		$this->gui_list = array_map(
			function (array $section): array {
				foreach ($section['parameters'] as $paramName => &$paramDef)
				{
					if (isset($paramDef['showon']))
					{
						// Parse showon
						$paramDef['showon'] = $this->parseShowOnConditions($paramDef['showon']);
					}
				}

				return $section;
			},
			$this->gui_list
		);

		return $this->gui_list;
	}

	/**
	 * Parses the installer JSON files and returns an array of installers and their data
	 *
	 * @param   boolean  $forDisplay  If true only returns the information relevant for displaying the GUI
	 *
	 * @return  array
	 */
	public function getInstallerList(bool $forDisplay = false): array
	{
		// Try to serve cached data first
		if (!empty($this->installer_list) && is_array($this->installer_list))
		{
			if (count($this->installer_list) > 0)
			{
				return $this->installer_list;
			}
		}

		// Find absolute path to normal and plugins directories
		$path_list = [
			Platform::getInstance()->get_installer_images_path(),
		];

		// Initialize the array where we store our data
		$this->installer_list = [];

		// Loop for the paths where engines can be found
		foreach ($path_list as $path)
		{
			if (!@is_dir($path))
			{
				continue;
			}

			if (!@is_readable($path))
			{
				continue;
			}

			$di = new DirectoryIterator($path);

			/** @var DirectoryIterator $file */
			foreach ($di as $file)
			{
				if (!$file->isFile())
				{
					continue;
				}

				// PHP 5.3.5 and earlier do not support getExtension
				if ($file->getExtension() !== 'json')
				{
					continue;
				}

				$rawData = file_get_contents($file->getRealPath());
				$data    = empty($rawData) ? [] : json_decode($rawData, true);

				if ($forDisplay)
				{
					$innerData = reset($data);

					if (array_key_exists('listinoptions', $innerData))
					{
						if ($innerData['listinoptions'] == 0)
						{
							continue;
						}
					}
				}

				foreach ($data as $key => $values)
				{
					$this->installer_list[$key] = [];

					foreach ($values as $key2 => $value)
					{
						$this->installer_list[$key][$key2] = $value;
					}
				}
			}
		}

		return $this->installer_list;
	}

	/**
	 * Returns the JSON representation of the GUI definition and the associated values
	 *
	 * @return   string
	 */
	public function getJsonGuiDefinition(): string
	{
		// Initialize the array which will be converted to JSON representation
		$json_array = [
			'engines'    => [],
			'installers' => [],
			'gui'        => [],
		];

		// Get a reference to the configuration
		$configuration = Factory::getConfiguration();

		// Get data for all engines
		$engine_types = [
			'archiver',
			'dump',
			'scan',
			'writer',
			'postproc',
		];

		foreach ($engine_types as $type)
		{
			$engines = $this->getEnginesList($type);

			$tempArray    = [];
			$engineTitles = [];

			foreach ($engines as $engine_name => $engine_data)
			{
				// Translate information
				foreach ($engine_data['information'] as $key => $value)
				{
					switch ($key)
					{
						case 'title':
						case 'content':
						case 'description':
							$value = Platform::getInstance()->translate($value);
							break;
					}

					$tempArray[$engine_name]['information'][$key] = $value;

					if ($key == 'title')
					{
						$engineTitles[$engine_name] = $value;
					}
				}

				// Process parameters
				$parameters = [];

				foreach ($engine_data['parameters'] as $param_key => $param)
				{
					$param['default'] = $configuration->get($param_key, $param['default'], false);

					foreach ($param as $option_key => $option_value)
					{
						// Translate title, description, enumkeys
						switch ($option_key)
						{
							case 'title':
							case 'description':
							case 'content':
							case 'labelempty':
							case 'labelnotempty':
								$param[$option_key] = Platform::getInstance()->translate($option_value);
								break;

							case 'enumkeys':
								$enumkeys = explode('|', $option_value);
								$new_keys = [];
								foreach ($enumkeys as $old_key)
								{
									$new_keys[] = Platform::getInstance()->translate($old_key);
								}
								$param[$option_key] = implode('|', $new_keys);
								break;

							case 'showon':
								$param[$option_key] = $this->parseShowOnConditions($param[$option_key]);

							default:
						}
					}

					$parameters[$param_key] = $param;
				}

				// Add processed parameters
				$tempArray[$engine_name]['parameters'] = $parameters;
			}

			asort($engineTitles);

			foreach ($engineTitles as $engineName => $title)
			{
				$json_array['engines'][$type][$engineName] = $tempArray[$engineName];
			}
		}

		// Get data for GUI elements
		$json_array['gui'] = [];
		$groupdefs         = $this->getGUIGroups();

		foreach ($groupdefs as $groupKey => $definition)
		{
			$group_name = '';

			if (isset($definition['information']) && isset($definition['information']['description']))
			{
				$group_name = Platform::getInstance()->translate($definition['information']['description']);
			}

			// Skip no-name groups
			if (empty($group_name))
			{
				continue;
			}

			$parameters = [];

			foreach ($definition['parameters'] as $param_key => $param)
			{
				$param['default'] = $configuration->get($param_key, $param['default'], false);

				foreach ($param as $option_key => $option_value)
				{
					// Translate title, description, enumkeys
					switch ($option_key)
					{
						case 'title':
						case 'description':
						case 'content':
							$param[$option_key] = Platform::getInstance()->translate($option_value);
							break;

						case 'enumkeys':
							$enumkeys = explode('|', $option_value);
							$new_keys = [];
							foreach ($enumkeys as $old_key)
							{
								$new_keys[] = Platform::getInstance()->translate($old_key);
							}
							$param[$option_key] = implode('|', $new_keys);
							break;

						default:
					}
				}
				$parameters[$param_key] = $param;
			}
			$json_array['gui'][$group_name] = $parameters;
		}

		// Get data for the installers
		$json_array['installers'] = $this->getInstallerList(true);

		uasort($json_array['installers'], function ($a, $b) {
			if ($a['name'] == $b['name'])
			{
				return 0;
			}

			return ($a['name'] < $b['name']) ? -1 : 1;
		});

		$json = json_encode($json_array);

		return $json;
	}

	/**
	 * Returns a volatile scripting parameter for the active backup type
	 *
	 * @param   string  $key      The relative key, e.g. core.createarchive
	 * @param   mixed   $default  Default value
	 *
	 * @return  mixed  The scripting parameter's value
	 */
	public function getScriptingParameter(string $key, $default = null)
	{
		$configuration = Factory::getConfiguration();

		if (is_null($this->activeType))
		{
			$this->activeType = $configuration->get('akeeba.basic.backup_type', 'full');
		}

		return $configuration->get('volatile.scripting.' . $this->activeType . '.' . $key, $default);
	}

	/**
	 * Imports the volatile scripting parameters to the registry
	 *
	 * @return  void
	 */
	public function importScriptingToRegistry(): void
	{
		$scripting     = $this->loadScripting();
		$configuration = Factory::getConfiguration();
		$configuration->mergeArray($scripting['data'], false);
	}

	/**
	 * Loads the scripting.json and returns an array with the domains, the scripts and the raw data
	 *
	 * @return  array  The parsed scripting.json. Array keys: domains, scripts, data
	 */
	public function loadScripting(?string $jsonPath = ''): ?array
	{
		if (!empty($this->scripting))
		{
			return $this->scripting;
		}

		$this->scripting = [];
		$jsonPath        = $jsonPath ?: Factory::getAkeebaRoot() . '/Core/scripting.json';

		if (!@file_exists($jsonPath))
		{
			return $this->scripting;
		}

		$rawData          = file_get_contents($jsonPath);
		$rawScriptingData = empty($rawData) ? [] : json_decode($rawData, true);
		$domain_keys      = explode('|', $rawScriptingData['volatile.akeebaengine.domains']);
		$domains          = [];

		foreach ($domain_keys as $key)
		{
			$record        = [
				'domain' => $rawScriptingData['volatile.domain.' . $key . '.domain'],
				'class'  => $rawScriptingData['volatile.domain.' . $key . '.class'],
				'text'   => $rawScriptingData['volatile.domain.' . $key . '.text'],
			];
			$domains[$key] = $record;
		}

		$script_keys = explode('|', $rawScriptingData['volatile.akeebaengine.scripts']);
		$scripts     = [];

		foreach ($script_keys as $key)
		{
			$record        = [
				'chain' => explode('|', $rawScriptingData['volatile.scripting.' . $key . '.chain']),
				'text'  => $rawScriptingData['volatile.scripting.' . $key . '.text'],
			];
			$scripts[$key] = $record;
		}

		$this->scripting = [
			'domains' => $domains,
			'scripts' => $scripts,
			'data'    => $rawScriptingData,
		];

		return $this->scripting;
	}

	/**
	 * Parses an engine JSON file returning two arrays, one with the general information
	 * of that engine and one with its configuration variables' definitions
	 *
	 * @param   string  $jsonPath     Absolute path to engine JSON file
	 * @param   array   $information  [out] The engine information hash array
	 * @param   array   $parameters   [out] The parameters hash array
	 *
	 * @return  bool  True if the file was loaded
	 */
	public function parseEngineJSON(string $jsonPath, array &$information, array &$parameters): bool
	{
		if (!file_exists($jsonPath))
		{
			return false;
		}

		$information = [
			'title'       => '',
			'description' => '',
		];

		$parameters = [];

		$rawData  = file_get_contents($jsonPath);
		$jsonData = empty($rawData) ? [] : json_decode($rawData, true);

		foreach ($jsonData ?? [] as $section => $data)
		{
			if (is_array($data))
			{
				if ($section == '_information')
				{
					// Parse information
					foreach ($data as $key => $value)
					{
						$information[$key] = $value;
					}
				}
				elseif (substr($section, 0, 1) != '_')
				{
					// Parse parameters
					$newparam = [
						'title'       => '',
						'description' => '',
						'type'        => 'string',
						'default'     => '',
					];

					foreach ($data as $key => $value)
					{
						$newparam[$key] = $value;
					}
					$parameters[$section] = $newparam;
				}
			}
		}

		return true;
	}

	/**
	 * Parses a graphical interface JSON file returning two arrays, one with the general
	 * information of that configuration section and one with its configuration variables'
	 * definitions.
	 *
	 * @param   string  $jsonPath     Absolute path to engine JSON file
	 * @param   array   $information  [out] The GUI information hash array
	 * @param   array   $parameters   [out] The parameters hash array
	 *
	 * @return bool True if the file was loaded
	 */
	public function parseInterfaceJSON(string $jsonPath, array &$information, array &$parameters): bool
	{
		if (!file_exists($jsonPath))
		{
			return false;
		}

		$information = [
			'description' => '',
		];

		$parameters = [];
		$rawData    = file_get_contents($jsonPath);
		$jsonData   = empty($rawData) ? [] : json_decode($rawData, true);

		foreach ($jsonData as $section => $data)
		{
			if (is_array($data))
			{
				if ($section == '_group')
				{
					// Parse information
					foreach ($data as $key => $value)
					{
						$information[$key] = $value;
					}

					continue;
				}

				if (substr($section, 0, 1) != '_')
				{
					// Parse parameters
					$newparam = [
						'title'       => '',
						'description' => '',
						'type'        => 'string',
						'default'     => '',
						'protected'   => 0,
					];

					foreach ($data as $key => $value)
					{
						$newparam[$key] = $value;
					}

					$parameters[$section] = $newparam;
				}
			}
		}

		return true;
	}

	/**
	 * Add a path to the beginning of the paths list for a specific section
	 *
	 * @param   string  $path     Absolute filesystem path to add
	 * @param   string  $section  The section to add it to (gui, engine, installer, filters)
	 *
	 * @return  void
	 */
	public function prependPath(string $path, string $section = 'gui'): void
	{
		$path = Factory::getFilesystemTools()->TranslateWinPath($path);

		// If the array is empty, populate with the defaults
		if (!array_key_exists($section, $this->enginePartPaths))
		{
			$this->getEnginePartPaths($section);
		}

		// If the path doesn't already exist, add it
		if (!in_array($path, $this->enginePartPaths[$section]))
		{
			array_unshift($this->enginePartPaths[$section], $path);
		}
	}

	/**
	 * Parse the `showon` conditions text into an instructions array for the ShowOn JavaScript
	 *
	 * @param   string|null  $showOn     The `showon` conditions text
	 * @param   string|null  $arrayName  The array all of our parameters are members of, default 'var'.
	 *
	 * @return  array  The ShowOn JavaScript instructions
	 *
	 * @since   9.3.1
	 */
	private function parseShowOnConditions(?string $showOn, ?string $arrayName = 'var'): array
	{
		if (empty($showOn))
		{
			return [];
		}

		$showOnData  = [];
		$showOnParts = preg_split('#(\[AND\]|\[OR\])#', $showOn, -1, PREG_SPLIT_DELIM_CAPTURE);
		$op          = '';

		foreach ($showOnParts as $showOnPart)
		{
			if (in_array($showOnPart, ['[AND]', '[OR]']))
			{
				$op = trim($showOnPart, '[]');

				continue;
			}

			$compareEqual     = strpos($showOnPart, '!:') === false;
			$showOnPartBlocks = explode(($compareEqual ? ':' : '!:'), $showOnPart, 2);

			$field = $arrayName
				? sprintf("%s[%s]", $arrayName, $showOnPartBlocks[0])
				: $showOnPartBlocks[0];

			$showOnData[] = [
				'field'  => $field,
				'values' => explode(',', $showOnPartBlocks[1]),
				'sign'   => $compareEqual === true ? '=' : '!=',
				'op'     => $op,
			];

			$op = '';
		}

		return $showOnData;
	}
}
Util/FactoryStorage.php000060400000016162152456704520011142 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use InvalidArgumentException;
use RuntimeException;

/**
 * Management class for temporary storage of the serialised engine state.
 */
class FactoryStorage
{
	protected static $tempFileStoragePath;

	/**
	 * Returns the fully qualified path to the storage file
	 *
	 * @param   string  $tag        Backup tag
	 * @param   string  $extension  File extension, default is php
	 *
	 * @return  string
	 */
	public function get_storage_filename($tag = null, $extension = 'php')
	{
		if (is_null(self::$tempFileStoragePath))
		{
			$registry                  = Factory::getConfiguration();
			self::$tempFileStoragePath = $registry->get('akeeba.basic.output_directory', '');

			if (empty(self::$tempFileStoragePath))
			{
				throw new InvalidArgumentException('You have not set a backup output directory.');
			}

			self::$tempFileStoragePath = rtrim(self::$tempFileStoragePath, '/\\');

			if (!is_writable(self::$tempFileStoragePath) || !is_readable(self::$tempFileStoragePath))
			{
				throw new InvalidArgumentException(sprintf('Backup output directory %s needs to be both readable and writeable to PHP for this backup software to function correctly.', self::$tempFileStoragePath));
			}
		}

		$tag      = empty($tag) ? '' : $tag;
		$filename = sprintf("akstorage%s%s.%s", empty($tag) ? '' : '_', $tag, $extension);

		return self::$tempFileStoragePath . DIRECTORY_SEPARATOR . $filename;
	}

	/**
	 * Resets the storage. This method removes all stored values.
	 *
	 * @param   null  $tag
	 *
	 * @return    bool    True on success
	 */
	public function reset($tag = null)
	{
		$filename = $this->get_storage_filename($tag);

		if (!is_file($filename) && !is_link($filename))
		{
			$filename = $this->get_storage_filename($tag, 'dat');
		}

		if (!is_file($filename) && !is_link($filename))
		{
			return false;
		}

		return @unlink($this->get_storage_filename($tag));
	}

	/**
	 * Stores a value to the storage
	 *
	 * @param   string       $value      Serialised value to store
	 * @param   string|null  $tag        Backup tag
	 * @param   string       $extension  File extension to use, default is php
	 *
	 * @return  bool  True on success
	 */
	public function set($value, $tag = null, $extension = 'php')
	{
		$storage_filename = $this->get_storage_filename($tag, $extension);

		if (file_exists($storage_filename))
		{
			@unlink($storage_filename);
		}

		$isPHPFile = strtolower($extension) == 'php';

		return @file_put_contents($storage_filename, $this->encode($value, $isPHPFile)) !== false;
	}

	/**
	 * Retrieves a value from storage
	 *
	 * @param   string|null  $tag  Backup tag. Used to determine the session state (memory) file name.
	 *
	 * @return  string|null
	 */
	public function &get($tag = null)
	{
		$ret              = null;
		$storage_filename = $this->get_storage_filename($tag);
		$isPHPFile        = true;
		$data             = @file_get_contents($storage_filename);

		/**
		 * Some hosts, like WPEngine, do not allow us to use .php files for storing the factory state. In these case we
		 * fall back to using the far less secure .dat extension. This if-block caters for that case.
		 */
		if ($data === false)
		{
			$storage_filename = $this->get_storage_filename($tag, 'dat');
			$isPHPFile        = false;
			$data             = @file_get_contents($storage_filename);
		}

		if ($data === false)
		{
			return $ret;
		}

		try
		{
			$ret = $this->decode($data, $isPHPFile);
		}
		catch (RuntimeException $e)
		{
			$ret = null;
		}

		unset($data);

		return $ret;
	}

	/**
	 * Encodes the (serialized) data in a format suitable for storing in a deliberately web-inaccessible PHP file.
	 *
	 * IMPORTANT: On some hosts we HAVE to fall back to a .dat file. This is nowhere near as secure. This is not a
	 * problem with Akeeba Backup but with the host, e.g. WPEngine. We WANT to do things securely but hosts' misguided
	 * attempts at "security" force us to have a very insecure fallback. Please do not report this as a security issue
	 * with us. report it to the host. We can't do something the host doesn't allow our code to do, obviously!
	 *
	 * @param   string  $data       The data to encode
	 * @param   bool    $isPHPFile  Is this file extension .php?
	 *
	 * @return  string  The encoded data
	 */
	public function encode(&$data, $isPHPFile = true)
	{
		$encodingMethod = $this->getEncodingMethod();
		
		switch ($encodingMethod)
		{
			case 'base64':
				$ret = base64_encode($data);
				break;

			case 'uuencode':
				$ret = convert_uuencode($data);
				break;

			case 'plain':
			default:
				$ret = $data;
				break;
		}

		if ($isPHPFile)
		{
			return '<' . '?' . 'php die(); ' . '>' . '?' . "\n" .
				$encodingMethod . "\n" . $ret;
		}

		return $encodingMethod . "\n" . $ret;
	}

	/**
	 * Decodes the data read from the deliberately web-inaccessible PHP file.
	 *
	 * @param   string  $data       The data read from the file
	 * @param   bool    $isPHPFile  Does the memory file have a .php extension?
	 *
	 * @return  false|string  The decoded data. False if the decoding failed.
	 */
	public function decode(&$data, $isPHPFile = true)
	{
		// Parts: 0 = PHP die line; 1 = encoding mode; 2 = data
		$parts = explode("\n", $data, 3);

		$expectedPartsCount = $isPHPFile ? 3 : 2;

		if (count($parts) != $expectedPartsCount)
		{
			throw new RuntimeException("Invalid backup temporary data (memory file)");
		}

		$encodingIndex = $isPHPFile ? 1 : 0;
		$dataIndex     = $isPHPFile ? 2 : 1;

		switch ($parts[$encodingIndex])
		{
			case 'base64';
				return base64_decode($parts[$dataIndex]);
				break;

			case 'uuencode':
				return convert_uudecode($parts[$dataIndex]);
				break;

			case 'plain':
				return $parts[$dataIndex];
				break;

			default:
				throw new RuntimeException(sprintf('Unsupported encoding method “%s”', $parts[$encodingIndex]));
				break;
		}
	}

	/**
	 * Get the recommended method for encoding the temporary data
	 *
	 * @return string
	 */
	protected function getEncodingMethod()
	{
		// Preferred encoding: base sixty four, handled by PHP
		if (function_exists('base64_encode') && function_exists('base64_decode'))
		{
			return 'base64';
		}

		// Fallback: UUencoding
		if (function_exists('convert_uuencode') && function_exists('convert_uudecode'))
		{
			return 'uuencode';
		}

		// Final fallback (should NOT be necessary): plain text encoding
		return 'plain';
	}
}
Util/Transfer/RemoteResourceInterface.php000060400000003151152456704520014550 0ustar00<?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\Util\Transfer;

defined('AKEEBAENGINE') || die();

/**
 * An interface for Transfer adapters which support remote resources, allowing us to efficient read from / write to
 * remote locations as if they were local files.
 */
interface RemoteResourceInterface
{
	/**
	 * Return a string with the appropriate stream wrapper protocol for $path. You can use the result with all PHP
	 * functions / classes which accept file paths such as DirectoryIterator, file_get_contents, file_put_contents,
	 * fopen etc.
	 *
	 * @param   string  $path
	 *
	 * @return  string
	 */
	public function getWrapperStringFor($path);

	/**
	 * Return the raw server listing for the requested folder.
	 *
	 * @param   string  $folder  The path name to list
	 *
	 * @return  string
	 */
	public function getRawList($folder);
}
Util/Transfer/Ftp.php000060400000041746152456704520010531 0ustar00<?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\Util\Transfer;

defined('AKEEBAENGINE') || die();

use Exception;
use RuntimeException;

/**
 * FTP transfer object, using PHP as the transport backend
 */
class Ftp implements TransferInterface, RemoteResourceInterface
{
	/**
	 * FTP server's hostname or IP address
	 *
	 * @var  string
	 */
	protected $host = 'localhost';

	/**
	 * FTP server's port, default: 21
	 *
	 * @var  integer
	 */
	protected $port = 21;

	/**
	 * Username used to authenticate to the FTP server
	 *
	 * @var  string
	 */
	protected $username = '';

	/**
	 * Password used to authenticate to the FTP server
	 *
	 * @var  string
	 */
	protected $password = '';

	/**
	 * FTP initial directory
	 *
	 * @var  string
	 */
	protected $directory = '/';

	/**
	 * Should I use SSL to connect to the server (FTP over explicit SSL, a.k.a. FTPS)?
	 *
	 * @var  boolean
	 */
	protected $ssl = false;

	/**
	 * Should I use FTP passive mode?
	 *
	 * @var bool
	 */
	protected $passive = true;

	/**
	 * Timeout for connecting to the FTP server, default: 10
	 *
	 * @var  integer
	 */
	protected $timeout = 10;

	/**
	 * The FTP connection handle
	 *
	 * @var  resource|null
	 */
	private $connection = null;

	/**
	 * Public constructor
	 *
	 * @param   array  $options  Configuration options
	 *
	 * @return  void
	 *
	 * @throws  RuntimeException
	 */
	public function __construct(array $options)
	{
		if (isset($options['host']))
		{
			$this->host = $options['host'];
		}

		if (isset($options['port']))
		{
			$this->port = (int) $options['port'];
		}

		if (isset($options['username']))
		{
			$this->username = $options['username'];
		}

		if (isset($options['password']))
		{
			$this->password = $options['password'];
		}

		if (isset($options['directory']))
		{
			$this->directory = '/' . ltrim(trim($options['directory']), '/');
		}

		if (isset($options['ssl']))
		{
			$this->ssl = $options['ssl'];
		}

		if (isset($options['passive']))
		{
			$this->passive = $options['passive'];
		}

		if (isset($options['timeout']))
		{
			$this->timeout = max(1, (int) $options['timeout']);
		}

		$this->connect();
	}

	/**
	 * Is this transfer method blocked by a server firewall?
	 *
	 * @param   array  $params  Any additional parameters you might need to pass
	 *
	 * @return  boolean  True if the firewall blocks connections to a known host
	 */
	public static function isFirewalled(array $params = [])
	{
		try
		{
			$connector = new static([
				'host'      => 'test.rebex.net',
				'port'      => 21,
				'username'  => 'demo',
				'password'  => 'password',
				'directory' => '',
				'ssl'       => $params['ssl'] ?? false,
				'passive'   => true,
				'timeout'   => 5,
			]);

			$data = $connector->read('readme.txt');

			if (empty($data))
			{
				return true;
			}
		}
		catch (Exception $e)
		{
			return true;
		}

		return false;
	}

	/**
	 * Save all parameters on serialization except the connection resource
	 *
	 * @return  array
	 */
	public function __sleep()
	{
		return ['host', 'port', 'username', 'password', 'directory', 'ssl', 'passive', 'timeout'];
	}

	/**
	 * Reconnect to the server on unserialize
	 *
	 * @return  void
	 */
	public function __wakeup()
	{
		$this->connect();
	}

	/**
	 * Connect to the FTP server
	 *
	 * @throws  RuntimeException
	 */
	public function connect()
	{
		// Try to connect to the server
		if ($this->ssl)
		{
			if (function_exists('ftp_ssl_connect'))
			{
				$this->connection = @ftp_ssl_connect($this->host, $this->port);
			}
			else
			{
				$this->connection = false;

				throw new RuntimeException('ftp_ssl_connect not available on this server', 500);
			}
		}
		else
		{
			$this->connection = @ftp_connect($this->host, $this->port, $this->timeout);
		}

		if ($this->connection === false)
		{
			throw new RuntimeException(sprintf('Cannot connect to FTP server [host:port] = %s:%s', $this->host, $this->port), 500);
		}

		// Attempt to authenticate
		if (!@ftp_login($this->connection, $this->username, $this->password))
		{
			@ftp_close($this->connection);
			$this->connection = null;

			throw new RuntimeException(sprintf('Cannot log in to FTP server [username:password] = %s:%s', $this->username, $this->password), 500);
		}

		// Attempt to change to the initial directory
		$defaultDir  = @ftp_pwd($this->connection) ?: '/';
		$directories = [
			$this->directory,
			rtrim($this->directory, '/'),
			trim($this->directory, '/'),
			$defaultDir,
		];

		foreach ($directories as $dir)
		{
			$changedDir = @ftp_chdir($this->connection, $dir);

			if ($changedDir)
			{
				$this->directory = $dir;

				break;
			}
		}

		if (!$changedDir)
		{
			@ftp_close($this->connection);
			$this->connection = null;

			throw new RuntimeException(sprintf('Cannot change to initial FTP directory "%s" – make sure the folder exists and that you have adequate permissions to it. Pro tip: the default directory of your FTP connection is reported to be %s', $this->directory, $defaultDir), 500);
		}

		// Apply the passive mode preference
		@ftp_pasv($this->connection, $this->passive);
	}

	/**
	 * Public destructor, closes any open FTP connections
	 */
	public function __destruct()
	{
		if (!is_null($this->connection))
		{
			@ftp_close($this->connection);
		}
	}

	/**
	 * Write the contents into the file
	 *
	 * @param   string  $fileName  The full path to the file
	 * @param   string  $contents  The contents to write to the file
	 *
	 * @return  boolean  True on success
	 */
	public function write($fileName, $contents)
	{
		// Make sure the buffer:// wrapper is loaded
		class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);

		$handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+');
		fwrite($handle, $contents);
		rewind($handle);

		$cwd            = $this->cwd();
		$remoteFilename = '/' . ltrim($fileName, '/');
		$remotePath     = dirname($remoteFilename);
		$remoteName     = basename($remoteFilename);

		if (!$this->isDir($remotePath))
		{
			$this->mkdir($remotePath);
		}

		$changedDir = @ftp_chdir($this->connection, $remotePath);

		$ret = $changedDir && @ftp_fput($this->connection, $remoteName, $handle, FTP_BINARY);

		if ($changedDir)
		{
			@ftp_chdir($this->connection, $cwd);
		}

		fclose($handle);

		return $ret;
	}

	/**
	 * Uploads a local file to the remote storage
	 *
	 * @param   string  $localFilename   The full path to the local file
	 * @param   string  $remoteFilename  The full path to the remote file
	 * @param   bool    $useExceptions   Throw an exception instead of returning "false" on connection error.
	 *
	 * @return  boolean  True on success
	 */
	public function upload($localFilename, $remoteFilename, $useExceptions = true)
	{
		$handle = @fopen($localFilename, 'r');

		if ($handle === false)
		{
			if ($useExceptions)
			{
				throw new RuntimeException("Unreadable local file $localFilename");
			}

			return false;
		}

		$cwd            = $this->cwd();
		$remoteFilename = '/' . ltrim($remoteFilename, '/');
		$remotePath     = dirname($remoteFilename);
		$remoteName     = basename($remoteFilename);

		if (!$this->isDir($remotePath))
		{
			$this->mkdir($remotePath);
		}

		$changedDir = @ftp_chdir($this->connection, $remotePath);
		$ret        = $changedDir && @ftp_fput($this->connection, $remoteName, $handle, FTP_BINARY);

		if ($changedDir)
		{
			@ftp_chdir($this->connection, $cwd);
		}

		@fclose($handle);

		return $ret;
	}

	/**
	 * Read the contents of a remote file into a string
	 *
	 * @param   string  $fileName  The full path to the remote file
	 *
	 * @return  string  The contents of the remote file
	 */
	public function read($fileName)
	{
		// Make sure the buffer:// wrapper is loaded
		class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);

		$handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+');

		$cwd            = $this->cwd();
		$remoteFilename = '/' . ltrim($fileName, '/');
		$remotePath     = dirname($remoteFilename);
		$remoteName     = basename($remoteFilename);
		$changedDir     = @ftp_chdir($this->connection, $remotePath);
		$result         = $changedDir && @ftp_fget($this->connection, $handle, $remoteName, FTP_BINARY);

		if ($changedDir)
		{
			@ftp_chdir($this->connection, $cwd);
		}

		if ($result === false)
		{
			fclose($handle);
			throw new RuntimeException("Can not download remote file $fileName");
		}

		rewind($handle);

		$ret = '';

		while (!feof($handle))
		{
			$ret .= fread($handle, 131072);
		}

		fclose($handle);

		return $ret;
	}

	/**
	 * Download a remote file into a local file
	 *
	 * @param   string  $remoteFilename  The remote file path to download from
	 * @param   string  $localFilename   The local file path to download to
	 * @param   bool    $useExceptions   Throw an exception instead of returning "false" on connection error.
	 *
	 * @return  boolean  True on success
	 */
	public function download($remoteFilename, $localFilename, $useExceptions = true)
	{
		$cwd            = $this->cwd();
		$remoteFilename = '/' . ltrim($remoteFilename, '/');
		$remotePath     = dirname($remoteFilename);
		$remoteName     = basename($remoteFilename);
		$changedDir     = @ftp_chdir($this->connection, $remotePath);

		$ret = $changedDir && @ftp_get($this->connection, $localFilename, $remoteName, FTP_BINARY);

		if ($changedDir)
		{
			@ftp_chdir($this->connection, $cwd);
		}

		if (!$ret && $useExceptions)
		{
			throw new RuntimeException("Cannot download remote file $remoteFilename through FTP.");
		}

		return $ret;
	}

	/**
	 * Delete a file (remove it from the disk)
	 *
	 * @param   string  $fileName  The full path to the file
	 *
	 * @return  boolean  True on success
	 */
	public function delete($fileName)
	{
		$cwd            = $this->cwd();
		$remoteFilename = '/' . ltrim($fileName, '/');
		$remotePath     = dirname($remoteFilename);
		$remoteName     = basename($remoteFilename);

		if (!$this->isDir($remotePath))
		{
			$this->mkdir($remotePath);
		}

		$changedDir = @ftp_chdir($this->connection, $remotePath);
		$ret        = $changedDir && @ftp_delete($this->connection, $remoteName);;

		if ($changedDir)
		{
			ftp_chdir($this->connection, $cwd);
		}

		return $ret;
	}

	/**
	 * Create a copy of the file. Actually, we have to read it in memory and upload it again.
	 *
	 * @param   string  $from  The full path of the file to copy from
	 * @param   string  $to    The full path of the file that will hold the copy
	 *
	 * @return  boolean  True on success
	 */
	public function copy($from, $to)
	{
		// Make sure the buffer:// wrapper is loaded
		class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);

		$handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+');

		$cwd            = $this->cwd();
		$remoteFilename = '/' . ltrim($from, '/');
		$remotePath     = dirname($remoteFilename);
		$remoteName     = basename($remoteFilename);
		$changedDir     = @ftp_chdir($this->connection, $remotePath);

		$ret = $changedDir && @ftp_fget($this->connection, $handle, $remoteName, FTP_BINARY);

		if ($ret !== false)
		{
			rewind($handle);

			$remoteFilename = '/' . ltrim($to, '/');
			$remotePath     = dirname($remoteFilename);
			$remoteName     = basename($remoteFilename);

			if (!$this->isDir($remotePath))
			{
				$this->mkdir($remotePath);
			}

			$changedDir = @ftp_chdir($this->connection, $remotePath);
			$ret        = $changedDir && @ftp_fput($this->connection, $remoteName, $handle, FTP_BINARY);
		}

		if ($changedDir)
		{
			@ftp_chdir($this->connection, $cwd);
		}

		fclose($handle);

		return $ret;
	}

	/**
	 * Move or rename a file
	 *
	 * @param   string  $from  The full path of the file to move
	 * @param   string  $to    The full path of the target file
	 *
	 * @return  boolean  True on success
	 */
	public function move($from, $to)
	{
		return @ftp_rename($this->connection, $from, $to);
	}

	/**
	 * Change the permissions of a file
	 *
	 * @param   string   $fileName     The full path of the file whose permissions will change
	 * @param   integer  $permissions  The new permissions, e.g. 0644 (remember the leading zero in octal numbers!)
	 *
	 * @return  boolean  True on success
	 */
	public function chmod($fileName, $permissions)
	{
		if (@ftp_chmod($this->connection, $permissions, $fileName) !== false)
		{
			return true;
		}

		$permissionsOctal = decoct((int) $permissions);

		if (@ftp_site($this->connection, "CHMOD $permissionsOctal $fileName") !== false)
		{
			return true;
		}

		return false;
	}

	/**
	 * Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all
	 * intermediate directories if they do not already exist.
	 *
	 * @param   string   $dirName      The full path of the directory to create
	 * @param   integer  $permissions  The permissions of the created directory
	 *
	 * @return  boolean  True on success
	 */
	public function mkdir($dirName, $permissions = 0755)
	{
		$targetDir = rtrim($dirName, '/');

		$directories = explode('/', $targetDir);

		$remoteDir = '';

		foreach ($directories as $dir)
		{
			if (!$dir)
			{
				continue;
			}

			$remoteDir .= '/' . $dir;

			// Continue if the folder already exists. Otherwise I'll get a an error even if everything is fine
			if ($this->isDir($remoteDir))
			{
				continue;
			}

			$ret = @ftp_mkdir($this->connection, $remoteDir);

			if ($ret === false)
			{
				return $ret;
			}
		}

		$this->chmod($dirName, $permissions);

		return true;
	}

	/**
	 * Checks if the given directory exists
	 *
	 * @param   string  $path  The full path of the remote directory to check
	 *
	 * @return  boolean  True if the directory exists
	 */
	public function isDir($path)
	{
		$cur_dir = ftp_pwd($this->connection);

		if (@ftp_chdir($this->connection, $path))
		{
			// If it is a directory, then change the directory back to the original directory
			ftp_chdir($this->connection, $cur_dir);

			return true;
		}
		else
		{
			return false;
		}
	}

	/**
	 * Get the current working directory
	 *
	 * @return  string
	 */
	public function cwd()
	{
		return ftp_pwd($this->connection);
	}

	/**
	 * Returns the absolute remote path from a path relative to the initial directory configured when creating the
	 * transfer object.
	 *
	 * @param   string  $fileName  The relative path of a file or directory
	 *
	 * @return  string  The absolute path for use by the transfer object
	 */
	public function getPath($fileName)
	{
		$fileName = str_replace('\\', '/', $fileName);

		if (strpos($fileName, $this->directory) === 0)
		{
			return $fileName;
		}

		$fileName = trim($fileName, '/');
		$fileName = rtrim($this->directory, '/') . '/' . $fileName;

		return $fileName;
	}

	/**
	 * Lists the subdirectories inside an FTP directory
	 *
	 * @param   null|string  $dir  The directory to scan. Skip to use the current directory.
	 *
	 * @return  array|bool  A list of folders, or false if we could not get a listing
	 *
	 * @throws  RuntimeException  When the server is incompatible with our FTP folder scanner
	 */
	public function listFolders($dir = null)
	{
		if (!@ftp_chdir($this->connection, $dir))
		{
			throw new RuntimeException(sprintf('Cannot change to FTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $dir), 500);
		}

		$list = @ftp_rawlist($this->connection, '.');

		if ($list === false)
		{
			throw new RuntimeException("Sorry, your FTP server doesn't support our FTP directory browser.");
		}

		$folders = [];

		foreach ($list as $v)
		{
			$vInfo = preg_split("/[\s]+/", $v, 9);

			if ($vInfo[0] !== "total")
			{
				$perms = $vInfo[0];

				if (substr($perms, 0, 1) == 'd')
				{
					$folders[] = $vInfo[8];
				}
			}
		}

		asort($folders);

		return $folders;
	}

	/**
	 * Return a string with the appropriate stream wrapper protocol for $path. You can use the result with all PHP
	 * functions / classes which accept file paths such as DirectoryIterator, file_get_contents, file_put_contents,
	 * fopen etc.
	 *
	 * @param   string  $path
	 *
	 * @return  string
	 */
	public function getWrapperStringFor($path)
	{
		$usernameEncoded = urlencode($this->username);
		$passwordEncoded = urlencode($this->password);
		$hostname        = $this->host . ($this->port ? ":{$this->port}" : '');
		$protocol        = $this->ssl ? "ftps" : "ftp";

		return "{$protocol}://{$usernameEncoded}:{$passwordEncoded}@{$hostname}{$path}";
	}

	/**
	 * Return the raw server listing for the requested folder.
	 *
	 * @param   string  $folder  The path name to list
	 *
	 * @return  string
	 */
	public function getRawList($folder)
	{
		return ftp_rawlist($this->connection, $folder);
	}
}
Util/Transfer/TransferInterface.php000060400000012544152456704520013377 0ustar00<?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\Util\Transfer;

defined('AKEEBAENGINE') || die();

use RuntimeException;

/**
 * An interface for Transfer adapters, used to transfer files to remote servers over FTP, FTPS, SFTP and possibly other
 * file transfer methods we might implement.
 *
 * @package   Akeeba\Engine\Util\Transfer
 */
interface TransferInterface
{
	/**
	 * Creates the uploader
	 *
	 * @param   array  $config
	 */
	public function __construct(array $config);

	/**
	 * Is this transfer method blocked by a server firewall?
	 *
	 * @param   array  $params  Any additional parameters you might need to pass
	 *
	 * @return  boolean  True if the firewall blocks connections to a known host
	 */
	public static function isFirewalled(array $params = []);

	/**
	 * Write the contents into the file
	 *
	 * @param   string  $fileName  The full path to the remote file
	 * @param   string  $contents  The contents to write to the file
	 *
	 * @return  boolean  True on success
	 */
	public function write($fileName, $contents);

	/**
	 * Uploads a local file to the remote storage
	 *
	 * @param   string  $localFilename   The full path to the local file
	 * @param   string  $remoteFilename  The full path to the remote file
	 * @param   bool    $useExceptions   Throw an exception instead of returning "false" on connection error.
	 *
	 * @return  boolean  True on success
	 */
	public function upload($localFilename, $remoteFilename, $useExceptions = true);

	/**
	 * Read the contents of a remote file into a string
	 *
	 * @param   string  $fileName  The full path to the remote file
	 *
	 * @return  string  The contents of the remote file
	 */
	public function read($fileName);

	/**
	 * Download a remote file into a local file
	 *
	 * @param   string  $remoteFilename  The remote file path to download from
	 * @param   string  $localFilename   The local file path to download to
	 * @param   bool    $useExceptions   Throw an exception instead of returning "false" on connection error.
	 *
	 * @return  boolean  True on success
	 */
	public function download($remoteFilename, $localFilename, $useExceptions = true);

	/**
	 * Delete a remote file
	 *
	 * @param   string  $fileName  The full path to the remote file
	 *
	 * @return  boolean  True on success
	 */
	public function delete($fileName);

	/**
	 * Create a copy of the remote file
	 *
	 * @param   string  $from  The full path of the remote file to copy from
	 * @param   string  $to    The full path of the remote file that will hold the copy
	 *
	 * @return  boolean  True on success
	 */
	public function copy($from, $to);

	/**
	 * Move or rename a file
	 *
	 * @param   string  $from  The full remote path of the file to move
	 * @param   string  $to    The full remote path of the target file
	 *
	 * @return  boolean  True on success
	 */
	public function move($from, $to);

	/**
	 * Change the permissions of a file
	 *
	 * @param   string   $fileName     The full path of the remote file whose permissions will change
	 * @param   integer  $permissions  The new permissions, e.g. 0644 (remember the leading zero in octal numbers!)
	 *
	 * @return  boolean  True on success
	 */
	public function chmod($fileName, $permissions);

	/**
	 * Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all
	 * intermediate directories if they do not already exist.
	 *
	 * @param   string   $dirName      The full path of the remote directory to create
	 * @param   integer  $permissions  The permissions of the created directory
	 *
	 * @return  boolean  True on success
	 */
	public function mkdir($dirName, $permissions = 0755);

	/**
	 * Checks if the given directory exists
	 *
	 * @param   string  $path  The full path of the remote directory to check
	 *
	 * @return  boolean  True if the directory exists
	 */
	public function isDir($path);

	/**
	 * Get the current working directory
	 *
	 * @return  string
	 */
	public function cwd();

	/**
	 * Returns the absolute remote path from a path relative to the initial directory configured when creating the
	 * transfer object.
	 *
	 * @param   string  $fileName  The relative path of a file or directory
	 *
	 * @return  string  The absolute path for use by the transfer object
	 */
	public function getPath($fileName);

	/**
	 * Lists the subdirectories inside a directory
	 *
	 * @param   null|string  $dir  The directory to scan. Skip to use the current directory.
	 *
	 * @return  array|bool  A list of folders, or false if we could not get a listing
	 *
	 * @throws  RuntimeException  When the server is incompatible with our folder scanner
	 */
	public function listFolders($dir = null);
}
Util/Transfer/FtpCurl.php000060400000046377152456704520011364 0ustar00<?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\Util\Transfer;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Postproc\ProxyAware;
use RuntimeException;

/**
 * FTP transfer object, using cURL as the transport backend
 */
class FtpCurl extends Ftp implements TransferInterface
{
	use ProxyAware;

	/**
	 * Timeout for transferring data to the FTP server, default: 10 minutes
	 *
	 * @var  integer
	 */
	protected $timeout = 600;

	/**
	 * Should I ignore the IP returned by the server during Passive mode transfers?
	 *
	 * @var   bool
	 *
	 * @see   http://www.elitehosts.com/blog/php-ftp-passive-ftp-server-behind-nat-nightmare/
	 */
	private $skipPassiveIP = true;

	/**
	 * Should we enable verbose output to STDOUT? Useful for debugging.
	 *
	 * @var   bool
	 */
	private $verbose = false;

	/**
	 * Public constructor
	 *
	 * @param   array  $options  Configuration options
	 *
	 * @throws  RuntimeException
	 */
	public function __construct(array $options)
	{
		parent::__construct($options);

		if (isset($options['passive_fix']))
		{
			$this->skipPassiveIP = $options['passive_fix'] ? true : false;
		}

		if (isset($options['verbose']))
		{
			$this->verbose = $options['verbose'] ? true : false;
		}
	}

	/**
	 * Save all parameters on serialization except the connection resource
	 *
	 * @return  array
	 */
	public function __sleep()
	{
		return [
			'host',
			'port',
			'username',
			'password',
			'directory',
			'ssl',
			'passive',
			'timeout',
			'skipPassiveIP',
			'verbose',
		];
	}

	/**
	 * Test the connection to the FTP server and whether the initial directory is correct. This is done by attempting to
	 * list the contents of the initial directory. The listing is not parsed (we don't really care!) and we do NOT check
	 * if we can upload files to that remote folder.
	 *
	 * @throws  RuntimeException
	 */
	public function connect()
	{
		$ch = $this->getCurlHandle($this->directory . '/');
		curl_setopt($ch, CURLOPT_HEADER, 1);
		curl_setopt($ch, CURLOPT_NOBODY, 1);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

		curl_exec($ch);

		$errNo = curl_errno($ch);
		$error = curl_error($ch);
		curl_close($ch);

		if ($errNo)
		{
			throw new RuntimeException("cURL Error $errNo connecting to remote FTP server: $error", 500);
		}
	}

	/**
	 * Write the contents into the file
	 *
	 * @param   string  $fileName  The full path to the file
	 * @param   string  $contents  The contents to write to the file
	 *
	 * @return  boolean  True on success
	 */
	public function write($fileName, $contents)
	{
		// Make sure the buffer:// wrapper is loaded
		class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);

		$handle = fopen('buffer://akeeba_engine_transfer_ftp_curl', 'r+');
		fwrite($handle, $contents);

		// Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle
		try
		{
			$this->uploadFromHandle($fileName, $handle);
		}
		catch (RuntimeException $e)
		{
			return false;
		}

		return true;
	}

	/**
	 * Uploads a local file to the remote storage
	 *
	 * @param   string  $localFilename   The full path to the local file
	 * @param   string  $remoteFilename  The full path to the remote file
	 * @param   bool    $useExceptions   Throw an exception instead of returning "false" on connection error.
	 *
	 * @return  boolean  True on success
	 */
	public function upload($localFilename, $remoteFilename, $useExceptions = true)
	{
		$fp = @fopen($localFilename, 'r');

		if ($fp === false)
		{
			throw new RuntimeException("Unreadable local file $localFilename");
		}

		// Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle
		try
		{
			$this->uploadFromHandle($remoteFilename, $fp);
		}
		catch (RuntimeException $e)
		{
			if ($useExceptions)
			{
				throw $e;
			}

			return false;
		}

		return true;
	}

	/**
	 * Read the contents of a remote file into a string
	 *
	 * @param   string  $fileName  The full path to the remote file
	 *
	 * @return  string  The contents of the remote file
	 */
	public function read($fileName)
	{
		try
		{
			return $this->downloadToString($fileName);
		}
		catch (RuntimeException $e)
		{
			throw new RuntimeException("Can not download remote file $fileName", 500, $e);
		}
	}

	/**
	 * Download a remote file into a local file
	 *
	 * @param   string  $remoteFilename  The remote file path to download from
	 * @param   string  $localFilename   The local file path to download to
	 * @param   bool    $useExceptions   Throw an exception instead of returning "false" on connection error.
	 *
	 * @return  boolean  True on success
	 */
	public function download($remoteFilename, $localFilename, $useExceptions = true)
	{
		$fp = @fopen($localFilename, 'w');

		if ($fp === false)
		{
			if ($useExceptions)
			{
				throw new RuntimeException(sprintf('Download from FTP failed. Can not open local file %s for writing.', $localFilename));
			}

			return false;
		}

		// Note: don't manually close the file pointer, it's closed automatically by downloadToHandle
		try
		{
			$this->downloadToHandle($remoteFilename, $fp);
		}
		catch (RuntimeException $e)
		{
			if ($useExceptions)
			{
				throw $e;
			}

			return false;
		}

		return true;
	}

	/**
	 * Delete a file (remove it from the disk)
	 *
	 * @param   string  $fileName  The full path to the file
	 *
	 * @return  boolean  True on success
	 */
	public function delete($fileName)
	{
		$commands = [
			'DELE ' . $this->getPath($fileName),
		];

		try
		{
			$this->executeServerCommands($commands);
		}
		catch (RuntimeException $e)
		{
			return false;
		}

		return true;
	}

	/**
	 * Create a copy of the file. Actually, we have to read it in memory and upload it again.
	 *
	 * @param   string  $from  The full path of the file to copy from
	 * @param   string  $to    The full path of the file that will hold the copy
	 *
	 * @return  boolean  True on success
	 */
	public function copy($from, $to)
	{
		// Make sure the buffer:// wrapper is loaded
		class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);

		$handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+');

		try
		{
			$this->downloadToHandle($from, $handle, false);
			$this->uploadFromHandle($to, $handle);
		}
		catch (RuntimeException $e)
		{
			return false;
		}

		return true;
	}

	/**
	 * Move or rename a file
	 *
	 * @param   string  $from  The full path of the file to move
	 * @param   string  $to    The full path of the target file
	 *
	 * @return  boolean  True on success
	 */
	public function move($from, $to)
	{
		$from = $this->getPath($from);
		$to   = $this->getPath($to);

		$commands = [
			'RNFR /' . $from,
			'RNTO /' . $to,
		];

		try
		{
			$this->executeServerCommands($commands);
		}
		catch (RuntimeException $e)
		{
			return false;
		}

		return true;
	}

	/**
	 * Change the permissions of a file
	 *
	 * @param   string   $fileName     The full path of the file whose permissions will change
	 * @param   integer  $permissions  The new permissions, e.g. 0644 (remember the leading zero in octal numbers!)
	 *
	 * @return  boolean  True on success
	 */
	public function chmod($fileName, $permissions)
	{
		// Make sure permissions are in an octal string representation
		if (!is_string($permissions))
		{
			$permissions = decoct($permissions);
		}

		$commands = [
			'SITE CHMOD ' . $permissions . ' /' . $this->getPath($fileName),
		];

		try
		{
			$this->executeServerCommands($commands);
		}
		catch (RuntimeException $e)
		{
			return false;
		}

		return true;
	}

	/**
	 * Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all
	 * intermediate directories if they do not already exist.
	 *
	 * @param   string   $dirName      The full path of the directory to create
	 * @param   integer  $permissions  The permissions of the created directory
	 *
	 * @return  boolean  True on success
	 */
	public function mkdir($dirName, $permissions = 0755)
	{
		$targetDir = rtrim($dirName, '/');

		$directories = explode('/', $targetDir);

		$remoteDir = '';

		foreach ($directories as $dir)
		{
			if (!$dir)
			{
				continue;
			}

			$remoteDir .= '/' . $dir;

			// Continue if the folder already exists. Otherwise I'll get a an error even if everything is fine
			if ($this->isDir($remoteDir))
			{
				continue;
			}

			$commands = [
				'MKD ' . $remoteDir,
			];

			try
			{
				$this->executeServerCommands($commands);
			}
			catch (RuntimeException $e)
			{
				return false;
			}
		}

		$this->chmod($dirName, $permissions);

		return true;
	}

	/**
	 * Checks if the given directory exists
	 *
	 * @param   string  $path  The full path of the remote directory to check
	 *
	 * @return  boolean  True if the directory exists
	 */
	public function isDir($path)
	{
		$ch = $this->getCurlHandle($path . '/');
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

		curl_exec($ch);

		$errNo = curl_errno($ch);
		curl_close($ch);

		if ($errNo)
		{
			return false;
		}

		return true;
	}

	/**
	 * Get the current working directory. NOT IMPLEMENTED.
	 *
	 * @return  string
	 */
	public function cwd()
	{
		$commands = [
			'PWD',
		];

		try
		{
			$result = $this->executeServerCommands($commands, '');
		}
		catch (RuntimeException $e)
		{
			return '/';
		}

		return $result;
	}

	/**
	 * Lists the subdirectories inside an FTP directory
	 *
	 * @param   null|string  $dir  The directory to scan. Skip to use the current directory.
	 *
	 * @return  array|bool   A list of folders, or false if we could not get a listing
	 *
	 * @throws  RuntimeException  When the server is incompatible with our FTP folder scanner
	 */
	public function listFolders($dir = null)
	{
		if (empty($dir))
		{
			$dir = $this->directory;
		}

		$dir = rtrim($dir, '/');

		$ch = $this->getCurlHandle($dir . '/');
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

		$list = curl_exec($ch);

		$errNo = curl_errno($ch);
		$error = curl_error($ch);
		curl_close($ch);

		if ($errNo)
		{
			throw new RuntimeException(sprintf("cURL Error $errNo ($error) while listing contents of directory \"%s\" – make sure the folder exists and that you have adequate permissions to it", $dir), 500);
		}

		if (empty($list))
		{
			throw new RuntimeException("Sorry, your FTP server doesn't support our FTP directory browser.");
		}

		$folders = [];

		// Convert the directory listing into an array of lines without *NIX/Windows/Mac line ending characters
		$list = explode("\n", $list);
		$list = array_map('rtrim', $list);

		foreach ($list as $v)
		{
			$vInfo = preg_split("/[\s]+/", $v, 9);

			if ($vInfo[0] !== "total")
			{
				$perms = $vInfo[0];

				if (substr($perms, 0, 1) == 'd')
				{
					$folders[] = $vInfo[8];
				}
			}
		}

		asort($folders);

		return $folders;
	}

	/**
	 * Is the verbose debug option set?
	 *
	 * @return  boolean
	 */
	public function isVerbose()
	{
		return $this->verbose;
	}

	/**
	 * Set the verbose debug option
	 *
	 * @param   boolean  $verbose
	 *
	 * @return  void
	 */
	public function setVerbose($verbose)
	{
		$this->verbose = $verbose;
	}

	/**
	 * Returns the absolute remote path from a path relative to the initial directory configured when creating the
	 * transfer object.
	 *
	 * @param   string  $fileName  The relative path of a file or directory
	 *
	 * @return  string  The absolute path for use by the transfer object
	 */
	public function getPath($fileName)
	{
		$fileName = ltrim(str_replace('\\', '/', $fileName), '/');

		$isInitialDirectory = $fileName === $this->directory;
		$startsWithInitialDirectory = (strpos($fileName, rtrim($this->directory, '/') . '/') === 0)
			|| (strpos($fileName, trim($this->directory, '/') . '/') === 0);

		// Relative file? Add the initial directory
		if (!$isInitialDirectory && !$startsWithInitialDirectory)
		{
			$fileName = '/' . trim($this->directory, '/') .
				(empty($fileName) ? '' : '/') . $fileName;
		}

		return '/' . ltrim($fileName, '/');
	}

	/**
	 * Returns a cURL resource handler for the remote FTP server
	 *
	 * @param   string  $remoteFile  Optional. The remote file / folder on the FTP server you'll be manipulating with cURL.
	 *
	 * @return  resource
	 */
	protected function getCurlHandle($remoteFile = '')
	{
		/**
		 * Get the FTP URI
		 *
		 * VERY IMPORTANT! WE NEED THE DOUBLE SLASH AFTER THE HOST NAME since we are giving an absolute path.
		 * @see https://technicalsanctuary.wordpress.com/2012/11/01/curl-curl-9-server-denied-you-to-change-to-the-given-directory/
		 */
		$ftpUri = 'ftp://' . $this->host . '//';

		$isInitialDirectory = $remoteFile === $this->directory;
		$startsWithInitialDirectory = (strpos($remoteFile, rtrim($this->directory, '/') . '/') === 0)
			|| (strpos($remoteFile, trim($this->directory, '/') . '/') === 0);

		// Relative file? Add the initial directory
		if (!$isInitialDirectory && !$startsWithInitialDirectory)
		{
			$ftpUri .= '/' . trim($this->directory, '/');
		}

		if (!empty($remoteFile) && substr($ftpUri, -2) !== '//')
		{
			$ftpUri .= '/';
		}

		// Add a remote file if necessary. The filename must be URL encoded since we're creating a URI.
		if (!empty($remoteFile))
		{
			$suffix = '';

			if (substr($remoteFile, -7, 6) == ';type=')
			{
				$suffix     = substr($remoteFile, -7);
				$remoteFile = substr($remoteFile, 0, -7);
			}

			$dirname = dirname($remoteFile);

			// Windows messing up dirname('/'). KILL ME.
			if ($dirname == '\\')
			{
				$dirname = '';
			}

			$dirname  = trim($dirname, '/');
			$basename = basename($remoteFile);

			if ((substr($remoteFile, -1) == '/') && !empty($basename))
			{
				$suffix = '/' . $suffix;
			}

			$ftpUri .= '/' . $dirname . (empty($dirname) ? '' : '/') . urlencode($basename) . $suffix;
		}

		// Colons in usernames must be URL escaped
		$username = str_replace(':', '%3A', $this->username);

		$ch = curl_init();

		$this->applyProxySettingsToCurl($ch);

		curl_setopt($ch, CURLOPT_URL, $ftpUri);
		curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $this->password);
		curl_setopt($ch, CURLOPT_PORT, $this->port);
		curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);

		// Should I enable Implict SSL?
		if ($this->ssl)
		{
			curl_setopt($ch, CURLOPT_FTP_SSL, CURLFTPSSL_ALL);
			curl_setopt($ch, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_DEFAULT);

			// Most FTPS servers use self-signed certificates. That's the only way to connect to them :(
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
		}

		// Should I ignore the server-supplied passive mode IP address?
		if ($this->passive && $this->skipPassiveIP)
		{
			curl_setopt($ch, CURLOPT_FTP_SKIP_PASV_IP, 1);
		}

		// Should I enable active mode?
		if (!$this->passive)
		{
			/**
			 * cURL always uses passive mode for FTP transfers. Setting the CURLOPT_FTPPORT flag enables the FTP PORT
			 * command which makes the connection active. Setting it to '-'  lets the library use your system's default
			 * IP address.
			 *
			 * @see https://curl.haxx.se/libcurl/c/CURLOPT_FTPPORT.html
			 */
			curl_setopt($ch, CURLOPT_FTPPORT, '-');
		}

		// Should I enable verbose output? Useful for debugging.
		if ($this->verbose)
		{
			curl_setopt($ch, CURLOPT_VERBOSE, 1);
		}

		// Automatically create missing directories
		curl_setopt($ch, CURLOPT_FTP_CREATE_MISSING_DIRS, 1);

		return $ch;
	}

	/**
	 * Uploads a file using file contents provided through a file handle
	 *
	 * @param   string    $remoteFilename  Remote file to write contents to
	 * @param   resource  $fp              File or stream handler of the source data to upload
	 *
	 * @return  void
	 *
	 * @throws  RuntimeException
	 */
	protected function uploadFromHandle($remoteFilename, $fp)
	{
		// We need the file size. We can do that by getting the file position at EOF
		fseek($fp, 0, SEEK_END);
		$filesize = ftell($fp);
		rewind($fp);

		/**
		 * The ;type=i suffix forces Binary file transfer mode
		 *
		 * @see  https://curl.haxx.se/mail/archive-2008-05/0089.html
		 */
		$ch = $this->getCurlHandle($remoteFilename . ';type=i');
		curl_setopt($ch, CURLOPT_UPLOAD, 1);
		curl_setopt($ch, CURLOPT_INFILE, $fp);
		curl_setopt($ch, CURLOPT_INFILESIZE, $filesize);

		curl_exec($ch);

		$error_no = curl_errno($ch);
		$error    = curl_error($ch);

		curl_close($ch);
		fclose($fp);

		if ($error_no)
		{
			throw new RuntimeException($error, $error_no);
		}
	}

	/**
	 * Downloads a remote file to the provided file handle
	 *
	 * @param   string    $remoteFilename  Filename on the remote server
	 * @param   resource  $fp              File handle where the downloaded content will be written to
	 * @param   bool      $close           Optional. Should I close the file handle when I'm done? (Default: true)
	 *
	 * @return  void
	 *
	 * @throws  RuntimeException
	 */
	protected function downloadToHandle($remoteFilename, $fp, $close = true)
	{
		/**
		 * The ;type=i suffix forces Binary file transfer mode
		 *
		 * @see  https://curl.haxx.se/mail/archive-2008-05/0089.html
		 */
		$ch = $this->getCurlHandle($remoteFilename . ';type=i');

		curl_setopt($ch, CURLOPT_FILE, $fp);

		curl_exec($ch);

		$error_no = curl_errno($ch);
		$error    = curl_error($ch);

		curl_close($ch);

		if ($close)
		{
			fclose($fp);
		}

		if ($error_no)
		{
			throw new RuntimeException($error, $error_no);
		}
	}

	/**
	 * Downloads a remote file and returns it as a string
	 *
	 * @param   string  $remoteFilename  Filename on the remote server
	 *
	 * @return  string
	 *
	 * @throws  RuntimeException
	 */
	protected function downloadToString($remoteFilename)
	{
		/**
		 * The ;type=i suffix forces Binary file transfer mode
		 *
		 * @see  https://curl.haxx.se/mail/archive-2008-05/0089.html
		 */
		$ch = $this->getCurlHandle($remoteFilename . ';type=i');

		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_HEADER, false);

		$ret = curl_exec($ch);

		$error_no = curl_errno($ch);
		$error    = curl_error($ch);

		curl_close($ch);

		if ($error_no)
		{
			throw new RuntimeException($error, $error_no);
		}

		return $ret;
	}

	/**
	 * Executes arbitrary FTP commands
	 *
	 * @param   string[]     $commands    An array with the FTP commands to be executed
	 * @param   string|null  $remoteFile  The remote file / folder to use in the cURL URI when executing the commands
	 *
	 * @return  string  The output of the executed commands
	 *
	 */
	protected function executeServerCommands(array $commands, ?string $remoteFile = null): string
	{
		$remoteFile = $remoteFile ?? ($this->directory . '/');

		$ch = $this->getCurlHandle($remoteFile);

		curl_setopt($ch, CURLOPT_QUOTE, $commands);
		curl_setopt($ch, CURLOPT_HEADER, 1);
		curl_setopt($ch, CURLOPT_NOBODY, 1);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

		$listing = curl_exec($ch);
		$errNo   = curl_errno($ch);
		$error   = curl_error($ch);
		curl_close($ch);

		if ($errNo)
		{
			throw new RuntimeException($error, $errNo);
		}

		return $listing;
	}
}
Util/Transfer/Sftp.php000060400000035342152456704520010707 0ustar00<?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\Util\Transfer;

defined('AKEEBAENGINE') || die();

use DirectoryIterator;
use Exception;
use RuntimeException;

/**
 * SFTP transfer object
 */
class Sftp implements TransferInterface, RemoteResourceInterface
{
	/**
	 * SFTP server's hostname or IP address
	 *
	 * @var  string
	 */
	private $host = 'localhost';

	/**
	 * SFTP server's port, default: 21
	 *
	 * @var  integer
	 */
	private $port = 22;

	/**
	 * Username used to authenticate to the SFTP server
	 *
	 * @var  string
	 */
	private $username = '';

	/**
	 * Password used to authenticate to the SFTP server
	 *
	 * @var  string
	 */
	private $password = '';

	/**
	 * SFTP initial directory
	 *
	 * @var  string
	 */
	private $directory = '/';

	/**
	 * The absolute filesystem path to a private key file used for authentication instead of a password.
	 *
	 * @var  string
	 */
	private $privateKey = '';

	/**
	 * The absolute filesystem path to a public key file used for authentication instead of a password.
	 *
	 * @var  string
	 */
	private $publicKey = '';

	/**
	 * The SSH2 connection handle
	 *
	 * @var  resource|null
	 */
	private $connection = null;

	/**
	 * The SFTP connection handle
	 *
	 * @var  resource|null
	 */
	private $sftpHandle = null;

	/**
	 * Public constructor
	 *
	 * @param   array  $options  Configuration options for the filesystem abstraction object
	 *
	 * @return  Sftp
	 *
	 * @throws  RuntimeException
	 */
	public function __construct(array $options)
	{
		if (isset($options['host']))
		{
			$this->host = $options['host'];
		}

		if (isset($options['port']))
		{
			$this->port = (int) $options['port'];
		}

		if (isset($options['username']))
		{
			$this->username = $options['username'];
		}

		if (isset($options['password']))
		{
			$this->password = $options['password'];
		}

		if (isset($options['directory']))
		{
			$this->directory = '/' . ltrim(trim($options['directory']), '/');
		}

		if (isset($options['privateKey']))
		{
			$this->privateKey = $options['privateKey'];
		}

		if (isset($options['publicKey']))
		{
			$this->publicKey = $options['publicKey'];
		}

		$this->connect();
	}

	/**
	 * Is this transfer method blocked by a server firewall?
	 *
	 * @param   array  $params  Any additional parameters you might need to pass
	 *
	 * @return  boolean  True if the firewall blocks connections to a known host
	 */
	public static function isFirewalled(array $params = [])
	{
		try
		{
			$connector = new static([
				'host'      => 'test.rebex.net',
				'port'      => 22,
				'username'  => 'demo',
				'password'  => 'password',
				'directory' => '',
			]);

			$data = $connector->read('readme.txt');

			if (empty($data))
			{
				return true;
			}
		}
		catch (Exception $e)
		{
			return true;
		}

		return false;
	}

	/**
	 * Save all parameters on serialization except the connection resource
	 *
	 * @return  array
	 */
	public function __sleep()
	{
		return ['host', 'port', 'username', 'password', 'directory', 'privateKey', 'publicKey'];
	}

	/**
	 * Reconnect to the server on unserialize
	 *
	 * @return  void
	 */
	public function __wakeup()
	{
		$this->connect();
	}

	public function __destruct()
	{
		if (is_resource($this->connection))
		{
			@ssh2_exec($this->connection, 'exit;');
			$this->connection = null;
			$this->sftpHandle = null;
		}
	}

	/**
	 * Connect to the FTP server
	 *
	 * @throws  RuntimeException
	 */
	public function connect()
	{
		// Try to connect to the SSH server
		if (!function_exists('ssh2_connect'))
		{
			throw new RuntimeException('Your web server does not have the SSH2 PHP module, therefore can not connect to SFTP servers.', 500);
		}

		$this->connection = ssh2_connect($this->host, $this->port);

		if ($this->connection === false)
		{
			$this->connection = null;

			throw new RuntimeException(sprintf('Cannot connect to SFTP server [host:port] = %s:%s', $this->host, $this->port), 500);
		}

		// Attempt to authenticate
		if (!empty($this->publicKey) && !empty($this->privateKey))
		{
			if (!@ssh2_auth_pubkey_file($this->connection, $this->username, $this->publicKey, $this->privateKey, $this->password))
			{
				$this->connection = null;

				throw new RuntimeException(sprintf('Cannot log in to SFTP server using key files [username:private_key_file:public_key_file:password] = %s:%s:%s:%s', $this->username, $this->privateKey, $this->publicKey, $this->password), 500);
			}
		}
		else
		{
			if (!@ssh2_auth_password($this->connection, $this->username, $this->password))
			{
				$this->connection = null;

				throw new RuntimeException(sprintf('Cannot log in to SFTP server [username:password] = %s:%s', $this->username, $this->password), 500);
			}
		}

		// Get an SFTP handle
		$this->sftpHandle = ssh2_sftp($this->connection);

		if ($this->sftpHandle === false)
		{
			throw new RuntimeException('Cannot start an SFTP session with the server', 500);
		}
	}

	/**
	 * Write the contents into the file
	 *
	 * @param   string  $fileName  The full path to the file
	 * @param   string  $contents  The contents to write to the file
	 *
	 * @return  boolean  True on success
	 */
	public function write($fileName, $contents)
	{
		$fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$fileName", 'w');

		if ($fp === false)
		{
			return false;
		}

		$ret = @fwrite($fp, $contents);

		@fclose($fp);

		return $ret;
	}

	/**
	 * Uploads a local file to the remote storage
	 *
	 * @param   string  $localFilename   The full path to the local file
	 * @param   string  $remoteFilename  The full path to the remote file
	 * @param   bool    $useExceptions   Throw an exception instead of returning "false" on connection error.
	 *
	 * @return  boolean  True on success
	 */
	public function upload($localFilename, $remoteFilename, $useExceptions = true)
	{
		$fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$remoteFilename", 'w');

		if ($fp === false)
		{
			if ($useExceptions)
			{
				throw new RuntimeException("Could not open remote SFTP file $remoteFilename for writing");
			}

			return false;
		}

		$localFp = @fopen($localFilename, 'r');

		if ($localFp === false)
		{
			fclose($fp);

			if ($useExceptions)
			{
				throw new RuntimeException("Could not open local file $localFilename for reading");
			}

			return false;
		}

		while (!feof($localFp))
		{
			$data = fread($localFp, 131072);
			$ret  = @fwrite($fp, $data);

			if ($ret < strlen($data))
			{
				fclose($fp);
				fclose($localFp);

				if ($useExceptions)
				{
					throw new RuntimeException("An error occurred while copying file $localFilename to $remoteFilename");
				}

				return false;
			}
		}

		@fclose($fp);
		@fclose($localFp);

		return true;
	}

	/**
	 * Read the contents of a remote file into a string
	 *
	 * @param   string  $fileName  The full path to the remote file
	 *
	 * @return  string  The contents of the remote file
	 */
	public function read($fileName)
	{
		$fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$fileName", 'r');

		if ($fp === false)
		{
			throw new RuntimeException("Can not download remote file $fileName");
		}

		$ret = '';

		while (!feof($fp))
		{
			$ret .= fread($fp, 131072);
		}

		@fclose($fp);

		return $ret;
	}

	/**
	 * Download a remote file into a local file
	 *
	 * @param   string  $remoteFilename  The remote file path to download from
	 * @param   string  $localFilename   The local file path to download to
	 * @param   bool    $useExceptions   Throw an exception instead of returning "false" on connection error.
	 *
	 * @return  boolean  True on success
	 */
	public function download($remoteFilename, $localFilename, $useExceptions = true)
	{
		$fp = @fopen("ssh2.sftp://{$this->sftpHandle}/$remoteFilename", 'r');

		if ($fp === false)
		{
			if ($useExceptions)
			{
				throw new RuntimeException("Could not open remote SFTP file $remoteFilename for reading");
			}

			return false;
		}

		$localFp = @fopen($localFilename, 'w');

		if ($localFp === false)
		{
			fclose($fp);

			if ($useExceptions)
			{
				throw new RuntimeException("Could not open local file $localFilename for writing");
			}

			return false;
		}

		while (!feof($fp))
		{
			$chunk = fread($fp, 131072);

			if ($chunk === false)
			{
				fclose($fp);
				fclose($localFp);

				if ($useExceptions)
				{
					throw new RuntimeException("An error occurred while copying file $remoteFilename to $localFilename");
				}

				return false;
			}

			fwrite($localFp, $chunk);
		}

		@fclose($fp);
		@fclose($localFp);

		return true;
	}

	/**
	 * Delete a file (remove it from the disk)
	 *
	 * @param   string  $fileName  The full path to the file
	 *
	 * @return  boolean  True on success
	 */
	public function delete($fileName)
	{
		try
		{
			$ret = @ssh2_sftp_unlink($this->sftpHandle, $fileName);
		}
		catch (Exception $e)
		{
			$ret = false;
		}

		return $ret;
	}

	/**
	 * Create a copy of the file. Actually, we have to read it in memory and upload it again.
	 *
	 * @param   string  $from  The full path of the file to copy from
	 * @param   string  $to    The full path of the file that will hold the copy
	 *
	 * @return  boolean  True on success
	 */
	public function copy($from, $to)
	{
		$contents = @file_get_contents($from);

		return $this->write($to, $contents);
	}

	/**
	 * Move or rename a file. Actually, we have to read it, upload it again and then delete the original.
	 *
	 * @param   string  $from  The full path of the file to move
	 * @param   string  $to    The full path of the target file
	 *
	 * @return  boolean  True on success
	 */
	public function move($from, $to)
	{
		$ret = $this->copy($from, $to);

		if ($ret)
		{
			$ret = $this->delete($from);
		}

		return $ret;
	}

	/**
	 * Change the permissions of a file
	 *
	 * @param   string   $fileName     The full path of the file whose permissions will change
	 * @param   integer  $permissions  The new permissions, e.g. 0644 (remember the leading zero in octal numbers!)
	 *
	 * @return  boolean  True on success
	 */
	public function chmod($fileName, $permissions)
	{
		// Prefer the SFTP way, if available
		if (function_exists('ssh2_sftp_chmod'))
		{
			return @ssh2_sftp_chmod($this->sftpHandle, $fileName, $permissions);
		}
		// Otherwise fall back to the (likely to fail) raw command mode
		else
		{
			$cmd = 'chmod ' . decoct($permissions) . ' ' . escapeshellarg($fileName);

			return @ssh2_exec($this->connection, $cmd);
		}
	}

	/**
	 * Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all
	 * intermediate directories if they do not already exist.
	 *
	 * @param   string   $dirName      The full path of the directory to create
	 * @param   integer  $permissions  The permissions of the created directory
	 *
	 * @return  boolean  True on success
	 */
	public function mkdir($dirName, $permissions = 0755)
	{
		$targetDir = rtrim($dirName, '/');

		$ret = @ssh2_sftp_mkdir($this->sftpHandle, $targetDir, $permissions, true);

		return $ret;
	}

	/**
	 * Checks if the given directory exists
	 *
	 * @param   string  $path  The full path of the remote directory to check
	 *
	 * @return  boolean  True if the directory exists
	 */
	public function isDir($path)
	{
		return @ssh2_sftp_stat($this->sftpHandle, $path);
	}

	/**
	 * Get the current working directory
	 *
	 * @return  string
	 */
	public function cwd()
	{
		return ssh2_sftp_realpath($this->sftpHandle, ".");
	}

	/**
	 * Returns the absolute remote path from a path relative to the initial directory configured when creating the
	 * transfer object.
	 *
	 * @param   string  $fileName  The relative path of a file or directory
	 *
	 * @return  string  The absolute path for use by the transfer object
	 */
	public function getPath($fileName)
	{
		$fileName = str_replace('\\', '/', $fileName);
		$fileName = rtrim($this->directory, '/') . '/' . $fileName;

		return $fileName;
	}

	/**
	 * Lists the subdirectories inside an SFTP directory
	 *
	 * @param   null|string  $dir  The directory to scan. Skip to use the current directory.
	 *
	 * @return  array|bool  A list of folders, or false if we could not get a listing
	 *
	 * @throws  RuntimeException  When the server is incompatible with our SFTP folder scanner
	 */
	public function listFolders($dir = null)
	{
		if (empty($dir))
		{
			$dir = $this->directory;
		}

		// Get a raw directory listing (hoping it's a UNIX server!)
		$list = [];
		$dir  = ltrim($dir, '/');

		try
		{
			$di = new DirectoryIterator("ssh2.sftp://" . $this->sftpHandle . "/$dir");
		}
		catch (Exception $e)
		{
			throw new RuntimeException(sprintf('Cannot change to SFTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $dir), 500);
		}

		if (!$di->valid())
		{
			throw new RuntimeException(sprintf('Cannot change to SFTP directory "%s" – make sure the folder exists and that you have adequate permissions to it', $dir), 500);
		}

		/** @var DirectoryIterator $entry */
		foreach ($di as $entry)
		{
			if ($entry->isDot())
			{
				continue;
			}

			if (!$entry->isDir())
			{
				continue;
			}

			$list[] = $entry->getFilename();
		}

		unset($di);

		if (!empty($list))
		{
			asort($list);
		}

		return $list;
	}

	/**
	 * Return a string with the appropriate stream wrapper protocol for $path. You can use the result with all PHP
	 * functions / classes which accept file paths such as DirectoryIterator, file_get_contents, file_put_contents,
	 * fopen etc.
	 *
	 * @param   string  $path
	 *
	 * @return  string
	 */
	public function getWrapperStringFor($path)
	{
		return "ssh2.sftp://{$this->sftpHandle}{$path}";
	}

	/**
	 * Return the raw server listing for the requested folder.
	 *
	 * @param   string  $folder  The path name to list
	 *
	 * @return  string
	 */
	public function getRawList($folder)
	{
		// First try the command for Linxu servers
		$res = $this->ssh2cmd('ls -l ' . escapeshellarg($folder));

		// If an error occurred let's try the command for Windows servers
		if (empty($res))
		{
			$res = $this->ssh2cmd('CMD /C ' . escapeshellarg($folder));
		}

		return $res;
	}

	private function ssh2cmd($command)
	{
		$stream = ssh2_exec($this->connection, $command);
		stream_set_blocking($stream, true);
		$res = @stream_get_contents($stream);
		@fclose($stream);

		return $res;
	}
}
Util/Transfer/SftpCurl.php000060400000046600152456704520011534 0ustar00<?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\Util\Transfer;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Postproc\ProxyAware;
use RuntimeException;

/**
 * SFTP transfer object, using cURL as the transport backend
 */
class SftpCurl extends Sftp implements TransferInterface
{
	use ProxyAware;

	/**
	 * SFTP server's hostname or IP address
	 *
	 * @var  string
	 */
	private $host = 'localhost';

	/**
	 * SFTP server's port, default: 21
	 *
	 * @var  integer
	 */
	private $port = 22;

	/**
	 * Username used to authenticate to the SFTP server
	 *
	 * @var  string
	 */
	private $username = '';

	/**
	 * Password used to authenticate to the SFTP server
	 *
	 * @var  string
	 */
	private $password = '';

	/**
	 * SFTP initial directory
	 *
	 * @var  string
	 */
	private $directory = '/';

	/**
	 * The absolute filesystem path to a private key file used for authentication instead of a password.
	 *
	 * @var  string
	 */
	private $privateKey = '';

	/**
	 * The absolute filesystem path to a public key file used for authentication instead of a password.
	 *
	 * @var  string
	 */
	private $publicKey = '';

	/**
	 * Timeout for connecting to the SFTP server, default: 10 minutes
	 *
	 * @var  integer
	 */
	private $timeout = 600;

	/**
	 * Should we enable verbose output to STDOUT? Useful for debugging.
	 *
	 * @var   bool
	 */
	private $verbose = false;

	/**
	 * Should I enabled the passive IP workaround for cURL?
	 *
	 * @var   bool
	 */
	private $skipPassiveIP = false;

	/**
	 * Public constructor
	 *
	 * @param   array  $options  Configuration options
	 *
	 * @return  self
	 *
	 * @throws  RuntimeException
	 */
	public function __construct(array $options)
	{
		if (isset($options['host']))
		{
			$this->host = $options['host'];
		}

		if (isset($options['port']))
		{
			$this->port = (int) $options['port'];
		}

		if (isset($options['username']))
		{
			$this->username = $options['username'];
		}

		if (isset($options['password']))
		{
			$this->password = $options['password'];
		}

		if (isset($options['directory']))
		{
			$this->directory = '/' . ltrim(trim($options['directory']), '/');
		}

		if (isset($options['privateKey']))
		{
			$this->privateKey = $options['privateKey'];
		}

		if (isset($options['publicKey']))
		{
			$this->publicKey = $options['publicKey'];
		}

		if (isset($options['timeout']))
		{
			$this->timeout = max(1, (int) $options['timeout']);
		}

		if (isset($options['passive_fix']))
		{
			$this->skipPassiveIP = $options['passive_fix'] ? true : false;
		}

		if (isset($options['verbose']))
		{
			$this->verbose = $options['verbose'] ? true : false;
		}
	}

	/**
	 * Save all parameters on serialization except the connection resource
	 *
	 * @return  array
	 */
	public function __sleep()
	{
		return [
			'host',
			'port',
			'username',
			'password',
			'directory',
			'privateKey',
			'publicKey',
			'timeout',
			'skipPassiveIP',
			'verbose',
		];
	}

	/**
	 * Test the connection to the SFTP server and whether the initial directory is correct. This is done by attempting to
	 * list the contents of the initial directory. The listing is not parsed (we don't really care!) and we do NOT check
	 * if we can upload files to that remote folder.
	 *
	 * @throws  RuntimeException
	 */
	public function connect()
	{
		$ch = $this->getCurlHandle($this->directory . '/');
		curl_setopt($ch, CURLOPT_HEADER, 1);
		curl_setopt($ch, CURLOPT_NOBODY, 1);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

		$listing = curl_exec($ch);
		$errNo   = curl_errno($ch);
		$error   = curl_error($ch);
		curl_close($ch);

		if ($errNo)
		{
			throw new RuntimeException("cURL Error $errNo connecting to remote SFTP server: $error", 500);
		}
	}

	/**
	 * Write the contents into the file
	 *
	 * @param   string  $fileName  The full path to the file
	 * @param   string  $contents  The contents to write to the file
	 *
	 * @return  boolean  True on success
	 */
	public function write($fileName, $contents)
	{
		// Make sure the buffer:// wrapper is loaded
		class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);

		$handle = fopen('buffer://akeeba_engine_transfer_ftp_curl', 'r+');
		fwrite($handle, $contents);

		// Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle
		try
		{
			$this->uploadFromHandle($fileName, $handle);
		}
		catch (RuntimeException $e)
		{
			return false;
		}

		return true;
	}

	/**
	 * Uploads a local file to the remote storage
	 *
	 * @param   string  $localFilename   The full path to the local file
	 * @param   string  $remoteFilename  The full path to the remote file
	 * @param   bool    $useExceptions   Throw an exception instead of returning "false" on connection error.
	 *
	 * @return  boolean  True on success
	 */
	public function upload($localFilename, $remoteFilename, $useExceptions = true)
	{
		$fp = @fopen($localFilename, 'r');

		if ($fp === false)
		{
			throw new RuntimeException("Unreadable local file $localFilename");
		}

		// Note: don't manually close the file pointer, it's closed automatically by uploadFromHandle
		try
		{
			$this->uploadFromHandle($remoteFilename, $fp);
		}
		catch (RuntimeException $e)
		{
			if ($useExceptions)
			{
				throw $e;
			}

			return false;
		}

		return true;
	}

	/**
	 * Read the contents of a remote file into a string
	 *
	 * @param   string  $fileName  The full path to the remote file
	 *
	 * @return  string  The contents of the remote file
	 */
	public function read($fileName)
	{
		try
		{
			return $this->downloadToString($fileName);
		}
		catch (RuntimeException $e)
		{
			throw new RuntimeException("Can not download remote file $fileName", 500, $e);
		}
	}

	/**
	 * Download a remote file into a local file
	 *
	 * @param   string  $remoteFilename  The remote file path to download from
	 * @param   string  $localFilename   The local file path to download to
	 * @param   bool    $useExceptions   Throw an exception instead of returning "false" on connection error.
	 *
	 * @return  boolean  True on success
	 */
	public function download($remoteFilename, $localFilename, $useExceptions = true)
	{
		$fp = @fopen($localFilename, 'w');

		if ($fp === false)
		{
			if ($useExceptions)
			{
				throw new RuntimeException(sprintf('Download from FTP failed. Can not open local file %s for writing.', $localFilename));
			}

			return false;
		}

		// Note: don't manually close the file pointer, it's closed automatically by downloadToHandle
		try
		{
			$this->downloadToHandle($remoteFilename, $fp);
		}
		catch (RuntimeException $e)
		{
			if ($useExceptions)
			{
				throw $e;
			}

			return false;
		}

		return true;
	}

	/**
	 * Delete a file (remove it from the disk)
	 *
	 * @param   string  $fileName  The full path to the file
	 *
	 * @return  boolean  True on success
	 */
	public function delete($fileName)
	{
		$commands = [
			'rm ' . $this->getPath($fileName),
		];

		try
		{
			$this->executeServerCommands($commands);
		}
		catch (RuntimeException $e)
		{
			return false;
		}

		return true;
	}

	/**
	 * Create a copy of the file. Actually, we have to read it in memory and upload it again.
	 *
	 * @param   string  $from  The full path of the file to copy from
	 * @param   string  $to    The full path of the file that will hold the copy
	 *
	 * @return  boolean  True on success
	 */
	public function copy($from, $to)
	{
		// Make sure the buffer:// wrapper is loaded
		class_exists('\\Akeeba\\Engine\\Util\\Buffer', true);

		$handle = fopen('buffer://akeeba_engine_transfer_ftp', 'r+');

		try
		{
			$this->downloadToHandle($from, $handle, false);
			$this->uploadFromHandle($to, $handle);
		}
		catch (RuntimeException $e)
		{
			return false;
		}

		return true;
	}

	/**
	 * Move or rename a file
	 *
	 * @param   string  $from  The full path of the file to move
	 * @param   string  $to    The full path of the target file
	 *
	 * @return  boolean  True on success
	 */
	public function move($from, $to)
	{
		$from = $this->getPath($from);
		$to   = $this->getPath($to);

		$commands = [
			'rename ' . $from . ' ' . $to,
		];

		try
		{
			$this->executeServerCommands($commands);
		}
		catch (RuntimeException $e)
		{
			return false;
		}

		return true;
	}

	/**
	 * Change the permissions of a file
	 *
	 * @param   string   $fileName     The full path of the file whose permissions will change
	 * @param   integer  $permissions  The new permissions, e.g. 0644 (remember the leading zero in octal numbers!)
	 *
	 * @return  boolean  True on success
	 */
	public function chmod($fileName, $permissions)
	{
		// Make sure permissions are in an octal string representation
		if (!is_string($permissions))
		{
			$permissions = decoct($permissions);
		}

		$commands = [
			'chmod ' . $permissions . ' ' . $this->getPath($fileName),
		];

		try
		{
			$this->executeServerCommands($commands);
		}
		catch (RuntimeException $e)
		{
			return false;
		}

		return true;
	}

	/**
	 * Create a directory if it doesn't exist. The operation is implicitly recursive, i.e. it will create all
	 * intermediate directories if they do not already exist.
	 *
	 * @param   string   $dirName      The full path of the directory to create
	 * @param   integer  $permissions  The permissions of the created directory
	 *
	 * @return  boolean  True on success
	 */
	public function mkdir($dirName, $permissions = 0755)
	{
		$targetDir = rtrim($dirName, '/');

		$directories = explode('/', $targetDir);

		$remoteDir = '';

		foreach ($directories as $dir)
		{
			if (!$dir)
			{
				continue;
			}

			$remoteDir .= '/' . $dir;

			// Continue if the folder already exists. Otherwise I'll get a an error even if everything is fine
			if ($this->isDir($remoteDir))
			{
				continue;
			}

			$commands = [
				'mkdir ' . $remoteDir,
			];

			try
			{
				$this->executeServerCommands($commands);
			}
			catch (RuntimeException $e)
			{
				return false;
			}
		}

		$this->chmod($dirName, $permissions);

		return true;
	}

	/**
	 * Checks if the given directory exists
	 *
	 * @param   string  $path  The full path of the remote directory to check
	 *
	 * @return  boolean  True if the directory exists
	 */
	public function isDir($path)
	{
		$ch = $this->getCurlHandle($path . '/');
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

		$list = curl_exec($ch);

		$errNo = curl_errno($ch);
		curl_close($ch);

		if ($errNo)
		{
			return false;
		}

		return true;
	}

	/**
	 * Get the current working directory. NOT IMPLEMENTED.
	 *
	 * @return  string
	 */
	public function cwd()
	{
		return '';
	}

	/**
	 * Returns the absolute remote path from a path relative to the initial directory configured when creating the
	 * transfer object.
	 *
	 * @param   string  $fileName  The relative path of a file or directory
	 *
	 * @return  string  The absolute path for use by the transfer object
	 */
	public function getPath($fileName)
	{
		$fileName = str_replace('\\', '/', $fileName);

		if (strpos($fileName, $this->directory) === 0)
		{
			return $fileName;
		}

		$fileName = trim($fileName, '/');
		$fileName = rtrim($this->directory, '/') . '/' . $fileName;

		return $fileName;
	}

	/**
	 * Lists the subdirectories inside an SFTP directory
	 *
	 * @param   null|string  $dir  The directory to scan. Skip to use the current directory.
	 *
	 * @return  array|bool  A list of folders, or false if we could not get a listing
	 *
	 * @throws  RuntimeException  When the server is incompatible with our SFTP folder scanner
	 */
	public function listFolders($dir = null)
	{
		if (empty($dir))
		{
			$dir = $this->directory;
		}

		$dir = rtrim($dir, '/');

		$ch = $this->getCurlHandle($dir . '/');
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

		$list = curl_exec($ch);

		$errNo = curl_errno($ch);
		$error = curl_error($ch);
		curl_close($ch);

		if ($errNo)
		{
			throw new RuntimeException(sprintf("cURL Error $errNo ($error) while listing contents of directory \"%s\" – make sure the folder exists and that you have adequate permissions to it", $dir), 500);
		}

		if (empty($list))
		{
			throw new RuntimeException("Sorry, your SFTP server doesn't support our SFTP directory browser.");
		}

		$folders = [];

		// Convert the directory listing into an array of lines without *NIX/Windows/Mac line ending characters
		$list = explode("\n", $list);
		$list = array_map('rtrim', $list);

		foreach ($list as $v)
		{
			$vInfo = preg_split("/[\s]+/", $v, 9);

			if ($vInfo[0] !== "total")
			{
				$perms = $vInfo[0];

				if (substr($perms, 0, 1) == 'd')
				{
					$folders[] = $vInfo[8];
				}
			}
		}

		asort($folders);

		return $folders;
	}

	/**
	 * Is the verbose debug option set?
	 *
	 * @return  boolean
	 */
	public function isVerbose()
	{
		return $this->verbose;
	}

	/**
	 * Set the verbose debug option
	 *
	 * @param   boolean  $verbose
	 *
	 * @return  void
	 */
	public function setVerbose($verbose)
	{
		$this->verbose = $verbose;
	}

	/**
	 * Returns a cURL resource handler for the remote SFTP server
	 *
	 * @param   string  $remoteFile  Optional. The remote file / folder on the SFTP server you'll be manipulating with cURL.
	 *
	 * @return  resource
	 */
	protected function getCurlHandle($remoteFile = '')
	{
		// Remember, the username has to be URL encoded as it's part of a URI!
		$authentication = urlencode($this->username);

		// We will only use username and password authentication if there are no certificates configured.
		if (empty($this->publicKey))
		{
			// Remember, both the username and password have to be URL encoded as they're part of a URI!
			$password       = urlencode($this->password);
			$authentication .= ':' . $password;
		}

		$ftpUri = 'sftp://' . $authentication . '@' . $this->host;

		if (!empty($this->port))
		{
			$ftpUri .= ':' . (int) $this->port;
		}

		// Relative path? Append the initial directory.
		if (substr($remoteFile, 0, 1) != '/')
		{
			$ftpUri .= $this->directory;
		}

		// Add a remote file if necessary. The filename must be URL encoded since we're creating a URI.
		if (!empty($remoteFile))
		{
			$suffix = '';

			$dirname = dirname($remoteFile);

			// Windows messing up dirname('/'). KILL ME.
			if ($dirname == '\\')
			{
				$dirname = '';
			}

			$dirname  = trim($dirname, '/');
			$basename = basename($remoteFile);

			if ((substr($remoteFile, -1) == '/') && !empty($basename))
			{
				$suffix = '/' . $suffix;
			}

			$ftpUri .= '/' . $dirname . (empty($dirname) ? '' : '/') . urlencode($basename) . $suffix;
		}

		$ch = curl_init();

		$this->applyProxySettingsToCurl($ch);

		curl_setopt($ch, CURLOPT_URL, $ftpUri);
		curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);

		// Do I have to use certificate authentication?
		if (!empty($this->publicKey))
		{
			// We always need to provide a public key file
			curl_setopt($ch, CURLOPT_SSH_PUBLIC_KEYFILE, $this->publicKey);

			// Since SSH certificates are self-signed we cannot have cURL verify their signatures against a CA.
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
			curl_setopt($ch, CURLOPT_SSL_VERIFYSTATUS, 0);

			/**
			 * This is optional because newer versions of cURL can extract the private key file from a combined
			 * certificate file.
			 */
			if (!empty($this->privateKey))
			{
				curl_setopt($ch, CURLOPT_SSH_PRIVATE_KEYFILE, $this->privateKey);
			}

			/**
			 * In case of encrypted (a.k.a. password protected) private key files you need to also specify the
			 * certificate decryption key in the password field. However, if libcurl is compiled against the GnuTLS
			 * library (instead of OpenSSL) this will NOT work because of bugs / missing features in GnuTLS. It's the
			 * same problem you get when libssh is compiled against GnuTLS. The solution to that is having an
			 * unencrypted private key file.
			 */
			if (!empty($this->password))
			{
				curl_setopt($ch, CURLOPT_KEYPASSWD, $this->password);
			}
		}

		// Should I enable verbose output? Useful for debugging.
		if ($this->verbose)
		{
			curl_setopt($ch, CURLOPT_VERBOSE, 1);
		}

		// Automatically create missing directories
		curl_setopt($ch, CURLOPT_FTP_CREATE_MISSING_DIRS, 1);

		return $ch;
	}

	/**
	 * Uploads a file using file contents provided through a file handle
	 *
	 * @param   string    $remoteFilename
	 * @param   resource  $fp
	 *
	 * @return  void
	 *
	 * @throws  RuntimeException
	 */
	protected function uploadFromHandle($remoteFilename, $fp)
	{
		// We need the file size. We can do that by getting the file position at EOF
		fseek($fp, 0, SEEK_END);
		$filesize = ftell($fp);
		rewind($fp);

		$ch = $this->getCurlHandle($remoteFilename);
		curl_setopt($ch, CURLOPT_UPLOAD, 1);
		curl_setopt($ch, CURLOPT_INFILE, $fp);
		curl_setopt($ch, CURLOPT_INFILESIZE, $filesize);

		curl_exec($ch);

		$error_no = curl_errno($ch);
		$error    = curl_error($ch);

		curl_close($ch);
		fclose($fp);

		if ($error_no)
		{
			throw new RuntimeException($error, $error_no);
		}
	}

	/**
	 * Downloads a remote file to the provided file handle
	 *
	 * @param   string    $remoteFilename  Filename on the remote server
	 * @param   resource  $fp              File handle where the downloaded content will be written to
	 * @param   bool      $close           Optional. Should I close the file handle when I'm done? (Default: true)
	 *
	 * @return  void
	 *
	 * @throws  RuntimeException
	 */
	protected function downloadToHandle($remoteFilename, $fp, $close = true)
	{
		$ch = $this->getCurlHandle($remoteFilename);

		curl_setopt($ch, CURLOPT_FILE, $fp);

		curl_exec($ch);

		$error_no = curl_errno($ch);
		$error    = curl_error($ch);

		curl_close($ch);

		if ($close)
		{
			fclose($fp);
		}

		if ($error_no)
		{
			throw new RuntimeException($error, $error_no);
		}
	}

	/**
	 * Downloads a remote file and returns it as a string
	 *
	 * @param   string  $remoteFilename  Filename on the remote server
	 *
	 * @return  string
	 *
	 * @throws  RuntimeException
	 */
	protected function downloadToString($remoteFilename)
	{
		$ch = $this->getCurlHandle($remoteFilename);

		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_HEADER, false);

		$ret = curl_exec($ch);

		$error_no = curl_errno($ch);
		$error    = curl_error($ch);

		curl_close($ch);

		if ($error_no)
		{
			throw new RuntimeException($error, $error_no);
		}

		return $ret;
	}

	/**
	 * Executes arbitrary SFTP commands
	 *
	 * @param   array  $commands  An array with the SFTP commands to be executed
	 *
	 * @return  string  The output of the executed commands
	 *
	 * @throws  RuntimeException
	 */
	protected function executeServerCommands($commands)
	{
		$ch = $this->getCurlHandle($this->directory . '/');

		curl_setopt($ch, CURLOPT_QUOTE, $commands);
		curl_setopt($ch, CURLOPT_HEADER, 1);
		curl_setopt($ch, CURLOPT_NOBODY, 1);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

		$listing = curl_exec($ch);
		$errNo   = curl_errno($ch);
		$error   = curl_error($ch);
		curl_close($ch);

		if ($errNo)
		{
			throw new RuntimeException($error, $errNo);
		}

		return $listing;
	}
}
Util/Utf8.php000060400000005773152456704520007042 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

/**
 * Replacement for the utf8_encode and utf8_decode functions on PHP 8.2 and later.
 *
 * @see https://wiki.php.net/rfc/remove_utf8_decode_and_utf8_encode
 */
class Utf8
{
	public static function utf8_encode($s)
	{
		if (version_compare(PHP_VERSION, '8.1.999', 'le'))
		{
			return utf8_encode($s);
		}

		if (function_exists('mb_convert_encoding'))
		{
			return mb_convert_encoding($s, 'UTF-8', 'ISO-8859-1');
		}

		if (class_exists('UConverter'))
		{
			return UConverter::transcode($s, 'UTF8', 'ISO-8859-1');
		}

		if (function_exists('iconv'))
		{
			return iconv('ISO-8859-1', 'UTF-8', $s);
		}

		/**
		 * Fallback to the pure PHP implementation from Symfony Polyfill for PHP 7.2
		 *
		 * @see https://github.com/symfony/polyfill-php72/blob/v1.26.0/Php72.php
		 */
		$s .= $s;
		$len = \strlen($s);

		for ($i = $len >> 1, $j = 0; $i < $len; ++$i, ++$j) {
			switch (true) {
				case $s[$i] < "\x80": $s[$j] = $s[$i]; break;
				case $s[$i] < "\xC0": $s[$j] = "\xC2"; $s[++$j] = $s[$i]; break;
				default: $s[$j] = "\xC3"; $s[++$j] = \chr(\ord($s[$i]) - 64); break;
			}
		}

		return substr($s, 0, $j);
	}

	public static function utf8_decode($s)
	{
		if (version_compare(PHP_VERSION, '8.1.999', 'le'))
		{
			return utf8_decode($s);
		}

		if (function_exists('mb_convert_encoding'))
		{
			return mb_convert_encoding($s, 'ISO-8859-1', 'UTF-8');
		}

		if (class_exists('UConverter'))
		{
			return UConverter::transcode($s, 'ISO-8859-1', 'UTF8');
		}

		if (function_exists('iconv'))
		{
			return iconv('UTF-8', 'ISO-8859-1', $s);
		}

		/**
		 * Fallback to the pure PHP implementation from Symfony Polyfill for PHP 7.2
		 *
		 * @see https://github.com/symfony/polyfill-php72/blob/v1.26.0/Php72.php
		 */
		$s = (string) $s;
		$len = \strlen($s);

		for ($i = 0, $j = 0; $i < $len; ++$i, ++$j) {
			switch ($s[$i] & "\xF0") {
				case "\xC0":
				case "\xD0":
					$c = (\ord($s[$i] & "\x1F") << 6) | \ord($s[++$i] & "\x3F");
					$s[$j] = $c < 256 ? \chr($c) : '?';
					break;

				case "\xF0":
					++$i;
				// no break

				case "\xE0":
					$s[$j] = '?';
					$i += 2;
					break;

				default:
					$s[$j] = $s[$i];
			}
		}

		return substr($s, 0, $j);
	}
}Util/Complexify.php000060400000036763152456704520010336 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Platform;
use RuntimeException;

/**
 * PHP port of http://github.com/danpalmer/jquery.complexify.js
 * Retrieved from https://github.com/mcrumley/php-complexify/blob/master/src/Complexify/Complexify.php
 * Error reporting is based on https://github.com/kislyuk/node-complexify
 */
class Complexify
{
	private static $MIN_COMPLEXITY = 66;

	private static $MAX_COMPLEXITY = 120; //  25 chars, all charsets

	private static $CHARSETS = [
		// Commonly Used
		////////////////////
		[0x0020, 0x0020], // Space
		[0x0030, 0x0039], // Numbers
		[0x0041, 0x005A], // Uppercase
		[0x0061, 0x007A], // Lowercase
		[0x0021, 0x002F], // Punctuation
		[0x003A, 0x0040], // Punctuation
		[0x005B, 0x0060], // Punctuation
		[0x007B, 0x007E], // Punctuation
		// Everything Else
		////////////////////
		[0x0080, 0x00FF], // Latin-1 Supplement
		[0x0100, 0x017F], // Latin Extended-A
		[0x0180, 0x024F], // Latin Extended-B
		[0x0250, 0x02AF], // IPA Extensions
		[0x02B0, 0x02FF], // Spacing Modifier Letters
		[0x0300, 0x036F], // Combining Diacritical Marks
		[0x0370, 0x03FF], // Greek
		[0x0400, 0x04FF], // Cyrillic
		[0x0530, 0x058F], // Armenian
		[0x0590, 0x05FF], // Hebrew
		[0x0600, 0x06FF], // Arabic
		[0x0700, 0x074F], // Syriac
		[0x0780, 0x07BF], // Thaana
		[0x0900, 0x097F], // Devanagari
		[0x0980, 0x09FF], // Bengali
		[0x0A00, 0x0A7F], // Gurmukhi
		[0x0A80, 0x0AFF], // Gujarati
		[0x0B00, 0x0B7F], // Oriya
		[0x0B80, 0x0BFF], // Tamil
		[0x0C00, 0x0C7F], // Telugu
		[0x0C80, 0x0CFF], // Kannada
		[0x0D00, 0x0D7F], // Malayalam
		[0x0D80, 0x0DFF], // Sinhala
		[0x0E00, 0x0E7F], // Thai
		[0x0E80, 0x0EFF], // Lao
		[0x0F00, 0x0FFF], // Tibetan
		[0x1000, 0x109F], // Myanmar
		[0x10A0, 0x10FF], // Georgian
		[0x1100, 0x11FF], // Hangul Jamo
		[0x1200, 0x137F], // Ethiopic
		[0x13A0, 0x13FF], // Cherokee
		[0x1400, 0x167F], // Unified Canadian Aboriginal Syllabics
		[0x1680, 0x169F], // Ogham
		[0x16A0, 0x16FF], // Runic
		[0x1780, 0x17FF], // Khmer
		[0x1800, 0x18AF], // Mongolian
		[0x1E00, 0x1EFF], // Latin Extended Additional
		[0x1F00, 0x1FFF], // Greek Extended
		[0x2000, 0x206F], // General Punctuation
		[0x2070, 0x209F], // Superscripts and Subscripts
		[0x20A0, 0x20CF], // Currency Symbols
		[0x20D0, 0x20FF], // Combining Marks for Symbols
		[0x2100, 0x214F], // Letterlike Symbols
		[0x2150, 0x218F], // Number Forms
		[0x2190, 0x21FF], // Arrows
		[0x2200, 0x22FF], // Mathematical Operators
		[0x2300, 0x23FF], // Miscellaneous Technical
		[0x2400, 0x243F], // Control Pictures
		[0x2440, 0x245F], // Optical Character Recognition
		[0x2460, 0x24FF], // Enclosed Alphanumerics
		[0x2500, 0x257F], // Box Drawing
		[0x2580, 0x259F], // Block Elements
		[0x25A0, 0x25FF], // Geometric Shapes
		[0x2600, 0x26FF], // Miscellaneous Symbols
		[0x2700, 0x27BF], // Dingbats
		[0x2800, 0x28FF], // Braille Patterns
		[0x2E80, 0x2EFF], // CJK Radicals Supplement
		[0x2F00, 0x2FDF], // Kangxi Radicals
		[0x2FF0, 0x2FFF], // Ideographic Description Characters
		[0x3000, 0x303F], // CJK Symbols and Punctuation
		[0x3040, 0x309F], // Hiragana
		[0x30A0, 0x30FF], // Katakana
		[0x3100, 0x312F], // Bopomofo
		[0x3130, 0x318F], // Hangul Compatibility Jamo
		[0x3190, 0x319F], // Kanbun
		[0x31A0, 0x31BF], // Bopomofo Extended
		[0x3200, 0x32FF], // Enclosed CJK Letters and Months
		[0x3300, 0x33FF], // CJK Compatibility
		[0x3400, 0x4DB5], // CJK Unified Ideographs Extension A
		[0x4E00, 0x9FFF], // CJK Unified Ideographs
		[0xA000, 0xA48F], // Yi Syllables
		[0xA490, 0xA4CF], // Yi Radicals
		[0xAC00, 0xD7A3], // Hangul Syllables
		[0xD800, 0xDB7F], // High Surrogates
		[0xDB80, 0xDBFF], // High Private Use Surrogates
		[0xDC00, 0xDFFF], // Low Surrogates
		[0xE000, 0xF8FF], // Private Use
		[0xF900, 0xFAFF], // CJK Compatibility Ideographs
		[0xFB00, 0xFB4F], // Alphabetic Presentation Forms
		[0xFB50, 0xFDFF], // Arabic Presentation Forms-A
		[0xFE20, 0xFE2F], // Combining Half Marks
		[0xFE30, 0xFE4F], // CJK Compatibility Forms
		[0xFE50, 0xFE6F], // Small Form Variants
		[0xFE70, 0xFEFE], // Arabic Presentation Forms-B
		[0xFEFF, 0xFEFF], // Specials
		[0xFF00, 0xFFEF], // Halfwidth and Fullwidth Forms
		[0xFFF0, 0xFFFD]  // Specials
	];

	// Generated from 500 worst passwords and 370 Banned Twitter lists found at
	// @source http://www.skullsecurity.org/wiki/index.php/Passwords
	private static $BANLIST = [
		'0', '1111', '1212', '1234', '1313', '2000', '2112', '2222',
		'3333', '4128', '4321', '4444', '5150', '5555', '6666', '6969', '7777', 'aaaa',
		'alex', 'asdf', 'baby', 'bear', 'beer', 'bill', 'blue', 'cock', 'cool', 'cunt',
		'dave', 'dick', 'eric', 'fire', 'fish', 'ford', 'fred', 'fuck', 'girl', 'golf',
		'jack', 'jake', 'john', 'king', 'love', 'mark', 'matt', 'mike', 'mine', 'pass',
		'paul', 'porn', 'rock', 'sexy', 'shit', 'slut', 'star', 'test', 'time', 'tits',
		'wolf', 'xxxx', '11111', '12345', 'angel', 'apple', 'beach', 'billy', 'bitch',
		'black', 'boobs', 'booty', 'brian', 'bubba', 'buddy', 'chevy', 'chris', 'cream',
		'david', 'dirty', 'eagle', 'enjoy', 'enter', 'frank', 'girls', 'great', 'green',
		'happy', 'hello', 'horny', 'house', 'james', 'japan', 'jason', 'juice', 'kelly',
		'kevin', 'kitty', 'lover', 'lucky', 'magic', 'money', 'movie', 'music', 'naked',
		'ou812', 'paris', 'penis', 'peter', 'porno', 'power', 'pussy', 'qwert', 'sammy',
		'scott', 'smith', 'stars', 'steve', 'super', 'teens', 'tiger', 'video', 'viper',
		'white', 'women', 'xxxxx', 'young', '111111', '112233', '121212', '123123',
		'123456', '131313', '232323', '654321', '666666', '696969', '777777', '987654',
		'aaaaaa', 'abc123', 'abcdef', 'access', 'action', 'albert', 'alexis', 'amanda',
		'andrea', 'andrew', 'angela', 'angels', 'animal', 'apollo', 'apples', 'arthur',
		'asdfgh', 'ashley', 'august', 'austin', 'badboy', 'bailey', 'banana', 'barney',
		'batman', 'beaver', 'beavis', 'bigdog', 'birdie', 'biteme', 'blazer', 'blonde',
		'blowme', 'bonnie', 'booboo', 'booger', 'boomer', 'boston', 'brandy', 'braves',
		'brazil', 'bronco', 'buster', 'butter', 'calvin', 'camaro', 'canada', 'carlos',
		'carter', 'casper', 'cheese', 'coffee', 'compaq', 'cookie', 'cooper', 'cowboy',
		'dakota', 'dallas', 'daniel', 'debbie', 'dennis', 'diablo', 'doctor', 'doggie',
		'donald', 'dragon', 'dreams', 'driver', 'eagle1', 'eagles', 'edward', 'erotic',
		'falcon', 'fender', 'flower', 'flyers', 'freddy', 'fucked', 'fucker', 'fuckme',
		'gators', 'gemini', 'george', 'giants', 'ginger', 'golden', 'golfer', 'gordon',
		'guitar', 'gunner', 'hammer', 'hannah', 'harley', 'helpme', 'hentai', 'hockey',
		'horney', 'hotdog', 'hunter', 'iceman', 'iwantu', 'jackie', 'jaguar', 'jasper',
		'jeremy', 'johnny', 'jordan', 'joseph', 'joshua', 'junior', 'justin', 'killer',
		'knight', 'ladies', 'lakers', 'lauren', 'legend', 'little', 'london', 'lovers',
		'maddog', 'maggie', 'magnum', 'marine', 'martin', 'marvin', 'master', 'matrix',
		'member', 'merlin', 'mickey', 'miller', 'monica', 'monkey', 'morgan', 'mother',
		'muffin', 'murphy', 'nascar', 'nathan', 'nicole', 'nipple', 'oliver', 'orange',
		'parker', 'peanut', 'pepper', 'player', 'please', 'pookie', 'prince', 'purple',
		'qazwsx', 'qwerty', 'rabbit', 'rachel', 'racing', 'ranger', 'redsox', 'robert',
		'rocket', 'runner', 'russia', 'samson', 'sandra', 'saturn', 'scooby', 'secret',
		'sexsex', 'shadow', 'shaved', 'sierra', 'silver', 'skippy', 'slayer', 'smokey',
		'snoopy', 'soccer', 'sophie', 'spanky', 'sparky', 'spider', 'squirt', 'steven',
		'sticky', 'stupid', 'suckit', 'summer', 'surfer', 'sydney', 'taylor', 'tennis',
		'teresa', 'tester', 'theman', 'thomas', 'tigers', 'tigger', 'tomcat', 'topgun',
		'toyota', 'travis', 'tucker', 'turtle', 'united', 'vagina', 'victor', 'viking',
		'voodoo', 'walter', 'willie', 'wilson', 'winner', 'winter', 'wizard', 'xavier',
		'xxxxxx', 'yamaha', 'yankee', 'yellow', 'zxcvbn', 'zzzzzz', '1234567', '7777777',
		'8675309', 'abgrtyu', 'amateur', 'anthony', 'arsenal', 'asshole', 'bigcock',
		'bigdick', 'bigtits', 'bitches', 'blondes', 'blowjob', 'bond007', 'brandon',
		'broncos', 'bulldog', 'cameron', 'captain', 'charles', 'charlie', 'chelsea',
		'chester', 'chicago', 'chicken', 'college', 'cowboys', 'crystal', 'cumming',
		'cumshot', 'diamond', 'dolphin', 'extreme', 'ferrari', 'fishing', 'florida',
		'forever', 'freedom', 'fucking', 'fuckyou', 'gandalf', 'gateway', 'gregory',
		'heather', 'hooters', 'hunting', 'jackson', 'jasmine', 'jessica', 'johnson',
		'leather', 'letmein', 'madison', 'matthew', 'maxwell', 'melissa', 'michael',
		'monster', 'mustang', 'naughty', 'ncc1701', 'newyork', 'nipples', 'packers',
		'panther', 'panties', 'patrick', 'peaches', 'phantom', 'phoenix', 'porsche',
		'private', 'pussies', 'raiders', 'rainbow', 'rangers', 'rebecca', 'richard',
		'rosebud', 'scooter', 'scorpio', 'shannon', 'success', 'testing', 'thunder',
		'thx1138', 'tiffany', 'trouble', 'twitter', 'voyager', 'warrior', 'welcome',
		'william', 'winston', 'yankees', 'zxcvbnm', '11111111', '12345678', 'access14',
		'baseball', 'bigdaddy', 'butthead', 'cocacola', 'computer', 'corvette',
		'danielle', 'dolphins', 'einstein', 'firebird', 'football', 'hardcore',
		'iloveyou', 'internet', 'jennifer', 'marlboro', 'maverick', 'mercedes',
		'michelle', 'midnight', 'mistress', 'mountain', 'nicholas', 'password',
		'princess', 'qwertyui', 'redskins', 'redwings', 'rush2112', 'samantha',
		'scorpion', 'srinivas', 'startrek', 'starwars', 'steelers', 'sunshine',
		'superman', 'swimming', 'trustno1', 'victoria', 'whatever', 'xxxxxxxx',
		'password1', 'password12', 'password123',
	];

	private $minimumChars = 8;

	private $strengthScaleFactor = 1;

	private $bannedPasswords = [];

	private $banMode = 'strict'; // (strict|loose)

	private $encoding = 'UTF-8';

	/**
	 * Constructor
	 *
	 * @param   array  $options  Override default options using an associative array of options
	 *
	 * Options:
	 *  - minimumChars: Minimum password length (default: 8)
	 *  - strengthScaleFactor: Required password strength multiplier (default: 1)
	 *  - bannedPasswords: Custom list of banned passwords (default: long list of common passwords)
	 *  - banMode: Use strict or loose comparisons for banned passwords. "strict" = don't allow a substring of a banned
	 *  password, "loose" = only ban exact matches (default: strict)
	 *  - encoding: Character set encoding of the password (default: UTF-8)
	 */
	public function __construct(array $options = [])
	{
		$this->bannedPasswords = self::$BANLIST;

		foreach ($options as $opt => $val)
		{
			if ($opt === 'banmode')
			{
				trigger_error('The lowercase banmode option is deprecated. Use banMode instead.', E_USER_DEPRECATED);
				$opt = 'banMode';
			}

			$this->{$opt} = $val;
		}
	}

	/**
	 * Checks if a password is strong enough for use on a live site. Used to check the front-end Secret Word.
	 *
	 * @param   string  $password         The password to check
	 * @param   bool    $throwExceptions  Throw an exception if the password is not strong enough?
	 *
	 * @return  bool
	 */
	public static function isStrongEnough($password, $throwExceptions = true)
	{
		$complexify = new self();

		$res = (object) [
			'valid'      => strlen($password) >= 32,
			'complexity' => 50,
			'errors'     => (strlen($password) >= 32) ? [] : ['tooshort'],
		];

		if (function_exists('mb_strlen') && function_exists('mb_convert_encoding') &&
			function_exists('mb_substr') && function_exists('mb_convert_case'))
		{
			$res = $complexify->evaluateSecurity($password);
		}


		if ($res->valid)
		{
			return true;
		}

		if (!$throwExceptions)
		{
			return false;
		}

		$error = count($res->errors) ? array_shift($res->errors) : 'toosimple';

		$errorMessage = Platform::getInstance()->translate('COM_AKEEBA_CPANEL_ERR_FESECRETWORD_' . $error);

		throw new RuntimeException($errorMessage, 403);
	}

	/**
	 * Check the complexity of a password
	 *
	 * @param   string  $password  The password to check
	 *
	 * @return  object  StdClass object with properties "valid", "complexity", and "error"
	 *  - valid: TRUE if the password is complex enough, FALSE if it is not
	 *  - complexity: The complexity of the password as a percent
	 *  - errors: Array containing descriptions of what made the password fail. Possible values are: banned, toosimple,
	 *  tooshort
	 */
	public function evaluateSecurity($password)
	{
		$complexity = 0;
		$error      = [];

		// Reset complexity to 0 when banned password is found
		if (!$this->inBanlist($password))
		{
			// Add character complexity
			foreach (self::$CHARSETS as $charset)
			{
				$complexity += $this->additionalComplexityForCharset($password, $charset);
			}
		}
		else
		{
			array_push($error, 'banned');
			$complexity = 1;
		}

		// Use natural log to produce linear scale
		$complexity = log($complexity ** mb_strlen($password, $this->encoding)) * (1 / $this->strengthScaleFactor);

		if ($complexity <= self::$MIN_COMPLEXITY)
		{
			array_push($error, 'toosimple');
		}

		if (mb_strlen($password, $this->encoding) < $this->minimumChars)
		{
			array_push($error, 'tooshort');
		}

		// Scale to percentage, so it can be used for a progress bar
		$complexity = ($complexity / self::$MAX_COMPLEXITY) * 100;
		$complexity = ($complexity > 100) ? 100 : $complexity;

		return (object) ['valid' => (is_array($error) || $error instanceof \Countable ? count($error) : 0) === 0, 'complexity' => $complexity, 'errors' => $error];
	}

	/**
	 * Determine the complexity added from a character set if it is used in a string
	 *
	 * @param   string  $str       String to check
	 * @param   int  [2]    $charset  Array of unicode code points representing the lower and upper bound of the
	 *                             character range
	 *
	 * @return  int  0 if there are no characters from the character set, size of the character set if there are any
	 *               characters used in the string
	 */
	private function additionalComplexityForCharset($str, $charset)
	{
		$len = mb_strlen($str, $this->encoding);
		for ($i = 0; $i < $len; $i++)
		{
			$c =
				unpack('Nord', mb_convert_encoding(mb_substr($str, $i, 1, $this->encoding), 'UCS-4BE', $this->encoding));
			if ($charset[0] <= $c['ord'] && $c['ord'] <= $charset[1])
			{
				return $charset[1] - $charset[0] + 1;
			}
		}

		return 0;
	}

	/**
	 * Check if a string is in the banned password list
	 *
	 * @param   string  $str  String to check
	 *
	 * @return  bool  TRUE if $str is a banned password, or if it is a substring of a banned password and
	 *                $this->banMode is 'strict'
	 */
	private function inBanlist($str)
	{
		if ($str == '')
		{
			return false;
		}

		$str = mb_convert_case($str, MB_CASE_LOWER, $this->encoding);

		if ($this->banMode === 'strict')
		{
			for ($i = 0; $i < count($this->bannedPasswords); $i++)
			{
				if (mb_strpos($this->bannedPasswords[$i], $str, 0, $this->encoding) !== false)
				{
					return true;
				}
			}

			return false;
		}

		return in_array($str, $this->bannedPasswords);
	}
}
Util/SecureSettings.php000060400000013113152456704520011146 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;

/**
 * Implements encrypted settings handling features
 *
 * @author nicholas
 */
class SecureSettings
{
	/**
	 * The filename for the settings encryption key
	 *
	 * @var   string
	 */
	protected $keyFilename = 'serverkey.php';

	protected $key = null;

	/**
	 * Set the key filename e.g. 'serverkey.php';
	 *
	 * @param   string  $filename  The new filename to use
	 *
	 * @return  void
	 */
	public function setKeyFilename($filename)
	{
		$this->keyFilename = $filename;
	}

	/**
	 * Sets the server key, overriding an already loaded key.
	 *
	 * @param $key
	 */
	public function setKey($key)
	{
		$this->key = $key;
	}

	/**
	 * Gets the configured server key, automatically loading the server key storage file
	 * if required.
	 *
	 * @return string
	 */
	public function getKey()
	{
		if (is_null($this->key))
		{
			$this->key = '';

			if (!defined('AKEEBA_SERVERKEY'))
			{
				$filename = dirname(__FILE__) . '/../' . $this->keyFilename;
				$altFilename = $this->keyFilename;

				if (file_exists($filename))
				{
					include_once $filename;
				}
				elseif (file_exists($altFilename))
				{
					include_once $altFilename;
				}
			}

			if (defined('AKEEBA_SERVERKEY'))
			{
				$this->key = base64_decode(AKEEBA_SERVERKEY);
			}
		}

		return $this->key;
	}

	/**
	 * Do the server options allow us to use settings encryption?
	 *
	 * @return bool
	 */
	public function supportsEncryption()
	{
		// Do we have the encrypt.php plugin?
		if (!class_exists('\\Akeeba\\Engine\\Util\\Encrypt', true))
		{
			return false;
		}

		// Did the user intentionally disable settings encryption?
		$useEncryption = Platform::getInstance()->get_platform_configuration_option('useencryption', -1);

		if ($useEncryption == 0)
		{
			return false;
		}

		// Do we have base64_encode/_decode required for encryption?
		if (!function_exists('base64_encode') || !function_exists('base64_decode'))
		{
			return false;
		}

		// Pre-requisites met. We can encrypt and decrypt!
		return true;
	}

	/**
	 * Gets the preferred encryption mode. Currently, if mcrypt is installed and activated we will
	 * use AES128.
	 *
	 * @return string
	 */
	public function preferredEncryption()
	{
		$aes     = new Encrypt();
		$adapter = $aes->getAdapter();

		if (!$adapter->isSupported())
		{
			return 'CTR128';
		}

		return 'AES128';
	}

	/**
	 * Encrypts the settings using the automatically detected preferred algorithm
	 *
	 * @param   $rawSettings  string  The raw settings string
	 * @param   $key          string  The encryption key. Set to NULL to automatically find the key.
	 *
	 * @return  string  The encrypted data to store in the database
	 */
	public function encryptSettings($rawSettings, $key = null)
	{
		// Do we really support encryption?
		if (!$this->supportsEncryption())
		{
			return $rawSettings;
		}

		// Does any of the preferred encryption engines exist?
		$encryption = $this->preferredEncryption();

		if (empty($encryption))
		{
			return $rawSettings;
		}

		// Do we have a non-empty key to begin with?
		if (empty($key))
		{
			$key = $this->getKey();
		}

		if (empty($key))
		{
			return $rawSettings;
		}

		if ($encryption == 'AES128')
		{
			$encrypted = Factory::getEncryption()->AESEncryptCBC($rawSettings, $key);

			if (empty($encrypted))
			{
				$encryption = 'CTR128';
			}
			else
			{
				// Note: CBC returns the encrypted data as a binary string and requires Base 64 encoding
				$rawSettings = '###AES128###' . base64_encode($encrypted);
			}
		}

		if ($encryption == 'CTR128')
		{
			$encrypted = Factory::getEncryption()->AESEncryptCtr($rawSettings, $key, 128);

			if (!empty($encrypted))
			{
				// Note: CTR returns the encrypted data readily encoded in Base 64
				$rawSettings = '###CTR128###' . $encrypted;
			}
		}

		return $rawSettings;
	}

	/**
	 * Decrypts the encrypted settings and returns the plaintext INI string
	 *
	 * @param   string  $encrypted  The encrypted data
	 *
	 * @return  string  The decrypted data
	 */
	public function decryptSettings($encrypted, $key = null)
	{
		if (substr($encrypted, 0, 12) == '###AES128###')
		{
			$mode = 'AES128';
		}
		elseif (substr($encrypted, 0, 12) == '###CTR128###')
		{
			$mode = 'CTR128';
		}
		else
		{
			return $encrypted;
		}

		if (empty($key))
		{
			$key = $this->getKey();
		}

		if (empty($key))
		{
			return '';
		}

		$encrypted = substr($encrypted, 12);

		switch ($mode)
		{
			default:
			case 'AES128':
				$encrypted = base64_decode($encrypted);
				$decrypted = rtrim(Factory::getEncryption()->AESDecryptCBC($encrypted, $key), "\0");
				break;

			case 'CTR128':
				$decrypted = Factory::getEncryption()->AESDecryptCtr($encrypted, $key, 128);
				break;
		}

		if (empty($decrypted))
		{
			$decrypted = '';
		}

		return $decrypted;
	}
}
Util/PushMessagesInterface.php000060400000003720152456704520012432 0ustar00<?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\Util
 * @subpackage
 *
 * @copyright   A copyright
 * @license     A "Slug" license name e.g. GPL2
 */

namespace Akeeba\Engine\Util;

defined('AKEEBAENGINE') || die();

interface PushMessagesInterface
{
	/**
	 * Sends a push message to all connected devices. The intent is to provide the user with an information message,
	 * e.g. notify them about the progress of the backup.
	 *
	 * @param   string  $subject  The subject of the message, shown in the lock screen. Keep it short.
	 * @param   string  $details  Long(er) description of what the message is about. Plain text (no HTML).
	 *
	 * @return  void
	 */
	public function message($subject, $details = null);

	/**
	 * Sends a push message, containing a URL/URI, to all connected devices. The URL will be rendered as something
	 * clickable on most devices.
	 *
	 * @param   string  $url      The URL/URI
	 * @param   string  $subject  The subject of the message, shown in the lock screen. Keep it short.
	 * @param   string  $details  Long(er) description of what the message is about. Plain text (no HTML).
	 *
	 * @return  void
	 */
	public function link($url, $subject, $details = null);
}Util/TemporaryFiles.php000060400000014142152456704520011147 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;

/**
 * Temporary files management class. Handles creation, tracking and cleanup.
 */
class TemporaryFiles
{

	/**
	 * Creates a randomly-named temporary file, registers it with the temporary
	 * files management and returns its absolute path
	 *
	 * @return  string  The temporary file name
	 */
	public function createRegisterTempFile()
	{
		// Create a randomly named file in the temp directory
		$registry = Factory::getConfiguration();
		$tempFile = tempnam($registry->get('akeeba.basic.output_directory'), 'ak');

		// Register it and return its absolute path
		$tempName = basename($tempFile);

		return Factory::getTempFiles()->registerTempFile($tempName);
	}

	/**
	 * Registers a temporary file with the Akeeba Engine, storing the list of temporary files
	 * in another temporary flat database file.
	 *
	 * @param   string  $fileName  The path of the file, relative to the temporary directory
	 *
	 * @return  string  The absolute path to the temporary file, for use in file operations
	 */
	public function registerTempFile($fileName)
	{
		$configuration = Factory::getConfiguration();
		$tempFiles     = $configuration->get('volatile.tempfiles', false);
		if ($tempFiles === false)
		{
			$tempFiles = [];
		}
		else
		{
			$tempFiles = @unserialize($tempFiles);

			if ($tempFiles === false)
			{
				$tempFiles = [];
			}
		}

		if (!in_array($fileName, $tempFiles))
		{
			$tempFiles[] = $fileName;
			$configuration->set('volatile.tempfiles', serialize($tempFiles));
		}

		return Factory::getFilesystemTools()->TranslateWinPath($configuration->get('akeeba.basic.output_directory') . '/' . $fileName);
	}

	/**
	 * Unregister and delete a temporary file
	 *
	 * @param   string  $fileName      The filename to unregister and delete
	 * @param   bool    $removePrefix  The prefix to remove
	 *
	 * @return  bool  True on success
	 */
	public function unregisterAndDeleteTempFile($fileName, $removePrefix = false)
	{
		$configuration = Factory::getConfiguration();

		if ($removePrefix)
		{
			$fileName = str_replace(Factory::getFilesystemTools()->TranslateWinPath($configuration->get('akeeba.basic.output_directory')), '', $fileName);

			if ((substr($fileName, 0, 1) == '/') || (substr($fileName, 0, 1) == '\\'))
			{
				$fileName = substr($fileName, 1);
			}

			if ((substr($fileName, -1) == '/') || (substr($fileName, -1) == '\\'))
			{
				$fileName = substr($fileName, 0, -1);
			}
		}

		// Make sure this file is registered
		$configuration = Factory::getConfiguration();

		$serialised = $configuration->get('volatile.tempfiles', false);
		$tempFiles  = [];

		if ($serialised !== false)
		{
			$tempFiles = @unserialize($serialised);
		}

		if (!is_array($tempFiles))
		{
			return false;
		}

		if (!in_array($fileName, $tempFiles))
		{
			return false;
		}

		$file = $configuration->get('akeeba.basic.output_directory') . '/' . $fileName;
		Factory::getLog()->debug("-- Removing temporary file $fileName");
		$platform = strtoupper(PHP_OS);

		// Chown normally doesn't work on Windows but many years ago I found it necessary to delete temp files. No idea.
		if ((substr($platform, 0, 6) == 'CYGWIN') || (substr($platform, 0, 3) == 'WIN'))
		{
			// On Windows we have to chown() the file first to make it owned by Nobody
			Factory::getLog()->debug("-- Windows hack: chowning $fileName");
			@chown($file, 600);
		}

		$result = @$this->nullifyAndDelete($file);

		// Make sure the file is removed before unregistering it
		if (!@file_exists($file))
		{
			$aPos = array_search($fileName, $tempFiles);

			if ($aPos !== false)
			{
				unset($tempFiles[$aPos]);

				$configuration->set('volatile.tempfiles', serialize($tempFiles));
			}
		}

		return $result;
	}


	/**
	 * Deletes all temporary files
	 *
	 * @return  void
	 */
	public function deleteTempFiles()
	{
		$configuration = Factory::getConfiguration();

		$serialised = $configuration->get('volatile.tempfiles', false);
		$tempFiles  = [];

		if ($serialised !== false)
		{
			$tempFiles = @unserialize($serialised);
		}

		if (!is_array($tempFiles))
		{
			$tempFiles = [];
		}

		$fileName = null;

		if (!empty($tempFiles))
		{
			foreach ($tempFiles as $fileName)
			{
				Factory::getLog()->debug("-- Removing temporary file $fileName");
				$file     = $configuration->get('akeeba.basic.output_directory') . '/' . $fileName;
				$platform = strtoupper(PHP_OS);

				// Chown normally doesn't work on Windows but many years ago I found it necessary to delete temp files. No idea.
				if ((substr($platform, 0, 6) == 'CYGWIN') || (substr($platform, 0, 3) == 'WIN'))
				{
					// On Windows we have to chwon() the file first to make it owned by Nobody
					@chown($file, 600);
				}

				$ret = @$this->nullifyAndDelete($file);
			}
		}

		$tempFiles = [];
		$configuration->set('volatile.tempfiles', serialize($tempFiles));
	}

	/**
	 * Nullify the contents of the file and try to delete it as well
	 *
	 * @param   string  $filename  The absolute path to the file to delete
	 *
	 * @return  bool  True of the deletion is successful
	 */
	public function nullifyAndDelete($filename)
	{
		// Try to nullify (method #1)
		$fp = @fopen($filename, 'w');

		if (is_resource($fp))
		{
			@fclose($fp);
		}
		else
		{
			// Try to nullify (method #2)
			@file_put_contents($filename, '');
		}

		// Unlink
		return @unlink($filename);
	}
}
Util/FileLister.php000060400000007521152456704520010247 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;

/* Windows system detection */
if (!defined('_AKEEBA_IS_WINDOWS'))
{
	$isWindows = DIRECTORY_SEPARATOR == '\\';

	if (function_exists('php_uname'))
	{
		$isWindows = stristr(php_uname(), 'windows');
	}

	define('_AKEEBA_IS_WINDOWS', $isWindows);
}

/**
 * A filesystem scanner, for internal use
 */
class FileLister
{
	public function &getFiles($folder, $fullpath = false)
	{
		// Initialize variables
		$arr   = [];
		$false = false;

		if (!is_dir($folder) && !is_dir($folder . '/'))
		{
			return $false;
		}

		$handle = @opendir($folder);
		if ($handle === false)
		{
			$handle = @opendir($folder . '/');
		}
		// If directory is not accessible, just return FALSE
		if ($handle === false)
		{
			return $false;
		}

		$registry            = Factory::getConfiguration();
		$dereferencesymlinks = $registry->get('engine.archiver.common.dereference_symlinks');

		while ((($file = @readdir($handle)) !== false))
		{
			if (($file != '.') && ($file != '..'))
			{
				// # Fix 2.4.b1: Do not add DS if we are on the site's root and it's an empty string
				// # Fix 2.4.b2: Do not add DS is the last character _is_ DS
				$ds     = ($folder == '') || ($folder == '/') || (@substr($folder, -1) == '/') || (@substr($folder, -1) == DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR;
				$dir    = "$folder/$file";
				$isDir  = @is_dir($dir);
				$isLink = @is_link($dir);

				//if (!$isDir || ($isDir && $isLink && !$dereferencesymlinks) ) {
				if (!$isDir)
				{
					if ($fullpath)
					{
						$data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($dir) : $dir;
					}
					else
					{
						$data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($file) : $file;
					}
					if ($data)
					{
						$arr[] = $data;
					}
				}
			}
		}
		@closedir($handle);

		return $arr;
	}

	public function &getFolders($folder, $fullpath = false)
	{
		// Initialize variables
		$arr   = [];
		$false = false;

		if (!is_dir($folder) && !is_dir($folder . '/'))
		{
			return $false;
		}

		$handle = @opendir($folder);
		if ($handle === false)

		{
			$handle = @opendir($folder . '/');
		}

		// If directory is not accessible, just return FALSE
		if ($handle === false)
		{
			return $false;
		}

		$registry            = Factory::getConfiguration();
		$dereferencesymlinks = $registry->get('engine.archiver.common.dereference_symlinks');

		while ((($file = @readdir($handle)) !== false))
		{
			if (($file != '.') && ($file != '..'))
			{
				$dir    = "$folder/$file";
				$isDir  = @is_dir($dir);
				$isLink = @is_link($dir);

				if ($isDir)
				{
					//if(!$dereferencesymlinks && $isLink) continue;
					if ($fullpath)
					{
						$data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($dir) : $dir;
					}
					else
					{
						$data = _AKEEBA_IS_WINDOWS ? Factory::getFilesystemTools()->TranslateWinPath($file) : $file;
					}

					if ($data)
					{
						$arr[] = $data;
					}
				}
			}
		}
		@closedir($handle);

		return $arr;
	}
}
Util/Encrypt.php000060400000066365152456704520007644 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Util\AesAdapter\AdapterInterface;
use Akeeba\Engine\Util\AesAdapter\Mcrypt;
use Akeeba\Engine\Util\AesAdapter\OpenSSL;

/**
 * AES implementation in PHP (c) Chris Veness 2005-2016.
 * Right to use and adapt is granted for under a simple creative commons attribution
 * licence. No warranty of any form is offered.
 *
 * Heavily modified for Akeeba Backup by Nicholas K. Dionysopoulos
 * Also added AES-128 CBC mode (with mcrypt and OpenSSL) on top of AES CTR
 */
class Encrypt
{
	use HashTrait;

	// Sbox is pre-computed multiplicative inverse in GF(2^8) used in SubBytes and KeyExpansion [�5.1.1]
	protected $Sbox =
		[
			0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
			0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
			0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
			0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
			0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
			0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
			0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
			0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
			0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
			0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
			0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
			0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
			0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
			0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
			0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
			0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16,
		];

	// Rcon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [�5.2]
	protected $Rcon = [
		[0x00, 0x00, 0x00, 0x00],
		[0x01, 0x00, 0x00, 0x00],
		[0x02, 0x00, 0x00, 0x00],
		[0x04, 0x00, 0x00, 0x00],
		[0x08, 0x00, 0x00, 0x00],
		[0x10, 0x00, 0x00, 0x00],
		[0x20, 0x00, 0x00, 0x00],
		[0x40, 0x00, 0x00, 0x00],
		[0x80, 0x00, 0x00, 0x00],
		[0x1b, 0x00, 0x00, 0x00],
		[0x36, 0x00, 0x00, 0x00],
	];

	protected $passwords = [];

	/**
	 * The algorithm to use for PBKDF2. Must be a supported hash_hmac algorithm. Default: sha1
	 *
	 * @var  string
	 */
	private $pbkdf2Algorithm = 'sha1';

	/**
	 * Number of iterations to use for PBKDF2
	 *
	 * @var  int
	 */
	private $pbkdf2Iterations = 1000;

	/**
	 * Should we use a static salt for PBKDF2?
	 *
	 * @var  int
	 */
	private $pbkdf2UseStaticSalt = 0;

	/**
	 * The static salt to use for PBKDF2
	 *
	 * @var  string
	 */
	private $pbkdf2StaticSalt = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";

	/**
	 * AES Cipher function: encrypt 'input' with Rijndael algorithm
	 *
	 * @param   string  $input  message as byte-array (16 bytes)
	 * @param   array   $w      key schedule as 2D byte-array (Nr+1 x Nb bytes) -
	 *                          generated from the cipher key by KeyExpansion()
	 *
	 * @return      array  ciphertext as byte-array (16 bytes)
	 */
	public function Cipher($input, $w)
	{
		// main Cipher function [�5.1]
		$Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES)
		$Nr = count($w) / $Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys

		$state = []; // initialise 4xNb byte-array 'state' with input [�3.4]

		for ($i = 0; $i < 4 * $Nb; $i++)
		{
			$state[$i % 4][(int) floor($i / 4)] = $input[$i];
		}

		$state = $this->AddRoundKey($state, $w, 0, $Nb);

		for ($round = 1; $round < $Nr; $round++)
		{
			// apply Nr rounds
			$state = $this->SubBytes($state, $Nb);
			$state = $this->ShiftRows($state, $Nb);
			$state = $this->MixColumns($state, $Nb);
			$state = $this->AddRoundKey($state, $w, $round, $Nb);
		}

		$state = $this->SubBytes($state, $Nb);
		$state = $this->ShiftRows($state, $Nb);
		$state = $this->AddRoundKey($state, $w, $Nr, $Nb);

		$output = [4 * $Nb]; // convert state to 1-d array before returning [�3.4]

		for ($i = 0; $i < 4 * $Nb; $i++)
		{
			$output[$i] = $state[$i % 4][(int) floor($i / 4)];
		}

		return $output;
	}

	/**
	 * Key expansion for Rijndael Cipher(): performs key expansion on cipher key
	 * to generate a key schedule
	 *
	 * @param   array  $key  cipher key byte-array (16 bytes)
	 *
	 * @return    array key schedule as 2D byte-array (Nr+1 x Nb bytes)
	 */
	public function KeyExpansion($key)
	{
		// generate Key Schedule from Cipher Key [�5.2]
		$Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES)
		$Nk = count($key) / 4; // key length (in words): 4/6/8 for 128/192/256-bit keys
		$Nr = $Nk + 6; // no of rounds: 10/12/14 for 128/192/256-bit keys

		$w    = [];
		$temp = [];

		for ($i = 0; $i < $Nk; $i++)
		{
			$r     = [$key[4 * $i], $key[4 * $i + 1], $key[4 * $i + 2], $key[4 * $i + 3]];
			$w[$i] = $r;
		}

		for ($i = $Nk; $i < ($Nb * ($Nr + 1)); $i++)
		{
			$w[(int) $i] = [];

			for ($t = 0; $t < 4; $t++)
			{
				$temp[$t] = $w[(int) $i - 1][$t];
			}

			if ($i % $Nk == 0)
			{
				$temp = $this->SubWord($this->RotWord($temp));

				for ($t = 0; $t < 4; $t++)
				{
					$temp[$t] ^= $this->Rcon[(int) ($i / $Nk)][$t];
				}
			}
			elseif ($Nk > 6 && $i % $Nk == 4)
			{
				$temp = $this->SubWord($temp);
			}

			for ($t = 0; $t < 4; $t++)
			{
				$w[(int) $i][$t] = $w[(int) $i - $Nk][$t] ^ $temp[$t];
			}
		}

		return $w;
	}

	/**
	 * Encrypt a text using AES encryption in Counter mode of operation
	 *  - see http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
	 *
	 * Unicode multi-byte character safe
	 *
	 * @param   string  $plaintext  source text to be encrypted
	 * @param   string  $password   the password to use to generate a key
	 * @param   int     $nBits      number of bits to be used in the key (128, 192, or 256)
	 *
	 * @return string encrypted text
	 */
	public function AESEncryptCtr($plaintext, $password, $nBits)
	{
		$blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES

		// standard allows 128/192/256 bit keys
		if (!($nBits == 128 || $nBits == 192 || $nBits == 256))
		{
			return '';
		}

		// note PHP (5) gives us plaintext and password in UTF8 encoding!

		// use AES itself to encrypt password to get cipher key (using plain password as source for
		// key expansion) - gives us well encrypted key
		$nBytes  = $nBits / 8; // no bytes in key
		$pwBytes = [];

		for ($i = 0; $i < $nBytes; $i++)
		{
			$pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff;
		}

		$key = $this->Cipher($pwBytes, $this->KeyExpansion($pwBytes));
		$key = array_merge($key, array_slice($key, 0, $nBytes - 16)); // expand key to 16/24/32 bytes long

		// initialise counter block (NIST SP800-38A �B.2): millisecond time-stamp for nonce in
		// 1st 8 bytes, block counter in 2nd 8 bytes
		$counterBlock = [];
		$nonce        = floor(microtime(true) * 1000); // timestamp: milliseconds since 1-Jan-1970
		$nonceSec     = floor($nonce / 1000);
		$nonceMs      = $nonce % 1000;

		// encode nonce with seconds in 1st 4 bytes, and (repeated) ms part filling 2nd 4 bytes
		for ($i = 0; $i < 4; $i++)
		{
			$counterBlock[$i] = $this->urs($nonceSec, $i * 8) & 0xff;
		}

		for ($i = 0; $i < 4; $i++)
		{
			$counterBlock[$i + 4] = $nonceMs & 0xff;
		}

		// and convert it to a string to go on the front of the ciphertext
		$ctrTxt = '';

		for ($i = 0; $i < 8; $i++)
		{
			$ctrTxt .= chr($counterBlock[$i]);
		}

		// generate key schedule - an expansion of the key into distinct Key Rounds for each round
		$keySchedule = $this->KeyExpansion($key);

		$blockCount = ceil(strlen($plaintext) / $blockSize);
		$ciphertxt  = []; // ciphertext as array of strings

		for ($b = 0; $b < $blockCount; $b++)
		{
			// set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
			// done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB)
			for ($c = 0; $c < 4; $c++)
			{
				$counterBlock[15 - $c] = $this->urs($b, $c * 8) & 0xff;
			}

			for ($c = 0; $c < 4; $c++)
			{
				$counterBlock[15 - $c - 4] = $this->urs($b / 0x100000000, $c * 8);
			}

			$cipherCntr = $this->Cipher($counterBlock, $keySchedule); // -- encrypt counter block --

			// block size is reduced on final block
			$blockLength = $b < $blockCount - 1 ? $blockSize : (strlen($plaintext) - 1) % $blockSize + 1;
			$cipherByte  = [];

			for ($i = 0; $i < $blockLength; $i++)
			{ // -- xor plaintext with ciphered counter byte-by-byte --
				$cipherByte[$i] = $cipherCntr[$i] ^ ord(substr($plaintext, $b * $blockSize + $i, 1));
				$cipherByte[$i] = chr($cipherByte[$i]);
			}

			$ciphertxt[$b] = implode('', $cipherByte); // escape troublesome characters in ciphertext
		}

		// implode is more efficient than repeated string concatenation
		$ciphertext = $ctrTxt . implode('', $ciphertxt);
		$ciphertext = base64_encode($ciphertext);

		return $ciphertext;
	}

	/**
	 * Decrypt a text encrypted by AES in counter mode of operation
	 *
	 * @param   string  $ciphertext  source text to be decrypted
	 * @param   string  $password    the password to use to generate a key
	 * @param   int     $nBits       number of bits to be used in the key (128, 192, or 256)
	 *
	 * @return string decrypted text
	 */
	public function AESDecryptCtr($ciphertext, $password, $nBits)
	{
		$blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES

		// standard allows 128/192/256 bit keys
		if (!($nBits == 128 || $nBits == 192 || $nBits == 256))
		{
			return '';
		}

		$ciphertext = base64_decode($ciphertext);

		// use AES to encrypt password (mirroring encrypt routine)
		$nBytes  = $nBits / 8; // no bytes in key
		$pwBytes = [];

		for ($i = 0; $i < $nBytes; $i++)
		{
			$pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff;
		}

		$key = $this->Cipher($pwBytes, $this->KeyExpansion($pwBytes));
		$key = array_merge($key, array_slice($key, 0, $nBytes - 16)); // expand key to 16/24/32 bytes long

		// recover nonce from 1st element of ciphertext
		$counterBlock = [];
		$ctrTxt       = substr($ciphertext, 0, 8);

		for ($i = 0; $i < 8; $i++)
		{
			$counterBlock[$i] = ord(substr($ctrTxt, $i, 1));
		}

		// generate key schedule
		$keySchedule = $this->KeyExpansion($key);

		// separate ciphertext into blocks (skipping past initial 8 bytes)
		$nBlocks = ceil((strlen($ciphertext) - 8) / $blockSize);
		$ct      = [];

		for ($b = 0; $b < $nBlocks; $b++)
		{
			$ct[$b] = substr($ciphertext, 8 + $b * $blockSize, 16);
		}

		$ciphertext = $ct; // ciphertext is now array of block-length strings

		// plaintext will get generated block-by-block into array of block-length strings
		$plaintxt = [];

		for ($b = 0; $b < $nBlocks; $b++)
		{
			// set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
			for ($c = 0; $c < 4; $c++)
			{
				$counterBlock[15 - $c] = $this->urs($b, $c * 8) & 0xff;
			}

			for ($c = 0; $c < 4; $c++)
			{
				$counterBlock[15 - $c - 4] = $this->urs(($b + 1) / 0x100000000 - 1, $c * 8) & 0xff;
			}

			$cipherCntr = $this->Cipher($counterBlock, $keySchedule); // encrypt counter block

			$plaintxtByte = [];

			for ($i = 0; $i < strlen($ciphertext[$b]); $i++)
			{
				// -- xor plaintext with ciphered counter byte-by-byte --
				$plaintxtByte[$i] = $cipherCntr[$i] ^ ord(substr($ciphertext[$b], $i, 1));
				$plaintxtByte[$i] = chr($plaintxtByte[$i]);
			}

			$plaintxt[$b] = implode('', $plaintxtByte);
		}

		// join array of blocks into single plaintext string
		$plaintext = implode('', $plaintxt);

		return $plaintext;
	}

	/**
	 * AES encryption in CBC mode. This is the standard mode (the CTR methods
	 * actually use Rijndael-128 in CTR mode, which - technically - isn't AES).
	 * The data length is tucked as a 32-bit unsigned integer (little endian)
	 * after the ciphertext. It supports AES-128 only.
	 *
	 * @param   string  $plaintext  The data to encrypt
	 * @param   string  $password   Encryption password
	 *
	 * @return  string  The ciphertext
	 * @author Nicholas K. Dionysopoulos
	 *
	 * @since  3.0.1
	 */
	public function AESEncryptCBC($plaintext, $password)
	{
		$adapter = $this->getAdapter();

		if (!$adapter->isSupported())
		{
			return false;
		}

		// Get encryption parameters
		$rand          = new RandomValue();
		$params        = $this->getKeyDerivationParameters();
		$useStaticSalt = $params['useStaticSalt'];
		$keySizeBytes  = $params['keySize'];
		$salt          = null;

		if ($useStaticSalt)
		{
			$key = $this->getStaticSaltExpandedKey($password);
		}
		else
		{
			// Create a salt and derive a key from the password using PBKDF2
			$algorithm  = $params['algorithm'];
			$iterations = $params['iterations'];
			$salt       = $rand->generate(64);
			$key        = $this->pbkdf2($password, $salt, $algorithm, $iterations, $keySizeBytes);
		}


		// Also create a new, random IV
		$iv = $rand->generate($keySizeBytes);

		// The ciphertext is the encrypted string...
		$ciphertext = $adapter->encrypt($plaintext, $key, $iv);

		// ...minus the IV which was placed in front
		$ciphertext = substr($ciphertext, $keySizeBytes);

		if (!$useStaticSalt)
		{
			// ...plus the PBKDF2 setup values at the end (68 bytes)...
			$ciphertext .= 'JPST' . $salt;
		}

		// ...plus the IV at the end (20 bytes)...
		$ciphertext .= 'JPIV' . $iv;

		// ...plus the plaintext length (4 bytes).
		$ciphertext .= pack('V', strlen($plaintext));

		return $ciphertext;
	}

	/**
	 * Get the parameters fed into PBKDF2 to expand the user password into an encryption key. These are the static
	 * parameters (key size, hashing algorithm and number of iterations). A new salt is used for each encryption block
	 * to minimize the risk of attacks against the password.
	 *
	 * @return  array
	 */
	public function getKeyDerivationParameters()
	{
		return [
			'keySize'       => 16,
			'algorithm'     => $this->pbkdf2Algorithm,
			'iterations'    => $this->pbkdf2Iterations,
			'useStaticSalt' => $this->pbkdf2UseStaticSalt,
			'staticSalt'    => $this->pbkdf2StaticSalt,
		];
	}

	/**
	 * AES decryption in CBC mode. This is the standard mode (the CTR methods
	 * actually use Rijndael-128 in CTR mode, which - technically - isn't AES).
	 *
	 * It supports AES-128 only. It assumes that the last 4 bytes
	 * contain a little-endian unsigned long integer representing the unpadded
	 * data length.
	 *
	 * @param   string  $ciphertext  The data to encrypt
	 * @param   string  $password    Encryption password
	 *
	 * @return  string  The plaintext
	 * @author Nicholas K. Dionysopoulos
	 *
	 * @since  3.0.1
	 */
	public function AESDecryptCBC($ciphertext, $password)
	{
		$adapter = $this->getAdapter();

		if (!$adapter->isSupported())
		{
			return false;
		}

		// Read the data size
		$data_size = unpack('V', substr($ciphertext, -4));

		// Do I have a PBKDF2 salt?
		$salt             = substr($ciphertext, -92, 68);
		$rightStringLimit = -4;

		$params        = $this->getKeyDerivationParameters();
		$keySizeBytes  = $params['keySize'];
		$algorithm     = $params['algorithm'];
		$iterations    = $params['iterations'];
		$useStaticSalt = $params['useStaticSalt'];

		if (substr($salt, 0, 4) == 'JPST')
		{
			// We have a stored salt. Retrieve it and tell decrypt to process the string minus the last 44 bytes
			// (4 bytes for JPST, 16 bytes for the salt, 4 bytes for JPIV, 16 bytes for the IV, 4 bytes for the
			// uncompressed string length - note that using PBKDF2 means we're also using a randomized IV per the
			// format specification).
			$salt             = substr($salt, 4);
			$rightStringLimit -= 68;

			$key = $this->pbkdf2($password, $salt, $algorithm, $iterations, $keySizeBytes);
		}
		elseif ($useStaticSalt)
		{
			// We have a static salt. Use it for PBKDF2.
			$key = $this->getStaticSaltExpandedKey($password);
		}
		else
		{
			// Get the expanded key from the password. THIS USES THE OLD, INSECURE METHOD.
			$key = $this->expandKey($password);
		}

		// Try to get the IV from the data
		$iv = substr($ciphertext, -24, 20);

		if (substr($iv, 0, 4) == 'JPIV')
		{
			// We have a stored IV. Retrieve it and tell mdecrypt to process the string minus the last 24 bytes
			// (4 bytes for JPIV, 16 bytes for the IV, 4 bytes for the uncompressed string length)
			$iv               = substr($iv, 4);
			$rightStringLimit -= 20;
		}
		else
		{
			// No stored IV. Do it the dumb way.
			$iv = $this->createTheWrongIV($password);
		}

		// Decrypt
		$plaintext = $adapter->decrypt($iv . substr($ciphertext, 0, $rightStringLimit), $key);

		// Trim padding, if necessary
		if (strlen($plaintext) > $data_size)
		{
			$plaintext = substr($plaintext, 0, $data_size);
		}

		return $plaintext;
	}

	/**
	 * That's the old way of creating an IV that's definitely not cryptographically sound.
	 *
	 * DO NOT USE, EVER, UNLESS YOU WANT TO DECRYPT LEGACY DATA
	 *
	 * @param   string  $password  The raw password from which we create an IV in a super bozo way
	 *
	 * @return  string  A 16-byte IV string
	 *
	 * @since   4.6.0
	 * @author  Nicholas K. Dionysopoulos
	 */
	function createTheWrongIV($password)
	{
		static $ivs = [];

		$key = self::md5($password);

		if (!isset($ivs[$key]))
		{
			// Create an Initialization Vector (IV) based on the password, using the same technique as for the key
			$nBytes  = 16; // AES uses a 128 -bit (16 byte) block size, hence the IV size is always 16 bytes
			$pwBytes = [];

			for ($i = 0; $i < $nBytes; $i++)
			{
				$pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff;
			}

			$iv    = $this->Cipher($pwBytes, $this->KeyExpansion($pwBytes));
			$newIV = '';

			foreach ($iv as $int)
			{
				$newIV .= chr($int);
			}

			$ivs[$key] = $newIV;
		}

		return $ivs[$key];
	}

	/*
	 * Unsigned right shift function, since PHP has neither >>> operator nor unsigned ints
	 *
	 * @param a  number to be shifted (32-bit integer)
	 * @param b  number of bits to shift a to the right (0..31)
	 * @return   a right-shifted and zero-filled by b bits
	 */

	/**
	 * Expand the password to an appropriate 128-bit encryption key. THIS CODE IS OBSOLETE. DO NOT USE.
	 *
	 * @param   string  $password
	 *
	 * @return  string
	 *
	 * @since   5.2.0
	 * @author  Nicholas K. Dionysopoulos
	 */
	public function expandKey($password)
	{
		// Try to fetch cached key or create it if it doesn't exist
		$nBits     = 128;
		$lookupKey = self::md5($password . '-' . $nBits);

		if (array_key_exists($lookupKey, $this->passwords))
		{
			$key = $this->passwords[$lookupKey];

			return $key;
		}

		// use AES itself to encrypt password to get cipher key (using plain password as source for
		// key expansion) - gives us well encrypted key.
		$nBytes  = $nBits / 8; // Number of bytes in key
		$pwBytes = [];

		for ($i = 0; $i < $nBytes; $i++)
		{
			$pwBytes[$i] = ord(substr($password, $i, 1)) & 0xff;
		}

		$key    = $this->Cipher($pwBytes, $this->KeyExpansion($pwBytes));
		$key    = array_merge($key, array_slice($key, 0, $nBytes - 16)); // expand key to 16/24/32 bytes long
		$newKey = '';

		foreach ($key as $int)
		{
			$newKey .= chr($int);
		}

		$key = $newKey;

		$this->passwords[$lookupKey] = $key;

		return $key;
	}

	/**
	 * Returns the correct AES-128 CBC encryption adapter
	 *
	 * @return  AdapterInterface
	 *
	 * @since   5.2.0
	 * @author  Nicholas K. Dionysopoulos
	 */
	public function getAdapter()
	{
		static $adapter = null;

		if (is_object($adapter) && ($adapter instanceof AdapterInterface))
		{
			return $adapter;
		}

		$adapter = new OpenSSL();

		if (!$adapter->isSupported())
		{
			$adapter = new Mcrypt();
		}

		return $adapter;
	}

	/**
	 * Returns the length of a string in BYTES, not characters
	 *
	 * @param   string  $string  The string to get the length for
	 *
	 * @return int The size in BYTES
	 */
	public function stringLength($string)
	{
		return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string);
	}

	/**
	 * Attempt to use mbstring for getting parts of strings
	 *
	 * @param   string    $string
	 * @param   int       $start
	 * @param   int|null  $length
	 *
	 * @return  string
	 */
	public function subString($string, $start, $length = null)
	{
		return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') :
			substr($string, $start, $length);
	}

	/**
	 * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
	 *
	 * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
	 *
	 * This implementation of PBKDF2 was originally created by https://defuse.ca
	 * With improvements by http://www.variations-of-shadow.com
	 * Modified for Akeeba Engine by Akeeba Ltd (removed unnecessary checks to make it faster)
	 *
	 * @param   string  $password    The password.
	 * @param   string  $salt        A salt that is unique to the password.
	 * @param   string  $algorithm   The hash algorithm to use. Default is sha1.
	 * @param   int     $count       Iteration count. Higher is better, but slower. Default: 1000.
	 * @param   int     $key_length  The length of the derived key in bytes.
	 *
	 * @return  string  A string of $key_length bytes
	 */
	public function pbkdf2($password, $salt, $algorithm = 'sha1', $count = 1000, $key_length = 16)
	{
		if (function_exists("hash_pbkdf2"))
		{
			return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, true);
		}

		$hash_length = $this->stringLength(hash($algorithm, "", true));
		$block_count = ceil($key_length / $hash_length);

		$output = "";

		for ($i = 1; $i <= $block_count; $i++)
		{
			// $i encoded as 4 bytes, big endian.
			$last = $salt . pack("N", $i);

			// First iteration
			$xorResult = hash_hmac($algorithm, $last, $password, true);
			$last      = $xorResult;

			// Perform the other $count - 1 iterations
			for ($j = 1; $j < $count; $j++)
			{
				$last      = hash_hmac($algorithm, $last, $password, true);
				$xorResult ^= $last;
			}

			$output .= $xorResult;
		}

		return $this->subString($output, 0, $key_length);
	}

	/**
	 * @return string
	 */
	public function getPbkdf2Algorithm()
	{
		return $this->pbkdf2Algorithm;
	}

	/**
	 * @param   string  $pbkdf2Algorithm
	 *
	 * @return Encrypt
	 */
	public function setPbkdf2Algorithm($pbkdf2Algorithm)
	{
		$this->pbkdf2Algorithm = $pbkdf2Algorithm;

		return $this;
	}

	/**
	 * @return int
	 */
	public function getPbkdf2Iterations()
	{
		return $this->pbkdf2Iterations;
	}

	/**
	 * @param   int  $pbkdf2Iterations
	 *
	 * @return Encrypt
	 */
	public function setPbkdf2Iterations($pbkdf2Iterations)
	{
		$this->pbkdf2Iterations = $pbkdf2Iterations;

		return $this;
	}

	/**
	 * @return int
	 */
	public function getPbkdf2UseStaticSalt()
	{
		return $this->pbkdf2UseStaticSalt;
	}

	/**
	 * @param   int  $pbkdf2UseStaticSalt
	 *
	 * @return Encrypt
	 */
	public function setPbkdf2UseStaticSalt($pbkdf2UseStaticSalt)
	{
		$this->pbkdf2UseStaticSalt = $pbkdf2UseStaticSalt;

		return $this;
	}

	/**
	 * @return string
	 */
	public function getPbkdf2StaticSalt()
	{
		return $this->pbkdf2StaticSalt;
	}

	/**
	 * @param   string  $pbkdf2StaticSalt
	 *
	 * @return Encrypt
	 */
	public function setPbkdf2StaticSalt($pbkdf2StaticSalt)
	{
		$this->pbkdf2StaticSalt = $pbkdf2StaticSalt;

		return $this;
	}

	/**
	 * Get the expanded key from the user supplied password using a static salt. The results are cached for performance
	 * reasons.
	 *
	 * @param   string  $password  The user-supplied password, UTF-8 encoded.
	 *
	 * @return  string  The expanded key
	 */
	public function getStaticSaltExpandedKey($password)
	{
		$params       = $this->getKeyDerivationParameters();
		$keySizeBytes = $params['keySize'];
		$algorithm    = $params['algorithm'];
		$iterations   = $params['iterations'];
		$staticSalt   = $params['staticSalt'];

		$lookupKey = "PBKDF2-$algorithm-$iterations-" . self::md5($password . $staticSalt);

		if (!array_key_exists($lookupKey, $this->passwords))
		{
			$this->passwords[$lookupKey] = $this->pbkdf2($password, $staticSalt, $algorithm, $iterations, $keySizeBytes);
		}

		return $this->passwords[$lookupKey];
	}

	protected function AddRoundKey($state, $w, $rnd, $Nb)
	{
		// xor Round Key into state S [�5.1.4]
		for ($r = 0; $r < 4; $r++)
		{
			for ($c = 0; $c < $Nb; $c++)
			{
				$state[$r][$c] ^= $w[$rnd * 4 + $c][$r];
			}
		}

		return $state;
	}

	protected function SubBytes($s, $Nb)
	{
		// apply SBox to state S [�5.1.1]
		for ($r = 0; $r < 4; $r++)
		{
			for ($c = 0; $c < $Nb; $c++)
			{
				$s[$r][$c] = $this->Sbox[$s[$r][$c]];
			}
		}

		return $s;
	}

	protected function ShiftRows($s, $Nb)
	{
		// shift row r of state S left by r bytes [�5.1.2]
		$t = [4];

		for ($r = 1; $r < 4; $r++)
		{
			// shift into temp copy
			for ($c = 0; $c < 4; $c++)
			{
				$t[$c] = $s[$r][($c + $r) % $Nb];
			}

			// and copy back
			for ($c = 0; $c < 4; $c++)
			{
				$s[$r][$c] = $t[$c];
			}

		}

		// note that this will work for Nb=4,5,6, but not 7,8 (always 4 for AES):

		return $s; // see fp.gladman.plus.com/cryptography_technology/rijndael/aes.spec.311.pdf
	}

	protected function MixColumns($s, $Nb)
	{
		// combine bytes of each col of state S [�5.1.3]
		for ($c = 0; $c < 4; $c++)
		{
			$a = [4]; // 'a' is a copy of the current column from 's'
			$b = [4]; // 'b' is a�{02} in GF(2^8)

			for ($i = 0; $i < 4; $i++)
			{
				$a[$i] = $s[$i][$c];
				$b[$i] = $s[$i][$c] & 0x80 ? $s[$i][$c] << 1 ^ 0x011b : $s[$i][$c] << 1;
			}

			// a[n] ^ b[n] is a�{03} in GF(2^8)
			$s[0][$c] = $b[0] ^ $a[1] ^ $b[1] ^ $a[2] ^ $a[3]; // 2*a0 + 3*a1 + a2 + a3
			$s[1][$c] = $a[0] ^ $b[1] ^ $a[2] ^ $b[2] ^ $a[3]; // a0 * 2*a1 + 3*a2 + a3
			$s[2][$c] = $a[0] ^ $a[1] ^ $b[2] ^ $a[3] ^ $b[3]; // a0 + a1 + 2*a2 + 3*a3
			$s[3][$c] = $a[0] ^ $b[0] ^ $a[1] ^ $a[2] ^ $b[3]; // 3*a0 + a1 + a2 + 2*a3
		}

		return $s;
	}

	protected function SubWord($w)
	{
		// apply SBox to 4-byte word w
		for ($i = 0; $i < 4; $i++)
		{
			$w[$i] = $this->Sbox[$w[$i]];
		}

		return $w;
	}

	protected function RotWord($w)
	{
		// rotate 4-byte word w left by one byte
		$tmp = $w[0];

		for ($i = 0; $i < 3; $i++)
		{
			$w[$i] = $w[$i + 1];
		}

		$w[3] = $tmp;

		return $w;
	}

	protected function urs($a, $b)
	{
		$a &= 0xffffffff;
		$b &= 0x1f; // (bounds check)

		if ($a & 0x80000000 && $b > 0)
		{
			// if left-most bit set
			$a = ($a >> 1) & 0x7fffffff; //   right-shift one bit & clear left-most bit
			$a = $a >> ($b - 1); //   remaining right-shifts
		}
		else
		{
			// otherwise
			$a = ($a >> $b); //   use normal right-shift
		}

		return $a;
	}
}
Util/Statistics.php000060400000016527152456704520010345 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Exception;

class Statistics
{
	/** @var bool used to block multipart updating initializing the backup */
	private $multipart_lock = true;

	/** @var int The statistics record number of the current backup attempt */
	private $statistics_id = null;

	/** @var array Local cache of the stat record data */
	private $cached_data = [];

	/**
	 * Returns all the filenames of the backup archives for the specified stat record,
	 * or null if the backup type is wrong or the file doesn't exist. It takes into
	 * account the multipart nature of Split Backup Archives.
	 *
	 * @param   array  $stat             The backup statistics record
	 * @param   bool   $skipNonComplete  Skips over backups with no files produced
	 *
	 * @return array|null The filenames or null if it's not applicable
	 */
	public static function get_all_filenames($stat, $skipNonComplete = true)
	{
		// Shortcut for database entries marked as having no files
		if ($stat['filesexist'] == 0)
		{
			return [];
		}

		// Initialize
		$base_directory = @dirname($stat['absolute_path']);
		$base_filename  = $stat['archivename'];
		$filenames      = [$base_filename];

		if (empty($base_filename))
		{
			// This is a backup with a writer which doesn't store files on the server
			return null;
		}

		// Calculate all the filenames for this backup
		if ($stat['multipart'] > 1)
		{
			// Find the base filename and extension
			$dotpos    = strrpos($base_filename, '.');
			$extension = substr($base_filename, $dotpos);
			$basefile  = substr($base_filename, 0, $dotpos);

			// Calculate the multiple names
			$multipart = $stat['multipart'];

			for ($i = 1; $i < $multipart; $i++)
			{
				// Note: For $multipart = 10, it will produce i.e. .z01 through .z10
				// This is intentional. If the backup aborts and multipart=1, we
				// might be stuck with a .z01 file instead of a .zip. So do not
				// change the less than or equal with a straight less than.
				$filenames[] = $basefile . substr($extension, 0, 2) . sprintf('%02d', $i);
			}
		}

		// Check if the files exist, otherwise attempt to provide relocated filename
		$ret = [];

		$ds = DIRECTORY_SEPARATOR;
		// $test_file is the first file which must have been created
		$test_file = count($filenames) == 1 ? $filenames[0] : $filenames[1];

		if (
			(!@file_exists($base_directory . $ds . $test_file)) ||
			(!is_dir($base_directory))
		)
		{
			// The test file wasn't detected. Use the configured output directory.
			$registry       = Factory::getConfiguration();
			$base_directory = $registry->get('akeeba.basic.output_directory');
		}

		foreach ($filenames as $filename)
		{
			// Turn relative path to absolute
			$filename = $base_directory . $ds . $filename;

			// Return the new filename IF IT EXISTS!
			if (!@file_exists($filename))
			{
				$filename = '';
			}

			// Do not return filename for invalid backups
			if (!empty($filename))
			{
				$ret[] = $filename;
			}
		}

		// Edge case: still running backups, we have to brute force the scan
		// of existing files (multipart may be lying)
		if ($stat['status'] == 'run')
		{
			$base_filename = $stat['archivename'];
			$dotpos        = strrpos($base_filename, '.');
			$extension     = substr($base_filename, $dotpos);
			$basefile      = substr($base_filename, 0, $dotpos);

			$registry = Factory::getConfiguration();
			$dirs     = [
				@dirname($stat['absolute_path']),
				$registry->get('akeeba.basic.output_directory'),
			];

			// Look for base file
			foreach ($dirs as $dir)
			{
				if (@file_exists($dir . $ds . $base_filename))
				{
					$ret[] = $dir . $ds . $base_filename;

					break;
				}
			}

			// Look for added files
			$found = true;
			$i     = 0;

			while ($found)
			{
				$i++;
				$found          = false;
				$part_file_name = $basefile . substr($extension, 0, 2) . sprintf('%02d', $i);

				foreach ($dirs as $dir)
				{
					if (@file_exists($dir . $ds . $part_file_name))
					{
						$ret[] = $dir . $ds . $part_file_name;
						$found = true;

						break;
					}
				}
			}
		}

		if ((count($ret) == 0) && $skipNonComplete)
		{
			$ret = null;
		}

		if (!empty($ret) && is_array($ret))
		{
			$ret = array_unique($ret);
		}

		return $ret;
	}

	/**
	 * Releases the initial multipart lock
	 */
	public function release_multipart_lock()
	{
		$this->multipart_lock = false;
	}

	/**
	 * Updates the multipart status of the current backup attempt's statistics record
	 *
	 * @param   int  $multipart  The new multipart status
	 */
	public function updateMultipart($multipart)
	{
		if ($this->multipart_lock)
		{
			return;
		}

		Factory::getLog()->debug('Updating multipart status to ' . $multipart);

		// Cache this change and commit to db only after the backup is done, or failed
		$registry = Factory::getConfiguration();
		$registry->set('volatile.statistics.multipart', $multipart);
	}

	/**
	 * Sets or updates the statistics record of the current backup attempt
	 *
	 * @param   array  $data
	 *
	 * @return bool
	 * @throws Exception
	 */
	public function setStatistics($data)
	{
		$ret = Platform::getInstance()->set_or_update_statistics($this->statistics_id, $data);

		if ($ret === false)
		{
			return false;
		}

		if (!is_null($ret))
		{
			$this->statistics_id = $ret;
		}

		$this->cached_data = array_merge($this->cached_data, $data);

		return true;
	}

	/**
	 * Returns the statistics record ID (used in DB backup classes)
	 * @return int
	 */
	public function getId()
	{
		return $this->statistics_id;
	}

	/**
	 * Returns a copy of the cached data
	 * @return array
	 */
	public function getRecord()
	{
		return $this->cached_data;
	}

	/**
	 * Updates the "in step" flag of the current backup record.
	 *
	 * @param   false  $inStep  Am I currently executing a backup step? False if just finished.
	 *
	 * @return  bool
	 */
	public function updateInStep($inStep = false)
	{
		if (!$this->getId())
		{
			return false;
		}

		$data = $this->getRecord();

		/**
		 * We will only update the instep of running backups for two reasons:
		 *
		 * 1. The very last Kettenrad entry is after the backup process is complete. The record is marked 'complete'. I
		 *    must not touch it in this case.
		 *
		 * 2. When a record is marked 'fail' its instep is also set to 0. This happens in Factory::resetState().
		 */
		//
		if ($data['status'] == 'complete')
		{
			return true;
		}

		$data['instep']    = $inStep ? 1 : 0;
		$data['backupend'] = Platform::getInstance()->get_timestamp_database();

		try
		{
			return $this->setStatistics($data);
		}
		catch (Exception $e)
		{
			return false;
		}
	}
}
Util/ProfileMigration.php000060400000014073152456704520011457 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Exception;

/**
 * This helper class is used to migrate Akeeba Backup profiles to the new storage format implemented since version
 * 6.4.1
 *
 * @since       6.4.1
 */
abstract class ProfileMigration
{
	/**
	 * Tries to migrate a backup profile to the new JSON-based storage format used since version 6.4.1.
	 *
	 * @param   int  $profileID  The ID of the profile to migrate
	 *
	 * @return  bool  Whether we converted the profile
	 *
	 * @since   6.4.1
	 */
	public static function migrateProfile($profileID)
	{
		$platform = Platform::getInstance();
		$db       = Factory::getDatabase($platform->get_platform_database_options());

		// Is the database connected?
		if (!$db->connected())
		{
			return false;
		}

		// Load the raw data from the database
		try
		{
			$sql = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
				->select('*')
				->from($db->qn($platform->tableNameProfiles))
				->where($db->qn('id') . ' = ' . $db->q($profileID));

			$rawData = $db->setQuery($sql)->loadAssoc();
		}
		catch (Exception $e)
		{
			return false;
		}

		// Decrypt the configuration data if required
		$rawData['configuration'] = self::decryptConfiguration($rawData['configuration']);
		$migrated                 = false;

		// Migrate the configuration from INI to JSON format
		if (self::looksLikeIni($rawData['configuration']))
		{
			$rawData['configuration'] = self::convertINItoJSON($rawData['configuration']);

			$migrated = true;
		}

		// Migrate the filters from INI to JSON format
		if (self::looksLikeSerialized($rawData['filters']))
		{
			$rawData['filters'] = self::convertSerializedToJSON($rawData['filters']);

			$migrated = true;
		}

		if (!$migrated)
		{
			return false;
		}

		$rawData['configuration'] = self::encryptConfiguration($rawData['configuration']);

		$sql = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->update($db->qn($platform->tableNameProfiles))
			->set($db->qn('configuration') . ' = ' . $db->q($rawData['configuration']))
			->set($db->qn('filters') . ' = ' . $db->q($rawData['filters']))
			->where($db->qn('id') . ' = ' . $db->q($profileID));

		$db->setQuery($sql);

		try
		{
			$result = $db->query();
		}
		catch (Exception $exc)
		{
			return false;
		}

		return ($result == true);
	}

	/**
	 * Decrypt the configuration data if necessary. Returns the decrypted data.
	 *
	 * @param   string  $configData  The possibly encrypted data.
	 *
	 * @return  string  The decrypted data
	 *
	 * @since   6.4.1
	 */
	public static function decryptConfiguration($configData)
	{
		$noData    = empty($configData);
		$signature = ($noData || (strlen($configData) < 12)) ? '' : substr($configData, 0, 12);

		if (in_array($signature, ['###AES128###', '###CTR128###']))
		{
			return Factory::getSecureSettings()->decryptSettings($configData);
		}

		return $configData;
	}

	/**
	 * Encrypt the configuration data if necessary.
	 *
	 * @param   string  $configData  The raw configuration data
	 *
	 * @return  string  The possibly encrypted configuration data
	 *
	 * @since   6.4.1
	 */
	public static function encryptConfiguration($configData)
	{
		$secureSettings = Factory::getSecureSettings();

		return $secureSettings->encryptSettings($configData);
	}

	/**
	 * Does the provided configuration data look like it's INI encoded?
	 *
	 * @param   string  $configData  The unencrypted configuration data we read from the database.
	 *
	 * @return  bool
	 *
	 * @since   6.4.1
	 */
	public static function looksLikeIni($configData)
	{
		if (empty($configData))
		{
			return false;
		}

		if (strlen($configData) < 8)
		{
			return false;
		}

		if ((substr($configData, 0, 8) == '[global]') || substr($configData, 0, 8) == '[akeeba]')
		{
			return true;
		}

		return false;
	}

	/**
	 * Convert the INI-encoded data to JSON-encoded data
	 *
	 * @param   string  $configData  The INI-encoded data
	 *
	 * @return  string  The JSON-encoded data
	 *
	 * @since   6.4.1
	 */
	public static function convertINItoJSON($configData)
	{
		$dataArray = ParseIni::parse_ini_file($configData, true, true);

		return json_encode($dataArray, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_FORCE_OBJECT | JSON_PRETTY_PRINT);
	}

	/**
	 * Does the raw filters string provided seems to use serialized data? We actually check if it looks like JSON.
	 * If it's not, we assume it's serialized data.
	 *
	 * @param   string  $rawFilters  The raw filters string
	 *
	 * @return  bool  Does it look like a serialized string?
	 *
	 * @since   6.4.1
	 */
	public static function looksLikeSerialized($rawFilters)
	{
		if (empty($rawFilters))
		{
			return false;
		}

		if (substr($rawFilters, 0, 1) == '{')
		{
			return false;
		}

		return true;
	}

	/**
	 * Convert the serialized array in $rawFilters to JSON representation
	 *
	 * @param   string  $rawFilters  Raw serialized string
	 *
	 * @return  string  JSON-encoded string
	 *
	 * @since   6.4.1
	 */
	public static function convertSerializedToJSON($rawFilters)
	{
		$filters = unserialize($rawFilters);

		if (empty($filters))
		{
			$filters = [];
		}

		return json_encode($filters, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_FORCE_OBJECT | JSON_PRETTY_PRINT);
	}
}
Util/Logger.php000060400000035407152456704520007430 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Util\Log\LogInterface;
use Akeeba\Engine\Util\Log\WarningsLoggerAware;
use Akeeba\Engine\Util\Log\WarningsLoggerInterface;
use Akeeba\Engine\Psr\Log\InvalidArgumentException;
use Akeeba\Engine\Psr\Log\LoggerInterface;
use Akeeba\Engine\Psr\Log\LogLevel;

/**
 * Writes messages to the backup log file
 */
class Logger implements LoggerInterface, LogInterface, WarningsLoggerInterface
{
	use WarningsLoggerAware;

	/** @var  string  Full path to log file */
	protected $logName = null;

	/** @var  string  The current log tag */
	protected $currentTag = null;

	/** @var  resource  The file pointer to the current log file */
	protected $fp = null;

	/** @var  bool  Is the logging currently paused? */
	protected $paused = false;

	/** @var  int  The minimum log level */
	protected $configuredLoglevel;

	/** @var  string  The untranslated path to the site's root */
	protected $site_root_untranslated;

	/** @var  string  The translated path to the site's root */
	protected $site_root;

	/**
	 * Public constructor. Initialises the properties with the parameters from the backup profile and platform.
	 */
	public function __construct()
	{
		$this->initialiseWithProfileParameters();
	}

	/**
	 * When shutting down this class always close any open log files.
	 */
	public function __destruct()
	{
		$this->close();
	}

	/**
	 * Clears the logfile
	 *
	 * @param   string  $tag  Backup origin
	 */
	public function reset($tag = null)
	{
		// Pause logging
		$this->pause();

		// Get the file names for the default log and the tagged log
		$currentLogName = $this->logName;
		$this->logName  = $this->getLogFilename($tag);

		// Close the file if it's open
		if ($currentLogName == $this->logName)
		{
			$this->close();
		}

		// Remove the log file if it exists
		@unlink($this->logName);

		// Reset the log file
		$fp = @fopen($this->logName, 'w');
		$hasWritten = false;

		if ($fp !== false)
		{
			$hasWritten = fwrite($fp, '<' . '?' . 'php die(); ' . '?' . '>' . "\n") !== false;
			@fclose($fp);
		}

		// If I could not write to a .log.php file try using a .log file instead.
		if (!$hasWritten)
		{
			$this->logName  = $this->getLogFilename($tag, '');
			$fp = @fopen($this->logName, 'w');
			$hasWritten = false;

			if ($fp !== false)
			{
				$hasWritten = fwrite($fp, "\n") !== false;
				@fclose($fp);
			}
		}

		// Delete the default log file(s) if they exists
		$defaultLog     = $this->getLogFilename(null);

		if (!empty($tag) && @file_exists($defaultLog))
		{
			@unlink($defaultLog);
		}

		$defaultLog     = $this->getLogFilename(null, '');

		if (!empty($tag) && @file_exists($defaultLog))
		{
			@unlink($defaultLog);
		}

		// Set the current log tag
		$this->currentTag = $tag;

		// Unpause logging
		$this->unpause();
	}

	/**
	 * Writes a line to the log, if the log level is high enough
	 *
	 * @param   string  $level    The log level
	 * @param   string  $message  The message to write to the log
	 * @param   array   $context  The logging context. For PSR-3 compatibility but not used in text file logs.
	 *
	 * @return  void
	 */
	public function log($level, $message = '', array $context = [])
	{
		// Warnings are enqueued no matter what is the minimum log level to report in the log file
		if (in_array($level, [LogLevel::WARNING, LogLevel::NOTICE]))
		{
			$this->enqueueWarning($message);
		}

		// If we are told to not log anything we can't continue
		if ($this->configuredLoglevel == 0)
		{
			return;
		}

		// Open the log if it's closed
		if (is_null($this->fp))
		{
			$this->open($this->currentTag);
		}

		// If the log could not be opened we can't continue
		if (is_null($this->fp))
		{
			return;
		}

		// If the logging is paused we can't continue
		if ($this->paused)
		{
			return;
		}

		// Get the log level as an integer (compatibility with our minimum log level configuration parameter)
		switch ($level)
		{
			case LogLevel::EMERGENCY:
			case LogLevel::ALERT:
			case LogLevel::CRITICAL:
			case LogLevel::ERROR:
				$intLevel = 1;
				break;

			case LogLevel::WARNING:
			case LogLevel::NOTICE:
				$intLevel = 2;
				break;

			case LogLevel::INFO:
				$intLevel = 3;
				break;

			case LogLevel::DEBUG:
				$intLevel = 4;
				break;

			default:
				throw new InvalidArgumentException("Unknown log level $level", 500);
				break;
		}

		// If the minimum log level is lower than what we're trying to log we cannot continue
		if ($this->configuredLoglevel < $intLevel)
		{
			return;
		}

		$translateRoot = true;

		if (array_key_exists('root_translate', $context))
		{
			$translateRoot = ($context['root_translate'] === 1) || ($context['root_translate'] === '1') || ($context['root_translate'] === true);
		}

		// Replace the site's root with <root> in the log file
		if ($translateRoot && !defined('AKEEBADEBUG'))
		{
			$message = str_replace($this->site_root_untranslated, "<root>", $message);
			$message = str_replace($this->site_root, "<root>", $message);
		}

		// Replace new lines
		$message = str_replace("\r\n", "\n", $message);
		$message = str_replace("\r", "\n", $message);
		$message = str_replace("\n", ' \n ', $message);

		switch ($level)
		{
			case LogLevel::EMERGENCY:
			case LogLevel::ALERT:
			case LogLevel::CRITICAL:
			case LogLevel::ERROR:
				$string = "ERROR   |";
				break;

			case LogLevel::WARNING:
			case LogLevel::NOTICE:
				$string = "WARNING |";
				break;

			case LogLevel::INFO:
				$string = "INFO    |";
				break;

			default:
				$string = "DEBUG   |";
				break;
		}

		$string .= gmdate('Ymd H:i:s') . "|$message\r\n";

		@fwrite($this->fp, $string);
	}

	/**
	 * Calculates the absolute path to the log file
	 *
	 * @param   string  $tag  The backup run's tag
	 *
	 * @return    string    The absolute path to the log file
	 */
	public function getLogFilename($tag = null, $extension = '.php')
	{
		if (empty($tag))
		{
			$fileName = 'akeeba.log' . $extension;
		}
		else
		{
			$fileName = "akeeba.$tag.log" . $extension;
		}

		// Get output directory
		$registry        = Factory::getConfiguration();
		$outputDirectory = $registry->get('akeeba.basic.output_directory');

		// Get the log file name
		$absoluteLogFilename = Factory::getFilesystemTools()->TranslateWinPath($outputDirectory . DIRECTORY_SEPARATOR . $fileName);

		return $absoluteLogFilename;
	}

	/**
	 * Close the currently active log and set the current tag to null.
	 *
	 * @return  void
	 */
	public function close()
	{
		// The log file changed. Close the old log.
		if (is_resource($this->fp))
		{
			@fclose($this->fp);
		}

		$this->fp         = null;
		$this->currentTag = null;
	}

	/**
	 * Open a new log instance with the specified tag. If another log is already open it is closed before switching to
	 * the new log tag. If the tag is null use the default log defined in the logging system.
	 *
	 * @param   string|null  $tag  The log to open
	 *
	 * @return void
	 */
	public function open($tag = null, $extension = '.php')
	{
		// If the log is already open do nothing
		if (is_resource($this->fp) && ($tag == $this->currentTag))
		{
			return;
		}

		// If another log is open, close it
		if (is_resource($this->fp))
		{
			$this->close();
		}

		// Re-initialise site root and minimum log level since the active profile might have changed in the meantime
		$this->initialiseWithProfileParameters();

		// Set the current tag
		$this->currentTag = $tag;

		// Get the log filename
		$this->logName = $this->getLogFilename($tag, $extension);

		// Touch the file
		@touch($this->logName);

		// Open the log file. DO NOT USE APPEND ('ab') MODE. I NEED TO SEEK INTO THE FILE. SEE FURTHER BELOW!
		$this->fp = @fopen($this->logName, 'c');

		// If we couldn't open the file set the file pointer to null
		if ($this->fp === false)
		{
			$this->fp = null;

			return;
		}

		// Go to the end of the file, emulating append mode. DO NOT REPLACE THE fopen() FILE MODE!
		if (@fseek($this->fp, 0, SEEK_END) === -1)
		{
			@fclose($this->fp);
			@unlink($this->logName);

			$this->fp = null;

			return;
		}

		/**
		 * The following sounds pretty stupid but there is a reason for that convoluted code.
		 *
		 * Some hosts, like WP Engine, will now allow you to write to a log file with a .php extension. The code below
		 * tries to anticipate that when the log extension is .php. It will try to write to the *.log.php file and the
		 * text is actually resembling PHP code. Hosts like WP Engine will fail the fwrite() which will cause this
		 * method to terminate early and return a null pointer. Our code will catch this case and try to use a .log
		 * extension as a safe fallback.
		 */
		if ($extension !== '.php')
		{
			return;
		}

		// Try to write something into the file
		$written = @fwrite($this->fp, '<?php die("test"); ?>' . "\n");

		if ($written === false)
		{
			@fclose($this->fp);
			@unlink($this->logName);

			$this->fp = null;

			$this->open($tag, '');

			return;
		}

		// Store truncate offset, we will have to rewind the internal pointer to it
		$truncate_point = ftell($this->fp) - $written;

		if (ftruncate($this->fp, $truncate_point) === false)
		{
			@fclose($this->fp);
			@unlink($this->logName);

			$this->fp = null;

			$this->open($tag, '');

			return;
		}

		// Finally, move the file pointer at the truncation point. Otherwise PHP will append NULL bytes to the string
		// to "pad" the file length to the internal file pointer. No need to check if the operation was successful,
		// worst case scenario we will have some extra NULL bytes, there's no need to kill the log operation
		@fseek($this->fp, $truncate_point);
	}

	/**
	 * Temporarily pause log output. The log() method MUST respect this.
	 *
	 * @return  void
	 */
	public function pause()
	{
		$this->paused = true;
	}

	/**
	 * Resume the previously paused log output. The log() method MUST respect this.
	 *
	 * @return  void
	 */
	public function unpause()
	{
		$this->paused = false;
	}

	/**
	 * Returns the timestamp (in UNIX time long integer format) of the last log message written to the log with the
	 * specific tag. The timestamp MUST be read from the log itself, not from the logger object. It is used by the
	 * engine to find out the age of stalled backups which may have crashed.
	 *
	 * @param   string|null  $tag  The log tag for which the last timestamp is returned
	 *
	 * @return  int|null  The timestamp of the last log message, in UNIX time. NULL if we can't get the timestamp.
	 */
	public function getLastTimestamp($tag = null)
	{
		$fileName = $this->getLogFilename($tag);

		/**
		 * The log file akeeba.tag.log.php may not exist but the akeeba.tag.log does. This would be the case in some bad
		 * hosts, like WPEngine, which do not allow us to create .php files EVEN THOUGH that's the only way to ensure
		 * the privileged information in the log file is not readable over the web. You can't fix bad hosts, you can
		 * only work around them.
		 */
		if (!@file_exists($fileName) && @file_exists(substr($fileName, 0, -4)))
		{
			$fileName = substr($fileName, 0, -4);
		}

		$timestamp = @filemtime($fileName);

		if ($timestamp === false)
		{
			return null;
		}

		return $timestamp;
	}

	/**
	 * System is unusable.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function emergency($message, array $context = [])
	{
		$this->log(LogLevel::EMERGENCY, $message, $context);
	}

	/**
	 * Action must be taken immediately.
	 *
	 * Example: Entire website down, database unavailable, etc. This should
	 * trigger the SMS alerts and wake you up.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function alert($message, array $context = [])
	{
		$this->log(LogLevel::ALERT, $message, $context);
	}

	/**
	 * Critical conditions.
	 *
	 * Example: Application component unavailable, unexpected exception.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function critical($message, array $context = [])
	{
		$this->log(LogLevel::CRITICAL, $message, $context);
	}

	/**
	 * Runtime errors that do not require immediate action but should typically
	 * be logged and monitored.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function error($message, array $context = [])
	{
		$this->log(LogLevel::ERROR, $message, $context);
	}

	/**
	 * \Exceptional occurrences that are not errors.
	 *
	 * Example: Use of deprecated APIs, poor use of an API, undesirable things
	 * that are not necessarily wrong.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function warning($message, array $context = [])
	{
		$this->log(LogLevel::WARNING, $message, $context);
	}

	/**
	 * Normal but significant events.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function notice($message, array $context = [])
	{
		$this->log(LogLevel::NOTICE, $message, $context);
	}

	/**
	 * Interesting events.
	 *
	 * Example: User logs in, SQL logs.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function info($message, array $context = [])
	{
		$this->log(LogLevel::INFO, $message, $context);
	}

	/**
	 * Detailed debug information.
	 *
	 * @param   string  $message
	 * @param   array   $context
	 *
	 * @return void
	 */
	public function debug($message, array $context = [])
	{
		$this->log(LogLevel::DEBUG, $message, $context);
	}

	/**
	 * Initialise the logger properties with parameters from the backup profile and the platform
	 *
	 * @return  void
	 */
	protected function initialiseWithProfileParameters()
	{
		// Get the site's translated and untranslated root
		$this->site_root_untranslated = Platform::getInstance()->get_site_root();
		$this->site_root              = Factory::getFilesystemTools()->TranslateWinPath($this->site_root_untranslated);

		// Load the registry and fetch log level
		$registry                 = Factory::getConfiguration();
		$this->configuredLoglevel = $registry->get('akeeba.basic.log_level');
		$this->configuredLoglevel = $this->configuredLoglevel * 1;
	}
}
Util/ListingParser.php000060400000026701152456704520010774 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

/**
 * Parses directory listings of the standard UNIX or MS-DOS style, i.e. what is most commonly returned by FTP and SFTP
 * servers running on *NIX and Windows machines.
 *
 * This class is intended to be used with the result RemoteResourceInterface::getRawList, parsing the raw folder listing
 * returned by an (S)FTP server -meant to be read by a human- into something you can programmatically work with. Using
 * RemoteResourceInterface::getWrapperStringFor with DirectoryIterator is generally preferable, if only much slower due
 * to the synchronous nature of remote stat() requests on each iterated element.
 */
class ListingParser
{
	/**
	 * Parse a UNIX- or MS-DOS-style directory listing.
	 *
	 * You get a hash array with entries. Each entry has the following keys:
	 * name:    the file / folder name.
	 * type:    file, dir or link.
	 * target:  link target (when type == link).
	 * user:    owner user, numeric or text. IIS FTP fakes this with the literal string "owner".
	 * group:   owner group, numeric or text. IIS FTP fakes this with the literal string "group".
	 * size:    size in bytes; note that some Linux servers report non-zero sizes for directories.
	 * date:    file creation date, most likely blatantly wrong; see below
	 * perms:   permissions in decimal format. Cast with dec2oct to get the 4 digit permissions string, e.g. 1755
	 *
	 * Important Notes
	 *
	 * Some UNIX systems report a size for directories. Do not assume that something is a directory if it's size is 0,
	 * you will be surprised. Look at the 'type' element instead.
	 *
	 * Dates can be off. UNIX-style directory listings state either the year or the time, not both. If the file was
	 * modified during this year you will get a date with a resolution of 1 minute. If the file was modified on a
	 * different year you'll get a date with a resolution of 1 day. MS-DOS listings always contain the time. Again, the
	 * resolution is 1 minute.
	 *
	 * Most MS-DOS style listings don't list junctions and symlinks. As a result they will be reported as regular
	 * directories / files. This is the case for the IIS FTP server. Other servers may return the raw "dir" command
	 * results (as CMD.EXE would parse it) in which case links and link targets do get reported.
	 *
	 * @param   string  $list   The raw listing
	 * @param   bool    $quick  True to only include name, type, size and link target for each file.
	 *
	 * @return  array
	 */
	public function parseListing($list, $quick = false)
	{
		$res = $this->parseUnixListing($list, $quick);

		if (empty($res))
		{
			$res = $this->parseMSDOSListing($list, $quick);
		}

		return $res;
	}

	/**
	 * Parse a UNIX-style directory listing. This is the format produced by ls -la on *NIX systems.
	 *
	 * You get a hash array with entries. Each entry has the following keys:
	 * name: the file / folder name.
	 * type: file, dir or link.
	 * target: link target (when type == link).
	 * user: owner user, numeric or text. IIS FTP fakes this with the literal string "owner".
	 * group: owner group, numeric or text. IIS FTP fakes this with the literal string "group".
	 * size: size in bytes; note that some Linux servers report non-zero sizes for directories.
	 * date: file creation date, most likely blatantly wrong; see below
	 * perms: permissions in decimal format. Cast with dec2oct to get the 4 digit permissions string, e.g. 1755
	 *
	 * @param   string  $list   The raw listing
	 * @param   bool    $quick  True to only include name, type, size and link target for each file.
	 *
	 * @return  array
	 */
	protected function parseUnixListing($list, $quick = false)
	{
		$ret = [];

		$list = str_replace(["\r\n", "\r", "\n\n"], ["\n", "\n", "\n"], $list);
		$list = explode("\n", $list);
		$list = array_map('rtrim', $list);

		foreach ($list as $v)
		{
			$vInfo = preg_split("/[\s]+/", $v, 9);

			if ((is_array($vInfo) || $vInfo instanceof \Countable ? count($vInfo) : 0) != 9)
			{
				continue;
			}

			$entry = [
				'name'   => '',
				'type'   => 'file',
				'target' => '',
				'user'   => '0',
				'group'  => '0',
				'size'   => '0',
				'date'   => '0',
				'perms'  => '0',
			];

			if ($quick)
			{
				$entry = [
					'name'   => '',
					'type'   => 'file',
					'size'   => '0',
					'target' => '',
				];
			}

			// ===== Parse permissions =====
			$permString    = $vInfo[0];
			$permStringLen = strlen($permString);
			$typeBit       = '-';
			$userPerms     = 'r--';
			$groupPerms    = 'r--';
			$otherPerms    = 'r--';

			if ($permStringLen)
			{
				$typeBit = substr($permString, 0, 1);
			}

			switch ($typeBit)
			{
				case "d":
					$entry['type'] = 'dir';
					break;

				case "l":
					$entry['type'] = 'link';
					break;
			}

			// ===== Parse size =====
			$entry['size'] = $vInfo[4];

			if (!$quick)
			{
				if ($permStringLen >= 4)
				{
					$userPerms = substr($permString, 1, 3);
				}

				if ($permStringLen >= 7)
				{
					$groupPerms = substr($permString, 4, 3);
				}

				if ($permStringLen >= 10)
				{
					$otherPerms = substr($permString, 7, 3);
				}

				$bitPart   = 0;
				$permsPart = '';

				[$thisPerms, $thisBit] = $this->textPermsDecode($userPerms);
				$bitPart   += 4 * $thisBit; // SetUID
				$permsPart .= $thisPerms;

				[$thisPerms, $thisBit] = $this->textPermsDecode($groupPerms);
				$bitPart   += 2 * $thisBit; // SetGID
				$permsPart .= $thisPerms;

				[$thisPerms, $thisBit] = $this->textPermsDecode($otherPerms);
				$bitPart   += $thisBit; // Sticky (restricted deletion)
				$permsPart .= $thisPerms;

				$entry['perms'] = octdec($bitPart . $permsPart);

				// ===== Parse ownership =====
				$entry['user']  = $vInfo[2];
				$entry['group'] = $vInfo[3];

				// ===== Parse date =====
				$dateString    = $vInfo[6] . ' ' . $vInfo[5] . ' ' . $vInfo[7];
				$x             = date_create($dateString);
				$entry['date'] = ($x === false) ? 0 : $x->getTimestamp();
			}

			// ===== Parse name =====
			$name = $vInfo[8];

			// Ubuntu (possibly others?) tacks a start when either suid/sgid bits is set
			if (substr($name, -1) == '*')
			{
				$name = substr($name, 0, -1);
			}

			// Link target parsing
			if (strpos($name, '->') !== false)
			{
				[$name, $target] = explode('->', $name);

				$entry['target'] = trim($target);
			}

			$entry['name'] = trim($name);

			// ===== Return the entry =====
			$ret[] = $entry;
		}

		return $ret;
	}

	/**
	 * Parse am MS-DOS-style directory listing. This is the format produced by dir on MS-DOS and Windows systems.
	 *
	 * You get a hash array with entries. Each entry has the following keys:
	 * name: the file / folder name.
	 * type: file, dir or link.
	 * target: link target (when type == link).
	 * user: owner user, numeric or text. IIS FTP fakes this with the literal string "owner".
	 * group: owner group, numeric or text. IIS FTP fakes this with the literal string "group".
	 * size: size in bytes; note that some Linux servers report non-zero sizes for directories.
	 * date: file creation date, most likely blatantly wrong; see below
	 * perms: permissions in decimal format. Cast with dec2oct to get the 4 digit permissions string, e.g. 1755
	 *
	 * @param   string  $list   The raw listing
	 * @param   bool    $quick  True to only include name, type, size and link target for each file.
	 *
	 * @return  array
	 */
	protected function parseMSDOSListing($list, $quick = false)
	{
		$ret = [];

		$list = str_replace(["\r\n", "\r", "\n\n"], ["\n", "\n", "\n"], $list);
		$list = explode("\n", $list);
		$list = array_map('rtrim', $list);

		foreach ($list as $v)
		{
			$vInfo = preg_split("/[\s]+/", $v, 5);

			if ((is_array($vInfo) || $vInfo instanceof \Countable ? count($vInfo) : 0) < 4)
			{
				continue;
			}

			$entry = [
				'name'   => '',
				'type'   => 'file',
				'target' => '',
				'user'   => '0',
				'group'  => '0',
				'size'   => '0',
				'date'   => '0',
				'perms'  => '0',
			];

			if ($quick)
			{
				$entry = [
					'name'   => '',
					'type'   => 'file',
					'size'   => '0',
					'target' => '',
				];
			}

			// The first two fields are date and time
			$dateString = $vInfo[0] . ' ' . $vInfo[1];

			// If position 2 is AM/PM append it and remove it from the list
			if (in_array(strtoupper($vInfo[2]), ['AM', 'PM']))
			{
				$dateString .= ' ' . $vInfo[2];

				// This trick is required to remove the element and fix the indices for the rest of the parsing to work.
				unset ($vInfo[2]);
				$vInfo = array_merge($vInfo);
			}

			if (!$quick)
			{
				$x             = date_create($dateString);
				$entry['date'] = ($x === false) ? 0 : $x->getTimestamp();
			}

			// The third field is either a special type indicator or the file size
			switch (strtoupper($vInfo[2]))
			{
				// Regular directory
				case '<DIR>':
					$entry['type'] = 'dir';
					break;

				// Junction (like a directory symlink, pre-Win7)
				case '<JUNCTION>':
					// File symlink
				case '<SYMLINK>':
					// Directory symlink
				case '<SYMLINKD>':
					$entry['type'] = 'link';
					break;

				default:
					$entry['size'] = (int) $vInfo[2];
					break;
			}

			// And finally the file name. If it's a link it's in the format 'name [target]'
			preg_match('/(.*)[\s]+\[(.*)\]/', $vInfo[3], $matches);

			if (empty($matches))
			{
				$entry['name'] = $vInfo[3];
			}
			else
			{
				$entry['type']   = 'link';
				$entry['name']   = $matches[1];
				$entry['target'] = $matches[2];
			}

			// ===== Return the entry =====
			$ret[] = $entry;
		}

		return $ret;
	}

	/**
	 * Decode a textual permissions representation for a user, group or others to a pair of octal digits (permissions
	 * and flags). For example "r--" is converted to [4, 0], "r-x" to [5, 0], "r-t" to [5, 1]
	 *
	 * @param   string  $perms  The textual permissions representation for a user, group or others
	 *
	 * @return  array  Two octal digits for permissions and flags (suid/sgid/sticky bit)
	 */
	private function textPermsDecode($perms)
	{
		$permBit  = 0;
		$flagBits = 0;

		if (strpos($perms, 'r'))
		{
			$permBit += 4;
		}

		if (strpos($perms, 'w'))
		{
			$permBit += 2;
		}

		/**
		 * Both s and t denote flag set and imply the execute permissions is also granted. For user/groups it's
		 * SetUID/SetGID respectively, for others it's the "sticky" bit (restricted deletion). Since only one of x, s
		 * and t can be present at one time we use an if/elseif block. I don't use a switch because a. I am not 100%
		 * sure that all servers will report the text permissions in rwx order and b. I am not sure that switch and
		 * substr are faster than strpos (and too lazy to benchmark; sorry).
		 */
		if (strpos($perms, 'x'))
		{
			$permBit += 1;
		}
		elseif (strpos($perms, 't'))
		{
			$flagBits += 1;
		}
		elseif (strpos($perms, 's'))
		{
			$flagBits += 1;
		}

		return [$permBit, $flagBits];
	}
}
Util/ConfigurationCheck.php000060400000034715152456704520011757 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;

/**
 * Quirk detection helper class
 */
class ConfigurationCheck
{
	/**
	 * The configuration checks to perform
	 *
	 * @var  array
	 */
	protected $configurationChecks = [
		['code'        => '001', 'severity' => 'critical', 'callback' => [null, 'q001'],
		 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q001',
		],
		['code'        => '003', 'severity' => 'critical', 'callback' => [null, 'q003'],
		 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q003',
		],
		['code'        => '004', 'severity' => 'critical', 'callback' => [null, 'q004'],
		 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q004',
		],

		['code'        => '101', 'severity' => 'high', 'callback' => [null, 'q101'],
		 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q101',
		],
		['code'        => '103', 'severity' => 'high', 'callback' => [null, 'q103'],
		 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q103',
		],
		['code'        => '104', 'severity' => 'high', 'callback' => [null, 'q104'],
		 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q104',
		],
		['code'        => '106', 'severity' => 'high', 'callback' => [null, 'q106'],
		 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q106',
		],

		['code'        => '201', 'severity' => 'medium', 'callback' => [null, 'q201'],
		 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q201',
		],
		['code'        => '202', 'severity' => 'medium', 'callback' => [null, 'q202'],
		 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q202',
		],
		['code'        => '204', 'severity' => 'medium', 'callback' => [null, 'q204'],
		 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q204',
		],

		['code'        => '203', 'severity' => 'medium', 'callback' => [null, 'q203'],
		 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q203',
		],
//		['code'        => '401', 'severity' => 'low', 'callback' => [null, 'q401'],
//		 'description' => 'COM_AKEEBA_CPANEL_WARNING_Q401',
//		],
	];

	/**
	 * The public constructor replaces the missing object reference in the configuration check callbacks
	 */
	function __construct()
	{
		$temp = [];

		foreach ($this->configurationChecks as $check)
		{
			$check['callback'] = [$this, $check['callback'][1]];
			$temp[]            = $check;
		}

		$this->configurationChecks = $temp;
	}

	/**
	 * Returns the output & temporary folder writable status
	 *
	 * @return  array  A hash array with the writable status
	 */
	public function getFolderStatus()
	{
		static $status = null;

		if (is_null($status))
		{
			$stock_dirs = Platform::getInstance()->get_stock_directories();

			// Get output writable status
			$registry = Factory::getConfiguration();
			$outdir   = $registry->get('akeeba.basic.output_directory');

			foreach ($stock_dirs as $macro => $replacement)
			{
				$outdir = str_replace($macro, $replacement, $outdir);
			}

			$status['output'] = @is_writable($outdir);
		}

		return $status;
	}

	/**
	 * Returns the overall status. It's true when both the temporary and output directories are writable and there are
	 * no critical configuration check failures.
	 *
	 * @return  boolean
	 */
	public function getShortStatus()
	{
		// Base the status on directory writeable status
		$status = $this->getFolderStatus();
		$ret    = $status['output'];

		// Scan for high severity configuration check errors
		$detailedStatus = $this->getDetailedStatus();

		if (!empty($detailedStatus))
		{
			foreach ($detailedStatus as $configCheck)
			{
				if ($configCheck['severity'] == 'critical')
				{
					$ret = false;
				}
			}
		}

		// Return status
		return $ret;
	}

	/**
	 * Add a configuration check definition
	 *
	 * @param   string  $code         The configuration check code (three digit number)
	 * @param   string  $severity     The severity (low, medium, high, critical)
	 * @param   string  $description  The description key for this configuration check
	 * @param   null    $callback     The callback used to determine the status of the configuration check
	 *
	 * @return  void
	 */
	public function addConfigurationCheckDefinition($code, $severity = 'low', $description = null, $callback = null)
	{
		if (!is_callable($callback))
		{
			$callback = [$this, 'q' . $code];
		}

		if (empty($description))
		{
			$description = 'COM_AKEEBA_CPANEL_WARNING_Q' . $code;
		}

		$newConfigurationCheck = [
			'code'        => $code,
			'severity'    => $severity,
			'description' => $description,
			'callback'    => $callback,
		];

		$this->configurationChecks[$code] = $newConfigurationCheck;
	}

	/**
	 * Remove a configuration check definition
	 *
	 * @param   string  $code  The code of the configuration check to remove
	 *
	 * @return  void
	 */
	public function removeConfigurationCheckDefinition($code)
	{
		if (isset($this->configurationChecks[$code]))
		{
			unset($this->configurationChecks[$code]);
		}
	}

	/**
	 * Clear the configuration check definitions
	 *
	 * @return  void
	 */
	public function clearConfigurationCheckDefinitions()
	{
		$this->configurationChecks = [];
	}

	/**
	 * Runs the configuration check scripts. These are potential problems related to server
	 * configuration, out of Akeeba's control. They are intended to give the user a
	 * chance to fix them before they cause the backup to fail.
	 *
	 * Numbering scheme:
	 * Q0xx    No-go errors
	 * Q1xx    Critical system configuration errors
	 * Q2xx    Medium and low system configuration warnings
	 * Q3xx    Critical software configuration errors
	 * Q4xx    Medium and low component configuration warnings
	 *
	 * @param   boolean  $low_priority       Should I include low priority quirks?
	 * @param   string   $help_url_template  The sprintf template from creating a help URL from a config check code
	 *
	 * @return  array
	 */
	public function getDetailedStatus($low_priority = false, $help_url_template = 'https://www.akeeba.com/documentation/warnings/q%s.html')
	{
		static $detailedStatus = null;

		if (is_null($detailedStatus) || $low_priority)
		{
			$detailedStatus = [];

			foreach ($this->configurationChecks as $quirkDef)
			{
				if (!$low_priority && ($quirkDef['severity'] == 'low'))
				{
					continue;
				}

				$this->checkConfiguration($detailedStatus, $quirkDef, $help_url_template);
			}
		}

		return $detailedStatus;
	}

	/**
	 * Checks if a path is restricted by open_basedirs
	 *
	 * @param   string  $check  The path to check
	 *
	 * @return  bool  True if the path is restricted (which is bad)
	 */
	public function checkOpenBasedirs($check)
	{
		static $paths;

		if (empty($paths))
		{
			$open_basedir = ini_get('open_basedir');

			if (empty($open_basedir))
			{
				return false;
			}

			$delimiter  = strpos($open_basedir, ';') !== false ? ';' : ':';
			$paths_temp = explode($delimiter, $open_basedir);

			// Some open_basedirs are using environemtn variables
			$paths = [];

			foreach ($paths_temp as $path)
			{
				if (array_key_exists($path, $_ENV))
				{
					$paths[] = $_ENV[$path];
				}
				else
				{
					$paths[] = $path;
				}
			}
		}

		if (empty($paths))
		{
			return false; // no restrictions
		}
		else
		{
			$newcheck = @realpath($check); // Resolve symlinks, like PHP does

			if (!($newcheck === false))
			{
				$check = $newcheck;
			}

			$included = false;

			foreach ($paths as $path)
			{
				$newpath = @realpath($path);

				if (!($newpath === false))
				{
					$path = $newpath;
				}

				if (strlen($check) >= strlen($path))
				{
					// Only check if the path to check is longer than the inclusion path.
					// Otherwise, I guarantee it's not included!!
					// If the path to check begins with an inclusion path, it's permitted. Easy, huh?
					if (substr($check, 0, strlen($path)) == $path)
					{
						$included = true;
					}
				}
			}

			return !$included;
		}
	}

	/**
	 * Make a configuration check and adds it to the list if it raises a warning / error
	 *
	 * @param   array   $detailedStatus     The configuration checks status array
	 * @param   array   $quirkDef           The configuration check definition
	 * @param   string  $help_url_template  The sprintf template from creating a help URL from a quirk code
	 *
	 * @return  void
	 */
	protected function checkConfiguration(&$detailedStatus, $quirkDef, $help_url_template)
	{
		if (call_user_func($quirkDef['callback']))
		{
			$description = Platform::getInstance()->translate($quirkDef['description']);

			$detailedStatus[(string) $quirkDef['code']] = [
				'code'        => $quirkDef['code'],
				'severity'    => $quirkDef['severity'],
				'description' => $description,
				'help_url'    => sprintf($help_url_template, $quirkDef['code']),
			];
		}
	}

	/**
	 * Q001 - HIGH - Output directory unwriteable
	 *
	 * @return  bool
	 */
	private function q001()
	{
		$status = $this->getFolderStatus();

		return !$status['output'];
	}

	/**
	 * Q003 - HIGH - Backup output or temporary set to site's root
	 *
	 * @return  bool
	 */
	private function q003()
	{
		$stock_dirs = Platform::getInstance()->get_stock_directories();

		$registry = Factory::getConfiguration();
		$outdir   = $registry->get('akeeba.basic.output_directory');

		foreach ($stock_dirs as $macro => $replacement)
		{
			$outdir = str_replace($macro, $replacement, $outdir);
		}

		$outdir_real = @realpath($outdir);

		if (!empty($outdir_real))
		{
			$outdir = $outdir_real;
		}

		$siteroot      = Platform::getInstance()->get_site_root();
		$siteroot_real = @realpath($siteroot);

		if (!empty($siteroot_real))
		{
			$siteroot = $siteroot_real;
		}

		return ($siteroot == $outdir);
	}

	/**
	 * Q004 - HIGH - Free memory too low
	 *
	 * @return bool
	 */
	private function q004()
	{
		// If we can't figure this out, don't report a problem. It doesn't
		// really matter, as the backup WILL crash eventually.
		if (!function_exists('ini_get'))
		{
			return false;
		}

		$memLimit = ini_get("memory_limit");
		$memLimit = $this->_return_bytes($memLimit);

		if ($memLimit <= 0)
		{
			return false;
		}

		// No limit?
		$availableRAM = $memLimit - memory_get_usage();

		// We need at least 12Mb of free memory
		return ($availableRAM <= (12 * 1024 * 1024));
	}

	/**
	 * Q101 - HIGH - open_basedir on output directory
	 *
	 * @return  bool
	 */
	private function q101()
	{
		$stock_dirs = Platform::getInstance()->get_stock_directories();

		// Get output writable status
		$registry = Factory::getConfiguration();
		$outdir   = $registry->get('akeeba.basic.output_directory');

		foreach ($stock_dirs as $macro => $replacement)
		{
			$outdir = str_replace($macro, $replacement, $outdir);
		}

		return $this->checkOpenBasedirs($outdir);
	}

	/**
	 * Q103 - HIGH - Less than 10" of max_execution_time with PHP Safe Mode enabled
	 *
	 * @return  bool
	 */
	private function q103()
	{
		$exectime = ini_get('max_execution_time');
		$safemode = ini_get('safe_mode');

		if (!$safemode)
		{
			return false;
		}

		if (!is_numeric($exectime))
		{
			return false;
		}

		if ($exectime <= 0)
		{
			return false;
		}

		return $exectime < 10;
	}

	/**
	 * Q104 - HIGH - Temp directory is the same as the site's root
	 *
	 * @return  bool
	 */
	private function q104()
	{

		$siteroot      = Platform::getInstance()->get_site_root();
		$siteroot_real = @realpath($siteroot);

		if (!empty($siteroot_real))
		{
			$siteroot = $siteroot_real;
		}

		$stockDirs      = Platform::getInstance()->get_stock_directories();
		$temp_directory = $stockDirs['[SITETMP]'];
		$temp_directory = @realpath($temp_directory);

		if (empty($temp_directory))
		{
			$temp_directory = $siteroot;
		}

		return ($siteroot == $temp_directory);
	}

	/**
	 * Q106 - HIGH  - Table name prefix contains uppercase characters
	 *
	 * @return  bool
	 */
	private function q106()
	{
		$filters   = Factory::getFilters();
		$databases = $filters->getInclusions('db');

		foreach ($databases as $db)
		{
			if (!isset($db['prefix']))
			{
				continue;
			}

			if (preg_match('/[A-Z]/', $db['prefix']))
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * Q201 - MEDIUM - Outdated PHP version.
	 *
	 * We currently check for PHP lower than 8.0.
	 *
	 * @return  bool
	 */
	private function q201()
	{
		return version_compare(PHP_VERSION, '8.0.0', 'lt');
	}

	/**
	 * Q202 - MED  - CRC problems with hash extension not present
	 *
	 * @return  bool
	 */
	private function q202()
	{
		$registry = Factory::getConfiguration();
		$archiver = $registry->get('akeeba.advanced.archiver_engine');

		if ($archiver != 'zip')
		{
			return false;
		}

		return !function_exists('hash_file');
	}

	/**
	 * Q203 - MED  - Default output directory in use
	 *
	 * @return  bool
	 */
	private function q203()
	{
		$stock_dirs = Platform::getInstance()->get_stock_directories();

		$registry = Factory::getConfiguration();
		$outdir   = $registry->get('akeeba.basic.output_directory');

		foreach ($stock_dirs as $macro => $replacement)
		{
			$outdir = str_replace($macro, $replacement, $outdir);
		}

		$default = $stock_dirs['[DEFAULT_OUTPUT]'];

		$outdir  = Factory::getFilesystemTools()->TranslateWinPath($outdir);
		$default = Factory::getFilesystemTools()->TranslateWinPath($default);

		return $outdir == $default;
	}

	/**
	 * Q204 - MED  - Disabled functions may affect operation
	 *
	 * @return  bool
	 */
	private function q204()
	{
		$disabled = ini_get('disabled_functions');

		return (!empty($disabled));
	}

	/**
	 * Q401 - LOW  - ZIP format selected
	 *
	 * @return  bool
	 */
	private function q401()
	{
		$registry = Factory::getConfiguration();
		$archiver = $registry->get('akeeba.advanced.archiver_engine');

		return $archiver == 'zip';
	}

	private function _return_bytes($setting)
	{
		$val  = trim($setting);
		$last = strtolower(substr($val, -1));
		$val  = substr($val, 0, -1);

		if (is_numeric($last))
		{
			return $setting;
		}

		switch ($last)
		{
			case 't':
				$val *= 1024;
			case 'g':
				$val *= 1024;
			case 'm':
				$val *= 1024;
			case 'k':
				$val *= 1024;
		}

		return (int) $val;
	}
}
Util/AesAdapter/AdapterInterface.php000060400000007240152456704520013415 0ustar00<?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\Util\AesAdapter;

defined('AKEEBAENGINE') || die();

/**
 * Interface for AES encryption adapters
 */
interface AdapterInterface
{
	/**
	 * Sets the AES encryption mode.
	 *
	 * WARNING: The strength is deprecated as it has a different effect in MCrypt and OpenSSL. MCrypt was abandoned in
	 * 2003 before the Rijndael-128 algorithm was officially the Advanced Encryption Standard (AES). MCrypt also offered
	 * Rijndael-192 and Rijndael-256 algorithms with different block sizes. These are NOT used in AES. OpenSSL, however,
	 * implements AES correctly. It always uses a 128-bit (16 byte) block. The 192 and 256 bit strengths refer to the
	 * key size, not the block size. Therefore using different strengths in MCrypt and OpenSSL will result in different
	 * and incompatible ciphertexts.
	 *
	 * TL;DR: Always use $strength = 128!
	 *
	 * @param   string  $mode      Choose between CBC (recommended) or ECB
	 * @param   int     $strength  Bit strength of the key (128, 192 or 256 bits). DEPRECATED. READ NOTES ABOVE.
	 *
	 * @return  mixed
	 */
	public function setEncryptionMode($mode = 'cbc', $strength = 128);

	/**
	 * Encrypts a string. Returns the raw binary ciphertext.
	 *
	 * WARNING: The plaintext is zero-padded to the algorithm's block size. You are advised to store the size of the
	 * plaintext and trim the string to that length upon decryption.
	 *
	 * @param   string       $plainText  The plaintext to encrypt
	 * @param   string       $key        The raw binary key (will be zero-padded or chopped if its size is different than the block size)
	 * @param   null|string  $iv         The initialization vector (for CBC mode algorithms)
	 *
	 * @return  string  The raw encrypted binary string.
	 */
	public function encrypt($plainText, $key, $iv = null);

	/**
	 * Decrypts a string. Returns the raw binary plaintext.
	 *
	 * $ciphertext MUST start with the IV followed by the ciphertext, even for EBC data (the first block of data is
	 * dropped in EBC mode since there is no concept of IV in EBC).
	 *
	 * WARNING: The returned plaintext is zero-padded to the algorithm's block size during encryption. You are advised
	 * to trim the string to the original plaintext's length upon decryption. While rtrim($decrypted, "\0") sounds
	 * appealing it's NOT the correct approach for binary data (zero bytes may actually be part of your plaintext, not
	 * just padding!).
	 *
	 * @param   string  $cipherText  The ciphertext to encrypt
	 * @param   string  $key         The raw binary key (will be zero-padded or chopped if its size is different than the block size)
	 *
	 * @return  string  The raw unencrypted binary string.
	 */
	public function decrypt($cipherText, $key);

	/**
	 * Returns the encryption block size in bytes
	 *
	 * @return  int
	 */
	public function getBlockSize();

	/**
	 * Is this adapter supported?
	 *
	 * @return  bool
	 */
	public function isSupported();
}
Util/AesAdapter/Mcrypt.php000060400000006630152456704520011474 0ustar00<?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\Util\AesAdapter;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Util\RandomValue;

class Mcrypt extends AbstractAdapter implements AdapterInterface
{
	protected $cipherType = MCRYPT_RIJNDAEL_128;

	protected $cipherMode = MCRYPT_MODE_CBC;

	public function setEncryptionMode($mode = 'cbc', $strength = 128)
	{
		switch ((int) $strength)
		{
			default:
			case '128':
				$this->cipherType = MCRYPT_RIJNDAEL_128;
				break;

			case '192':
				$this->cipherType = MCRYPT_RIJNDAEL_192;
				break;

			case '256':
				$this->cipherType = MCRYPT_RIJNDAEL_256;
				break;
		}

		switch (strtolower($mode))
		{
			case 'ecb':
				$this->cipherMode = MCRYPT_MODE_ECB;
				break;

			default:
			case 'cbc':
				$this->cipherMode = MCRYPT_MODE_CBC;
				break;
		}

	}

	public function encrypt($plainText, $key, $iv = null)
	{
		$iv_size = $this->getBlockSize();
		$key     = $this->resizeKey($key, $iv_size);
		$iv      = $this->resizeKey($iv, $iv_size);

		if (empty($iv))
		{
			$randVal = new RandomValue();
			$iv      = $randVal->generate($iv_size);
		}

		$cipherText = mcrypt_encrypt($this->cipherType, $key, $plainText, $this->cipherMode, $iv);
		$cipherText = $iv . $cipherText;

		return $cipherText;
	}

	public function decrypt($cipherText, $key)
	{
		$iv_size    = $this->getBlockSize();
		$key        = $this->resizeKey($key, $iv_size);
		$iv         = substr($cipherText, 0, $iv_size);
		$cipherText = substr($cipherText, $iv_size);
		$plainText  = mcrypt_decrypt($this->cipherType, $key, $cipherText, $this->cipherMode, $iv);

		return $plainText;
	}

	public function isSupported()
	{
		if (!function_exists('mcrypt_get_key_size'))
		{
			return false;
		}

		if (!function_exists('mcrypt_get_iv_size'))
		{
			return false;
		}

		if (!function_exists('mcrypt_create_iv'))
		{
			return false;
		}

		if (!function_exists('mcrypt_encrypt'))
		{
			return false;
		}

		if (!function_exists('mcrypt_decrypt'))
		{
			return false;
		}

		if (!function_exists('mcrypt_list_algorithms'))
		{
			return false;
		}

		if (!function_exists('hash'))
		{
			return false;
		}

		if (!function_exists('hash_algos'))
		{
			return false;
		}

		$algorightms = mcrypt_list_algorithms();

		if (!in_array('rijndael-128', $algorightms))
		{
			return false;
		}

		if (!in_array('rijndael-192', $algorightms))
		{
			return false;
		}

		if (!in_array('rijndael-256', $algorightms))
		{
			return false;
		}

		$algorightms = hash_algos();

		if (!in_array('sha256', $algorightms))
		{
			return false;
		}

		return true;
	}

	public function getBlockSize()
	{
		return mcrypt_get_iv_size($this->cipherType, $this->cipherMode);
	}
}
Util/AesAdapter/AbstractAdapter.php000060400000004673152456704520013267 0ustar00<?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\Util\AesAdapter;

defined('AKEEBAENGINE') || die();

/**
 * Abstract AES encryption class
 */
abstract class AbstractAdapter
{
	/**
	 * Trims or zero-pads a key / IV
	 *
	 * @param   string  $key   The key or IV to treat
	 * @param   int     $size  The block size of the currently used algorithm
	 *
	 * @return  null|string  Null if $key is null, treated string of $size byte length otherwise
	 */
	public function resizeKey($key, $size)
	{
		if (empty($key))
		{
			return null;
		}

		$keyLength = strlen($key);

		if (function_exists('mb_strlen'))
		{
			$keyLength = mb_strlen($key, 'ASCII');
		}

		if ($keyLength == $size)
		{
			return $key;
		}

		if ($keyLength > $size)
		{
			if (function_exists('mb_substr'))
			{
				return mb_substr($key, 0, $size, 'ASCII');
			}

			return substr($key, 0, $size);
		}

		return $key . str_repeat("\0", ($size - $keyLength));
	}

	/**
	 * Returns null bytes to append to the string so that it's zero padded to the specified block size
	 *
	 * @param   string  $string     The binary string which will be zero padded
	 * @param   int     $blockSize  The block size
	 *
	 * @return  string  The zero bytes to append to the string to zero pad it to $blockSize
	 */
	protected function getZeroPadding($string, $blockSize)
	{
		$stringSize = strlen($string);

		if (function_exists('mb_strlen'))
		{
			$stringSize = mb_strlen($string, 'ASCII');
		}

		if ($stringSize == $blockSize)
		{
			return '';
		}

		if ($stringSize < $blockSize)
		{
			return str_repeat("\0", $blockSize - $stringSize);
		}

		$paddingBytes = $stringSize % $blockSize;

		return str_repeat("\0", $blockSize - $paddingBytes);
	}
}
Util/AesAdapter/OpenSSL.php000060400000007547152456704520011511 0ustar00<?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\Util\AesAdapter;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Util\RandomValue;

class OpenSSL extends AbstractAdapter implements AdapterInterface
{
	/**
	 * The OpenSSL options for encryption / decryption
	 *
	 * @var  int
	 */
	protected $openSSLOptions = 0;

	/**
	 * The encryption method to use
	 *
	 * @var  string
	 */
	protected $method = 'aes-128-cbc';

	public function __construct()
	{
		$this->openSSLOptions = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
	}

	public function setEncryptionMode($mode = 'cbc', $strength = 128)
	{
		static $availableAlgorithms = null;
		static $defaultAlgo = 'aes-128-cbc';

		if (!is_array($availableAlgorithms))
		{
			$availableAlgorithms = openssl_get_cipher_methods();

			foreach ([
				         'aes-256-cbc', 'aes-256-ecb', 'aes-192-cbc',
				         'aes-192-ecb', 'aes-128-cbc', 'aes-128-ecb',
			         ] as $algo)
			{
				if (in_array($algo, $availableAlgorithms))
				{
					$defaultAlgo = $algo;
					break;
				}
			}
		}

		$strength = (int) $strength;
		$mode     = strtolower($mode);

		if (!in_array($strength, [128, 192, 256]))
		{
			$strength = 256;
		}

		if (!in_array($mode, ['cbc', 'ebc']))
		{
			$mode = 'cbc';
		}

		$algo = 'aes-' . $strength . '-' . $mode;

		if (!in_array($algo, $availableAlgorithms))
		{
			$algo = $defaultAlgo;
		}

		$this->method = $algo;
	}

	public function encrypt($plainText, $key, $iv = null)
	{
		$iv_size = $this->getBlockSize();
		$key     = $this->resizeKey($key, $iv_size);
		$iv      = $this->resizeKey($iv, $iv_size);

		if (empty($iv))
		{
			$randVal = new RandomValue();
			$iv      = $randVal->generate($iv_size);
		}

		$plainText  .= $this->getZeroPadding($plainText, $iv_size);
		$cipherText = openssl_encrypt($plainText, $this->method, $key, $this->openSSLOptions, $iv);
		$cipherText = $iv . $cipherText;

		return $cipherText;
	}

	public function decrypt($cipherText, $key)
	{
		$iv_size    = $this->getBlockSize();
		$key        = $this->resizeKey($key, $iv_size);
		$iv         = substr($cipherText, 0, $iv_size);
		$cipherText = substr($cipherText, $iv_size);
		$plainText  = openssl_decrypt($cipherText, $this->method, $key, $this->openSSLOptions, $iv);

		return $plainText;
	}

	public function isSupported()
	{
		if (!function_exists('openssl_get_cipher_methods'))
		{
			return false;
		}

		if (!function_exists('openssl_random_pseudo_bytes'))
		{
			return false;
		}

		if (!function_exists('openssl_cipher_iv_length'))
		{
			return false;
		}

		if (!function_exists('openssl_encrypt'))
		{
			return false;
		}

		if (!function_exists('openssl_decrypt'))
		{
			return false;
		}

		if (!function_exists('hash'))
		{
			return false;
		}

		if (!function_exists('hash_algos'))
		{
			return false;
		}

		$algorightms = openssl_get_cipher_methods();

		if (!in_array('aes-128-cbc', $algorightms))
		{
			return false;
		}

		$algorightms = hash_algos();

		if (!in_array('sha256', $algorightms))
		{
			return false;
		}

		return true;
	}

	/**
	 * @return int
	 */
	public function getBlockSize()
	{
		return openssl_cipher_iv_length($this->method);
	}
}
Util/CRC32.php000060400000006113152456704520006755 0ustar00<?php
/**
 * Akeeba Engine
 *
 * @package   akeebaengine
 * @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

namespace Akeeba\Engine\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;

/**
 * A handy class to abstract the calculation of CRC32 of files under various
 * server conditions and versions of PHP.
 */
class CRC32
{
	/**
	 * Returns the CRC32 of a file, selecting the more appropriate algorithm.
	 *
	 * @param   string   $filename                    Absolute path to the file being processed
	 * @param   integer  $AkeebaPackerZIP_CHUNK_SIZE  Obsoleted
	 *
	 * @return integer The CRC32 in numerical form
	 */
	public function crc32_file($filename, $AkeebaPackerZIP_CHUNK_SIZE)
	{
		static $configuration;

		if (!$configuration)
		{
			$configuration = Factory::getConfiguration();
		}

		$res = false;

		if (function_exists("hash_file"))
		{
			$res        = $this->crc32UsingHashExtension($filename);
			$calcMethod = 'HASH_FILE';
		}
		elseif (function_exists("file_get_contents") && (@filesize($filename) <= $AkeebaPackerZIP_CHUNK_SIZE))
		{
			$res        = $this->crc32Legacy($filename);
			$calcMethod = 'FILE_GET_CONTENTS';
		}
		else
		{
			$res        = 0;
			$calcMethod = 'FAKE - CANNOT CALCULATE';
		}

		if ($res === false)
		{
			$res = 0;

			Factory::getLog()->warning("File $filename - NOT READABLE: CRC32 IS WRONG!");
		}
		else
		{
			try
			{
				$humanReadable = dechex($res);
			}
			catch (\Throwable $e)
			{
			}

			if (($humanReadable ?? null) === null)
			{
				$temp = $res > 2147483647 ? -($res - 2147483648) : $res;
				$humanReadable = dechex($res);
			}

			Factory::getLog()->debug(sprintf("File %s - CRC32 = %s [%s]", $filename, $humanReadable ?? '(cannot display - you have a 32-bit version of PHP)', $calcMethod));
		}

		return $res;
	}

	/**
	 * Very efficient CRC32 calculation using the PHP 'hash' extension.
	 *
	 * @param   string  $filename  Absolute filepath
	 *
	 * @return integer The CRC32
	 */
	protected function crc32UsingHashExtension($filename)
	{
		// Detection of buggy PHP hosts
		static $mustInvert = null;

		if (is_null($mustInvert))
		{
			$test_crc   = @hash('crc32b', 'test', false);
			$mustInvert = (strtolower($test_crc) == '0c7e7fd8'); // Normally, it's D87F7E0C :)

			if ($mustInvert)
			{
				Factory::getLog()->warning('Your server has a buggy PHP version which produces inverted CRC32 values. Attempting a workaround. ZIP files may appear as corrupt.');
			}
		}

		$res = @hash_file('crc32b', $filename, false);

		if ($mustInvert)
		{
			// Workaround for buggy PHP versions (I think before 5.1.8) which produce inverted CRC32 sums
			$res2 = substr($res, 6, 2) . substr($res, 4, 2) . substr($res, 2, 2) . substr($res, 0, 2);
			$res  = $res2;
		}

		$res = hexdec($res);

		return $res;
	}

	/**
	 * A compatible CRC32 calculation using file_get_contents, utilizing immense amounts of RAM
	 *
	 * @param   string  $filename
	 *
	 * @return integer
	 */
	protected function crc32Legacy($filename)
	{
		return crc32(@file_get_contents($filename));
	}
}
Util/RandomValue.php000060400000010731152456704520010417 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

/**
 * Crypto-safe random value generator. Based on the Randval class of the Aura for PHP's Session package.
 * The following is the license file accompanying the original file.
 *
 * ********************************************************************************
 * Copyright (c) 2011-2016, Aura for PHP
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ********************************************************************************
 *
 * Please note that this is a MODIFIED copy of the Randval class, mainly to allow it to be used on hosts
 * which lack both mbcrypt and OpenSSL PHP modules.
 */
class RandomValue
{
	/**
	 *
	 * Returns a cryptographically secure random value.
	 *
	 * @param   integer  $bytes  How many bytes to return
	 *
	 * @return  string
	 */
	public function generate($bytes = 32)
	{
		return random_bytes($bytes);
	}

	/**
	 * Generates a random string with the specified length. WARNING: You get to specify the number of
	 * random characters in the string, not the number of random bytes. The character pool is 64 characters
	 * (6 bits) long. The entropy of your string is 6 * $characters bits. This means that a random string
	 * of 32 characters has an entropy of 192 bits whereas a random sequence of 32 bytes returned by generate()
	 * has an entropy of 8 * 32 = 256 bits.
	 *
	 * @param   int    $characters    Number of characters
	 * @param   string $characterSet  Characters to pick from
	 *
	 * @return string
	 */
	public function generateString($characters = 32, $characterSet = 'abcdefghijklmnopqrstuvwxyz-ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789')
	{
		$sourceString = str_split('abcdefghijklmnopqrstuvwxyz-ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789', 1);
		$ret          = '';

		$bytes     = ceil($characters / 4) * 3;
		$randBytes = $this->generate($bytes);

		for ($i = 0; $i < $bytes; $i += 3)
		{
			$subBytes = substr($randBytes, $i, 3);
			$subBytes = str_split($subBytes, 1);
			$subBytes = ord($subBytes[0]) * 65536 + ord($subBytes[1]) * 256 + ord($subBytes[2]);
			$subBytes = $subBytes & bindec('00000000111111111111111111111111');

			$b    = [];
			$b[0] = $subBytes >> 18;
			$b[1] = ($subBytes >> 12) & bindec('111111');
			$b[2] = ($subBytes >> 6) & bindec('111111');
			$b[3] = $subBytes & bindec('111111');

			$ret .= $sourceString[$b[0]] . $sourceString[$b[1]] . $sourceString[$b[2]] . $sourceString[$b[3]];
		}

		return substr($ret, 0, $characters);
	}
}
Util/Buffer.php000060400000011316152456704520007413 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

/**
 * Generic Buffer stream handler
 *
 * This class provides a generic buffer stream.  It can be used to store/retrieve/manipulate
 * string buffers with the standard PHP filesystem I/O methods.
 */
class Buffer
{

	/**
	 * Stream position
	 *
	 * @var    integer
	 */
	public $position = 0;

	/**
	 * Buffer name
	 *
	 * @var    string
	 */
	public $name = null;

	/**
	 * Buffer hash
	 *
	 * @var    array
	 */
	public $_buffers = [];

	/**
	 * Function to open file or url
	 *
	 * @param   string   $path          The URL that was passed
	 * @param   string   $mode          Mode used to open the file @see fopen
	 * @param   integer  $options       Flags used by the API, may be STREAM_USE_PATH and
	 *                                  STREAM_REPORT_ERRORS
	 * @param   string  &$opened_path   Full path of the resource. Used with STREAM_USE_PATH option
	 *
	 * @return  boolean
	 *
	 * @see     streamWrapper::stream_open
	 */
	public function stream_open($path, $mode, $options, &$opened_path)
	{
		$url                         = parse_url($path);
		$this->name                  = $url["host"];
		$this->_buffers[$this->name] = null;
		$this->position              = 0;

		return true;
	}

	/**
	 * Read stream
	 *
	 * @param   integer  $count  How many bytes of data from the current position should be returned.
	 *
	 * @return  mixed    The data from the stream up to the specified number of bytes (all data if
	 *                   the total number of bytes in the stream is less than $count. Null if
	 *                   the stream is empty.
	 *
	 * @see     streamWrapper::stream_read
	 */
	public function stream_read($count)
	{
		$ret            = substr($this->_buffers[$this->name], $this->position, $count);
		$this->position += strlen($ret);

		return $ret;
	}

	/**
	 * Write stream
	 *
	 * @param   string  $data  The data to write to the stream.
	 *
	 * @return  integer
	 *
	 * @see     streamWrapper::stream_write
	 */
	public function stream_write($data)
	{
		$left                        = substr($this->_buffers[$this->name], 0, $this->position);
		$right                       = substr($this->_buffers[$this->name], $this->position + strlen($data));
		$this->_buffers[$this->name] = $left . $data . $right;
		$this->position              += strlen($data);

		return strlen($data);
	}

	/**
	 * Function to get the current position of the stream
	 *
	 * @return  integer
	 *
	 * @see     streamWrapper::stream_tell
	 */
	public function stream_tell()
	{
		return $this->position;
	}

	/**
	 * Function to test for end of file pointer
	 *
	 * @return  boolean  True if the pointer is at the end of the stream
	 *
	 * @see     streamWrapper::stream_eof
	 */
	public function stream_eof()
	{
		return $this->position >= strlen($this->_buffers[$this->name]);
	}

	/**
	 * The read write position updates in response to $offset and $whence
	 *
	 * @param   integer  $offset   The offset in bytes
	 * @param   integer  $whence   Position the offset is added to
	 *                             Options are SEEK_SET, SEEK_CUR, and SEEK_END
	 *
	 * @return  boolean  True if updated
	 *
	 * @see     streamWrapper::stream_seek
	 */
	public function stream_seek($offset, $whence)
	{
		switch ($whence)
		{
			case SEEK_SET:
				if ($offset < strlen($this->_buffers[$this->name]) && $offset >= 0)
				{
					$this->position = $offset;

					return true;
				}
				else
				{
					return false;
				}
				break;

			case SEEK_CUR:
				if ($offset >= 0)
				{
					$this->position += $offset;

					return true;
				}
				else
				{
					return false;
				}
				break;

			case SEEK_END:
				if (strlen($this->_buffers[$this->name]) + $offset >= 0)
				{
					$this->position = strlen($this->_buffers[$this->name]) + $offset;

					return true;
				}
				else
				{
					return false;
				}
				break;

			default:
				return false;
		}
	}
}

// Register the stream
stream_wrapper_register("buffer", Buffer::class);
Util/PushMessages.php000060400000010440152456704520010606 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Util\Pushbullet\Connector;
use Exception;

class PushMessages implements PushMessagesInterface
{
	/**
	 * The PushBullet connector
	 *
	 * @var Connector[]
	 */
	private $connectors = [];

	/**
	 * Should we send push messages?
	 *
	 * @var bool
	 */
	private $enabled = true;

	/**
	 * Creates the push messaging object
	 */
	public function __construct()
	{
		$pushPreference = Platform::getInstance()->get_platform_configuration_option('push_preference', '0');
		$apiKey         = Platform::getInstance()->get_platform_configuration_option('push_apikey', '');

		// No API key? No push messages are enabled, so no point continuing really...
		if (empty($apiKey))
		{
			$pushPreference = 0;
		}

		// We use a switch in case we add support for more push APIs in the future. The push_preference platform
		// option will tell us which service to use. In that case we'll have to refactor this class, but the public
		// API will remain the same.
		switch ($pushPreference)
		{
			default:
			case 0:
				$this->enabled = false;
				break;

			case 1:
				$keys = explode(',', $apiKey);
				$keys = array_map('trim', $keys);

				foreach ($keys as $key)
				{
					try
					{
						$connector = new Connector($key);
						$connector->getDevices();
						$this->connectors[] = $connector;
					}
					catch (Exception $e)
					{
						Factory::getLog()->warning("Push messages cannot be sent with API key $key. Error received when trying to establish PushBullet connection: " . $e->getMessage());
					}
				}

				if (empty($this->connectors))
				{
					Factory::getLog()->warning('No push messages can be sent: none of the provided API keys is usable. Push messages have been deactivated.');

					$this->enabled = false;
				}

				break;
		}
	}

	/**
	 * Sends a push message to all connected devices. The intent is to provide the user with an information message,
	 * e.g. notify them about the progress of the backup.
	 *
	 * @param   string  $subject  The subject of the message, shown in the lock screen. Keep it short.
	 * @param   string  $details  Long(er) description of what the message is about. Plain text (no HTML).
	 *
	 * @return  void
	 */
	public function message($subject, $details = null)
	{
		if (!$this->enabled)
		{
			return;
		}

		foreach ($this->connectors as $connector)
		{
			try
			{
				$connector->pushNote('', $subject, $details);
			}
			catch (Exception $e)
			{
				Factory::getLog()->warning('Push messages suspended. Error received when trying to send push message:' . $e->getMessage());
				$this->enabled = false;
			}
		}
	}

	/**
	 * Sends a push message, containing a URL/URI, to all connected devices. The URL will be rendered as something
	 * clickable on most devices.
	 *
	 * @param   string  $url      The URL/URI
	 * @param   string  $subject  The subject of the message, shown in the lock screen. Keep it short.
	 * @param   string  $details  Long(er) description of what the message is about. Plain text (no HTML).
	 *
	 * @return  void
	 */
	public function link($url, $subject, $details = null)
	{
		if (!$this->enabled)
		{
			return;
		}

		foreach ($this->connectors as $connector)
		{
			try
			{
				$connector->pushLink('', $subject, $url, $details);
			}
			catch (Exception $e)
			{
				Factory::getLog()->warning('Push messages suspended. Error received when trying to send push message with a link:' . $e->getMessage());
				$this->enabled = false;
			}
		}
	}
}
Util/ParseIni.php000060400000017564152456704520007727 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

/**
 * A utility class to parse INI files.
 *
 * This is marked deprecated since Akeeba Engine 6.4.1. The configuration of the engine is no longer stored as INI data.
 * Moreover, we will be migrating away from the current INI files used for defining engine and GUI configuration
 * parameters.
 *
 * @package     Akeeba\Engine\Util
 *
 * @deprecated  6.4.1
 */
abstract class ParseIni
{
	/**
	 * Parse an INI file and return an associative array. This monstrosity is required because some so-called hosts
	 * have disabled PHP's parse_ini_file() function for "security reasons". Apparently their blatant ignorance doesn't
	 * allow them to discern between the innocuous parse_ini_file and the potentially dangerous ini_set, leading them to
	 * disable the former and let the latter enabled.
	 *
	 * @param   string  $file              The file name or raw INI data to process
	 * @param   bool    $process_sections  True to also process INI sections
	 * @param   bool    $rawdata           Is this raw INI data? False when $file is a filepath.
	 * @param   bool    $forcePHP          Should I force the use of the pure-PHP INI file parser?
	 *
	 * @return   array    An associative array of sections, keys and values
	 */
	public static function parse_ini_file($file, $process_sections = false, $rawdata = false, $forcePHP = false)
	{
		/**
		 * WARNING: DO NOT USE INI_SCANNER_RAW IN THE parse_ini_string / parse_ini_file FUNCTION CALLS WITHOUT POST-
		 *          PROCESSING!
		 *
		 * Sometimes we need to save data which is either multiline or has double quotes in the Engine's
		 * configuration. For this reason we have to manually escape \r, \n, \t and \" in
		 * Akeeba\Engine\Configuration::dumpObject(). If we don't we end up with multiline INI values which
		 * won't work. However, if we are using INI_SCANNER_RAW these characters are not escaped back to their
		 * original form. As a result we end up with broken data which cause various problems, the most visible
		 * of which is that Google Storage integration is broken since the JSON data included in the config is
		 * now unparseable.
		 *
		 * However, not using raw mode introduces other problems. For example, the sequence \$ is converted to $ because
		 * it's assumed to be an escaped dollar sign. Things like $foo are addressed as variable interpolation, i.e.
		 * "This is ${foo} wrong" results in "This is  wrong" because $foo is considered as an interpolated variable.
		 *
		 * The solution to that is to use raw mode to parse the INI files and THEN unescape the variables. However, we
		 * cannot simply use stripslashes/stripcslashes because we could end up replacing more than we should (unlike
		 * addcslashes we cannot specify a list of escaped characters to consider). We have to do a slower string
		 * replace instead.
		 *
		 * The next problem to consider is that when $process_sections is true some of the values generated are arrays
		 * or even nested arrays. If you try to string replace on them hilarity ensues. Therefore we have the recursive
		 * unescape method which takes care of that. To make things faster and maintain the array keys we use array_map
		 * to apply recursiveUnescape to the array.
		 */

		if ($rawdata)
		{
			if (!function_exists('parse_ini_string'))
			{
				return self::parse_ini_file_php($file, $process_sections, $rawdata);
			}

			// !!! VERY IMPORTANT !!! Read the warning above before touching this line
			return array_map([
				__CLASS__, 'recursiveUnescape',
			], parse_ini_string($file, $process_sections, INI_SCANNER_RAW));
		}

		if (!function_exists('parse_ini_file'))
		{
			return self::parse_ini_file_php($file, $process_sections);
		}

		// !!! VERY IMPORTANT !!! Read the warning above before touching this line
		return array_map([__CLASS__, 'recursiveUnescape'], parse_ini_file($file, $process_sections, INI_SCANNER_RAW));
	}

	/**
	 * Recursively unescape values which have been escaped by Akeeba\Engine\Configuration::dumpObject().
	 *
	 * @param   string|array  $value
	 *
	 * @return  string|array  Unescaped result
	 */
	static function recursiveUnescape($value)
	{
		if (is_array($value))
		{
			return array_map([__CLASS__, 'recursiveUnescape'], $value);
		}

		return str_replace(['\r', '\n', '\t', '\"'], ["\r", "\n", "\t", '"'], $value);
	}

	/**
	 * A PHP based INI file parser.
	 *
	 * Thanks to asohn ~at~ aircanopy ~dot~ net for posting this handy function on
	 * the parse_ini_file page on http://gr.php.net/parse_ini_file
	 *
	 * @param   string  $file              Filename to process
	 * @param   bool    $process_sections  True to also process INI sections
	 * @param   bool    $rawdata           If true, the $file contains raw INI data, not a filename
	 *
	 * @return    array    An associative array of sections, keys and values
	 */
	static function parse_ini_file_php($file, $process_sections = false, $rawdata = false)
	{
		$process_sections = ($process_sections !== true) ? false : true;

		if (!$rawdata)
		{
			$ini = file($file);
		}
		else
		{
			$file = str_replace("\r", "", $file);
			$ini  = explode("\n", $file);
		}

		if (!is_array($ini))
		{
			return [];
		}

		if (count($ini) == 0)
		{
			return [];
		}

		$sections = [];
		$values   = [];
		$result   = [];
		$globals  = [];
		$i        = 0;
		foreach ($ini as $line)
		{
			$line = trim($line);
			$line = str_replace("\t", " ", $line);

			// Comments
			if (!preg_match('/^[a-zA-Z0-9[]/', $line))
			{
				continue;
			}

			// Sections
			if ($line[0] == '[')
			{
				$tmp        = explode(']', $line);
				$sections[] = trim(substr($tmp[0], 1));
				$i++;
				continue;
			}

			// Key-value pair
			$lineParts = explode('=', $line, 2);
			if (count($lineParts) != 2)
			{
				continue;
			}
			$key   = trim($lineParts[0]);
			$value = trim($lineParts[1]);
			unset($lineParts);

			if (strstr($value, ";"))
			{
				$tmp = explode(';', $value);
				if (count($tmp) == 2)
				{
					if ((($value[0] != '"') && ($value[0] != "'")) ||
						preg_match('/^".*"\s*;/', $value) || preg_match('/^".*;[^"]*$/', $value) ||
						preg_match("/^'.*'\s*;/", $value) || preg_match("/^'.*;[^']*$/", $value)
					)
					{
						$value = $tmp[0];
					}
				}
				else
				{
					if ($value[0] == '"')
					{
						$value = preg_replace('/^"(.*)".*/', '$1', $value);
					}
					elseif ($value[0] == "'")
					{
						$value = preg_replace("/^'(.*)'.*/", '$1', $value);
					}
					else
					{
						$value = $tmp[0];
					}
				}
			}
			$value = trim($value);
			$value = trim($value, "'\"");

			if ($i == 0)
			{
				if (substr($line, -1, 2) == '[]')
				{
					$globals[$key][] = $value;
				}
				else
				{
					$globals[$key] = $value;
				}
			}
			else
			{
				if (substr($line, -1, 2) == '[]')
				{
					$values[$i - 1][$key][] = $value;
				}
				else
				{
					$values[$i - 1][$key] = $value;
				}
			}
		}

		for ($j = 0; $j < $i; $j++)
		{
			if ($process_sections === true)
			{
				if (isset($sections[$j]) && isset($values[$j]))
				{
					$result[$sections[$j]] = $values[$j];
				}
			}
			else
			{
				if (isset($values[$j]))
				{
					$result[] = $values[$j];
				}
			}
		}

		return $result + $globals;
	}
}
Util/Pushbullet/ApiException.php000060400000001731152456704520012721 0ustar00<?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\Util\Pushbullet;

defined('AKEEBAENGINE') || die();

use Exception;

class ApiException extends Exception
{
	// Exception thrown by Pushbullet
}
Util/Pushbullet/Connector.php000060400000030600152456704520012260 0ustar00<?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\Util\Pushbullet;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Postproc\ProxyAware;
use CURLFile;

/**
 * Based on Pushbullet-for-PHP 2.10.1 – https://github.com/ivkos/Pushbullet-for-PHP/tree/v2
 *
 * The license for the original class is as follows:
 * ----------
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Ivaylo Stoyanov
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * ----------
 *
 * The following class is a derivative work, NOT the original work.
 */
class Connector
{
	use ProxyAware;

	public const URL_PUSHES = 'https://api.pushbullet.com/v2/pushes';
	public const URL_DEVICES = 'https://api.pushbullet.com/v2/devices';
	public const URL_CONTACTS = 'https://api.pushbullet.com/v2/contacts';
	public const URL_UPLOAD_REQUEST = 'https://api.pushbullet.com/v2/upload-request';
	public const URL_USERS = 'https://api.pushbullet.com/v2/users';
	public const URL_SUBSCRIPTIONS = 'https://api.pushbullet.com/v2/subscriptions';
	public const URL_CHANNEL_INFO = 'https://api.pushbullet.com/v2/channel-info';
	public const URL_EPHEMERALS = 'https://api.pushbullet.com/v2/ephemerals';
	public const URL_PHONEBOOK = 'https://api.pushbullet.com/v2/permanents/phonebook';
	private $_apiKey;
	private $_curlCallback;

	/**
	 * Pushbullet constructor.
	 *
	 * @param   string  $apiKey  API key.
	 *
	 * @throws ApiException
	 */
	public function __construct($apiKey)
	{
		$this->_apiKey = $apiKey;

		if (!function_exists('curl_init'))
		{
			throw new ApiException('cURL library is not loaded.');
		}
	}

	/**
	 * Parse recipient.
	 *
	 * @param   string  $recipient  Recipient string.
	 * @param   array   $data       Data array to populate with the correct recipient parameter.
	 */
	private static function _parseRecipient($recipient, array &$data)
	{
		if (!empty($recipient))
		{
			if (filter_var($recipient, FILTER_VALIDATE_EMAIL) !== false)
			{
				$data['email'] = $recipient;
			}
			else
			{
				if (substr($recipient, 0, 1) == "#")
				{
					$data['channel_tag'] = substr($recipient, 1);
				}
				else
				{
					$data['device_iden'] = $recipient;
				}
			}
		}
	}

	/**
	 * Push a note.
	 *
	 * @param   string  $recipient  Recipient. Can be device_iden, email or channel #tagname.
	 * @param   string  $title      The note's title.
	 * @param   string  $body       The note's message.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function pushNote($recipient, $title, $body = null)
	{
		$data = [];

		Connector::_parseRecipient($recipient, $data);
		$data['type']  = 'note';
		$data['title'] = $title;
		$data['body']  = $body;

		return $this->_curlRequest(self::URL_PUSHES, 'POST', $data);
	}

	/**
	 * Push a link.
	 *
	 * @param   string  $recipient  Recipient. Can be device_iden, email or channel #tagname.
	 * @param   string  $title      The link's title.
	 * @param   string  $url        The URL to open.
	 * @param   string  $body       A message associated with the link.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function pushLink($recipient, $title, $url, $body = null)
	{
		$data = [];

		Connector::_parseRecipient($recipient, $data);
		$data['type']  = 'link';
		$data['title'] = $title;
		$data['url']   = $url;
		$data['body']  = $body;

		return $this->_curlRequest(self::URL_PUSHES, 'POST', $data);
	}

	/**
	 * Push a checklist.
	 *
	 * @param   string    $recipient  Recipient. Can be device_iden, email or channel #tagname.
	 * @param   string    $title      The list's title.
	 * @param   string[]  $items      The list items.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function pushList($recipient, $title, array $items)
	{
		$data = [];

		Connector::_parseRecipient($recipient, $data);
		$data['type']  = 'list';
		$data['title'] = $title;
		$data['items'] = $items;

		return $this->_curlRequest(self::URL_PUSHES, 'POST', $data);
	}

	/**
	 * Push a file.
	 *
	 * @param   string  $recipient    Recipient. Can be device_iden, email or channel #tagname.
	 * @param   string  $filePath     The path of the file to push.
	 * @param   string  $mimeType     The MIME type of the file. If null, we'll try to guess it.
	 * @param   string  $title        The title of the push notification.
	 * @param   string  $body         The body of the push notification.
	 * @param   string  $altFileName  Alternative file name to use instead of the original one.
	 *                                For example, you might want to push 'someFile.tmp' as 'image.jpg'.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function pushFile($recipient, $filePath, $mimeType = null, $title = null, $body = null, $altFileName = null)
	{
		$data = [];

		$fullFilePath = realpath($filePath);

		if (!is_readable($fullFilePath))
		{
			throw new ApiException('File: File does not exist or is unreadable.');
		}

		if (filesize($fullFilePath) > 25 * 1024 * 1024)
		{
			throw new ApiException('File: File size exceeds 25 MB.');
		}

		$data['file_name'] = $altFileName ?? basename($fullFilePath);

		// Try to guess the MIME type if the argument is NULL
		$data['file_type'] = $mimeType ?? mime_content_type($fullFilePath);

		// Request authorization to upload the file
		$response         = $this->_curlRequest(self::URL_UPLOAD_REQUEST, 'GET', $data);
		$data['file_url'] = $response->file_url;

		$response->data->file = new CURLFile($fullFilePath);

		// Upload the file
		$this->_curlRequest($response->upload_url, 'POST', $response->data, false, false);

		Connector::_parseRecipient($recipient, $data);
		$data['type']  = 'file';
		$data['title'] = $title;
		$data['body']  = $body;

		return $this->_curlRequest(self::URL_PUSHES, 'POST', $data);
	}

	/**
	 * Get push history.
	 *
	 * @param   int     $modifiedAfter  Request pushes modified after this UNIX timestamp.
	 * @param   string  $cursor         Request the next page via its cursor from a previous response. See the API
	 *                                  documentation (https://docs.pushbullet.com/http/) for a detailed description.
	 * @param   int     $limit          Maximum number of objects on each page.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function getPushHistory($modifiedAfter = 0, $cursor = null, $limit = null)
	{
		$data                   = [];
		$data['modified_after'] = $modifiedAfter;

		if ($cursor !== null)
		{
			$data['cursor'] = $cursor;
		}

		if ($limit !== null)
		{
			$data['limit'] = $limit;
		}

		return $this->_curlRequest(self::URL_PUSHES, 'GET', $data);
	}

	/**
	 * Dismiss a push.
	 *
	 * @param   string  $pushIden  push_iden of the push notification.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function dismissPush($pushIden)
	{
		return $this->_curlRequest(self::URL_PUSHES . '/' . $pushIden, 'POST', ['dismissed' => true]);
	}

	/**
	 * Delete a push.
	 *
	 * @param   string  $pushIden  push_iden of the push notification.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function deletePush($pushIden)
	{
		return $this->_curlRequest(self::URL_PUSHES . '/' . $pushIden, 'DELETE');
	}

	/**
	 * Get a list of available devices.
	 *
	 * @param   int     $modifiedAfter  Request devices modified after this UNIX timestamp.
	 * @param   string  $cursor         Request the next page via its cursor from a previous response. See the API
	 *                                  documentation (https://docs.pushbullet.com/http/) for a detailed description.
	 * @param   int     $limit          Maximum number of objects on each page.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function getDevices($modifiedAfter = 0, $cursor = null, $limit = null)
	{
		$data                   = [];
		$data['modified_after'] = $modifiedAfter;

		if ($cursor !== null)
		{
			$data['cursor'] = $cursor;
		}

		if ($limit !== null)
		{
			$data['limit'] = $limit;
		}

		return $this->_curlRequest(self::URL_DEVICES, 'GET', $data);
	}

	/**
	 * Get information about the current user.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function getUserInformation()
	{
		return $this->_curlRequest(self::URL_USERS . '/me', 'GET');
	}

	/**
	 * Update preferences for the current user.
	 *
	 * @param   array  $preferences  Preferences.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function updateUserPreferences($preferences)
	{
		return $this->_curlRequest(self::URL_USERS . '/me', 'POST', ['preferences' => $preferences]);
	}

	/**
	 * Add a callback function that will be invoked right before executing each cURL request.
	 *
	 * @param   callable  $callback  The callback function.
	 */
	public function addCurlCallback(callable $callback)
	{
		$this->_curlCallback = $callback;
	}

	/**
	 * Send a request to a remote server using cURL.
	 *
	 * @param   string  $url         URL to send the request to.
	 * @param   string  $method      HTTP method.
	 * @param   array   $data        Query data.
	 * @param   bool    $sendAsJSON  Send the request as JSON.
	 * @param   bool    $auth        Use the API key to authenticate
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	private function _curlRequest($url, $method, $data = null, $sendAsJSON = true, $auth = true)
	{
		$curl = curl_init();

		$this->applyProxySettingsToCurl($curl);

		if ($method == 'GET' && $data !== null)
		{
			$url .= '?' . http_build_query($data);
		}

		curl_setopt($curl, CURLOPT_URL, $url);

		if ($auth)
		{
			curl_setopt($curl, CURLOPT_USERPWD, $this->_apiKey);
		}

		curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);

		if ($method == 'POST' && $data !== null)
		{
			if ($sendAsJSON)
			{
				$data = json_encode($data);
				curl_setopt($curl, CURLOPT_HTTPHEADER, [
					'Content-Type: application/json',
					'Content-Length: ' . strlen($data),
				]);
			}

			curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
		}

		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curl, CURLOPT_HEADER, false);

		@curl_setopt($curl, CURLOPT_CAINFO, AKEEBA_CACERT_PEM);
		curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
		curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);

		if ($this->_curlCallback !== null)
		{
			$curlCallback = $this->_curlCallback;
			$curlCallback($curl);
		}

		$response = curl_exec($curl);

		if ($response === false)
		{
			$curlError = curl_error($curl);
			curl_close($curl);
			throw new ApiException('cURL Error: ' . $curlError);
		}

		$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

		if ($httpCode >= 400)
		{
			curl_close($curl);
			$responseParsed = json_decode($response);
			throw new ApiException('HTTP Error ' . $httpCode .
				' (' . $responseParsed->error->type . '): ' . $responseParsed->error->message);
		}

		curl_close($curl);

		return json_decode($response);
	}
}
Util/FileCloseAware.php000060400000002136152456704520011027 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Throwable;

trait FileCloseAware
{
	protected function conditionalFileClose($fp): bool
	{
		if (!is_resource($fp))
		{
			return false;
		}

		try
		{
			return @fclose($fp);
		}
		catch (Throwable $e)
		{
			return false;
		}
	}

}Util/FileSystem.php000060400000034077152456704520010277 0ustar00<?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\Util;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;

/**
 * Utility functions related to filesystem objects, e.g. path translation
 */
class FileSystem
{
	/**
	 * Are we running under Windows?
	 *
	 * @var   bool
	 */
	private $isWindows = false;

	/**
	 * Local cache of the platform stock directories
	 *
	 * @var   array|null
	 * @since 7.0.3
	 */
	protected static $stockDirs = null;

	/**
	 * Initialise the object
	 */
	public function __construct()
	{
		$this->isWindows = (DIRECTORY_SEPARATOR == '\\');
	}

	/**
	 * Makes a Windows path more UNIX-like, by turning backslashes to forward slashes.
	 * It takes into account UNC paths, e.g. \\myserver\some\folder becomes
	 * \\myserver/some/folder.
	 *
	 * This function will also fix paths with multiple slashes, e.g. convert /var//www////html to /var/www/html
	 *
	 * @param   string  $p_path  The path to transform
	 *
	 * @return  string
	 */
	public function TranslateWinPath($p_path)
	{
		$is_unc = false;

		if ($this->isWindows)
		{
			// Is this a UNC path?
			$is_unc = (substr($p_path, 0, 2) == '\\\\') || (substr($p_path, 0, 2) == '//');

			// Change potential windows directory separator
			if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0, 1) == '\\'))
			{
				$p_path = strtr($p_path, '\\', '/');
			}
		}

		// Remove multiple slashes
		$p_path = str_replace('///', '/', $p_path);
		$p_path = str_replace('//', '/', $p_path);

		// Fix UNC paths
		if ($is_unc)
		{
			$p_path = '//' . ltrim($p_path, '/');
		}

		return $p_path;
	}

	/**
	 * Removes trailing slash or backslash from a pathname
	 *
	 * @param   string  $path  The path to treat
	 *
	 * @return  string  The path without the trailing slash/backslash
	 */
	public function TrimTrailingSlash($path)
	{
		$newpath = $path;

		if (substr($path, strlen($path) - 1, 1) == '\\')
		{
			$newpath = substr($path, 0, strlen($path) - 1);
		}

		if (substr($path, strlen($path) - 1, 1) == '/')
		{
			$newpath = substr($path, 0, strlen($path) - 1);
		}

		return $newpath;
	}

	/**
	 * Returns an array with the archive name variables and their values. This is used to replace variables in archive
	 * and directory names, etc.
	 *
	 * If there is a non-empty configuration value called volatile.core.archivenamevars with a serialised array it will
	 * be unserialised and used. Otherwise the name variables will be calculated on-the-fly.
	 *
	 * IMPORTANT: These variables do NOT include paths such as [SITEROOT]
	 *
	 * @return  array
	 */
	public function get_archive_name_variables()
	{
		$variables = [];

		$registry   = Factory::getConfiguration();
		$serialized = $registry->get('volatile.core.archivenamevars', null);

		if (!empty($serialized))
		{
			$variables = @unserialize($serialized);
		}

		if (empty($variables) || !is_array($variables))
		{
			$host         = Platform::getInstance()->get_host();
			$version      = defined('AKEEBA_VERSION') ? AKEEBA_VERSION : 'svn';
			$version      = defined('AKEEBABACKUP_VERSION') ? AKEEBABACKUP_VERSION : $version;
			$platformVars = Platform::getInstance()->getPlatformVersion();

			$siteName = $this->stringUrlUnicodeSlug(Platform::getInstance()->get_site_name());

			if (strlen($siteName) > 50)
			{
				$siteName = substr($siteName, 0, 50);
			}

			/**
			 * Time components. Expressed in whatever timezone the Platform decides to use.
			 */
			// Raw timezone, e.g. "EEST"
			$rawTz = Platform::getInstance()->get_local_timestamp("T");
			// Filename-safe timezone, e.g. "eest". Note the lowercase letters.
			$fsSafeTZ = strtolower(str_replace([' ', '/', ':'], ['_', '_', '_'], $rawTz));

			$randVal = new RandomValue();

			$variables = [
				'[DATE]'             => Platform::getInstance()->get_local_timestamp("Ymd"),
				'[YEAR]'             => Platform::getInstance()->get_local_timestamp("Y"),
				'[MONTH]'            => Platform::getInstance()->get_local_timestamp("m"),
				'[DAY]'              => Platform::getInstance()->get_local_timestamp("d"),
				'[TIME]'             => Platform::getInstance()->get_local_timestamp("His"),
				'[TIME_TZ]'          => Platform::getInstance()->get_local_timestamp("His") . $fsSafeTZ,
				'[WEEK]'             => Platform::getInstance()->get_local_timestamp("W"),
				'[WEEKDAY]'          => Platform::getInstance()->get_local_timestamp("l"),
				'[TZ]'               => $fsSafeTZ,
				'[TZ_RAW]'           => $rawTz,
				'[GMT_OFFSET]'       => Platform::getInstance()->get_local_timestamp("O"),
				'[HOST]'             => empty($host) ? 'unknown_host' : $host,
				'[VERSION]'          => $version,
				'[PLATFORM_NAME]'    => $platformVars['name'],
				'[PLATFORM_VERSION]' => $platformVars['version'],
				'[SITENAME]'         => $siteName,
				'[RANDOM]'           => $randVal->generateString(16, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789'),
			];
		}

		return $variables;
	}

	/**
	 * Expands the archive name variables in $source. For example "[DATE]-foobar" would be expanded to something
	 * like "141101-foobar". IMPORTANT: These variables do NOT include paths.
	 *
	 * @param   string  $source  The input string, possibly containing variables in the form of [VARIABLE]
	 *
	 * @return  string  The expanded string
	 */
	public function replace_archive_name_variables($source)
	{
		$tagReplacements = $this->get_archive_name_variables();

		return str_replace(array_keys($tagReplacements), array_values($tagReplacements), $source);
	}

	/**
	 * Expand the platform-specific stock directories variables in the input string. For example "[SITEROOT]/foobar"
	 * would be expanded to something like "/var/www/html/mysite/foobar"
	 *
	 * @param   string  $folder               The input string to expand
	 * @param   bool    $translate_win_dirs   Should I translate Windows path separators to UNIX path separators? (default: false)
	 * @param   bool    $trim_trailing_slash  Should I remove the trailing slash (default: false)
	 *
	 * @return  string  The expanded string
	 */
	public function translateStockDirs($folder, $translate_win_dirs = false, $trim_trailing_slash = false)
	{
		if (is_null(self::$stockDirs))
		{
			self::$stockDirs = Platform::getInstance()->get_stock_directories();
		}

		$temp = $folder;

		foreach (self::$stockDirs as $find => $replace)
		{
			$temp = str_replace($find, $replace, $temp);
		}

		if ($translate_win_dirs)
		{
			$temp = $this->TranslateWinPath($temp);
		}

		if ($trim_trailing_slash)
		{
			$temp = $this->TrimTrailingSlash($temp);
		}

		return $temp;
	}

	/**
	 * Rebase a path to the platform filesystem variables (most to least specific).
	 *
	 * This is the inverse procedure of translateStockDirs().
	 *
	 * @param   string  $path
	 *
	 * @return  string
	 * @since   7.3.0
	 */
	public function rebaseFolderToStockDirs(string $path): string
	{
		// Normalize the path
		$path = $this->TrimTrailingSlash($path);
		$path = $this->TranslateWinPath($path);

		// Get the stock directories, normalize them and sort them by longest to shortest
		$stock_directories = Platform::getInstance()->get_stock_directories();

		$stock_directories = array_map(function ($path) {
			$path = $this->TrimTrailingSlash($path);

			return $this->TranslateWinPath($path);
		}, $stock_directories);

		uasort($stock_directories, function ($a, $b) {
			return -($a <=> $b);
		});

		// Start replacing paths with variables
		foreach ($stock_directories as $var => $stockPath)
		{
			if (empty($stockPath))
			{
				continue;
			}

			if (strpos($path, $stockPath) !== 0)
			{
				continue;
			}

			$path = $var . substr($path, strlen($stockPath));
		}

		return $path;
	}

	/**
	 * Generates a set of files which prevent direct web access or at least web listing of the folder contents.
	 *
	 * This method generates a .htaccess for Apache, Lighttpd and Litespeed; a web.config file for IIS 7 or later; an
	 * index.php, index.html and index.htm file for all other browsers.
	 *
	 * Despite this security precaution it is STRONGLY advised to keep your backup archives in a directory outside the
	 * site's web root as explained in the Security Information chapter of the documentation. This method is designed
	 * to only provide a defence of last resort.
	 *
	 * @param   string  $dir    The output directory to secure against web access
	 * @param   bool    $force  Forcibly overwrite existing files
	 *
	 * @return  void
	 * @since   7.0.3
	 */
	public function ensureNoAccess($dir, $force = false)
	{
		// Create a .htaccess file to prevent all web access (Apache 1.3+, Lightspeed, Lighttpd, ...)
		if (!is_file($dir . '/.htaccess') || $force)
		{
			$htaccess = <<< APACHE
## This file was generated automatically by the Akeeba Backup Engine
##
## DO NOT REMOVE THIS FILE
##
## This file makes sure that your backup output directory is not directly accessible from the web if you are using
## the Apache, Lighttpd and Litespeed web server. This prevents unauthorized access to your backup archive files and
## backup log files. Removing this file could have security implications for your site.
##
## You are strongly advised to never delete or modify any of the files automatically created in this folder by the
## Akeeba Backup Engine, namely:
##
## * .htaccess
## * web.config
## * index.html
## * index.htm
## * index.php
##
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
<IfModule mod_authz_core.c>
  <RequireAll>
    Require all denied
  </RequireAll>
</IfModule>
APACHE;

			@file_put_contents($dir . '/.htaccess', $htaccess);
		}

		// Create a web.config to prevent all web access (IIS 7+)
		if (!is_file($dir . '/web.config') || $force)
		{
			$webConfig = <<< XML
<?xml version="1.0"?>
<!--
This file was generated automatically by the Akeeba Backup Engine

DO NOT REMOVE THIS FILE

This file makes sure that your backup output directory is not directly accessible from the web if you are using the
Microsoft Internet Information Services (IIS) web server, version 7 or later. This prevents unauthorized access to your
backup archive files and backup log files. Removing this file could have security implications for your site.

As noted above, this only works on IIS 7 or later.
See https://www.iis.net/configreference/system.webserver/security/requestfiltering/fileextensions

You are strongly advised to never delete or modify any of the files automatically created in this folder by the
Akeeba Backup Engine, namely:

* .htaccess
* web.config
* index.html
* index.htm
* index.php

-->
<configuration>
    <system.webServer>
        <security>
            <requestFiltering>
                <fileExtensions allowUnlisted="false" >
                    <clear />
                    <add fileExtension=".html" allowed="true"/>
                </fileExtensions>
            </requestFiltering>
        </security>
    </system.webServer>
</configuration>
XML;
			@file_put_contents($dir . '/web.config', $webConfig);
		}

		// Create a blank index.html or index.htm to prevent directory listings (all servers)
		$blankHtml = <<< HTML
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>Access Denied</title>
  </head>
  <body>
	  <h1>Access Denied</h1>
  </body>
</html>
HTML;

		if (!is_file($dir . '/index.html') || $force)
		{
			@file_put_contents($dir . '/index.html', $blankHtml);
		}

		if (!is_file($dir . '/index.htm') || $force)
		{
			@file_put_contents($dir . '/index.htm', $blankHtml);
		}

		// Create a default index.php to prevent directory listings with an error (all servers)
		if (!is_file($dir . '/index.php') || $force)
		{
			$deadPHP = '<' . '?' . 'php header(\'HTTP/1.1 403 Forbidden\'); return;' . '?' . ">\n";
			$deadPHP .= <<< TEXT
This file was generated automatically by the Akeeba Backup Engine

DO NOT REMOVE THIS FILE

This file tells your web server to not list the contents of this directory, instead returning an HTTP 403 Forbidden
error. This makes it implausible for a malicious third party to successfully guess the filenames of your backup
archives. Therefore, even if this folder is directly web accessible – despite the .htaccess and web.config file already
put in place by the Akeeba Backup Engine – it will still be reasonably protected against malicious users trying to
download your backup archives.

Please do not remove this file as it could have security implications for your site.

You are strongly advised to never delete or modify any of the files automatically created in this folder by the
Akeeba Backup Engine, namely:

* .htaccess
* web.config
* index.html
* index.htm
* index.php

TEXT;

			@file_put_contents($dir . '/index.php', $deadPHP);
		}
	}

	/**
	 * Convert a string to a (Unicode) slug
	 *
	 * @param   string  $string  String to process
	 *
	 * @return  string  Processed string
	 *
	 * @since   7.5.0
	 */
	public function stringUrlUnicodeSlug(string $string): string
	{
		// Replace double byte whitespaces by single byte (East Asian languages)
		$str = preg_replace('/\xE3\x80\x80/', ' ', $string);

		// Remove any '-' from the string as they will be used as concatenator.
		$str = str_replace('-', ' ', $str);

		// Replace forbidden characters by whitespaces
		$str = preg_replace('#[:\?\#\*"@+=;!><&\.%()\]\/\'\\\\|\[]#', "\x20", $str);

		// Delete all '?'
		$str = str_replace('?', '', $str);

		// Trim white spaces at beginning and end of alias and make lowercase
		$str = trim(strtolower($str));

		// Remove any duplicate whitespace and replace whitespaces by hyphens
		$str = preg_replace('#\x20+#', '-', $str);

		return $str;
	}

}
Util/HashTrait.php000060400000006042152456704520010071 0ustar00<?php
/**
 * Akeeba Engine
 *
 * @package   akeebaengine
 * @copyright Copyright (c)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\Util;

/**
 * PHP 8.4+ workaround for standalone MD5 and SHA-1 functions.
 *
 * PHP 8.4 deprecates the standalone md5(), md5_file(), sha1(), and sha1_file() functions. This trait creates shims
 * which use the hash() and hash_file() functions instead where available.
 *
 * IMPORTANT! PHP 7.4 made the ext/hash extension mandatory. These shims are here only as a backwards compatibility aid.
 * Eventually, we need to remove them, replacing their use by the direct use of hash() and hash_file().
 *
 * @deprecated 10.0
 */
trait HashTrait
{
	/**
	 * @deprecated 10.0 Use hash() instead
	 */
	private static function md5($string, $binary = false)
	{
		static $shouldUseHash = null;

		if ($shouldUseHash === null)
		{
			$shouldUseHash = function_exists('hash')
			                 && function_exists('hash_algos')
			                 && in_array('md5', hash_algos());
		}

		return $shouldUseHash ? hash('md5', $string, $binary) : md5($string, $binary);
	}

	/**
	 * @deprecated 10.0 Use hash() instead
	 */
	private static function sha1($string, $binary = false)
	{
		static $shouldUseHash = null;

		if ($shouldUseHash === null)
		{
			$shouldUseHash = function_exists('hash')
			                 && function_exists('hash_algos')
			                 && in_array('sha1', hash_algos());
		}

		return $shouldUseHash ? hash('sha1', $string, $binary) : sha1($string, $binary);
	}

	/**
	 * @deprecated 10.0 Use hash_file() instead
	 */
	private static function md5_file($filename, $binary = false)
	{
		static $shouldUseHash = null;

		if ($shouldUseHash === null)
		{
			$shouldUseHash = function_exists('hash')
			                 && function_exists('hash_algos')
			                 && in_array('md5', hash_algos());
		}

		return $shouldUseHash ? hash_file('md5', $filename, $binary) : md5_file($filename, $binary);
	}

	/**
	 * @deprecated 10.0 Use hash_file() instead
	 */
			private static function sha1_file($filename, $binary = false)
	{
		static $shouldUseHash = null;

		if ($shouldUseHash === null)
		{
			$shouldUseHash = function_exists('hash')
			                 && function_exists('hash_algos')
			                 && in_array('sha1', hash_algos());
		}

		return $shouldUseHash ? hash_file('sha1', $filename, $binary) : sha1_file($filename, $binary);
	}
}Factory.php000060400000072102152456704520006674 0ustar00<?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;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Core\Database;
use Akeeba\Engine\Core\Filters;
use Akeeba\Engine\Core\Kettenrad;
use Akeeba\Engine\Core\Timer;
use Akeeba\Engine\Driver\Base;
use Akeeba\Engine\Dump\Native;
use Akeeba\Engine\Postproc\PostProcInterface;
use Akeeba\Engine\Util\ConfigurationCheck;
use Akeeba\Engine\Util\Encrypt;
use Akeeba\Engine\Util\EngineParameters;
use Akeeba\Engine\Util\FactoryStorage;
use Akeeba\Engine\Util\FileLister;
use Akeeba\Engine\Util\FileSystem;
use Akeeba\Engine\Util\Logger;
use Akeeba\Engine\Util\PushMessagesInterface;
use Akeeba\Engine\Util\RandomValue;
use Akeeba\Engine\Util\SecureSettings;
use Akeeba\Engine\Util\Statistics;
use Akeeba\Engine\Util\TemporaryFiles;
use DateTime;
use DateTimeZone;
use Exception;
use RuntimeException;

// Try to kill errors display
if (function_exists('ini_set') && !defined('AKEEBADEBUG'))
{
	ini_set('display_errors', false);
}

// Make sure the class autoloader is loaded
require_once __DIR__ . '/Autoloader.php';

/**
 * The Akeeba Engine Factory class
 *
 * This class is responsible for instantiating all Akeeba Engine classes
 */
abstract class Factory
{
	/**
	 * The absolute path to Akeeba Engine's installation
	 *
	 * @var  string
	 */
	private static $root;

	/**
	 * Partial class names of the loaded engines e.g. 'archiver' => 'Archiver\\Jpa'. Survives serialization.
	 *
	 * @var  array
	 */
	private static $engineClassnames = [];

	/**
	 * A list of instantiated objects which will persist after serialisation / unserialisation
	 *
	 * @var   array
	 */
	private static $objectList = [];

	/**
	 * A list of instantiated objects which will NOT persist after serialisation / unserialisation
	 *
	 * @var   array
	 */
	private static $temporaryObjectList = [];

	/**
	 * The class to use for push messages
	 *
	 * @since 9.3.1
	 * @var   string
	 */
	private static $pushClassName = 'Util\\PushMessages';

	/**
	 * Gets a serialized snapshot of the Factory for safekeeping (hibernate)
	 *
	 * @return  string  The serialized snapshot of the Factory
	 */
	public static function serialize(): string
	{
		// Call _onSerialize in all objects known to the factory
		foreach (static::$objectList as $object)
		{
			if (method_exists($object, '_onSerialize'))
			{
				call_user_func([$object, '_onSerialize']);
			}
		}

		// Serialise an array with all the engine information
		$engineInfo = [
			'root'             => static::$root,
			'objectList'       => static::$objectList,
			'engineClassnames' => static::$engineClassnames,
			'pushClassname'    => static::$pushClassName,
		];

		// Serialize the factory
		return base64_encode(serialize($engineInfo));
	}

	/**
	 * Regenerates the full Factory state from a serialized snapshot (resume)
	 *
	 * @param   string  $serializedData  The serialized snapshot to resume from
	 *
	 * @return  void
	 */
	public static function unserialize(string $serializedData): void
	{
		static::nuke();

		$engineInfo = unserialize(base64_decode($serializedData));

		static::$root                = $engineInfo['root'] ?? '';
		static::$objectList          = $engineInfo['objectList'] ?? [];
		static::$engineClassnames    = $engineInfo['engineClassnames'] ?? [];
		static::$pushClassName       = $engineInfo['pushClassname'] ?? 'Utils\\PushMessages';
		static::$temporaryObjectList = [];
	}

	/**
	 * Reset the internal factory state, freeing all previously created objects
	 *
	 * @return  void
	 */
	public static function nuke()
	{
		foreach (static::$objectList as &$object)
		{
			$object = null;
		}

		foreach (static::$temporaryObjectList as &$object)
		{
			$object = null;
		}

		static::$objectList          = [];
		static::$temporaryObjectList = [];
	}

	/**
	 * Saves the engine state to temporary storage
	 *
	 * @param   string|null  $tag       The backup origin to save. Leave empty to get from already loaded Kettenrad
	 *                                  instance.
	 * @param   string|null  $backupId  The backup ID to save. Leave empty to get from already loaded Kettenrad
	 *                                  instance.
	 *
	 * @return  void
	 *
	 * @throws  RuntimeException  When the state save fails for any reason
	 * @noinspection PhpUnused
	 */
	public static function saveState(?string $tag = null, ?string $backupId = null): void
	{
		$kettenrad = static::getKettenrad();
		$tag       = $tag ?: $kettenrad->getTag();
		$backupId  = $backupId ?: $kettenrad->getBackupId();

		$saveTag = rtrim($tag . '.' . ($backupId ?: ''), '.');
		$ret     = $kettenrad->getStatusArray();

		if ($ret['HasRun'] == 1)
		{
			Factory::getLog()->debug("Will not save a finished Kettenrad instance");

			return;
		}

		Factory::getLog()->debug("Saving Kettenrad instance $tag");

		// Save a Factory snapshot
		$factoryStorage = static::getFactoryStorage();

		$logger = static::getLog();
		$logger->resetWarnings();

		$serializedFactoryData = static::serialize();
		$memoryFileExtension   = 'php';
		$result                = $factoryStorage->set($serializedFactoryData, $saveTag, $memoryFileExtension);

		/**
		 * Some hosts, such as WPEngine, do not allow us to save the memory files in .php files. In this case we use the
		 * far more insecure .dat extension.
		 */
		if ($result === false)
		{
			$memoryFileExtension = 'dat';
			$result              = $factoryStorage->set($serializedFactoryData, $saveTag, $memoryFileExtension);
		}

		if ($result === false)
		{
			$saveKey      = $factoryStorage->get_storage_filename($saveTag, $memoryFileExtension);
			$errorMessage = "Cannot save factory state in storage, storage filename $saveKey";

			$logger->error($errorMessage);

			throw new RuntimeException($errorMessage);
		}
	}

	/**
	 * Loads the engine state from the storage (if it exists).
	 *
	 * When failIfMissing is true (default) an exception will be thrown if the memory file / database record is no
	 * longer there. This is a clear indication of an issue with the storage engine, e.g. the host deleting the memory
	 * files in the middle of the backup step. Therefore, we'll switch the storage engine type before throwing the
	 * exception.
	 *
	 * When failIfMissing is false we do NOT throw an exception. Instead, we do a hard reset of the backup factory. This
	 * is required by the resetState method when we ask it to reset multiple origins at once.
	 *
	 * @param   string|null  $tag            The backup origin to load
	 * @param   string|null  $backupId       The backup ID to load
	 * @param   bool         $failIfMissing  Throw an exception if the memory data is no longer there
	 *
	 * @return  void
	 */
	public static function loadState(?string $tag = null, ?string $backupId = null, bool $failIfMissing = true): void
	{
		/** @noinspection PhpUndefinedConstantInspection */
		$tag     = $tag ?: (defined('AKEEBA_BACKUP_ORIGIN') ? AKEEBA_BACKUP_ORIGIN : 'backend');
		$loadTag = rtrim($tag . '.' . ($backupId ?: ''), '.');

		// In order to load anything, we need to have the correct profile loaded. Let's assume
		// that the latest backup record in this tag has the correct profile number set.
		$config = static::getConfiguration();

		if (empty($config->activeProfile))
		{
			$profile = Platform::getInstance()->get_active_profile();

			if (empty($profile) || ($profile <= 1))
			{
				// Only bother loading a configuration if none has been already loaded
				$filters = [
					['field' => 'tag', 'value' => $tag],
				];

				if (!empty($backupId))
				{
					$filters[] = ['field' => 'backupid', 'value' => $backupId];
				}

				$statList = Platform::getInstance()->get_statistics_list([
						'filters' => $filters, 'order' => [
							'by' => 'id', 'order' => 'DESC',
						],
					]
				);

				if (is_array($statList))
				{
					$stat    = array_pop($statList) ?? [];
					$profile = $stat['profile_id'] ?? 1;
				}
			}

			Platform::getInstance()->load_configuration($profile);
		}

		$profile = $config->activeProfile;

		Factory::getLog()->open($loadTag);
		Factory::getLog()->debug("Kettenrad :: Attempting to load from database ($tag) [$loadTag]");

		$serializedFactory = static::getFactoryStorage()->get($loadTag);

		if ($serializedFactory === null)
		{
			if ($failIfMissing)
			{
				throw new RuntimeException("Akeeba Engine detected a problem while saving temporary data. Please restart your backup.", 500);
			}

			// There is no serialized factory. Nuke the in-memory factory.
			Factory::getLog()->debug(" -- Stored Akeeba Factory ($tag) [$loadTag] not found - hard reset");
			static::nuke();
			Platform::getInstance()->load_configuration($profile);
		}
		else
		{
			Factory::getLog()->debug(" -- Loaded stored Akeeba Factory ($tag) [$loadTag]");
			static::unserialize($serializedFactory);
		}


		unset($serializedFactory);
	}

	// ========================================================================
	// Public factory interface
	// ========================================================================

	/**
	 * Resets the engine state, wiping out any pending backups and/or stale temporary data.
	 *
	 * The configuration parameters are:
	 *
	 * * global  `bool`  True to reset all backups, regardless of the origin or profile ID
	 * * log     `bool`  True to log our actions (default: false)
	 * * maxrun  `int`   Only backup records older than this number of seconds will be reset (default: 180)
	 *
	 * Special considerations:
	 *
	 * * If global = true all backups from all origins are taken into account to determine which ones are stuck (over
	 *   the maxrun threshold since their last database entry).
	 *
	 * * If global = false only backups from the current backup origin are taken into account.
	 *
	 * * If global = false AND the current origin is 'backend' all pending and idle backups with the 'backup' origin are
	 *   considered stuck regardless of their age. In other words, maxrun is effectively set to 0. The idea is that only
	 *   a single person, from a single browser, should be taking backend backups at a time. Resetting single origin
	 *   backups is only ever meant to be called by the consumer when starting a backup.
	 *
	 * * Corollary to the above: starting a frontend, CLI or JSON API backup with the same backup profile DOES NOT reset
	 *   a previously failed backup if the new backup starts less than 'maxrun' seconds since the last step of the
	 *   failed backup started.
	 *
	 * * The time information for the backup age is taken from the database, namely the backupend field. If no time
	 *   is recorded for the last step we use the backupstart field instead.
	 *
	 * @param   array  $config  Configuration parameters for the reset operation
	 *
	 * @return  void
	 * @throws  Exception
	 * @noinspection PhpUnused
	 */
	public static function resetState(array $config = []): void
	{
		$defaultConfig = [
			'global' => true,
			'log'    => false,
			'maxrun' => 180,
		];

		$config = (object) array_merge($defaultConfig, $config);

		// Pause logging if so desired
		if (!$config->log)
		{
			Factory::getLog()->pause();
		}

		// Get the origin to clear, depending on the 'global' setting
		$originTag = $config->global ? null : Platform::getInstance()->get_backup_origin();

		// Cache the factory before proceeding
		$factory = static::serialize();

		// Get all running backups for the selected origin (or all origins, if global was false).
		$runningList = Platform::getInstance()->get_running_backups($originTag);

		// Sanity check
		if (!is_array($runningList))
		{
			$runningList = [];
		}

		// If the current origin is 'backend' we assume maxrun = 0 per the method docblock notes.
		$maxRun = ($originTag == 'backend') ? 0 : $config->maxrun;

		// Filter out entries by backup age
		$now         = time();
		$cutOff      = $now - $maxRun;
		$runningList = array_filter($runningList, function (array $running) use ($cutOff, $maxRun) {
			// No cutoff time: include all currently running backup records
			if ($maxRun == 0)
			{
				return true;
			}

			// Try to get the last backup tick timestamp
			try
			{
				$backupTickTime = !empty($running['backupend']) ? $running['backupend'] : $running['backupstart'];
				$tz             = new DateTimeZone('UTC');
				$tstamp         = (new DateTime($backupTickTime, $tz))->getTimestamp();
			}
			catch (Exception $e)
			{
				$tstamp = Factory::getLog()->getLastTimestamp($running['origin']);
			}

			if (is_null($tstamp))
			{
				return false;
			}

			// Only include still running backups whose last tick was BEFORE the cutoff time
			return $tstamp <= $cutOff;
		});

		// Mark running backups as failed
		foreach ($runningList as $running)
		{
			// Delete the failed backup's leftover archive parts
			$filenames = Factory::getStatistics()->get_all_filenames($running, false);
			$filenames = is_null($filenames) ? [] : $filenames;
			$totalSize = 0;

			foreach ($filenames as $failedArchive)
			{
				if (!@file_exists($failedArchive))
				{
					continue;
				}

				$totalSize += (int) @filesize($failedArchive);
				Platform::getInstance()->unlink($failedArchive);
			}

			// Mark the backup failed
			$running['status']     = 'fail';
			$running['instep']     = 0;
			$running['total_size'] = empty($running['total_size']) ? $totalSize : $running['total_size'];
			$running['multipart']  = 0;

			Platform::getInstance()->set_or_update_statistics($running['id'], $running);

			// Remove the temporary data
			$backupId = isset($running['backupid']) ? ('.' . $running['backupid']) : '';

			self::removeTemporaryData($running['origin'] . $backupId);
		}

		// Reload the factory
		static::unserialize($factory);
		unset($factory);

		// Unpause logging if it was previously paused
		if (!$config->log)
		{
			Factory::getLog()->unpause();
		}
	}

	/**
	 * Returns an Akeeba Configuration object
	 *
	 * @return  Configuration  The Akeeba Configuration object
	 */
	public static function getConfiguration(): Configuration
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getObjectInstance(Configuration::class);
	}

	/**
	 * Returns a statistics object, used to track current backup's progress
	 *
	 * @return  Statistics
	 */
	public static function getStatistics(): Statistics
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getObjectInstance(Statistics::class);
	}

	/**
	 * Returns the currently configured archiver engine
	 *
	 * @param   bool  $reset  Should I try to forcibly create a new instance?
	 *
	 * @return  Archiver\Base|null
	 */
	public static function getArchiverEngine(bool $reset = false): ?Archiver\Base
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getEngineInstance(
			'archiver', 'akeeba.advanced.archiver_engine',
			'Archiver\\', 'Archiver\\Jpa',
			$reset
		);
	}

	/**
	 * Returns the currently configured dump engine
	 *
	 * @param   boolean  $reset  Should I try to forcibly create a new instance?
	 *
	 * @return  Dump\Base|Native|null
	 */
	public static function getDumpEngine(bool $reset = false): ?object
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getEngineInstance(
			'dump', 'akeeba.advanced.dump_engine',
			'Dump\\', 'Dump\\Native',
			$reset
		);
	}

	/**
	 * Returns the filesystem scanner engine instance
	 *
	 * @param   bool  $reset  Should I try to forcibly create a new instance?
	 *
	 * @return  Scan\Base|null  The scanner engine
	 */
	public static function getScanEngine(bool $reset = false): ?Scan\Base
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getEngineInstance(
			'scan', 'akeeba.advanced.scan_engine',
			'Scan\\', 'Scan\\Large',
			$reset
		);
	}

	/**
	 * Returns the current post-processing engine. If no class is specified we
	 * return the post-processing engine configured in akeeba.advanced.postproc_engine
	 *
	 * @param   string|null  $engine  The name of the post-processing class to forcibly return
	 *
	 * @return  PostProcInterface|null
	 */
	public static function getPostprocEngine(?string $engine = null): ?PostProcInterface
	{
		if (!is_null($engine))
		{
			static::$engineClassnames['postproc'] = 'Postproc\\' . ucfirst($engine);

			/** @noinspection PhpIncompatibleReturnTypeInspection */
			return static::getObjectInstance(static::$engineClassnames['postproc']);
		}

		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getEngineInstance(
			'postproc', 'akeeba.advanced.postproc_engine',
			'Postproc\\', 'Postproc\\None',
			true
		);
	}

	// ========================================================================
	// Core objects which are part of the engine state
	// ========================================================================

	/**
	 * Returns an instance of the Filters feature class
	 *
	 * @return  Filters  The Filters feature class' object instance
	 */
	public static function getFilters(): Filters
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getObjectInstance(Filters::class);
	}

	/**
	 * Returns an instance of the specified filter group class. Do note that it does not
	 * work with platform filter classes. They are handled internally by AECoreFilters.
	 *
	 * @param   string  $filter_name  The filter class to load, without AEFilter prefix
	 *
	 * @return  Filter\Base|null  The filter class' object instance
	 */
	public static function getFilterObject(string $filter_name): ?Filter\Base
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getObjectInstance('Filter\\' . ucfirst($filter_name));
	}

	/**
	 * Loads an engine domain class and returns its associated object
	 *
	 * @param   string  $domainName  The name of the domain, e.g. installer for AECoreDomainInstaller
	 *
	 * @return  Part|null
	 */
	public static function getDomainObject(string $domainName): ?Part
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getObjectInstance('Core\\Domain\\' . ucfirst($domainName));
	}

	/**
	 * Returns a database connection object. It's an alias of AECoreDatabase::getDatabase()
	 *
	 * !!! IMPORTANT !!!
	 * DO NOT STATIC TYPE THIS METHOD.
	 *
	 * Akeeba Backup for Joomla is using a decorator to the Joomla DB object which makes use of the magic __call method
	 * to proxy driver calls. As a result it cannot adhere to an object declaration or interface. Until this is
	 * refactored we have to keep this method untyped.
	 *
	 * @param   array|null  $options  Options to use when instantiating the database connection
	 *
	 * @return  Base
	 */
	public static function getDatabase(?array $options = null)
	{
		if (is_null($options))
		{
			$options = Platform::getInstance()->get_platform_database_options();
		}

		if (isset($options['username']) && !isset($options['user']))
		{
			$options['user'] = $options['username'];
		}

		return Database::getDatabase($options);
	}

	/**
	 * Returns a database connection object. It's an alias of AECoreDatabase::getDatabase()
	 *
	 * @param   array|null  $options  Options to use when instantiating the database connection
	 *
	 * @return  void
	 */
	public static function unsetDatabase(?array $options = null): void
	{
		if (is_null($options))
		{
			$options = Platform::getInstance()->get_platform_database_options();
		}

		$db = Database::getDatabase($options);
		$db->close();

		Database::unsetDatabase($options);
	}

	/**
	 * Get a reference to the Akeeba Engine's timer
	 *
	 * @return  Timer
	 */
	public static function getTimer(): Timer
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getObjectInstance(Timer::class);
	}

	/**
	 * Get a reference to Akeeba Engine's main controller called Kettenrad
	 *
	 * @return  Kettenrad
	 */
	public static function getKettenrad(): Kettenrad
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getObjectInstance(Kettenrad::class);
	}

	/**
	 * Returns an instance of the factory temporary storage class
	 *
	 * @return  FactoryStorage
	 */
	public static function getFactoryStorage(): FactoryStorage
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getTempObjectInstance(FactoryStorage::class);
	}

	/**
	 * Returns an instance of the encryption class
	 *
	 * @return  Encrypt
	 */
	public static function getEncryption(): Encrypt
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getTempObjectInstance(Encrypt::class);
	}

	/**
	 * Returns an instance of the crypto-safe random value generator class
	 *
	 * @return  RandomValue
	 */
	public static function getRandval(): RandomValue
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getTempObjectInstance(RandomValue::class);
	}

	/**
	 * Returns an instance of the filesystem tools class
	 *
	 * @return  FileSystem
	 */
	public static function getFilesystemTools(): FileSystem
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getTempObjectInstance(FileSystem::class);
	}

	/**
	 * Returns an instance of the filesystem tools class
	 *
	 * @return  FileLister
	 * @noinspection PhpUnused
	 */
	public static function getFileLister(): FileLister
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getTempObjectInstance(FileLister::class);
	}

	// ========================================================================
	// Temporary objects which are not part of the engine state
	// ========================================================================

	/**
	 * Returns an instance of the engine parameters provider which provides information on scripting, GUI configuration
	 * elements and engine parts
	 *
	 * @return  EngineParameters
	 */
	public static function getEngineParamsProvider(): EngineParameters
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getTempObjectInstance(EngineParameters::class);
	}

	/**
	 * Returns an instance of the log object
	 *
	 * @return  Logger
	 */
	public static function getLog(): Logger
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getTempObjectInstance(Logger::class);
	}

	/**
	 * Returns an instance of the configuration checks object
	 *
	 * @return  ConfigurationCheck
	 */
	public static function getConfigurationChecks(): ConfigurationCheck
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getTempObjectInstance(ConfigurationCheck::class);
	}

	/**
	 * Returns an instance of the secure settings handling object
	 *
	 * @return  SecureSettings
	 */
	public static function getSecureSettings(): SecureSettings
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getTempObjectInstance(SecureSettings::class);
	}

	/**
	 * Returns an instance of the secure settings handling object
	 *
	 * @return  TemporaryFiles
	 */
	public static function getTempFiles(): TemporaryFiles
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getTempObjectInstance(TemporaryFiles::class);
	}

	/**
	 * Get the connector object for push messages
	 *
	 * !!! WARNING !!! DO NOT STATIC TYPE
	 *
	 * The object type may change using setPushClass.
	 *
	 * @return  PushMessagesInterface
	 */
	public static function getPush()
	{
		/** @noinspection PhpIncompatibleReturnTypeInspection */
		return static::getObjectInstance(self::$pushClassName);
	}

	/**
	 * Set the push notifications helper class to use with this factory
	 *
	 * @param   string  $className  The classname to use
	 *
	 * @since   9.3.1
	 * @noinspection PhpUnused
	 */
	public static function setPushClass(string $className): void
	{
		self::$pushClassName = $className;
	}

	/**
	 * Returns the absolute path to Akeeba Engine's installation
	 *
	 * @return  string
	 */
	public static function getAkeebaRoot(): string
	{
		if (empty(static::$root))
		{
			static::$root = __DIR__;
		}

		return static::$root;
	}

	/**
	 * @param   string  $engineType  Engine type, e.g. 'archiver', 'postproc', ...
	 * @param   string  $configKey   Profile config key with configured engine e.g. 'akeeba.advanced.archiver_engine'
	 * @param   string  $prefix      Prefix for engine classes, e.g. 'Archiver\\'
	 * @param   string  $fallback    Fallback class if the configured one doesn't exist e.g. 'Archiver\\Jpa'. Empty for
	 *                               no fallback.
	 * @param   bool    $reset       Should I force-reload the engine? Default: false.
	 *
	 * @return  object|null  The Singleton engine object instance
	 */
	protected static function getEngineInstance(string $engineType, string $configKey, string $prefix, string $fallback, bool $reset = false): ?object
	{
		if (!$reset && !empty(static::$engineClassnames[$engineType]))
		{
			return static::getObjectInstance(static::$engineClassnames[$engineType]);
		}

		// Unset the existing engine object
		if (!empty(static::$engineClassnames[$engineType]))
		{
			static::unsetObjectInstance(static::$engineClassnames[$engineType]);
		}

		// Get the engine name from the backup profile, construct a class name and check if it exists
		$registry                              = static::getConfiguration();
		$engine                                = $registry->get($configKey);
		static::$engineClassnames[$engineType] = $prefix . ucfirst($engine);
		$object                                = static::getObjectInstance(static::$engineClassnames[$engineType]);

		// If the engine object does not exist, fall back to the default
		if (!empty($fallback) && !is_object($object))
		{
			static::unsetObjectInstance(static::$engineClassnames[$engineType]);

			static::$engineClassnames[$engineType] = $fallback;
		}

		return static::getObjectInstance(static::$engineClassnames[$engineType]);
	}

	/**
	 * Internal function which instantiates an object of a class named $class_name.
	 *
	 * @param   string  $className
	 *
	 * @return  object|null
	 */
	protected static function getObjectInstance(string $className): ?object
	{
		$className = trim($className, '\\');

		if (substr($className, 0, 14) === 'Akeeba\\Engine\\')
		{
			$searchClass = $className;
			$className = substr($className, 14);
		}
		else
		{
			$searchClass = '\\Akeeba\\Engine\\' . $className;
		}

		if (isset(static::$objectList[$className]))
		{
			return static::$objectList[$className];
		}

		static::$objectList[$className] = null;

		if (class_exists($searchClass))
		{
			static::$objectList[$className] = new $searchClass;
		}
		elseif (class_exists($className))
		{
			static::$objectList[$className] = new $className;
		}

		return static::$objectList[$className];
	}

	// ========================================================================
	// Handy functions
	// ========================================================================

	/**
	 * Internal function which removes the object of the class named $class_name
	 *
	 * @param   string  $className
	 *
	 * @return  void
	 */
	protected static function unsetObjectInstance(string $className): void
	{
		if (substr($className, 0, 14) === 'Akeeba\\Engine\\')
		{
			$className = substr($className, 14);
		}

		if (isset(static::$objectList[$className]))
		{
			static::$objectList[$className] = null;
			unset(static::$objectList[$className]);
		}
	}

	/**
	 * Internal function which instantiates an object of a class named $class_name. This is a temporary instance which
	 * will not survive serialisation and subsequent unserialisation.
	 *
	 * @param   string  $className
	 *
	 * @return  object|null
	 */
	protected static function getTempObjectInstance(string $className): ?object
	{
		$className = trim($className, '\\');

		if (substr($className, 0, 14) === 'Akeeba\\Engine\\')
		{
			$searchClass = $className;
			$className = substr($className, 14);
		}
		else
		{
			$searchClass = '\\Akeeba\\Engine\\' . $className;
		}

		if (!isset(static::$temporaryObjectList[$className]))
		{
			static::$temporaryObjectList[$className] = null;

			if (class_exists($searchClass))
			{
				static::$temporaryObjectList[$className] = new $searchClass;
			}
		}

		return static::$temporaryObjectList[$className];
	}

	/**
	 * Remote the temporary data for a specific backup tag.
	 *
	 * @param   string  $originTag  The backup tag to reset e.g. 'backend.id123' or 'frontend'.
	 *
	 * @return  void
	 */
	protected static function removeTemporaryData(string $originTag): void
	{
		static::loadState($originTag, null, false);
		// Remove temporary files
		Factory::getTempFiles()->deleteTempFiles();
		// Delete any stale temporary data
		static::getFactoryStorage()->reset($originTag);
	}
}

/**
 * Timeout handler. It is registered as a global PHP shutdown function.
 *
 * If a PHP reports a timeout we will log this before letting PHP kill us.
 */
function AkeebaTimeoutTrap(): void
{
	if (connection_status() >= 2)
	{
		Factory::getLog()->error('Akeeba Engine has timed out');
	}
}

register_shutdown_function("\\Akeeba\\Engine\\AkeebaTimeoutTrap");
.htaccess000060400000000246152456704520006352 0ustar00<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
<IfModule mod_authz_core.c>
  <RequireAll>
    Require all denied
  </RequireAll>
</IfModule>
FixMySQLHostname.php000060400000016172152456704520010405 0ustar00<?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;

defined('AKEEBAENGINE') || die();

trait FixMySQLHostname
{
	/**
	 * Tries to parse all the weird hostname definitions and normalize them into something that the MySQLi connector
	 * will understand. Please note that there are some differences to the old MySQL driver:
	 *
	 * * Port and socket MUST be provided separately from the hostname. Hostnames in the form of 127.0.0.1:8336 are no
	 *   longer acceptable.
	 *
	 * * The hostname "localhost" has special meaning. It means "use named pipes / sockets". Anything else uses TCP/IP.
	 *   This is the ONLY way to specify a. TCP/IP or b. named pipes / sockets connection.
	 *
	 * * You SHOULD NOT use a numeric TCP/IP port with hostname localhost. For some strange reason it's still allowed
	 *   but the manual is self-contradicting over what this really does...
	 *
	 * * Likewise you CANNOT use a socket / named pipe path with hostname other than localhost. Named pipes and sockets
	 *   can only be used with the local machine, therefore the hostname MUST be localhost.
	 *
	 * * You cannot give a TCP/IP port number in the socket parameter or a named pipe / socket path to the port
	 *   parameter. This leads to an error.
	 *
	 * * You cannot use an empty string, 0 or any other non-null value when you want to omit either of the port or
	 *   socket parameters.
	 *
	 * * Persistent connections must be prefixed with the string literal 'p:'. Therefore you cannot have a hostname
	 *   called 'p' (not to mention that'd be daft). You can also not specify something like 'p:1234' to make a
	 *   persistent connection to a port. This wasn't even supported by the old MySQL driver. As a result we don't even
	 *   try to catch that degenerate case.
	 *
	 * This method will try to apply all of the aforementioned rules with one additional disambiguation rule:
	 *
	 * A port / socket set in the hostname overrides a port specified separately. A port specified separately overrides
	 * a socket specified separately.
	 *
	 * @param   string  $host    The hostname. Can contain legacy hostname:port or hostname:sc=ocket definitions.
	 * @param   int     $port    The port. Alternatively it can contain the path to the socket.
	 * @param   string  $socket  The path to the socket. You could abuse it to enter the port number. DON'T!
	 *
	 * @return  void  All parameters are passed by reference.
	 *
	 * @since   9.2.3
	 */
	protected function fixHostnamePortSocket(&$host, &$port, &$socket)
	{
		// Is this a persistent connection? Persistent connections are indicated by the literal "p:" in front of the hostname
		$isPersistent = (substr($host, 0, 2) == 'p:');
		$host         = $isPersistent ? substr($host, 2) : $host;

		// If the hostname looks like a *NIX filename we need to treat it as a socket.
		if (preg_match('#^/([^/]*/)?[^/]#', $host))
		{
			$socket = $host;
			$host = null;
		}

		// Special case: Windows named pipe (\\.\something\or\another), with or without parentheses.
		$isNamedPipe = false;

		if (preg_match("#^\(?\\\\\\\\\.\\\\#", $host))
		{
			$isNamedPipe = true;
			$socket = $host;
			$host = '.';
		}

		/*
		 * Unlike mysql_connect(), mysqli_connect() takes the port and socket as separate arguments. Therefore, we
		 * have to extract them from the host string.
		 */
		$port = !empty($port) ? $port : 3306;

		if ($host === 'localhost')
		{
			$port = null;
		}
		// UNIX socket URI, e.g. 'unix:/path/to/unix/socket.sock'
		elseif (preg_match('/^unix:(?P<socket>[^:]+)$/', $host, $matches))
		{
			$host   = null;
			$socket = $matches['socket'];
			$port   = null;
		}
		// It's an IPv4 address with or without port
		elseif (preg_match('/^(?P<host>((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(:(?P<port>.+))?$/', $host, $matches))
		{
			$host = $matches['host'];

			if (!empty($matches['port']))
			{
				$port = $matches['port'];
			}
		}
		// Square-bracketed IPv6 address with or without port, e.g. [fe80:102::2%eth1]:3306
		elseif (preg_match('/^(?P<host>\[.*\])(:(?P<port>.+))?$/', $host, $matches))
		{
			$host = $matches['host'];

			if (!empty($matches['port']))
			{
				$port = $matches['port'];
			}
		}
		// Named host (e.g example.com or localhost) with or without port
		elseif (preg_match('/^(?P<host>(\w+:\/{2,3})?[a-z0-9\.\-]+)(:(?P<port>[^:]+))?$/i', $host, $matches))
		{
			$host = $matches['host'];

			if (!empty($matches['port']))
			{
				$port = $matches['port'];
			}
		}
		// Empty host, just port, e.g. ':3306'
		elseif (preg_match('/^:(?P<port>[^:]+)$/', $host, $matches))
		{
			$host = '127.0.0.1';
			$port = $matches['port'];
		}
		// ... else we assume normal (naked) IPv6 address, so host and port stay as they are or default

		// If there is both a valid port and a valid socket we will choose the socket instead
		if (is_numeric($port) && !empty($socket))
		{
			$port = null;
		}

		// Get the port number or socket name
		if (is_numeric($port))
		{
			$port   = (int) $port;
			$socket = '';
		}
		elseif (is_string($port) && empty($socket))
		{
			$socket = $port;
			$port = null;
		}

		// If there is a socket the hostname must be null
		if (!empty($socket))
		{
			$host = null;
		}

		// If there is a socket the port must be null
		if (!empty($socket))
		{
			$port = null;
		}

		// If there is a numeric port and the hostname is 'localhost' convert to 127.0.0.1
		if (is_numeric($port) && ($host === 'localhost'))
		{
			$host = '127.0.0.1';
		}

		/**
		 * Special case: MySQL sockets on Windows need to be enclosed with parentheses and have \\.\ in front.
		 *
		 * @see https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-shell-connection-socket.html
		 * @see https://www.php.net/manual/en/mysqli.quickstart.connections.php
		 */
		if (!empty($socket) && $isNamedPipe)
		{
			$host = '.';

			/**
			 * Remove any existing parentheses, otherwise URL-decode the socket (in case it was given in the correct
			 * percent encoded format).
			 */
			if (substr($socket, 0, 1) === '(' && substr($socket, -1) === ')')
			{
				$socket = substr($socket, 1, -1);

			}
			else
			{
				$socket = rawurldecode($socket);
			}

			// If the socket doesn't already start with \\.\ add it
			if (substr($socket, 0, 4) !== '\\\\.\\')
			{
				$socket = '\\\\.\\' . $socket;
			}

			$socket = '(' . $socket . ')';
		}

		// Finally, if it's a persistent connection we have to prefix the hostname with 'p:'
		$host = ($isPersistent && $host !== null) ? "p:$host" : $host;
	}

}Autoloader.php000060400000006362152456704520007371 0ustar00<?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;

defined('AKEEBAENGINE') || die();

/**
 * The main class autoloader for AkeebaEngine
 */
class Autoloader
{
	/**
	 * An instance of this autoloader
	 *
	 * @var   Autoloader
	 */
	public static $autoloader = null;

	/**
	 * The path to the Akeeba Engine root directory
	 *
	 * @var   string
	 */
	public static $enginePath = null;

	/**
	 * The directories where Akeeba Engine platforms are stored
	 *
	 * @var      array
	 */
	public static $platformDirs = null;

	/**
	 * Public constructor. Registers the autoloader with PHP.
	 */
	public function __construct()
	{
		self::$enginePath = __DIR__;

		spl_autoload_register([$this, 'autoload_akeeba_engine']);
	}

	/**
	 * Initialise this autoloader
	 *
	 * @return  Autoloader
	 */
	public static function init()
	{
		if (self::$autoloader == null)
		{
			self::$autoloader = new self;
		}

		return self::$autoloader;
	}

	/**
	 * The actual autoloader
	 *
	 * @param   string  $className  The name of the class to load
	 *
	 * @return  void
	 */
	public function autoload_akeeba_engine($className)
	{
		// Trim the trailing backslash
		$className = ltrim($className, '\\');

		// Make sure the class has an Akeeba\Engine prefix
		if (substr($className, 0, 13) != 'Akeeba\\Engine')
		{
			return;
		}

		// Remove the prefix and explode on backslashes
		$className = substr($className, 14);
		$class     = explode('\\', $className);

		// Do we have a list of platform directories?
		if (is_null(self::$platformDirs) && class_exists('\\Akeeba\\Engine\\Platform', false))
		{
			self::$platformDirs = Platform::getPlatformDirectories();

			if (!is_array(self::$platformDirs))
			{
				self::$platformDirs = [];
			}
		}

		$rootPaths = [self::$enginePath];

		if (is_array(self::$platformDirs))
		{
			$rootPaths = array_merge(
				self::$platformDirs, [self::$enginePath]
			);
		}

		foreach ($rootPaths as $rootPath)
		{
			// First try finding in structured directory format (preferred)
			$path = $rootPath . '/' . implode('/', $class) . '.php';

			if (@file_exists($path))
			{
				include_once $path;
			}

			// Then try the duplicate last name structured directory format (not recommended)
			if (!class_exists($className, false))
			{
				reset($class);
				$lastPart = end($class);
				$path     = $rootPath . '/' . implode('/', $class) . '/' . $lastPart . '.php';

				if (@file_exists($path))
				{
					include_once $path;
				}
			}
		}
	}
}

// Register the Akeeba Engine autoloader
Autoloader::init();
serverkey.php000060400000000166152456704520007305 0ustar00<?php defined('AKEEBAENGINE') or die(); define('AKEEBA_SERVERKEY', 'NjdiZmViODZmZjUxY2ZmNDU0YzY4NjkzMDMyOThiMWI='); ?>Archiver/BaseArchiver.php000060400000054544152456704520011400 0ustar00<?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\Archiver;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Base\Exceptions\WarningException;
use Akeeba\Engine\Factory;

if (!defined('AKEEBA_CHUNK'))
{
	$configuration = Factory::getConfiguration();
	$chunksize     = $configuration->get('engine.archiver.common.chunk_size', 1048576);
	define('AKEEBA_CHUNK', $chunksize);
}

if (!function_exists('aksubstr'))
{
	/**
	 * Attempt to use mbstring for getting parts of strings
	 *
	 * @param   string    $string
	 * @param   int       $start
	 * @param   int|null  $length
	 *
	 * @return  string
	 */
	function aksubstr($string, $start, $length = null)
	{
		return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') :
			substr($string, $start, $length);
	}
}

/**
 * Abstract class for custom archiver implementations
 */
abstract class BaseArchiver extends BaseFileManagement
{
	/** @var   array  The last part which has been finalized and waits to be post-processed */
	public $finishedPart = [];

	/** @var resource File pointer to the archive being currently written to */
	protected $fp = null;

	/** @var resource File pointer to the archive's central directory file (for ZIP) */
	protected $cdfp = null;

	/** @var string The name of the file holding the archive's data, which becomes the final archive */
	protected $_dataFileName;

	/** @var string Archive full path without extension */
	protected $dataFileNameWithoutExtension = '';

	/** @var bool Should I store symlinks as such (no dereferencing?) */
	protected $storeSymlinkTarget = false;

	/** @var int Part size for split archives, in bytes */
	protected $partSize = 0;

	/** @var bool Should I use Split ZIP? */
	protected $useSplitArchive = false;

	/** @var int Permissions for the backup archive part files */
	protected $permissions = null;

	/**
	 * Release file pointers when the object is being serialized
	 *
	 * @codeCoverageIgnore
	 *
	 * @return  void
	 */
	public function _onSerialize()
	{
		$this->_closeAllFiles();

		$this->fp   = null;
		$this->cdfp = null;
	}

	/**
	 * Release file pointers when the object is being destroyed
	 *
	 * @codeCoverageIgnore
	 *
	 * @return  void
	 */
	public function __destruct()
	{
		$this->_closeAllFiles();

		$this->fp   = null;
		$this->cdfp = null;
	}

	/**
	 * Create a new archive part file (but does NOT open it for writing)
	 *
	 * @param   bool  $finalPart  True if this is the final part
	 *
	 * @return  bool  False if creating a new part fails
	 */
	abstract protected function createNewPartFile($finalPart = false);

	/**
	 * Create a new part file and open it for writing
	 *
	 * @param   bool  $finalPart  Is this the final part?
	 *
	 * @return  void
	 */
	protected function createAndOpenNewPart($finalPart = false)
	{
		@$this->fclose($this->fp);
		$this->fp = null;

		// Not enough space on current part, create new part
		if (!$this->createNewPartFile($finalPart))
		{
			$extension = $this->getExtension();
			$extension = ltrim(strtoupper($extension), '.');

			throw new ErrorException("Could not create new $extension part file " . basename($this->_dataFileName));
		}

		$this->openArchiveForOutput(true);
	}

	/**
	 * Create a new backup archive
	 *
	 * @return  void
	 *
	 * @throws  ErrorException
	 */
	protected function createNewBackupArchive()
	{
		Factory::getLog()->debug(__CLASS__ . " :: Killing old archive");

		$this->fp = $this->fopen($this->_dataFileName, "w");

		if ($this->fp === false)
		{
			if (file_exists($this->_dataFileName))
			{
				@unlink($this->_dataFileName);
			}

			@touch($this->_dataFileName);
			@chmod($this->_dataFileName, 0666);

			$this->fp = $this->fopen($this->_dataFileName, "w");

			if ($this->fp !== false)
			{
				throw new ErrorException("Could not open archive file '{$this->_dataFileName}' for append!");
			}
		}

		@ftruncate($this->fp, 0);
	}

	/**
	 * Opens the backup archive file for output. Returns false if the archive file cannot be opened in binary append
	 * mode.
	 *
	 * @param   bool  $force  Should I forcibly reopen the file? If false, I'll only open the file if the current
	 *                        file pointer is null.
	 *
	 * @return  void
	 */
	protected function openArchiveForOutput($force = false)
	{
		if (is_null($this->fp) || $force)
		{
			$this->fp = $this->fopen($this->_dataFileName, "a");
		}

		if ($this->fp === false)
		{
			$this->fp = null;

			throw new ErrorException("Could not open archive file '{$this->_dataFileName}' for append!");
		}
	}

	/**
	 * Converts a human formatted size to integer representation of bytes,
	 * e.g. 1M to 1024768
	 *
	 * @param   string  $setting  The value in human readable format, e.g. "1M"
	 *
	 * @return  integer  The value in bytes
	 */
	protected function humanToIntegerBytes($setting)
	{
		$val  = trim($setting);
		$last = strtolower($val[strlen($val) - 1]);

		if (is_numeric($last))
		{
			return $setting;
		}

		switch ($last)
		{
			case 't':
				$val *= 1024;
			case 'g':
				$val *= 1024;
			case 'm':
				$val *= 1024;
			case 'k':
				$val *= 1024;
		}

		return (int) $val;
	}

	/**
	 * Get the PHP memory limit in bytes
	 *
	 * @return int|null  Memory limit in bytes or null if we can't figure it out.
	 */
	protected function getMemoryLimit()
	{
		if (!function_exists('ini_get'))
		{
			return null;
		}

		$memLimit = ini_get("memory_limit");

		if ((is_numeric($memLimit) && ($memLimit < 0)) || !is_numeric($memLimit))
		{
			$memLimit = 0; // 1.2a3 -- Rare case with memory_limit < 0, e.g. -1Mb!
		}

		$memLimit = $this->humanToIntegerBytes($memLimit);

		return $memLimit;
	}

	/**
	 * Enable storing of symlink target if we are not on Windows
	 *
	 * @return  void
	 */
	protected function enableSymlinkTargetStorage()
	{
		$configuration       = Factory::getConfiguration();
		$dereferenceSymlinks = $configuration->get('engine.archiver.common.dereference_symlinks', true);

		if ($dereferenceSymlinks)
		{
			return;
		}

		// We are told not to dereference symlinks. Are we on Windows?
		$isWindows = (DIRECTORY_SEPARATOR == '\\');

		if (function_exists('php_uname'))
		{
			$isWindows = stristr(php_uname(), 'windows');
		}

		// If we are not on Windows, enable symlink target storage
		$this->storeSymlinkTarget = !$isWindows;
	}

	/**
	 * Gets the file size and last modification time (also works on virtual files and symlinks)
	 *
	 * @param   string  $sourceNameOrData  File path to the source file or source data (if $isVirtual is true)
	 * @param   bool    $isVirtual         Is this a virtual file?
	 * @param   bool    $isSymlink         Is this a symlink?
	 * @param   bool    $isDir             Is this a directory?
	 *
	 * @return  array
	 */
	protected function getFileSizeAndModificationTime(&$sourceNameOrData, $isVirtual, $isSymlink, $isDir)
	{
		// Get real size before compression
		if ($isVirtual)
		{
			$fileSize    = akstrlen($sourceNameOrData);
			$fileModTime = time();

			return [$fileSize, $fileModTime];
		}


		if ($isSymlink)
		{
			$fileSize    = akstrlen(@readlink($sourceNameOrData));
			$fileModTime = 0;

			return [$fileSize, $fileModTime];
		}

		// Is the file readable?
		if (!is_readable($sourceNameOrData) && !$isDir)
		{
			// Really, REALLY check if it is readable (PHP sometimes lies, dammit!)
			$myFP = @$this->fopen($sourceNameOrData, 'r');

			if ($myFP === false)
			{
				// Unreadable file, skip it.
				throw new WarningException('Unreadable file ' . $sourceNameOrData . '. Check permissions');
			}

			@$this->fclose($myFP);
		}

		// Get the file size
		$fileSize    = $isDir ? 0 : @filesize($sourceNameOrData);
		$fileModTime = $isDir ? 0 : @filemtime($sourceNameOrData);

		return [$fileSize, $fileModTime];
	}

	/**
	 * Get the preferred compression method for a file
	 *
	 * @param   int   $fileSize   File size in bytes
	 * @param   int   $memLimit   Memory limit in bytes
	 * @param   bool  $isDir      Is it a directory?
	 * @param   bool  $isSymlink  Is it a symlink?
	 *
	 * @return  int  Compression method to use: 0 (uncompressed) or 1 (gzip deflate)
	 */
	protected function getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink)
	{
		// If we don't have gzip installed we can't compress anything
		if (!function_exists("gzcompress"))
		{
			return 0;
		}

		// Don't compress directories or symlinks
		if ($isDir || $isSymlink)
		{
			return 0;
		}

		// Do not compress files over the compression threshold
		if ($fileSize >= _AKEEBA_COMPRESSION_THRESHOLD)
		{
			return 0;
		}

		// No memory limit, file smaller than the compression threshold: always compress.
		if (is_numeric($memLimit) && ($memLimit == 0))
		{
			return 1;
		}

		// Non-zero memory limit, PHP can report memory usage, see if there's enough memory.
		if (is_numeric($memLimit) && function_exists("memory_get_usage"))
		{
			$availableRAM = $memLimit - memory_get_usage();
			// Conservative approach: if the file size is over 40% of the available memory we won't compress.
			$compressionMethod = (($availableRAM / 2.5) >= $fileSize) ? 1 : 0;

			return $compressionMethod;
		}

		// Non-zero memory limit, PHP can't report memory usage, compress only files up to 512Kb (very conservative)
		return ($fileSize <= 524288) ? 1 : 0;
	}

	/**
	 * Checks if the file exists and is readable
	 *
	 * @param   string  $sourceNameOrData  The path to the file being compressed, or the raw file data for virtual files
	 * @param   bool    $isVirtual         Is this a virtual file?
	 * @param   bool    $isSymlink         Is this a symlink?
	 * @param   bool    $isDir             Is this a directory?
	 *
	 * @return  void
	 *
	 * @throws  WarningException
	 */
	protected function testIfFileExists(&$sourceNameOrData, &$isVirtual, &$isDir, &$isSymlink)
	{
		if ($isVirtual || $isDir)
		{
			return;
		}

		if (!@file_exists($sourceNameOrData))
		{
			if ($isSymlink)
			{
				throw new WarningException('The symlink ' . $sourceNameOrData . ' points to a file or folder that no longer exists and will NOT be backed up.');
			}

			throw new WarningException('The file ' . $sourceNameOrData . ' no longer exists and will NOT be backed up. Are you backing up temporary or cache data?');
		}

		if (!@is_readable($sourceNameOrData))
		{
			throw new WarningException('Unreadable file ' . $sourceNameOrData . '. Check permissions.');
		}
	}

	/**
	 * Try to get the compressed data for a file
	 *
	 * @param   string  $sourceNameOrData
	 * @param   bool    $isVirtual
	 * @param   int     $compressionMethod
	 * @param   string  $zdata
	 * @param   int     $unc_len
	 * @param   int     $c_len
	 *
	 * @return  void
	 */
	protected function getZData(&$sourceNameOrData, &$isVirtual, &$compressionMethod, &$zdata, &$unc_len, &$c_len)
	{
		// Get uncompressed data
		$udata =& $sourceNameOrData;

		if (!$isVirtual)
		{
			$udata = @file_get_contents($sourceNameOrData);
		}

		// If the compression fails, we will let it behave like no compression was available
		$c_len             = $unc_len;
		$compressionMethod = 0;

		// Proceed with compression
		$zdata = @gzcompress($udata);

		if ($zdata !== false)
		{
			// The compression succeeded
			unset($udata);
			$compressionMethod = 1;
			$zdata             = aksubstr($zdata, 2, -4);
			$c_len             = akstrlen($zdata);
		}
	}

	/**
	 * Returns the bytes available for writing data to the current part file (i.e. part size minus current offset)
	 *
	 * @return  int
	 */
	protected function getPartFreeSize()
	{
		clearstatcache();
		$current_part_size = @filesize($this->_dataFileName);

		return (int) $this->partSize - ($current_part_size === false ? 0 : $current_part_size);
	}

	/**
	 * Enable split archive creation where possible
	 *
	 * @return  void
	 */
	protected function enableSplitArchives()
	{
		$configuration = Factory::getConfiguration();
		$partSize      = $configuration->get('engine.archiver.common.part_size', 0);

		// If the part size is less than 64Kb we won't enable split archives
		if ($partSize < 65536)
		{
			return;
		}

		$extension            = $this->getExtension();
		$altExtension         = substr($extension, 0, 2) . '01';
		$archiveTypeUppercase = strtoupper(substr($extension, 1));

		Factory::getLog()->info(__CLASS__ . " :: Split $archiveTypeUppercase creation enabled");

		$this->useSplitArchive              = true;
		$this->partSize                     = $partSize;
		$this->dataFileNameWithoutExtension =
			dirname($this->_dataFileName) . '/' . basename($this->_dataFileName, $extension);
		$this->_dataFileName                = $this->dataFileNameWithoutExtension . $altExtension;

		// Indicate that we have at least 1 part
		$statistics = Factory::getStatistics();
		$statistics->updateMultipart(1);
	}

	/**
	 * Write a file's GZip compressed data to the archive, taking into account archive splitting
	 *
	 * @param   string  $zdata  The compressed data to write to the archive
	 *
	 * @return  void
	 */
	protected function putRawDataIntoArchive(&$zdata)
	{
		// Single part archive. Just dump the compressed data.
		if (!$this->useSplitArchive)
		{
			$this->fwrite($this->fp, $zdata);

			return;
		}

		// Split JPA. Check if we need to split the part in the middle of the data.
		$freeSpaceInPart = $this->getPartFreeSize();

		// Nope. We have enough space to write all of the data in this part.
		if ($freeSpaceInPart >= akstrlen($zdata))
		{
			$this->fwrite($this->fp, $zdata);

			return;
		}

		$bytesLeftInData = akstrlen($zdata);

		while ($bytesLeftInData > 0)
		{
			// Try to write to the archive. We can only write as much bytes as the free space in the backup archive OR
			// the total data bytes left, whichever is lower.
			$bytesWritten = $this->fwrite($this->fp, $zdata, min($bytesLeftInData, $freeSpaceInPart));

			// Since we may have written fewer bytes than anticipated we use the real bytes written for calculations
			$freeSpaceInPart -= $bytesWritten;
			$bytesLeftInData -= $bytesWritten;

			// If we still have data to write, remove the part already written and keep the rest
			if ($bytesLeftInData > 0)
			{
				$zdata = aksubstr($zdata, -$bytesLeftInData);
			}

			// If the part file is full create a new one
			if ($freeSpaceInPart <= 0)
			{
				// Create new part
				$this->createAndOpenNewPart();

				// Get its free space
				$freeSpaceInPart = $this->getPartFreeSize();
			}
		}

		// Tell PHP to free up some memory
		$zdata = null;
	}

	/**
	 * Begin or resume adding an uncompressed file into the archive.
	 *
	 * IMPORTANT! Only this case can be spanned across steps: uncompressed, non-virtual data
	 *
	 * @param   string  $sourceNameOrData  The path to the file we are reading from.
	 * @param   int     $fileLength        The file size we are supposed to read, in bytes.
	 * @param   int     $resumeOffset      Offset in the file to resume reading from
	 *
	 * @return  bool  True to indicate more processing is required in the next step
	 */
	protected function putUncompressedFileIntoArchive(&$sourceNameOrData, $fileLength = 0, $resumeOffset = null)
	{
		// Copy the file contents, ignore directories
		$sourceFilePointer = @fopen($sourceNameOrData, "r");

		if ($sourceFilePointer === false)
		{
			// If we have already written the file header and can't read the data your archive is busted.
			throw new ErrorException('Unreadable file ' . $sourceNameOrData . '. Check permissions. Your archive is corrupt!');
		}

		// Seek to the resume point if required
		if (!is_null($resumeOffset))
		{
			// Seek to new offset
			$seek_result = @fseek($sourceFilePointer, $resumeOffset);

			if ($seek_result === -1)
			{
				// What?! We can't resume!
				$this->conditionalFileClose($sourceFilePointer);

				throw new ErrorException(sprintf('Could not resume packing of file %s. Your archive is damaged!', $sourceNameOrData));
			}

			// Change the uncompressed size to reflect the remaining data
			$fileLength -= $resumeOffset;
		}

		$mustBreak = $this->putDataFromFileIntoArchive($sourceFilePointer, $fileLength);

		$this->conditionalFileClose($sourceFilePointer);

		return $mustBreak;
	}

	/**
	 * Return the requested permissions for the backup archive file.
	 *
	 * @return  int
	 * @since   8.0.0
	 */
	protected function getPermissions(): int
	{
		if (!is_null($this->permissions))
		{
			return $this->permissions;
		}

		$configuration     = Factory::getConfiguration();
		$permissions       = $configuration->get('engine.archiver.common.permissions', '0666') ?: '0666';
		$this->permissions = octdec($permissions);

		return $this->permissions;
	}

	/**
	 * Put up to $fileLength bytes of the file pointer $sourceFilePointer into the backup archive. Returns true if we
	 * ran out of time and need to perform a step break. Returns false when the whole quantity of data has been copied.
	 * Throws an ErrorException if something terrible happens.
	 *
	 * @param   resource  $sourceFilePointer  The pointer to the input file
	 * @param   int       $fileLength         How many bytes to copy
	 *
	 * @return  bool  True to indicate we need to resume packing the file in the next step
	 */
	private function putDataFromFileIntoArchive(&$sourceFilePointer, &$fileLength)
	{
		// Get references to engine objects we're going to be using
		$configuration = Factory::getConfiguration();
		$timer         = Factory::getTimer();
		$isEOF         = false;

		// Quick copy data into the archive, AKEEBA_CHUNK bytes at a time
		while (!$isEOF && ($timer->getTimeLeft() > 0) && ($fileLength > 0))
		{
			// Normally I read up to AKEEBA_CHUNK bytes at a time, unless the remaining $fileLength is smaller.
			$chunkSize = min(AKEEBA_CHUNK, $fileLength);

			// Do I have a split ZIP?
			if ($this->useSplitArchive)
			{
				// I must only read up to the free space in the part file if it's less than AKEEBA_CHUNK.
				$free_space = $this->getPartFreeSize();
				$chunkSize  = min($free_space, AKEEBA_CHUNK);

				// If I ran out of free space I have to create a new part file.
				if ($free_space <= 0)
				{
					$this->createAndOpenNewPart();

					// We have created the part. If the user asked for immediate post-proc, break step now.
					if ($configuration->get('engine.postproc.common.after_part', 0))
					{
						$resumeOffset = @ftell($sourceFilePointer);
						$this->conditionalFileClose($sourceFilePointer);

						$configuration->set('volatile.engine.archiver.resume', $resumeOffset);
						$configuration->set('volatile.engine.archiver.processingfile', true);
						$configuration->set('volatile.breakflag', true);

						// Always close the open part when immediate post-processing is requested
						@$this->fclose($this->fp);
						$this->fp = null;

						return true;
					}

					// No immediate post-proc. Recalculate the optimal chunk size.
					$free_space = $this->getPartFreeSize();
					$chunkSize  = min($free_space, AKEEBA_CHUNK);
				}
			}

			// Read some data and write it to the backup archive part file
			$data         = fread($sourceFilePointer, $chunkSize);
			$bytesWritten = $this->fwrite($this->fp, $data, akstrlen($data));

			// Subtract the written bytes from the bytes left to write
			$fileLength -= $bytesWritten;

			/**
			 * Have we reached the End of File?
			 *
			 * When we have read _exactly_ as many bytes as the size of the file we have not, in fact, reached EOF. We
			 * reach the EOF if we try to read _beyond_ the actual end of file.
			 *
			 * So, if we have read exactly as many bytes as the file claimed to be at the start of backup (i.e. the
			 * $fileLength is now 0) we try to read one more byte. If this returns no data (or false, e.g. the file went
			 * away in the meantime) OR PHP reports we reached EOF then we consider we've reached EOF.
			 *
			 * If the file grew in size in the meantime this condition will fail and $isEOF will be false. We will still
			 * exit the while loop because $fileLength === 0 but the next if-block after the while-block will catch this
			 * discrepancy and issue a warning that the file grew in size.
			 */
			$isEOF = feof($sourceFilePointer);

			if (!$isEOF && $fileLength === 0)
			{
				$junk = fread($sourceFilePointer, 1);
				$isEOF = (($junk === false) || (is_string($junk) && strlen($junk) === 0)) || feof($sourceFilePointer);
				fseek($sourceFilePointer, -1, SEEK_CUR);
			}
		}

		/**
		 * We have finished reading the entire file as per its size before we started backing it up. However, we still
		 * have not reached EOF (there's more to the file). This means that the file grew in size in the meantime. Warn
		 * the user.
		 */
		if (!$isEOF && ($timer->getTimeLeft() > 0) && ($fileLength === 0))
		{
			Factory::getLog()->warning(
				'The file grew in size while putting it in the backup archive. If this is a temporary or cache file we advise you to exclude it, or exclude the contents of the temporary / cache folder it is contained in.'
			);
		}

		/**
		 * According to the file size we read when we were writing the file header we have more data to write. However,
		 * we reached the end of the file. This means the file went away or shrunk. We can't reliably go back and
		 * change the file header since it may be in a previous part file that's already been post-processed. All we can
		 * do is try to warn the user.
		 */
		if ($isEOF && ($timer->getTimeLeft() > 0) && ($fileLength > 0))
		{
			throw new ErrorException(
				'The file shrunk or went away while putting it in the backup archive. Your archive is damaged! If this is a temporary or cache file we advise you to exclude it, or exclude the contents of the temporary / cache folder it is contained in.'
			);
		}

		// WARNING!!! The extra $unc_len != 0 check is necessary as PHP won't reach EOF for 0-byte files.
		if (!feof($sourceFilePointer) && ($fileLength != 0))
		{
			// We have to break, or we'll time out!
			$resumeOffset = @ftell($sourceFilePointer);
			$this->conditionalFileClose($sourceFilePointer);

			$configuration->set('volatile.engine.archiver.resume', $resumeOffset);
			$configuration->set('volatile.engine.archiver.processingfile', true);

			return true;
		}

		return false;
	}
}
Archiver/jpa.json000060400000004120152456704520007757 0ustar00{
    "_information": {
        "title": "COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_JPA_TITLE",
        "description": "COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_JPA_DESCRIPTION"
    },
    "engine.archiver.common.dereference_symlinks": {
        "default": "0",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_TITLE",
        "description": "COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_DESCRIPTION"
    },
    "engine.archiver.common.part_size": {
        "default": "0",
        "type": "integer",
        "min": "0",
        "max": "2147483648",
        "shortcuts": "0|131072|262144|524288|1048576|2097152|5242880|10485760|20971520|52428800|104857600|268435456|536870912|1073741824|1610612736|2097152000",
        "scale": "1048576",
        "uom": "MB",
        "title": "COM_AKEEBA_CONFIG_PARTSIZE_TITLE",
        "description": "COM_AKEEBA_CONFIG_PARTSIZE_DESCRIPTION"
    },
    "engine.archiver.common.permissions": {
        "default": "0666",
        "type": "enum",
        "enumkeys": "COM_AKEEBA_CONFIG_PERMISSIONS_0600|COM_AKEEBA_CONFIG_PERMISSIONS_0644|COM_AKEEBA_CONFIG_PERMISSIONS_0666",
        "enumvalues": "0600|0644|0666",
        "title": "COM_AKEEBA_CONFIG_PERMISSIONS_TITLE",
        "description": "COM_AKEEBA_CONFIG_PERMISSIONS_DESCRIPTION"
    },
    "engine.archiver.common.chunk_size": {
        "default": "1048576",
        "type": "integer",
        "min": "65536",
        "max": "10485760",
        "shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
        "scale": "1048576",
        "uom": "MB",
        "title": "COM_AKEEBA_CONFIG_CHUNKSIZE_TITLE",
        "description": "COM_AKEEBA_CONFIG_CHUNKSIZE_DESCRIPTION"
    },
    "engine.archiver.common.big_file_threshold": {
        "default": "1048576",
        "type": "integer",
        "min": "65536",
        "max": "10485760",
        "shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
        "scale": "1048576",
        "uom": "MB",
        "title": "COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_TITLE",
        "description": "COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_DESCRIPTION"
    }
}Archiver/Base.php000060400000055415152456704520007712 0ustar00<?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\Archiver;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Base\Exceptions\WarningException;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Util\FileCloseAware;
use Akeeba\Engine\Util\FileSystem;
use Exception;
use RuntimeException;

/**
 * Abstract parent class of all archiver engines
 */
abstract class Base
{
	use FileCloseAware;

	/** @var   string  The archive's comment. It's currently used ONLY in the ZIP file format */
	protected $_comment;

	/** @var Filesystem Filesystem utilities object */
	protected $fsUtils = null;

	/** @var   resource  JPA transformation source handle */
	private $_xform_fp;

	/** @var   int  The total size of the source JPA file */
	private $totalSourceJPASize = 0;

	/**
	 * Public constructor
	 *
	 * @codeCoverageIgnore
	 *
	 * @return  void
	 */
	public function __construct()
	{
		$this->__bootstrap_code();
	}

	/**
	 * Wakeup (unserialization) function
	 *
	 * @codeCoverageIgnore
	 *
	 * @return  void
	 */
	public function __wakeup()
	{
		$this->__bootstrap_code();
	}

	/**
	 * Adds a single file in the archive
	 *
	 * @param   string  $file        The absolute path to the file to add
	 * @param   string  $removePath  Path to remove from $file
	 * @param   string  $addPath     Path to prepend to $file
	 *
	 * @return  void
	 *
	 * @throws  Exception
	 */
	public final function addFile($file, $removePath = '', $addPath = '')
	{
		$storedName = $this->addRemovePaths($file, $removePath, $addPath);

		$this->addFileRenamed($file, $storedName);
	}

	/**
	 * Adds a list of files into the archive, removing $removePath from the
	 * file names and adding $addPath to them.
	 *
	 * @param   array   $fileList    A simple string array of filepaths to include
	 * @param   string  $removePath  Paths to remove from the filepaths
	 * @param   string  $addPath     Paths to add in front of the filepaths
	 *
	 * @return  void
	 *
	 * @throws Exception
	 */
	public final function addFileList(&$fileList, $removePath = '', $addPath = '')
	{
		if (!is_array($fileList))
		{
			Factory::getLog()->warning('addFileList called without a file list array');

			return;
		}

		foreach ($fileList as $file)
		{
			$this->addFile($file, $removePath, $addPath);
		}
	}

	/**
	 * Adds a file to the archive, with a name that's different from the source
	 * filename
	 *
	 * @param   string  $sourceFile  Absolute path to the source file
	 * @param   string  $targetFile  Relative filename to store in archive
	 *
	 * @return  void
	 *
	 * @throws  Exception
	 */
	public function addFileRenamed($sourceFile, $targetFile)
	{
		$mb_encoding = '8bit';

		if (function_exists('mb_internal_encoding'))
		{
			$mb_encoding = mb_internal_encoding();
			mb_internal_encoding('ISO-8859-1');
		}

		try
		{
			$this->_addFile(false, $sourceFile, $targetFile);
		}
		catch (WarningException $e)
		{
			Factory::getLog()->warning($e->getMessage());
		}
		finally
		{
			if (function_exists('mb_internal_encoding'))
			{
				mb_internal_encoding($mb_encoding);
			}
		}
	}

	/**
	 * Adds a file to the archive, given the stored name and its contents
	 *
	 * @param   string  $fileName        The base file name
	 * @param   string  $addPath         The relative path to prepend to file name
	 * @param   string  $virtualContent  The contents of the file to be archived
	 *
	 * @return  void
	 */
	public final function addFileVirtual($fileName, $addPath, &$virtualContent)
	{
		$storedName  = $this->addRemovePaths($fileName, '', $addPath);
		$mb_encoding = '8bit';

		if (function_exists('mb_internal_encoding'))
		{
			$mb_encoding = mb_internal_encoding();
			mb_internal_encoding('ISO-8859-1');
		}

		try
		{
			$this->_addFile(true, $virtualContent, $storedName);
		}
		catch (WarningException $e)
		{
			Factory::getLog()->warning($e->getMessage());
		}
		finally
		{
			if (function_exists('mb_internal_encoding'))
			{
				mb_internal_encoding($mb_encoding);
			}
		}
	}

	/**
	 * Adds a file to the archive, given the stored name and its contents
	 *
	 * @param   string  $fileName        The base file name
	 * @param   string  $addPath         The relative path to prepend to file name
	 * @param   string  $virtualContent  The contents of the file to be archived
	 *
	 * @return  void
	 *
	 * @deprecated 7.0.0
	 */
	public final function addVirtualFile($fileName, $addPath, &$virtualContent)
	{
		Factory::getLog()->debug('DEPRECATED: addVirtualFile() has been renamed to addFileVirtual().');

		$this->addFileVirtual($fileName, $addPath, $virtualContent);
	}

	/**
	 * Makes whatever finalization is needed for the archive to be considered
	 * complete and useful (or, generally, clean up)
	 *
	 * @return  void
	 */
	abstract public function finalize();

	/**
	 * Returns a string with the extension (including the dot) of the files produced
	 * by this class.
	 *
	 * @return  string
	 */
	abstract public function getExtension();

	/**
	 * Initialises the archiver class, creating the archive from an existent
	 * installer's JPA archive. MUST BE OVERRIDEN BY CHILDREN CLASSES.
	 *
	 * @param   string  $targetArchivePath  Absolute path to the generated archive
	 * @param   array   $options            A named key array of options (optional)
	 *
	 * @return  void
	 */
	abstract public function initialize($targetArchivePath, $options = []);

	/**
	 * Notifies the engine on the backup comment and converts it to plain text for
	 * inclusion in the archive file, if applicable.
	 *
	 * @param   string  $comment  The archive's comment
	 *
	 * @return  void
	 */
	public function setComment($comment)
	{
		// First, sanitize the comment in a text-only format
		$comment        = str_replace("\n", " ", $comment); // Replace newlines with spaces
		$comment        = str_replace("<br>", "\n", $comment); // Replace HTML4 <br> with single newlines
		$comment        = str_replace("<br/>", "\n", $comment); // Replace HTML4 <br> with single newlines
		$comment        = str_replace("<br />", "\n", $comment); // Replace HTML <br /> with single newlines
		$comment        = str_replace("</p>", "\n\n", $comment); // Replace paragraph endings with double newlines
		$comment        = str_replace("<b>", "*", $comment); // Replace bold with star notation
		$comment        = str_replace("</b>", "*", $comment); // Replace bold with star notation
		$comment        = str_replace("<i>", "_", $comment); // Replace italics with underline notation
		$comment        = str_replace("</i>", "_", $comment); // Replace italics with underline notation
		$this->_comment = strip_tags($comment, '');
	}

	/**
	 * Transforms a JPA archive (containing an installer) to the native archive format
	 * of the class. It actually extracts the source JPA in memory and instructs the
	 * class to include each extracted file.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param   integer  $index   The index in the source JPA archive's list currently in use
	 * @param   integer  $offset  The source JPA archive's offset to use
	 *
	 * @return  array|bool  False if an error occurred, return array otherwise
	 */
	public function transformJPA($index, $offset)
	{
		$xform_source = null;

		// Do we have to open the file?
		if (!$this->_xform_fp)
		{
			// Get the source path
			$registry           = Factory::getConfiguration();
			$embedded_installer = $registry->get('akeeba.advanced.embedded_installer');

			// Fetch the name of the installer image
			$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
			$xform_source         = Platform::getInstance()->get_installer_images_path() .
				'/foobar.jpa'; // We need this as a "safe fallback"

			// Try to find a sane default if we are not given a valid embedded installer
			if (!array_key_exists($embedded_installer, $installerDescriptors))
			{
				$embedded_installer = 'angie';

				if (!array_key_exists($embedded_installer, $installerDescriptors))
				{
					$allInstallers = array_keys($installerDescriptors);

					foreach ($allInstallers as $anInstaller)
					{
						if ($anInstaller == 'none')
						{
							continue;
						}

						$embedded_installer = $anInstaller;
						break;
					}
				}
			}

			if (array_key_exists($embedded_installer, $installerDescriptors))
			{
				$packages  = $installerDescriptors[$embedded_installer]['package'] ?? '';
				$langPacks = $installerDescriptors[$embedded_installer]['language'] ?? '';

				if (empty($packages))
				{
					// No installer package specified. Pretend we are done!
					$retArray = [
						"filename" => '', // File name extracted
						"data"     => '', // File data
						"index"    => 0, // How many source JPA files I have
						"offset"   => 0, // Offset in JPA file
						"skip"     => false, // Skip this?
						"done"     => true, // Are we done yet?
						"filesize" => 0,
					];

					return $retArray;
				}

				$packages                 = explode(',', $packages);
				$langPacks                = explode(',', $langPacks);
				$this->totalSourceJPASize = 0;
				$pathPrefix               = Platform::getInstance()->get_installer_images_path() . '/';

				foreach ($packages as $package)
				{
					$filePath                 = $pathPrefix . $package;
					$this->totalSourceJPASize += (int) @filesize($filePath);
				}

				foreach ($langPacks as $langPack)
				{
					$filePath = $pathPrefix . $langPack;

					if (!is_file($filePath))
					{
						continue;
					}

					$packages[]               = $langPack;
					$this->totalSourceJPASize += (int) @filesize($filePath);
				}

				if (count($packages) < $index)
				{
					throw new RuntimeException(__CLASS__ . ":: Installer package index $index not found for embedded installer $embedded_installer");
				}

				$package = $packages[$index];

				// A package is specified, use it!
				$xform_source = $pathPrefix . $package;
			}

			// 2.3: Try to use sane default if the indicated installer doesn't exist
			if (!is_null($xform_source) && !file_exists($xform_source) && (basename($xform_source) != 'angie.jpa'))
			{
				throw new RuntimeException(__CLASS__ . ":: Installer package $xform_source of embedded installer $embedded_installer not found. Please go to the configuration page, select an Embedded Installer, save the configuration and try backing up again.");
			}

			// Try opening the file
			if (!is_null($xform_source) && file_exists($xform_source))
			{
				$this->_xform_fp = @fopen($xform_source, 'r');

				if ($this->_xform_fp === false)
				{
					throw new RuntimeException(__CLASS__ . ":: Can't seed archive with installer package " . $xform_source);
				}
			}
			else
			{
				throw new RuntimeException(__CLASS__ . ":: Installer package " . $xform_source . " does not exist!");
			}
		}

		$headerDataLength = 0;

		if (!$offset)
		{
			// First run detected!
			Factory::getLog()->debug('Initializing with JPA package ' . $xform_source);

			// Skip over the header and check no problem exists
			$offset = $this->_xformReadHeader();

			if ($offset === false)
			{
				throw new RuntimeException('JPA package file was not read');
			}

			$headerDataLength = $offset;
		}

		$ret = $this->_xformExtract($offset);

		$ret['index'] = $index;

		if (is_array($ret))
		{
			$ret['chunkProcessed'] = $headerDataLength + $ret['offset'] - $offset;
			$offset                = $ret['offset'];

			if (!$ret['skip'] && !$ret['done'])
			{
				Factory::getLog()->debug('  Adding ' . $ret['filename'] . '; Next offset:' . $offset);

				$this->addFileVirtual($ret['filename'], '', $ret['data']);
			}
			elseif ($ret['done'])
			{
				$registry             = Factory::getConfiguration();
				$embedded_installer   = $registry->get('akeeba.advanced.embedded_installer');
				$installerDescriptors = Factory::getEngineParamsProvider()->getInstallerList();
				$packages             = $installerDescriptors[$embedded_installer]['package'];
				$packages             = explode(',', $packages);
				$pathPrefix           = Platform::getInstance()->get_installer_images_path() . '/';
				$langPacks            = $installerDescriptors[$embedded_installer]['language'];
				$langPacks            = explode(',', $langPacks);

				foreach ($langPacks as $langPack)
				{
					$filePath = $pathPrefix . $langPack;

					if (!is_file($filePath))
					{
						continue;
					}

					$packages[] = $langPack;
				}

				Factory::getLog()->debug('  Done with package ' . $packages[$index]);

				if (count($packages) > ($index + 1))
				{
					$ret['done']     = false;
					$ret['index']    = $index + 1;
					$ret['offset']   = 0;
					$this->_xform_fp = null;
				}
				else
				{
					Factory::getLog()->debug('  Done with installer seeding.');
				}
			}
			else
			{
				$reason = '  Skipping ' . $ret['filename'];
				Factory::getLog()->debug($reason);
			}
		}
		else
		{
			throw new RuntimeException('JPA extraction returned FALSE. The installer image is corrupt.');
		}

		if ($ret['done'])
		{
			// We are finished! Close the file
			$this->conditionalFileClose($this->_xform_fp);
			Factory::getLog()->debug('Initializing with JPA package has finished');
		}

		$ret['filesize'] = $this->totalSourceJPASize;

		return $ret;
	}

	/**
	 * Common code which gets called on instance creation or wake-up (unserialization)
	 *
	 * @codeCoverageIgnore
	 *
	 * @return  void
	 */
	protected function __bootstrap_code()
	{
		$this->fsUtils = Factory::getFilesystemTools();
	}

	/**
	 * The most basic file transaction: add a single entry (file or directory) to
	 * the archive.
	 *
	 * @param   boolean  $isVirtual         If true, the next parameter contains file data instead of a file name
	 * @param   string   $sourceNameOrData  Absolute file name to read data from or the file data itself is $isVirtual
	 *                                      is true
	 * @param   string   $targetName        The (relative) file name under which to store the file in the archive
	 *
	 * @return  boolean  True on success, false otherwise. DEPRECATED: Use exceptions instead.
	 *
	 * @throws  WarningException  When there's a warning (the backup integrity is NOT compromised)
	 * @throws  ErrorException    When there's an error (the backup integrity is compromised – backup dead)
	 */
	abstract protected function _addFile($isVirtual, &$sourceNameOrData, $targetName);

	/**
	 * This function indicates if the path $p_path is under the $p_dir tree. Or,
	 * said in an other way, if the file or sub-dir $p_path is inside the dir
	 * $p_dir.
	 * The function indicates also if the path is exactly the same as the dir.
	 * This function supports path with duplicated '/' like '//', but does not
	 * support '.' or '..' statements.
	 *
	 * Copied verbatim from pclZip library
	 *
	 * @codeCoverageIgnore
	 *
	 * @param   string  $p_dir   Source tree
	 * @param   string  $p_path  Check if this is part of $p_dir
	 *
	 * @return  integer   0 if $p_path is not inside directory $p_dir,
	 *                    1 if $p_path is inside directory $p_dir
	 *                    2 if $p_path is exactly the same as $p_dir
	 */
	private function _PathInclusion($p_dir, $p_path)
	{
		$v_result = 1;

		// ----- Explode dir and path by directory separator
		$v_list_dir       = explode("/", $p_dir);
		$v_list_dir_size  = count($v_list_dir);
		$v_list_path      = explode("/", $p_path);
		$v_list_path_size = count($v_list_path);

		// ----- Study directories paths
		$i = 0;
		$j = 0;

		while (($i < $v_list_dir_size) && ($j < $v_list_path_size) && ($v_result))
		{
			// ----- Look for empty dir (path reduction)
			if ($v_list_dir[$i] == '')
			{
				$i++;

				continue;
			}

			if ($v_list_path[$j] == '')
			{
				$j++;

				continue;
			}

			// ----- Compare the items
			if (($v_list_dir[$i] != $v_list_path[$j]) && ($v_list_dir[$i] != '') && ($v_list_path[$j] != ''))
			{
				$v_result = 0;
			}

			// ----- Next items
			$i++;
			$j++;
		}

		// ----- Look if everything seems to be the same
		if ($v_result)
		{
			// ----- Skip all the empty items
			while (($j < $v_list_path_size) && ($v_list_path[$j] == ''))
			{
				$j++;
			}

			while (($i < $v_list_dir_size) && ($v_list_dir[$i] == ''))
			{
				$i++;
			}

			if (($i >= $v_list_dir_size) && ($j >= $v_list_path_size))
			{
				// ----- There are exactly the same
				$v_result = 2;
			}
			else if ($i < $v_list_dir_size)
			{
				// ----- The path is shorter than the dir
				$v_result = 0;
			}
		}

		// ----- Return
		return $v_result;
	}

	/**
	 * Extracts a file from the JPA archive and returns an in-memory array containing it
	 * and its file data. The data returned is an array, consisting of the following keys:
	 * "filename" => relative file path stored in the archive
	 * "data"     => file data
	 * "offset"   => next offset to use
	 * "skip"     => if this is not a file, just skip it...
	 * "done"     => No more files left in archive
	 *
	 * @codeCoverageIgnore
	 *
	 * @param   integer  $offset  The absolute data offset from archive's header
	 *
	 * @return  array|bool  See description for more information
	 */
	private function &_xformExtract($offset)
	{
		$false = false; // Used to return false values in case an error occurs

		// Generate a return array
		$retArray = [
			"filename" => '', // File name extracted
			"data"     => '', // File data
			"offset"   => 0, // Offset in ZIP file
			"skip"     => false, // Skip this?
			"done"     => false // Are we done yet?
		];

		// If we can't open the file, return an error condition
		if ($this->_xform_fp === false)
		{
			return $false;
		}

		// Go to the offset specified
		if (!fseek($this->_xform_fp, $offset) == 0)
		{
			return $false;
		}

		// Get and decode Entity Description Block
		$signature = fread($this->_xform_fp, 3);

		// Check signature
		if ($signature == 'JPF')
		{
			// This a JPA Entity Block. Process the header.

			// Read length of EDB and of the Entity Path Data
			$length_array = unpack('vblocksize/vpathsize', fread($this->_xform_fp, 4));
			// Read the path data
			$file = fread($this->_xform_fp, $length_array['pathsize']);
			// Read and parse the known data portion
			$bin_data    = fread($this->_xform_fp, 14);
			$header_data = unpack('Ctype/Ccompression/Vcompsize/Vuncompsize/Vperms', $bin_data);
			// Read any unknwon data
			$restBytes = $length_array['blocksize'] - (21 + $length_array['pathsize']);

			if ($restBytes > 0)
			{
				$junk = fread($this->_xform_fp, $restBytes);
			}

			$compressionType = $header_data['compression'];

			// Populate the return array
			$retArray['filename'] = $file;
			$retArray['skip']     = ($header_data['compsize'] == 0); // Skip over directories

			switch ($header_data['type'])
			{
				case 0:
					// directory
					break;

				case 1:
					// file
					switch ($compressionType)
					{
						case 0: // No compression
							if ($header_data['compsize'] > 0) // 0 byte files do not have data to be read
							{
								$retArray['data'] = fread($this->_xform_fp, $header_data['compsize']);
							}
							break;

						case 1: // GZip compression
							$zipData          = fread($this->_xform_fp, $header_data['compsize']);
							$retArray['data'] = gzinflate($zipData);
							break;

						case 2: // BZip2 compression
							$zipData          = fread($this->_xform_fp, $header_data['compsize']);
							$retArray['data'] = bzdecompress($zipData);
							break;
					}
					break;
			}
		}
		else
		{
			// This is not a file header. This means we are done.
			$retArray['done'] = true;
		}

		$retArray['offset'] = ftell($this->_xform_fp);

		return $retArray;
	}

	/**
	 * Skips over the JPA header entry and returns the offset file data starts from
	 *
	 * @codeCoverageIgnore
	 *
	 * @return  boolean|integer  False on failure, offset otherwise
	 */
	private function _xformReadHeader()
	{
		// Fail for unreadable files
		if ($this->_xform_fp === false)
		{
			return false;
		}

		// Go to the beggining of the file
		rewind($this->_xform_fp);

		// Read the signature
		$sig = fread($this->_xform_fp, 3);

		// Not a JPA Archive?
		if ($sig != 'JPA')
		{
			return false;
		}

		// Read and parse header length
		$header_length_array = unpack('v', fread($this->_xform_fp, 2));
		$header_length       = $header_length_array[1];

		// Read and parse the known portion of header data (14 bytes)
		$bin_data    = fread($this->_xform_fp, 14);
		$header_data = unpack('Cmajor/Cminor/Vcount/Vuncsize/Vcsize', $bin_data);

		// Load any remaining header data (forward compatibility)
		$rest_length = $header_length - 19;

		if ($rest_length > 0)
		{
			$junk = fread($this->_xform_fp, $rest_length);
		}

		return ftell($this->_xform_fp);
	}

	/**
	 * Removes the $p_remove_dir from $p_filename, while prepending it with $p_add_dir.
	 * Largely based on code from the pclZip library.
	 *
	 * @param   string  $p_filename    The absolute file name to treat
	 * @param   string  $p_remove_dir  The path to remove
	 * @param   string  $p_add_dir     The path to prefix the treated file name with
	 *
	 * @return  string  The treated file name
	 */
	private function addRemovePaths($p_filename, $p_remove_dir, $p_add_dir)
	{
		$p_filename   = $this->fsUtils->TranslateWinPath($p_filename);
		$p_remove_dir = ($p_remove_dir == '') ? '' :
			$this->fsUtils->TranslateWinPath($p_remove_dir); //should fix corrupt backups, fix by nicholas

		$v_stored_filename = $p_filename;

		if (!($p_remove_dir == ""))
		{
			if (substr($p_remove_dir, -1) != '/')
			{
				$p_remove_dir .= "/";
			}

			if ((substr($p_filename, 0, 2) == "./") || (substr($p_remove_dir, 0, 2) == "./"))
			{
				if ((substr($p_filename, 0, 2) == "./") && (substr($p_remove_dir, 0, 2) != "./"))
				{
					$p_remove_dir = "./" . $p_remove_dir;
				}

				if ((substr($p_filename, 0, 2) != "./") && (substr($p_remove_dir, 0, 2) == "./"))
				{
					$p_remove_dir = substr($p_remove_dir, 2);
				}
			}

			$v_compare = $this->_PathInclusion($p_remove_dir, $p_filename);

			if ($v_compare > 0)
			{
				if ($v_compare == 2)
				{
					$v_stored_filename = "";
				}
				else
				{
					$v_stored_filename =
						substr($p_filename, (function_exists('mb_strlen') ? mb_strlen($p_remove_dir, '8bit') :
							strlen($p_remove_dir)));
				}
			}
		}
		else
		{
			$v_stored_filename = $p_filename;
		}

		if (!($p_add_dir == ""))
		{
			if (substr($p_add_dir, -1) == "/")
			{
				$v_stored_filename = $p_add_dir . $v_stored_filename;
			}
			else
			{
				$v_stored_filename = $p_add_dir . "/" . $v_stored_filename;
			}
		}

		return $v_stored_filename;
	}
}
Archiver/zip.json000060400000005001152456704520010006 0ustar00{
    "_information": {
        "title": "COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_ZIP_TITLE",
        "description": "COM_AKEEBA_CONFIG_ENGINE_ARCHIVER_ZIP_DESCRIPTION"
    },
    "engine.archiver.common.dereference_symlinks": {
        "default": "0",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_TITLE",
        "description": "COM_AKEEBA_CONFIG_DEREFERENCESYMLINKS_DESCRIPTION"
    },
    "engine.archiver.common.part_size": {
        "default": "0",
        "type": "integer",
        "min": "0",
        "max": "2147483648",
        "shortcuts": "0|131072|262144|524288|1048576|2097152|5242880|10485760|20971520|52428800|104857600|268435456|536870912|1073741824|1610612736|2097152000",
        "scale": "1048576",
        "uom": "MB",
        "title": "COM_AKEEBA_CONFIG_PARTSIZE_TITLE",
        "description": "COM_AKEEBA_CONFIG_PARTSIZE_DESCRIPTION"
    },
    "engine.archiver.common.chunk_size": {
        "default": "1048576",
        "type": "integer",
        "min": "65536",
        "max": "10485760",
        "shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
        "scale": "1048576",
        "uom": "MB",
        "title": "COM_AKEEBA_CONFIG_CHUNKSIZE_TITLE",
        "description": "COM_AKEEBA_CONFIG_CHUNKSIZE_DESCRIPTION"
    },
    "engine.archiver.common.permissions": {
        "default": "0666",
        "type": "enum",
        "enumkeys": "COM_AKEEBA_CONFIG_PERMISSIONS_0600|COM_AKEEBA_CONFIG_PERMISSIONS_0644|COM_AKEEBA_CONFIG_PERMISSIONS_0666",
        "enumvalues": "0600|0644|0666",
        "title": "COM_AKEEBA_CONFIG_PERMISSIONS_TITLE",
        "description": "COM_AKEEBA_CONFIG_PERMISSIONS_DESCRIPTION"
    },
    "engine.archiver.common.big_file_threshold": {
        "default": "1048576",
        "type": "integer",
        "min": "65536",
        "max": "10485760",
        "shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
        "scale": "1048576",
        "uom": "MB",
        "title": "COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_TITLE",
        "description": "COM_AKEEBA_CONFIG_BIGFILETHRESHOLD_DESCRIPTION"
    },
    "engine.archiver.zip.cd_glue_chunk_size": {
        "default": "1048576",
        "type": "integer",
        "min": "65536",
        "max": "10485760",
        "shortcuts": "65536|131072|262144|524288|1048576|2097152|5242880|10485760",
        "scale": "1048576",
        "uom": "MB",
        "title": "COM_AKEEBA_CONFIG_ZIPCDGLUECHUNKSIZE_TITLE",
        "description": "COM_AKEEBA_CONFIG_ZIPCDGLUECHUNKSIZE_DESCRIPTION"
    }
}Archiver/BaseFileManagement.php000060400000014613152456704520012502 0ustar00<?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\Archiver;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Factory;

if (!function_exists('akstrlen'))
{
	/**
	 * Attempt to use mbstring for calculating the binary string length.
	 *
	 * @param $string
	 *
	 * @return int
	 */
	function akstrlen($string)
	{
		return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string);
	}
}

/**
 * Abstract class for an archiver using managed file pointers
 */
abstract class BaseFileManagement extends Base
{
	/** @var resource File pointer to the archive's central directory file (for ZIP) */
	protected $cdfp = null;

	/** @var resource File pointer to the archive being currently written to */
	protected $fp = null;

	/** @var   array  An array of the last open files for writing and their last written to offsets */
	private $fileOffsets = [];

	/** @var   array  An array of open file pointers */
	private $filePointers = [];

	/** @var   null|string  The last filename fwrite() wrote to */
	private $lastFileName = null;

	/** @var   null|resource  The last file pointer fwrite() wrote to */
	private $lastFilePointer = null;

	/**
	 * Release file pointers when the object is being destroyed
	 *
	 * @codeCoverageIgnore
	 *
	 * @return  void
	 */
	public function __destruct()
	{
		$this->_closeAllFiles();

		$this->fp   = null;
		$this->cdfp = null;
	}

	/**
	 * Release file pointers when the object is being serialized
	 *
	 * @codeCoverageIgnore
	 *
	 * @return  void
	 */
	public function _onSerialize()
	{
		$this->_closeAllFiles();

		$this->fp   = null;
		$this->cdfp = null;
	}

	/**
	 * Closes all open files known to this archiver object
	 *
	 * @return  void
	 */
	protected function _closeAllFiles()
	{
		if (!empty($this->filePointers))
		{
			foreach ($this->filePointers as $file => $fp)
			{
				$this->conditionalFileClose($fp);

				unset($this->filePointers[$file]);
			}
		}
	}

	/**
	 * Closes an already open file
	 *
	 * @param   resource  $fp  The file pointer to close
	 *
	 * @return  boolean
	 */
	protected function fclose(&$fp)
	{
		$result = true;

		$offset = array_search($fp, $this->filePointers, true);

		if (!is_null($fp) && is_resource($fp))
		{
			$result = $this->conditionalFileClose($fp);
		}

		if ($offset !== false)
		{
			unset($this->filePointers[$offset]);
		}

		$fp = null;

		return $result;
	}

	protected function fcloseByName($file)
	{
		if (!array_key_exists($file, $this->filePointers))
		{
			return true;
		}

		$ret = $this->fclose($this->filePointers[$file]);

		if (array_key_exists($file, $this->filePointers))
		{
			unset($this->filePointers[$file]);
		}

		return $ret;
	}

	/**
	 * Opens a file, if it's not already open, or returns its cached file pointer if it's already open
	 *
	 * @param   string  $file  The filename to open
	 * @param   string  $mode  File open mode, defaults to binary write
	 *
	 * @return  resource
	 */
	protected function fopen($file, $mode = 'w')
	{
		if (!array_key_exists($file, $this->filePointers))
		{
			//Factory::getLog()->debug("Opening backup archive $file with mode $mode");
			$this->filePointers[$file] = @fopen($file, $mode);

			// If we open a file for append we have to seek to the correct offset
			if (substr($mode, 0, 1) == 'a')
			{
				if (isset($this->fileOffsets[$file]))
				{
					Factory::getLog()->debug("Truncating backup archive file $file to " . $this->fileOffsets[$file] . " bytes");
					@ftruncate($this->filePointers[$file], $this->fileOffsets[$file]);
				}

				fseek($this->filePointers[$file], 0, SEEK_END);
			}
		}

		return $this->filePointers[$file];
	}

	/**
	 * Write to file, defeating magic_quotes_runtime settings (pure binary write)
	 *
	 * @param   resource  $fp     Handle to a file
	 * @param   string    $data   The data to write to the file
	 * @param   integer   $p_len  Maximum length of data to write
	 *
	 * @return  int  The number of bytes written
	 *
	 * @throws  ErrorException  When writing to the file is not possible
	 */
	protected function fwrite($fp, $data, $p_len = null)
	{
		if ($fp !== $this->lastFilePointer)
		{
			$this->lastFilePointer = $fp;
			$this->lastFileName    = array_search($fp, $this->filePointers, true);
		}

		$len = is_null($p_len) ? (akstrlen($data)) : $p_len;
		$ret = fwrite($fp, $data, $len);

		if (($ret === false) || (abs(($ret - $len)) >= 1))
		{
			// Log debug information about the archive file's existence and current size. This helps us figure out if
			// there is a server-imposed maximum file size limit.
			clearstatcache();
			$fileExists  = @file_exists($this->lastFileName) ? 'exists' : 'does NOT exist';
			$currentSize = @filesize($this->lastFileName);

			Factory::getLog()->debug(sprintf("%s::_fwrite() ERROR!! Cannot write to archive file %s. The file %s. File size %s bytes after writing %s of %d bytes. Please check the output directory permissions and make sure you have enough disk space available. If this does not help, please set up a Part Size for Split Archives LOWER than this size and retry backing up.", __CLASS__, $this->lastFileName, $fileExists, $currentSize, $ret, $len));

			throw new ErrorException(sprintf("Couldn\'t write to the archive file; check the output directory permissions and make sure you have enough disk space available. [len=%s / %s]", $ret, $len));
		}

		if ($this->lastFileName !== false)
		{
			$this->fileOffsets[$this->lastFileName] = @ftell($fp);
		}

		return $ret;
	}

	/**
	 * Removes a file path from the list of resumable offsets
	 *
	 * @param $filename
	 */
	protected function removeFromOffsetsList($filename)
	{
		if (isset($this->fileOffsets[$filename]))
		{
			unset($this->fileOffsets[$filename]);
		}
	}

}
Archiver/Zip.php000060400000102762152456704520007600 0ustar00<?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\Archiver;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Base\Exceptions\WarningException;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Util\CRC32;
use RuntimeException;

class Zip extends BaseArchiver
{
	/** @var string Beginning of central directory record. */
	private $centralDirectoryRecordStartSignature = "\x50\x4b\x01\x02";

	/** @var string End of central directory record. */
	private $centralDirectoryRecordEndSignature = "\x50\x4b\x05\x06";

	/** @var string Beginning of file contents. */
	private $fileHeaderSignature = "\x50\x4b\x03\x04";

	/** @var string The name of the temporary file holding the ZIP's Central Directory */
	private $centralDirectoryFilename;

	/** @var integer The total number of files and directories stored in the ZIP archive */
	private $totalFilesCount;

	/** @var integer The total size of data in the archive. Note: On 32-bit versions of PHP, this will overflow for archives over 2Gb! */
	private $totalCompressedSize = 0;

	/** @var integer The chunk size for CRC32 calculations */
	private $AkeebaPackerZIP_CHUNK_SIZE;

	/** @var int Current part file number */
	private $currentPartNumber = 1;

	/** @var int Total number of part files */
	private $totalParts = 1;

	/**
	 * Class constructor - initializes internal operating parameters
	 *
	 * @return  void
	 */
	public function __construct()
	{
		Factory::getLog()->debug(__CLASS__ . " :: New instance");

		// Find the optimal chunk size for ZIP archive processing
		$this->findOptimalChunkSize();

		Factory::getLog()->debug("Chunk size for CRC is now " . $this->AkeebaPackerZIP_CHUNK_SIZE . " bytes");

		// Should I use Symlink Target Storage?
		$this->enableSymlinkTargetStorage();

		parent::__construct();

		if (!function_exists('hash_file') || !function_exists('hash'))
		{
			$action = version_compare(PHP_VERSION, '7.4.0', 'lt')
				? 'Please ask your host to enable the PHP hash extension and to make sure the hash_file() and hash() functions are not disabled.'
				: 'Please ask your host to change their PHP configuration so that the hash_file() and hash() functions are not disabled.';

			Factory::getLog()->warning(
				sprintf(
					'Your server lacks support for the hash_file() and/or hash() functions. CRC32 checksum cannot be calculated. Third party ZIP extraction tools may report the backup archive as broken. %s',
					$action
				)
			);
		}
	}

	/**
	 * Initialises the archiver class, creating the archive from an existent
	 * installer's JPA archive.
	 *
	 * @param   string  $sourceJPAPath      Absolute path to an installer's JPA archive
	 * @param   string  $targetArchivePath  Absolute path to the generated archive
	 * @param   array   $options            A named key array of options (optional). This is currently not supported
	 *
	 * @return void
	 */
	public function initialize($targetArchivePath, $options = [])
	{
		Factory::getLog()->debug(__CLASS__ . " :: initialize - archive $targetArchivePath");

		// Get names of temporary files
		$this->_dataFileName = $targetArchivePath;

		// Should we enable split archive feature?
		$this->enableSplitArchives();

		// Create the Central Directory temporary file
		$this->createCentralDirectoryTempFile();

		// Try to kill the archive if it exists
		$this->createNewBackupArchive();

		// On split archives, include the "Split ZIP" header, for PKZIP 2.50+ compatibility
		if ($this->useSplitArchive)
		{
			$this->openArchiveForOutput();
			$this->fwrite($this->fp, "\x50\x4b\x07\x08");
		}
	}

	public function finalize()
	{
		$this->finalizeZIPFile();
	}

	/**
	 * Glues the Central Directory of the ZIP file to the archive and takes care about the differences between single
	 * and multipart archives.
	 *
	 * Official ZIP file format: http://www.pkware.com/appnote.txt
	 *
	 * @return  void
	 */
	public function finalizeZIPFile()
	{
		// 1. Get size of central directory
		clearstatcache();
		$cdOffset                  = @filesize($this->_dataFileName);
		$this->totalCompressedSize += $cdOffset;
		$cdSize                    = @filesize($this->centralDirectoryFilename);

		// 2. Append Central Directory to data file and remove the CD temp file afterwards
		if (!is_null($this->fp))
		{
			$this->fclose($this->fp);
		}

		if (!is_null($this->cdfp))
		{
			$this->fclose($this->cdfp);
		}

		$this->openArchiveForOutput(true);

		/**
		 * Do not remove the fcloseByName line! This is required for post-processing multipart archives when for any
		 * reason $this->filePointers[$this->centralDirectoryFilename] contains null instead of boolean false. In this
		 * case the while loop would be stuck forever and the backup would fail. This HAS happened and I have been able
		 * to reproduce it but I did not have enough time to identify the real root cause. This workaround, however,
		 * works.
		 */
		$this->fcloseByName($this->centralDirectoryFilename);
		$this->cdfp = $this->fopen($this->centralDirectoryFilename, "r");

		if ($this->cdfp === false)
		{
			// Already glued, return
			$this->fclose($this->fp);
			$this->fp   = null;
			$this->cdfp = null;

			return;
		}

		// Comment length (I need it before I start gluing the archive)
		$comment_length = akstrlen($this->_comment);

		// Special consideration for split ZIP files
		if ($this->useSplitArchive)
		{
			// Calculate size of Central Directory + EOCD records
			$total_cd_eocd_size = $cdSize + 22 + $comment_length;

			// Free space on the part
			$free_space = $this->getPartFreeSize();

			if (($free_space < $total_cd_eocd_size) && ($total_cd_eocd_size > 65536))
			{
				// Not enough space on archive for CD + EOCD, will go on separate part
				$this->createAndOpenNewPart(true);
			}
		}

		/**
		 * Write the CD record
		 *
		 * Note about is_resource: in some circumstances where multipart ZIP files are generated, the $this->cdfp will
		 * contain a null value. This seems to happen when $this->fopen returns null, i.e. $this->filePointers has a
		 * null value instead of a file pointer (resource). Why this happens is unclear but the workaround is to remove
		 * the null value from $this->filePointers and retry $this->fopen. Normally this should not be required since we
		 * already to the fcloseByName/fopen dance above. This if-block is our last hope to catch a potential issue
		 * which would either make the while loop go infinite (not anymore, I've patched it) or the Central Directory
		 * not get written to the archive, which results in a broken archive.
		 */
		if (!is_resource($this->cdfp))
		{
			$this->fcloseByName($this->centralDirectoryFilename);
			$this->cdfp = $this->fopen($this->centralDirectoryFilename, "r");

			// We tried reopening the central directory file and failed again. Time to report a fatal error.
			if (!$this->cdfp)
			{
				throw new RuntimeException("Cannot open central directory temporary file {$this->centralDirectoryFilename} for reading.");
			}
		}

		while (!feof($this->cdfp) && is_resource($this->cdfp))
		{
			/**
			 * Why not split the Central Directory between parts?
			 *
			 * APPNOTE.TXT §8.5.2 "The central directory may span segment boundaries, but no single record in the
			 * central directory should be split across segments."
			 *
			 * This would require parsing the CD temp file to prevent any CD record from spanning across two parts.
			 * But how many bytes is each CD record? It's about 100 bytes per file which gives us about 10,400 files
			 * per MB. Even a 2MB part size holds more than 20,000 file records. A typical 10Mb part size holds more
			 * files than the largest backup I've ever seen. Therefore there is no need to waste computational power
			 * to see if we need to span the Central Directory between parts.
			 */
			$chunk = fread($this->cdfp, _AKEEBA_DIRECTORY_READ_CHUNK);
			$this->fwrite($this->fp, $chunk);
		}

		unset($chunk);

		// Delete the temporary CD file
		$this->fclose($this->cdfp);
		$this->cdfp = null;
		Factory::getTempFiles()->unregisterAndDeleteTempFile($this->centralDirectoryFilename);

		// 3. Write the rest of headers to the end of the ZIP file
		$this->fwrite($this->fp, $this->centralDirectoryRecordEndSignature);

		if ($this->useSplitArchive)
		{
			// Split ZIP files, enter relevant disk number information
			$this->fwrite($this->fp, pack('v', $this->totalParts - 1)); /* Number of this disk. */
			$this->fwrite($this->fp, pack('v', $this->totalParts - 1)); /* Disk with central directory start. */
		}
		else
		{
			// Non-split ZIP files, the disk number MUST be 0
			$this->fwrite($this->fp, pack('V', 0));
		}

		$this->fwrite($this->fp, pack('v', $this->totalFilesCount)); /* Total # of entries "on this disk". */
		$this->fwrite($this->fp, pack('v', $this->totalFilesCount)); /* Total # of entries overall. */
		$this->fwrite($this->fp, pack('V', $cdSize)); /* Size of central directory. */
		$this->fwrite($this->fp, pack('V', $cdOffset)); /* Offset to start of central dir. */

		// 2.0.b2 -- Write a ZIP file comment
		$this->fwrite($this->fp, pack('v', $comment_length)); /* ZIP file comment length. */
		$this->fwrite($this->fp, $this->_comment);
		$this->fclose($this->fp);

		// If Split ZIP and there is no .zip file, rename the last fragment to .ZIP
		if ($this->useSplitArchive)
		{
			$extension = substr($this->_dataFileName, -3);

			if ($extension != '.zip')
			{
				Factory::getLog()->debug('Renaming last ZIP part to .ZIP extension');

				$newName = $this->dataFileNameWithoutExtension . '.zip';

				if (!@rename($this->_dataFileName, $newName))
				{
					throw new RuntimeException('Could not rename last ZIP part to .ZIP extension.');
				}

				$this->_dataFileName = $newName;
			}

			// If Split ZIP and only one fragment, change the signature
			if ($this->totalParts == 1)
			{
				$this->fp = $this->fopen($this->_dataFileName, 'r+');
				$this->fwrite($this->fp, "\x50\x4b\x30\x30");
			}
		}

		@chmod($this->_dataFileName, $this->getPermissions());
	}

	/**
	 * Returns a string with the extension (including the dot) of the files produced
	 * by this class.
	 *
	 * @return string
	 */
	public function getExtension()
	{
		return '.zip';
	}

	/**
	 * Extend the bootstrap code to add some define's used by the ZIP format engine
	 *
	 * @return  void
	 */
	protected function __bootstrap_code()
	{
		if (!defined('_AKEEBA_COMPRESSION_THRESHOLD'))
		{
			$config = Factory::getConfiguration();
			define("_AKEEBA_COMPRESSION_THRESHOLD", $config->get('engine.archiver.common.big_file_threshold')); // Don't compress files over this size
			define("_AKEEBA_DIRECTORY_READ_CHUNK", $config->get('engine.archiver.zip.cd_glue_chunk_size')); // How much data to read at once when finalizing ZIP archives
		}

		parent::__bootstrap_code();
	}

	/**
	 * The most basic file transaction: add a single entry (file or directory) to
	 * the archive.
	 *
	 * @param   bool    $isVirtual         If true, the next parameter contains file data instead of a file name
	 * @param   string  $sourceNameOrData  Absolute file name to read data from or the file data itself is $isVirtual is
	 *                                     true
	 * @param   string  $targetName        The (relative) file name under which to store the file in the archive
	 *
	 * @return bool True on success, false otherwise
	 */
	protected function _addFile($isVirtual, &$sourceNameOrData, $targetName)
	{
		$configuration = Factory::getConfiguration();

		// Note down the starting disk number for Split ZIP archives
		$starting_disk_number_for_this_file = 0;

		if ($this->useSplitArchive)
		{
			$starting_disk_number_for_this_file = $this->currentPartNumber - 1;
		}

		// Open data file for output
		$this->openArchiveForOutput();

		// Should I continue backing up a file from the previous step?
		$continueProcessingFile = $configuration->get('volatile.engine.archiver.processingfile', false);

		// Initialize with the default values. Why are *these* values default? If we are continuing file packing, by
		// definition we have an uncompressed, non-virtual file. Hence the default values.
		$isDir             = false;
		$isSymlink         = false;
		$compressionMethod = 1;
		$zdata             = null;
		// If we are continuing file packing we have an uncompressed, non-virtual file.
		$isVirtual = $continueProcessingFile ? false : $isVirtual;
		$resume    = $continueProcessingFile ? 0 : null;

		if (!$continueProcessingFile)
		{
			// Log the file being added
			$messageSource = $isVirtual ? '(virtual data)' : "(source: $sourceNameOrData)";
			Factory::getLog()->debug("-- Adding $targetName to archive $messageSource");

			$this->writeFileHeader($sourceNameOrData, $targetName, $isVirtual, $isSymlink, $isDir,
				$compressionMethod, $zdata, $unc_len,
				$storedName, $crc, $c_len, $hexdtime, $old_offset);
		}
		else
		{
			// Since we are continuing archiving, it's an uncompressed regular file. Set up the variables.
			$sourceNameOrData = $configuration->get('volatile.engine.archiver.sourceNameOrData', '');
			$resume           = $configuration->get('volatile.engine.archiver.resume', 0);
			$unc_len          = $configuration->get('volatile.engine.archiver.unc_len');
			$storedName       = $configuration->get('volatile.engine.archiver.storedName');
			$crc              = $configuration->get('volatile.engine.archiver.crc');
			$c_len            = $configuration->get('volatile.engine.archiver.c_len');
			$hexdtime         = $configuration->get('volatile.engine.archiver.hexdtime');
			$old_offset       = $configuration->get('volatile.engine.archiver.old_offset');

			// Log the file we continue packing
			Factory::getLog()->debug("-- Resuming adding file $sourceNameOrData to archive from position $resume (total size $unc_len)");
		}

		/* "File data" segment. */
		if ($compressionMethod == 8)
		{
			$this->putRawDataIntoArchive($zdata);
		}
		elseif ($isVirtual)
		{
			// Virtual data. Put into the archive.
			$this->putRawDataIntoArchive($sourceNameOrData);
		}
		elseif ($isSymlink)
		{
			$this->fwrite($this->fp, @readlink($sourceNameOrData));
		}
		elseif ((!$isDir) && (!$isSymlink))
		{
			// Uncompressed file.
			if ($this->putUncompressedFileIntoArchive($sourceNameOrData, $unc_len, $resume) === true)
			{
				// If it returns true we are doing a step break to resume packing in the next step. So we need to return
				// true here to avoid running the final bit of code which writes the central directory record and
				// uncaches the file resume data.
				return true;
			}
		}

		// Open the central directory file for append
		if (is_null($this->cdfp))
		{
			$this->cdfp = @$this->fopen($this->centralDirectoryFilename, "a");
		}

		if ($this->cdfp === false)
		{
			throw new ErrorException("Could not open Central Directory temporary file for append!");
		}

		$this->fwrite($this->cdfp, $this->centralDirectoryRecordStartSignature);

		if (!$isSymlink)
		{
			$this->fwrite($this->cdfp, "\x14\x00"); /* Version made by (always set to 2.0). */
			$this->fwrite($this->cdfp, "\x14\x00"); /* Version needed to extract */
			$this->fwrite($this->cdfp, pack('v', 2048)); /* General purpose bit flag */
			$this->fwrite($this->cdfp, ($compressionMethod == 8) ? "\x08\x00" : "\x00\x00"); /* Compression method. */
		}
		else
		{
			// Symlinks get special treatment
			$this->fwrite($this->cdfp, "\x14\x03"); /* Version made by (version 2.0 with UNIX extensions). */
			$this->fwrite($this->cdfp, "\x0a\x03"); /* Version needed to extract */
			$this->fwrite($this->cdfp, pack('v', 2048)); /* General purpose bit flag */
			$this->fwrite($this->cdfp, "\x00\x00"); /* Compression method. */
		}

		$this->fwrite($this->cdfp, $hexdtime); /* Last mod time/date. */
		$this->fwrite($this->cdfp, $crc); /* CRC 32 information. */
		$this->fwrite($this->cdfp, pack('V', $c_len)); /* Compressed filesize. */

		if ($compressionMethod == 0)
		{
			// When we are not compressing, $unc_len is being reduced to 0 while backing up.
			// With this trick, we always store the correct length, as in this case the compressed
			// and uncompressed length is always the same.
			$this->fwrite($this->cdfp, pack('V', $c_len)); /* Uncompressed filesize. */
		}
		else
		{
			// When compressing, the uncompressed length differs from compressed length
			// and this line writes the correct value.
			$this->fwrite($this->cdfp, pack('V', $unc_len)); /* Uncompressed filesize. */
		}

		$fn_length = akstrlen($storedName);
		$this->fwrite($this->cdfp, pack('v', $fn_length)); /* Length of filename. */
		$this->fwrite($this->cdfp, pack('v', 0)); /* Extra field length. */
		$this->fwrite($this->cdfp, pack('v', 0)); /* File comment length. */
		$this->fwrite($this->cdfp, pack('v', $starting_disk_number_for_this_file)); /* Disk number start. */
		$this->fwrite($this->cdfp, pack('v', 0)); /* Internal file attributes. */

		/* External file attributes */
		if (!$isSymlink)
		{
			// Archive bit set
			$this->fwrite($this->cdfp, pack('V', $isDir ? 0x41FF0010 : 0xFE49FFE0));
		}
		else
		{
			// For SymLinks we store UNIX file attributes
			$this->fwrite($this->cdfp, "\x20\x80\xFF\xA1");
		}

		$this->fwrite($this->cdfp, pack('V', $old_offset)); /* Relative offset of local header. */
		$this->fwrite($this->cdfp, $storedName); /* File name. */

		/* Optional extra field, file comment goes here. */

		// Finally, increase the file counter by one
		$this->totalFilesCount++;

		// Uncache data
		$configuration->set('volatile.engine.archiver.sourceNameOrData', null);
		$configuration->set('volatile.engine.archiver.unc_len', null);
		$configuration->set('volatile.engine.archiver.resume', null);
		$configuration->set('volatile.engine.archiver.hexdtime', null);
		$configuration->set('volatile.engine.archiver.crc', null);
		$configuration->set('volatile.engine.archiver.c_len', null);
		$configuration->set('volatile.engine.archiver.fn_length', null);
		$configuration->set('volatile.engine.archiver.old_offset', null);
		$configuration->set('volatile.engine.archiver.storedName', null);
		$configuration->set('volatile.engine.archiver.sourceNameOrData', null);

		$configuration->set('volatile.engine.archiver.processingfile', false);

		// ... and return TRUE = success
		return true;
	}

	/**
	 * Write the file header before putting the file data into the archive
	 *
	 * @param   string  $sourceNameOrData   The path to the file being compressed, or the raw file data for virtual files
	 * @param   string  $targetName         The target path to be stored inside the archive
	 * @param   bool    $isVirtual          Is this a virtual file?
	 * @param   bool    $isSymlink          Is this a symlink?
	 * @param   bool    $isDir              Is this a directory?
	 * @param   int     $compressionMethod  The compression method chosen for this file
	 * @param   string  $zdata              If we have compression method other than 0 this holds the compressed data.
	 *                                      We return that from this method to avoid having to compress the same data
	 *                                      twice (once to write the compressed data length in the header and once to
	 *                                      write the compressed data to the archive).
	 * @param   int     $unc_len            The uncompressed size of the file / source data
	 *
	 * @param   string  $storedName         The file path stored in the archive
	 * @param   string  $crc                CRC-32 for the file
	 * @param   int     $c_len              Compressed data length
	 * @param   string  $hexdtime           ZIP's hexadecimal notation if the file's modification date
	 * @param   int     $old_offset         Offset of the file header in the part file
	 */
	protected function writeFileHeader(&$sourceNameOrData, $targetName, &$isVirtual, &$isSymlink, &$isDir,
	                                   &$compressionMethod, &$zdata, &$unc_len, &$storedName, &$crc, &$c_len,
	                                   &$hexdtime, &$old_offset)
	{
		static $memLimit = null;

		if (is_null($memLimit))
		{
			$memLimit = $this->getMemoryLimit();
		}

		$configuration = Factory::getConfiguration();

		// See if it's a directory
		$isDir = $isVirtual ? false : is_dir($sourceNameOrData);

		// See if it's a symlink (w/out dereference)
		$isSymlink = false;

		if ($this->storeSymlinkTarget && !$isVirtual)
		{
			$isSymlink = is_link($sourceNameOrData);
		}

		// Get real size before compression
		[$unc_len, $fileModTime] =
			$this->getFileSizeAndModificationTime($sourceNameOrData, $isVirtual, $isSymlink, $isDir);

		// Decide if we will compress
		$compressionMethod = $this->getCompressionMethod($unc_len, $memLimit, $isDir, $isSymlink);

		if ($isVirtual)
		{
			Factory::getLog()->debug('  Virtual add:' . $targetName . ' (' . $unc_len . ') - ' . $compressionMethod);
		}

		/* "Local file header" segment. */

		$crc = $this->getCRCForEntity($sourceNameOrData, $isVirtual, $isDir, $isSymlink);

		$storedName = $targetName;

		if (!$isSymlink && $isDir)
		{
			$storedName .= "/";
			$unc_len    = 0;
		}

		// Test for non-existing or unreadable files
		$this->testIfFileExists($sourceNameOrData, $isVirtual, $isDir, $isSymlink);

		// Default compressed (archived) length = uncompressed length – valid unless we can actually compress the data.
		$c_len = $unc_len;

		// If we have to compress, read the data in memory and compress it
		if ($compressionMethod == 8)
		{
			$this->getZData($sourceNameOrData, $isVirtual, $compressionMethod, $zdata, $unc_len, $c_len);

			// The method modifies $compressionMethod to 0 (uncompressed) or 1 (Deflate) but the ZIP format needs it
			// to be 0 (uncompressed) or 8 (Deflate). So I just multiply by 8.
			$compressionMethod *= 8;
		}

		// Get the hex time.
		$hexdtime = pack('V', $this->unix2DOSTime($fileModTime));

		// If it's a split ZIP file, we've got to make sure that the header can fit in the part
		if ($this->useSplitArchive)
		{
			// Get header size, taking into account any extra header necessary
			$header_size = 30 + akstrlen($storedName);

			// Compare to free part space
			$free_space = $this->getPartFreeSize();

			if ($free_space <= $header_size)
			{
				// Not enough space on current part, create new part
				$this->createAndOpenNewPart();
			}
		}

		$old_offset = @ftell($this->fp);

		if ($this->useSplitArchive && ($old_offset == 0))
		{
			// Because in split ZIPs we have the split ZIP marker in the first four bytes.
			@fseek($this->fp, 4);
			$old_offset = @ftell($this->fp);
		}

		// Get the file name length in bytes
		$fn_length = akstrlen($storedName);

		$this->fwrite($this->fp, $this->fileHeaderSignature); /* Begin creating the ZIP data. */

		/* Version needed to extract. */
		if (!$isSymlink)
		{
			$this->fwrite($this->fp, "\x14\x00");
		}
		else
		{
			$this->fwrite($this->fp, "\x0a\x03");
		}

		$this->fwrite($this->fp, pack('v', 2048)); /* General purpose bit flag. Bit 11 set = use UTF-8 encoding for filenames & comments */
		$this->fwrite($this->fp, ($compressionMethod == 8) ? "\x08\x00" : "\x00\x00"); /* Compression method. */
		$this->fwrite($this->fp, $hexdtime); /* Last modification time/date. */
		$this->fwrite($this->fp, $crc); /* CRC 32 information. */
		$this->fwrite($this->fp, pack('V', $c_len)); /* Compressed filesize. */
		$this->fwrite($this->fp, pack('V', $unc_len)); /* Uncompressed filesize. */
		$this->fwrite($this->fp, pack('v', $fn_length)); /* Length of filename. */
		$this->fwrite($this->fp, pack('v', 0)); /* Extra field length. */
		$this->fwrite($this->fp, $storedName); /* File name. */

		// Cache useful information about the file
		if (!$isDir && !$isSymlink && !$isVirtual)
		{
			$configuration->set('volatile.engine.archiver.unc_len', $unc_len);
			$configuration->set('volatile.engine.archiver.hexdtime', $hexdtime);
			$configuration->set('volatile.engine.archiver.crc', $crc);
			$configuration->set('volatile.engine.archiver.c_len', $c_len);
			$configuration->set('volatile.engine.archiver.fn_length', $fn_length);
			$configuration->set('volatile.engine.archiver.old_offset', $old_offset);
			$configuration->set('volatile.engine.archiver.storedName', $storedName);
			$configuration->set('volatile.engine.archiver.sourceNameOrData', $sourceNameOrData);
		}
	}

	/**
	 * Get the preferred compression method for a file
	 *
	 * @param   int   $fileSize   File size in bytes
	 * @param   int   $memLimit   Memory limit in bytes
	 * @param   bool  $isDir      Is it a directory?
	 * @param   bool  $isSymlink  Is it a symlink?
	 *
	 * @return  int  Compression method to use
	 */
	protected function getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink)
	{
		// ZIP uses 0 for uncompressed and 8 for GZip Deflate whereas the parent method returns 0 and 1 respectively
		return 8 * parent::getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink);
	}

	/**
	 * Calculate the CRC-32 checksum
	 *
	 * @param   string  $sourceNameOrData  The path to the file being compressed, or the raw file data for virtual files
	 * @param   bool    $isVirtual         Is this a virtual file?
	 * @param   bool    $isSymlink         Is this a symlink?
	 * @param   bool    $isDir             Is this a directory?
	 *
	 * @return  int  The CRC-32
	 */
	protected function getCRCForEntity(&$sourceNameOrData, &$isVirtual, &$isDir, &$isSymlink)
	{
		// No hash? No CRC32!
		if (!function_exists('hash'))
		{
			return pack('V', 0);
		}

		// Do I need to reverse the endianness of CRC32b data?
		static $reverseEndianness = null;

		if ($reverseEndianness === null)
		{
			$reverseEndianness = hash('crc32b', 'The quick brown fox jumped over the lazy dog.', true) === pack('N', 2191738434);
		}

		$entityType = 'file';
		$loggedName = null;

		// Directories: dummy CRC-32
		if (!$isSymlink && $isDir)
		{
			$entityType = 'folder';
			$crc        = pack('V', 0);
		}
		// Symlinks: CRC32 of the link source
		elseif ($isSymlink)
		{
			$entityType = 'symlink';
			$crc        = hash('crc32b', @readlink($sourceNameOrData) ?: '', true);
		}
		// Virtual files: CRC32 of the contents
		elseif ($isVirtual)
		{
			$entityType = 'virtual file';
			$loggedName = sprintf('of size %u', strlen($sourceNameOrData));
			$crc        = hash('crc32b', $sourceNameOrData ?: '', true);
		}
		// Files: CRC32 of the file contents
		else
		{
			// Get the CRC32 for the file
			$crc = function_exists("hash_file") ? @hash_file('crc32b', $sourceNameOrData, true) : null;

			// If the file was unreadable skip it
			if ($crc === false)
			{
				throw new WarningException('Could not calculate CRC32 for ' . $sourceNameOrData . '. Looks like it is an unreadable file.');
			}

			// If hash_file is not available use a fake CRC32
			$crc = $crc ?: pack('V', 0);
		}

		/**
		 * If CRC32 returns Big Endian data I'll have to convert it to Little Endian, as required by ZIP.
		 *
		 * I cannot unpack as Big Endian and repack as Little Endian. The intermediate conversion to integer might
		 * overflow 32-bit versions of PHP as its internal integer type is platform-dependent.
		 *
		 * I cannot reverse the string using string functions because the internal character encoding may be something
		 * other than ASCII / 8-bit and mb_string might not be available.
		 *
		 * Instead, I am unpacking the binary string as an array of unsigned bytes and then repacking the bytes, in
		 * reverse order, into a binary string. pack() and unpack() are not affected by the character encoding. Using
		 * unsigned bytes guarantees they will fit into internal integer variables regardless of the platform used.
		 */
		if ($crc !== "\000\000\000\000" && $reverseEndianness)
		{
			$temp = array_values(unpack('C4', $crc));
			$crc  = pack('C*', $temp[3], $temp[2], $temp[1], $temp[0]);
		}

		// Log the calculated CRC32 if the site is in debug mode
		if (defined('AKEEBADEBUG'))
		{
			$asHexChars = array_map(
				function ($c) {
					return dechex($c);
				}, unpack('C*', $crc)
			);

			Factory::getLog()->debug(
				sprintf(
					'%s %s - CRC32 = %s',
					$entityType,
					$loggedName ?? $sourceNameOrData,
					implode('', $reverseEndianness ? array_reverse($asHexChars) : $asHexChars)
				)
			);
		}

		return $crc;
	}

	/**
	 * Converts a UNIX timestamp to a 4-byte DOS date and time format
	 * (date in high 2-bytes, time in low 2-bytes allowing magnitude
	 * comparison).
	 *
	 * @param   integer  $unixtime  The current UNIX timestamp.
	 *
	 * @return integer  The current date in a 4-byte DOS format.
	 */
	protected function unix2DOSTime($unixtime = null)
	{
		$timearray = (is_null($unixtime)) ? getdate() : getdate($unixtime);

		if ($timearray['year'] < 1980)
		{
			$timearray['year']    = 1980;
			$timearray['mon']     = 1;
			$timearray['mday']    = 1;
			$timearray['hours']   = 0;
			$timearray['minutes'] = 0;
			$timearray['seconds'] = 0;
		}

		return (($timearray['year'] - 1980) << 25) |
			($timearray['mon'] << 21) |
			($timearray['mday'] << 16) |
			($timearray['hours'] << 11) |
			($timearray['minutes'] << 5) |
			($timearray['seconds'] >> 1);
	}

	/**
	 * Creates a new part for the spanned archive
	 *
	 * @param   bool  $finalPart  Is this the final archive part?
	 *
	 * @return  bool  True on success
	 */
	protected function createNewPartFile($finalPart = false)
	{
		// Close any open file pointers
		if (is_resource($this->fp))
		{
			$this->fclose($this->fp);
		}

		if (is_resource($this->cdfp))
		{
			$this->fclose($this->cdfp);
		}

		// Remove the just finished part from the list of resumable offsets
		$this->removeFromOffsetsList($this->_dataFileName);

		// Set the file pointers to null
		$this->fp   = null;
		$this->cdfp = null;

		// Push the previous part if we have to post-process it immediately
		$configuration = Factory::getConfiguration();

		if ($configuration->get('engine.postproc.common.after_part', 0))
		{
			$this->finishedPart[] = $this->_dataFileName;
		}

		// Add the part's size to our rolling sum
		clearstatcache();
		$this->totalCompressedSize += filesize($this->_dataFileName);
		$this->totalParts++;
		$this->currentPartNumber = $this->totalParts;

		if ($finalPart)
		{
			$this->_dataFileName = $this->dataFileNameWithoutExtension . '.zip';
		}
		else
		{
			$this->_dataFileName = $this->dataFileNameWithoutExtension . '.z' . sprintf('%02d', $this->currentPartNumber);
		}

		Factory::getLog()->info('Creating new ZIP part #' . $this->currentPartNumber . ', file ' . $this->_dataFileName);

		// Inform the backup engine that we have changed the multipart number
		$statistics = Factory::getStatistics();
		$statistics->updateMultipart($this->totalParts);

		// Try to remove any existing file
		@unlink($this->_dataFileName);

		// Touch the new file
		$result = @touch($this->_dataFileName);

		@chmod($this->_dataFileName, $this->getPermissions());

		return $result;
	}

	/**
	 * Find the optimal chunk size for CRC32 calculations and file processing
	 *
	 * @return  void
	 */
	private function findOptimalChunkSize()
	{
		$configuration = Factory::getConfiguration();

		// The user has entered their own preference
		if ($configuration->get('engine.archiver.common.chunk_size', 0) > 0)
		{
			$this->AkeebaPackerZIP_CHUNK_SIZE = AKEEBA_CHUNK;

			return;
		}

		// Get the PHP memory limit
		$memLimit = $this->getMemoryLimit();

		// Can't get a PHP memory limit? Use 2Mb chunks (fairly large, right?)
		if (is_null($memLimit))
		{
			$this->AkeebaPackerZIP_CHUNK_SIZE = 2097152;

			return;
		}

		if (!function_exists("memory_get_usage"))
		{
			// PHP can't report memory usage, use a conservative 512Kb
			$this->AkeebaPackerZIP_CHUNK_SIZE = 524288;

			return;
		}

		// PHP *can* report memory usage, see if there's enough available memory
		$availableRAM = $memLimit - memory_get_usage();

		if ($availableRAM > 0)
		{
			$this->AkeebaPackerZIP_CHUNK_SIZE = $availableRAM * 0.5;

			return;
		}

		// NEGATIVE AVAILABLE MEMORY?!! Some borked PHP implementations also return the size of the httpd footprint.
		if (($memLimit - 6291456) > 0)
		{
			$this->AkeebaPackerZIP_CHUNK_SIZE = $memLimit - 6291456;

			return;
		}

		// If all else fails, use 2Mb and cross your fingers
		$this->AkeebaPackerZIP_CHUNK_SIZE = 2097152;
	}

	/**
	 * Create a Central Directory temporary file
	 *
	 * @return  void
	 *
	 * @throws  ErrorException
	 */
	private function createCentralDirectoryTempFile()
	{
		$configuration                  = Factory::getConfiguration();
		$this->centralDirectoryFilename = tempnam($configuration->get('akeeba.basic.output_directory'), 'akzcd');
		$this->centralDirectoryFilename = basename($this->centralDirectoryFilename);
		$pos                            = strrpos($this->centralDirectoryFilename, '/');

		if ($pos !== false)
		{
			$this->centralDirectoryFilename = substr($this->centralDirectoryFilename, $pos + 1);
		}

		$pos = strrpos($this->centralDirectoryFilename, '\\');

		if ($pos !== false)
		{
			$this->centralDirectoryFilename = substr($this->centralDirectoryFilename, $pos + 1);
		}

		$this->centralDirectoryFilename = Factory::getTempFiles()->registerTempFile($this->centralDirectoryFilename);

		Factory::getLog()->debug(__CLASS__ . " :: CntDir Tempfile = " . $this->centralDirectoryFilename);

		// Create temporary file
		if (!@touch($this->centralDirectoryFilename))
		{
			throw new ErrorException("Could not open temporary file for ZIP archiver. Please check your temporary directory's permissions!");
		}

		@chmod($this->centralDirectoryFilename, $this->getPermissions());
	}
}
Archiver/Jpa.php000060400000045716152456704520007555 0ustar00<?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\Archiver;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Factory;
use RuntimeException;

/**
 * JPA creation class
 *
 * JPA Format 1.2 implemented, minus BZip2 compression support
 */
class Jpa extends BaseArchiver
{
	/** @var string Standard Header signature */
	private const ARCHIVE_SIGNATURE = "\x4A\x50\x41";

	/** @var string Entity Block signature */
	private const FILE_HEADER_SIGNATURE = "\x4A\x50\x46";

	/** @var string Marks the split archive's extra header */
	private const SPLIT_ARCHIVE_EXTRA_HEADER = "\x4A\x50\x01\x01";

	/** @var string Marks the archive's 64-bit integer representations of file sizes (JPA v1.3) */
	private const ARCHIVE_LONGLONG_SIZES_EXTRA_HEADER = "\x4A\x50\x01\x02";

	/** @var integer How many files are contained in the archive */
	private $totalFilesCount = 0;

	/** @var integer The total size of files contained in the archive as they are stored */
	private $totalCompressedSize = 0;

	/** @var integer The total size of files contained in the archive when they are extracted to disk. */
	private $totalUncompressedSize = 0;

	/** @var int Current part file number */
	private $currentPartNumber = 1;

	/** @var int Total number of part files */
	private $totalParts = 1;

	/**
	 * Initialises the archiver class, creating the archive from an existent
	 * installer's JPA archive.
	 *
	 * @param   string  $targetArchivePath  Absolute path to the generated archive
	 * @param   array   $options            A named key array of options (optional)
	 *
	 * @return  void
	 */
	public function initialize($targetArchivePath, $options = [])
	{
		Factory::getLog()->debug(__CLASS__ . " :: new instance - archive $targetArchivePath");
		$this->_dataFileName = $targetArchivePath;

		// Should we enable Split ZIP feature?
		$this->enableSplitArchives();

		// Should I use Symlink Target Storage?
		$this->enableSymlinkTargetStorage();

		// Try to kill the archive if it exists
		$this->createNewBackupArchive();

		// Write the initial instance of the archive header
		$this->_writeArchiveHeader();
	}

	/**
	 * Updates the Standard Header with current information
	 *
	 * @return  void
	 */
	public function finalize()
	{
		if (is_resource($this->fp))
		{
			$this->fclose($this->fp);
		}

		if (is_resource($this->cdfp))
		{
			$this->fclose($this->cdfp);
		}

		$this->_closeAllFiles();

		// If Spanned JPA and there is no .jpa file, rename the last fragment to .jpa
		if ($this->useSplitArchive)
		{
			$extension = substr($this->_dataFileName, -4);

			if ($extension != '.jpa')
			{
				Factory::getLog()->debug('Renaming last JPA part to .JPA extension');

				$newName = $this->dataFileNameWithoutExtension . '.jpa';

				if (!@rename($this->_dataFileName, $newName))
				{
					throw new RuntimeException('Could not rename last JPA part to .JPA extension.');
				}

				$this->_dataFileName = $newName;
			}

			// Finally, point to the first part so that we can re-write the correct header information
			if ($this->totalParts > 1)
			{
				$this->_dataFileName = $this->dataFileNameWithoutExtension . '.j01';
			}
		}

		// Re-write the archive header
		$this->_writeArchiveHeader();
	}

	/**
	 * Returns a string with the extension (including the dot) of the files produced
	 * by this class.
	 *
	 * @return string
	 */
	public function getExtension()
	{
		return '.jpa';
	}

	/**
	 * Outputs a Standard Header at the top of the file
	 *
	 * @return  void
	 */
	protected function _writeArchiveHeader()
	{
		if (!is_null($this->fp))
		{
			$this->fclose($this->fp);
			$this->fp = null;
		}

		$this->fp = $this->fopen($this->_dataFileName, 'c');

		if ($this->fp === false)
		{
			throw new ErrorException('Could not open ' . $this->_dataFileName . ' for writing. Check permissions and open_basedir restrictions.');
		}

		// Calculate total header size
		$headerSize = 19; // Standard Header

		if ($this->useSplitArchive)
		{
			// Spanned JPA header
			$headerSize += 8;
		}

		$is64Bit = $this->is64Bit();

		if ($is64Bit)
		{
			// Version 1.3 long long sizes
			$headerSize += 22;

		}

		// Write the archive header
		$this->fwrite($this->fp, self::ARCHIVE_SIGNATURE); // ID string (JPA)
		$this->fwrite($this->fp, pack('v', $headerSize)); // Header length; fixed to 19 bytes
		$this->fwrite($this->fp, pack('C', _JPA_MAJOR)); // Major version
		$this->fwrite($this->fp, pack('C', _JPA_MINOR)); // Minor version
		$this->fwrite($this->fp, pack('V', $this->totalFilesCount)); // File count
		$this->fwrite($this->fp, pack('V', $this->totalUncompressedSize)); // Size of files when extracted
		$this->fwrite($this->fp, pack('V', $this->totalCompressedSize)); // Size of files when stored

		// Do I need to add a split archive's header too?
		if ($this->useSplitArchive)
		{
			$this->fwrite($this->fp, self::SPLIT_ARCHIVE_EXTRA_HEADER); // Signature
			$this->fwrite($this->fp, pack('v', 4)); // Extra field length
			$this->fwrite($this->fp, pack('v', $this->totalParts)); // Number of parts
		}

		if ($is64Bit)
		{
			// Version 1.3
			$this->fwrite($this->fp, self::ARCHIVE_LONGLONG_SIZES_EXTRA_HEADER); // Signature
			$this->fwrite($this->fp, pack('v', 18)); // Extra field length
			$this->fwrite($this->fp, pack('P', $this->totalUncompressedSize)); // Size of files when extracted
			$this->fwrite($this->fp, pack('P', $this->totalCompressedSize)); // Size of files when stored
		}

		$this->fclose($this->fp);

		@chmod($this->_dataFileName, $this->getPermissions());
	}

	/**
	 * Extend the bootstrap code to add some define's used by the JPA format engine
	 *
	 * @codeCoverageIgnore
	 *
	 * @return  void
	 */
	protected function __bootstrap_code()
	{
		if (!defined('_AKEEBA_COMPRESSION_THRESHOLD'))
		{
			$config = Factory::getConfiguration();
			define("_AKEEBA_COMPRESSION_THRESHOLD", $config->get('engine.archiver.common.big_file_threshold')); // Don't compress files over this size

			/**
			 * Akeeba Backup and JPA Format version change chart:
			 * Akeeba Backup 3.0: JPA Format 1.1
			 * Akeeba Backup 3.1: JPA Format 1.2 with file modification timestamp is used
			 * Akeeba Backup for Joomla 8.3/9.6, Solo/Akeeba Backup for WordPress 7.9: JPA Format 1.3
			 */
			define('_JPA_MAJOR', 1); // JPA Format major version number

			if ($this->is64Bit())
			{
				define('_JPA_MINOR', 3); // JPA Format minor version number
			}
			else
			{
				define('_JPA_MINOR', 2); // JPA Format minor version number
			}
		}
		parent::__bootstrap_code();
	}

	/**
	 * The most basic file transaction: add a single entry (file or directory) to
	 * the archive.
	 *
	 * @param   bool    $isVirtual         If true, the next parameter contains file data instead of a file name
	 * @param   string  $sourceNameOrData  Absolute file name to read data from or the file data itself is $isVirtual is
	 *                                     true
	 * @param   string  $targetName        The (relative) file name under which to store the file in the archive
	 *
	 * @return boolean True on success, false otherwise
	 *
	 * @since  1.2.1
	 */
	protected function _addFile($isVirtual, &$sourceNameOrData, $targetName)
	{
		// Get references to engine objects we're going to be using
		$configuration = Factory::getConfiguration();

		// Is this a virtual file?
		$isVirtual = (bool) $isVirtual;

		// Open data file for output
		$this->openArchiveForOutput();

		// Should I continue backing up a file from the previous step?
		$continueProcessingFile = $configuration->get('volatile.engine.archiver.processingfile', false);

		// Initialize with the default values. Why are *these* values default? If we are continuing file packing, by
		// definition we have an uncompressed, non-virtual file. Hence the default values.
		$isDir             = false;
		$isSymlink         = false;
		$compressionMethod = 0;
		$zdata             = null;
		// If we are continuing file packing we have an uncompressed, non-virtual file.
		$isVirtual = $continueProcessingFile ? false : $isVirtual;
		$resume    = $continueProcessingFile ? 0 : null;

		if (!$continueProcessingFile)
		{
			// Log the file being added
			$messageSource = $isVirtual ? '(virtual data)' : "(source: $sourceNameOrData)";
			Factory::getLog()->debug("-- Adding $targetName to archive $messageSource");

			// Write a file header
			$this->writeFileHeader($sourceNameOrData, $targetName, $isVirtual, $isSymlink, $isDir, $compressionMethod, $zdata, $unc_len);
		}
		else
		{
			$sourceNameOrData = $configuration->get('volatile.engine.archiver.sourceNameOrData', '');
			$unc_len          = $configuration->get('volatile.engine.archiver.unc_len', 0);
			$resume           = $configuration->get('volatile.engine.archiver.resume', 0);

			// Log the file we continue packing
			Factory::getLog()->debug("-- Resuming adding file $sourceNameOrData to archive from position $resume (total size $unc_len)");
		}

		/* "File data" segment. */
		if ($compressionMethod == 1)
		{
			// Compressed data. Put into the archive.
			$this->putRawDataIntoArchive($zdata);
		}
		elseif ($isVirtual)
		{
			// Virtual data. Put into the archive.
			$this->putRawDataIntoArchive($sourceNameOrData);
		}
		elseif ($isSymlink)
		{
			// Symlink. Just put the link target into the archive.
			$this->fwrite($this->fp, @readlink($sourceNameOrData));
		}
		elseif ((!$isDir) && (!$isSymlink))
		{
			// Uncompressed file.
			if ($this->putUncompressedFileIntoArchive($sourceNameOrData, $unc_len, $resume) === true)
			{
				// If it returns true we are doing a step break to resume packing in the next step. So we need to return
				// true here to avoid running the final bit of code which uncaches the file resume data.
				return true;
			}
		}

		// Factory::getLog()->debug("DEBUG -- Added $targetName to archive");

		// Uncache data
		$configuration->set('volatile.engine.archiver.sourceNameOrData', null);
		$configuration->set('volatile.engine.archiver.unc_len', null);
		$configuration->set('volatile.engine.archiver.resume', null);
		$configuration->set('volatile.engine.archiver.processingfile', false);

		// ... and return TRUE = success
		return true;
	}

	/**
	 * Write the file header to the backup archive.
	 *
	 * Only the first three parameters are input. All other are ignored for input and are overwritten.
	 *
	 * @param   string  $sourceNameOrData   The path to the file being compressed, or the raw file data for virtual files
	 * @param   string  $targetName         The target path to be stored inside the archive
	 * @param   bool    $isVirtual          Is this a virtual file?
	 * @param   bool    $isSymlink          Is this a symlink?
	 * @param   bool    $isDir              Is this a directory?
	 * @param   int     $compressionMethod  The compression method chosen for this file
	 * @param   string  $zdata              If we have compression method other than 0 this holds the compressed data.
	 *                                      We return that from this method to avoid having to compress the same data
	 *                                      twice (once to write the compressed data length in the header and once to
	 *                                      write the compressed data to the archive).
	 * @param   int     $unc_len            The uncompressed size of the file / source data
	 *
	 * @return  void
	 */
	protected function writeFileHeader(&$sourceNameOrData, &$targetName, &$isVirtual, &$isSymlink, &$isDir, &$compressionMethod, &$zdata, &$unc_len)
	{
		static $memLimit = null;

		if (is_null($memLimit))
		{
			$memLimit = $this->getMemoryLimit();
		}

		$configuration = Factory::getConfiguration();

		// Uncache data -- WHY DO THAT?!
		/**
		 * $configuration->set('volatile.engine.archiver.sourceNameOrData', null);
		 * $configuration->set('volatile.engine.archiver.unc_len', null);
		 * $configuration->set('volatile.engine.archiver.resume', null);
		 * $configuration->set('volatile.engine.archiver.processingfile',false);
		 * /**/

		// See if it's a directory
		$isDir = $isVirtual ? false : is_dir($sourceNameOrData);

		// See if it's a symlink (w/out dereference)
		$isSymlink = false;

		if ($this->storeSymlinkTarget && !$isVirtual)
		{
			$isSymlink = is_link($sourceNameOrData);
		}

		// Get real size before compression
		[$fileSize, $fileModTime] =
			$this->getFileSizeAndModificationTime($sourceNameOrData, $isVirtual, $isSymlink, $isDir);

		// Decide if we will compress
		$compressionMethod = $this->getCompressionMethod($fileSize, $memLimit, $isDir, $isSymlink);

		$storedName = $targetName;

		/* "Entity Description Block" segment. */
		$unc_len    = $fileSize; // File size
		$storedName .= ($isDir) ? "/" : "";

		/**
		 * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
		 * !!!! WARNING!!! DO NOT MOVE THIS BLOCK OF CODE AFTER THE testIfFileExists OR getZData!!!!       !!!!
		 * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
		 *
		 * PHP 5.6.3 IS BROKEN. Possibly the same applies for all old versions of PHP. If you try to get the file
		 * permissions after reading its contents PHP segfaults.
		 */
		// Get file permissions
		$perms = 0644;

		if (!$isVirtual)
		{
			$perms = @fileperms($sourceNameOrData);
		}

		// Test for non-existing or unreadable files
		$this->testIfFileExists($sourceNameOrData, $isVirtual, $isDir, $isSymlink);

		// Default compressed (archived) length = uncompressed length – valid unless we can actually compress the data.
		$c_len = $unc_len;

		if ($compressionMethod == 1)
		{
			$this->getZData($sourceNameOrData, $isVirtual, $compressionMethod, $zdata, $unc_len, $c_len);
		}

		$this->totalCompressedSize   += $c_len; // Update global data
		$this->totalUncompressedSize += $fileSize; // Update global data
		$this->totalFilesCount++;

		// Calculate Entity Description Block length
		$blockLength = 21 + akstrlen($storedName);

		// If we need to store the file mod date
		if ($fileModTime > 0)
		{
			$blockLength += 8;
		}

		$is64Bit = $this->is64Bit();

		if ($is64Bit)
		{
			// We need to account for the Long Long File Sizes Extra Field in JPA v1.3
			$blockLength += 20;
		}

		// Get file type
		$fileType = 1;

		if ($isSymlink)
		{
			$fileType = 2;
		}
		elseif ($isDir)
		{
			$fileType = 0;
		}

		// If it's a split JPA file, we've got to make sure that the header can fit in the part
		if ($this->useSplitArchive)
		{
			// Compare to free part space
			$free_space = $this->getPartFreeSize();

			if ($free_space <= $blockLength)
			{
				// Not enough space on current part, create new part
				$this->createAndOpenNewPart();
			}
		}

		$this->fwrite($this->fp, self::FILE_HEADER_SIGNATURE); // Entity Description Block header
		$this->fwrite($this->fp, pack('v', $blockLength)); // Entity Description Block header length
		$this->fwrite($this->fp, pack('v', akstrlen($storedName))); // Length of entity path
		$this->fwrite($this->fp, $storedName); // Entity path
		$this->fwrite($this->fp, pack('C', $fileType)); // Entity type
		$this->fwrite($this->fp, pack('C', $compressionMethod)); // Compression method
		$this->fwrite($this->fp, pack('V', $c_len)); // Compressed size
		$this->fwrite($this->fp, pack('V', $unc_len)); // Uncompressed size
		$this->fwrite($this->fp, pack('V', $perms)); // Entity permissions

		// Timestamp Extra Field, only for files
		if ($fileModTime > 0)
		{
			$this->fwrite($this->fp, "\x00\x01"); // Extra Field Identifier
			$this->fwrite($this->fp, pack('v', 8)); // Extra Field Length
			$this->fwrite($this->fp, pack('V', $fileModTime)); // Timestamp
		}

		if ($is64Bit)
		{
			// The Long Long File Sizes Extra Field
			$this->fwrite($this->fp, "\x00\x02"); // Extra Field Identifier
			$this->fwrite($this->fp, pack('v', 20)); // Extra Field Length
			$this->fwrite($this->fp, pack('P', $c_len)); // Compressed size
			$this->fwrite($this->fp, pack('P', $unc_len)); // Uncompressed size
		}

		// Cache useful information about the file
		if (!$isDir && !$isSymlink && !$isVirtual)
		{
			$configuration->set('volatile.engine.archiver.unc_len', $unc_len);
			$configuration->set('volatile.engine.archiver.sourceNameOrData', $sourceNameOrData);
		}
	}

	/**
	 * Creates a new part for the spanned archive
	 *
	 * @param   bool  $finalPart  Is this the final archive part?
	 *
	 * @return  bool  True on success
	 */
	protected function createNewPartFile($finalPart = false)
	{
		// Close any open file pointers
		if (!is_resource($this->fp))
		{
			$this->fclose($this->fp);
		}

		if (is_resource($this->cdfp))
		{
			$this->fclose($this->cdfp);
		}

		// Remove the just finished part from the list of resumable offsets
		$this->removeFromOffsetsList($this->_dataFileName);

		// Set the file pointers to null
		$this->fp   = null;
		$this->cdfp = null;

		// Push the previous part if we have to post-process it immediately
		$configuration = Factory::getConfiguration();

		if ($configuration->get('engine.postproc.common.after_part', 0))
		{
			// The first part needs its header overwritten during archive
			// finalization. Skip it from immediate processing.
			if ($this->currentPartNumber != 1)
			{
				$this->finishedPart[] = $this->_dataFileName;
			}
		}

		$this->totalParts++;
		$this->currentPartNumber = $this->totalParts;

		if ($finalPart)
		{
			$this->_dataFileName = $this->dataFileNameWithoutExtension . '.jpa';
		}
		else
		{
			$this->_dataFileName = $this->dataFileNameWithoutExtension . '.j' . sprintf('%02d', $this->currentPartNumber);
		}

		Factory::getLog()->info('Creating new JPA part #' . $this->currentPartNumber . ', file ' . $this->_dataFileName);
		$statistics = Factory::getStatistics();
		$statistics->updateMultipart($this->totalParts);

		// Try to remove any existing file
		@unlink($this->_dataFileName);

		// Touch the new file
		$result = @touch($this->_dataFileName);

		chmod($this->_dataFileName, $this->getPermissions());

		// Try to write 6 bytes to it
		if ($result)
		{
			$result = @file_put_contents($this->_dataFileName, 'AKEEBA') == 6;
		}

		if ($result)
		{
			@unlink($this->_dataFileName);

			$result = @touch($this->_dataFileName);
			@chmod($this->_dataFileName, $this->getPermissions());
		}

		return $result;
	}

	/**
	 * Is this a 64-bit version of PHP?
	 *
	 * @return  bool
	 *
	 * @since   9.6.1
	 * @see     https://www.php.net/manual/en/reserved.constants.php
	 */
	private function is64Bit(): bool
	{
		// 64-bit versions use 8 bytes for the Integer intrinsic type. We use >= for forward compatibility.
		return PHP_INT_SIZE >= 8;
	}
}
Filter/Regexskipdirs.php000060400000002337152456704520011340 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Subdirectories exclusion filter based on regular expressions
 */
class Regexskipdirs extends Base
{
	public function __construct()
	{
		$this->object  = 'dir';
		$this->subtype = 'children';
		$this->method  = 'regex';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Stack/README.html000060400000004266152456704520010677 0ustar00<?xml version="1.0" encoding="UTF-8" ?>
<!--~
  ~ 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/>.
  -->

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>AkeebaBackup :: Filter Stack</title>
</head>
<body>
<h1>What is this directory?</h1>

<p>In this directory, Akeeba Backup and Akeeba Solo store the optional filters (Optional Filters in the Configuration
    page). Unlike regular filters which are always loaded, optional filters are only loaded when the user chooses to
    enable them. Each filter consists of two files: <var>filtername</var>.ini and <var>filtername</var>.php. The former
    contains the filter-specific configuration options and the later contains the actual filter code.</p>

<p>
    If you want to create new optional filters, put them in here. Do note that the INI file must always contain a
    boolean key named core.filters.<var>filtername</var>.enabled which controls the loading of this particular filter.
</p>

<p>
    Optional filters are always named Akeeba\Engine\Filter\Stack\Stack<var>Filtername</var> so that the autoloader can
    find them. For the same reason their filename must be Stack<var>Filtername</var>.php   Please watch out for the
    letter case in the names, it's important.
</p>
</body>
</html>
Filter/Stack/StackDateconditional.php000060400000004131152456704520013643 0ustar00<?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\Filter\Stack;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Filter\Base;

/**
 * Date conditional filter
 *
 * It will only backup files modified after a specific date and time
 */
class StackDateconditional extends Base
{
	public function __construct()
	{
		$this->object  = 'file';
		$this->subtype = 'all';
		$this->method  = 'api';

	}

	protected function is_excluded_by_api($test, $root)
	{
		static $from_datetime;

		$config = Factory::getConfiguration();

		if (is_null($from_datetime))
		{
			$user_setting  = $config->get('core.filters.dateconditional.start');
			$from_datetime = strtotime($user_setting);
		}

		// Get the filesystem path for $root
		$fsroot   = $config->get('volatile.filesystem.current_root', '');
		$ds       = ($fsroot == '') || ($fsroot == '/') ? '' : DIRECTORY_SEPARATOR;
		$filename = $fsroot . $ds . $test;

		// Get the timestamp of the file
		$timestamp = @filemtime($filename);

		// If we could not get this information, include the file in the archive
		if ($timestamp === false)
		{
			return false;
		}

		// Compare it with the user-defined minimum timestamp and exclude if it's older than that
		if ($timestamp <= $from_datetime)
		{
			return true;
		}

		// No match? Just include the file!
		return false;
	}

}
Filter/Stack/StackErrorlogs.php000060400000003022152456704520012516 0ustar00<?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\Filter\Stack;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Filter\Base;

/**
 * Files exclusion filter based on regular expressions
 */
class StackErrorlogs extends Base
{
	function __construct()
	{
		$this->object  = 'file';
		$this->subtype = 'all';
		$this->method  = 'api';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}

	protected function is_excluded_by_api($test, $root)
	{
		// Is it an error log? Exclude the file.
		if (in_array(basename($test), [
			'php_error',
			'php_errorlog',
			'error_log',
			'error.log',
		]))
		{
			return true;
		}

		// No match? Just include the file!
		return false;
	}

}
Filter/Stack/StackHoststats.php000060400000002656152456704520012550 0ustar00<?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\Filter\Stack;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Filter\Base;

/**
 * Exclude folders and files belonging to the host web stat (ie Webalizer)
 */
class StackHoststats extends Base
{
	public function __construct()
	{
		$this->object  = 'dir';
		$this->subtype = 'all';
		$this->method  = 'api';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}

	protected function is_excluded_by_api($test, $root)
	{
		if ($test == 'stats')
		{
			return true;
		}

		// No match? Just include the file!
		return false;
	}

}
Filter/Stack/dateconditional.json000060400000001222152456704520013075 0ustar00{
    "core.filters.dateconditional.enabled": {
        "default": "0",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_DATECONDITIONAL_ENABLED_TITLE",
        "description": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_DATECONDITIONAL_ENABLED_DESCRIPTION",
        "bold": "1"
    },
    "core.filters.dateconditional.start": {
        "default": "1970-01-01 00:00 GMT",
        "type": "string",
        "title": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_DATECONDITIONAL_START_TITLE",
        "description": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_DATECONDITIONAL_START_DESCRIPTION",
        "showon": "core.filters.dateconditional.enabled:1"
    }
}Filter/Stack/hoststats.json000060400000000435152456704520011775 0ustar00{
    "core.filters.hoststats.enabled": {
        "default": "1",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_HOSTSTATS_ENABLED_TITLE",
        "description": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_HOSTSTATS_ENABLED_DESCRIPTION",
        "bold": "1"
    }
}Filter/Stack/errorlogs.json000060400000000435152456704520011757 0ustar00{
    "core.filters.errorlogs.enabled": {
        "default": "1",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_ERRORLOGS_ENABLED_TITLE",
        "description": "COM_AKEEBA_CONFIG_OPTIONALFILTERS_ERRORLOGS_ENABLED_DESCRIPTION",
        "bold": "1"
    }
}Filter/Skipdirs.php000060400000002276152456704520010307 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Subdirectories exclusion filter
 */
class Skipdirs extends Base
{
	public function __construct()
	{
		$this->object  = 'dir';
		$this->subtype = 'children';
		$this->method  = 'direct';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Regexdirectories.php000060400000002330152456704520012015 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Directory exclusion filter based on regular expressions
 */
class Regexdirectories extends Base
{
	public function __construct()
	{
		$this->object  = 'dir';
		$this->subtype = 'all';
		$this->method  = 'regex';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Tables.php000060400000002274152456704520007727 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Database table exclusion filter
 */
class Tables extends Base
{
	public function __construct()
	{
		$this->object  = 'dbobject';
		$this->subtype = 'all';
		$this->method  = 'direct';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Regexskipfiles.php000060400000002353152456704520011477 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Directory contents (files) exclusion filter based on regular expressions
 */
class Regexskipfiles extends Base
{
	public function __construct()
	{
		$this->object  = 'dir';
		$this->subtype = 'content';
		$this->method  = 'regex';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Regextables.php000060400000002300152456704520010750 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Database table exclusion filter
 */
class Regextables extends Base
{
	public function __construct()
	{
		$this->object  = 'dbobject';
		$this->subtype = 'all';
		$this->method  = 'regex';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Regextabledata.php000060400000002520152456704520011423 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Database table records exclusion filter
 *
 * This is simple stuff. If a table's on the list, it will backup just its structure, not
 * its contents. Fair and square...
 */
class Regextabledata extends Base
{
	public function __construct()
	{
		$this->object  = 'dbobject';
		$this->subtype = 'content';
		$this->method  = 'regex';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Base.php000060400000037402152456704520007370 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Util\FileSystem;

abstract class Base
{
	/** @var string Filter's internal name; defaults to filename without .php extension */
	public $filter_name = '';

	/** @var string The filtering object: dir|file|dbobject|db */
	public $object = 'dir';

	/** @var string The filtering subtype (all|content|children|inclusion) */
	public $subtype = null;

	/** @var string The filtering method (direct|regex|api) */
	public $method = 'direct';

	/** @var bool Is the filter active? */
	public $enabled = true;

	/** @var array An array holding filter or regex strings per root, i.e. $filter_data[$root] = array() */
	protected $filter_data = null;

	/** @var FileSystem  Used by treatDirectory */
	protected $fsTools = null;

	/**
	 * Public constructor
	 */
	public function __construct()
	{
		// Set the filter name if it's missing (filename in lowercase, minus the .php extension)
		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}
	}

	/**
	 * Extra SQL statements to append to the SQL dump file. Useful for extension
	 * filters which have to filter out specific database records. This method
	 * must be overridden in children classes.
	 *
	 * @param   string  $root  The database for which to get the extra SQL statements
	 *
	 * @return  array  Extra SQL statements
	 */
	public function getExtraSQL(string $root): array
	{
		return [];
	}

	public $canFilterDatabaseRowContent = false;

	public function filterDatabaseRowContent(string $root, string $tableAbstract, array &$row): void
	{
		// No operation
	}

	/**
	 * Returns filtering (exclusion) status of the $test object
	 *
	 * @param   string  $test     The string to check for filter status (e.g. filename, dir name, table name, etc)
	 * @param   string  $root     The exclusion root test belongs to
	 * @param   string  $object   What type of object is it? dir|file|dbobject
	 * @param   string  $subtype  Filter subtype (all|content|children)
	 *
	 * @return    bool    True if it excluded, false otherwise
	 */
	public function isFiltered($test, $root, $object, $subtype)
	{
		if (!$this->enabled)
		{
			return false;
		}

		//Factory::getLog()->log(LogLevel::DEBUG,"Filtering [$object:$subtype] $root // $test");

		// Inclusion filters do not qualify for exclusion
		if ($this->subtype == 'inclusion')
		{
			return false;
		}

		// The object and subtype must match
		if (($this->object != $object) || ($this->subtype != $subtype))
		{
			return false;
		}

		if (in_array($this->method, ['direct', 'regex']))
		{
			// -- Direct or regex based filters --

			// Get a local reference of the filter data, if necessary
			if (is_null($this->filter_data))
			{
				$filters           = Factory::getFilters();
				$this->filter_data = $filters->getFilterData($this->filter_name);
			}

			// Check if the root exists and if there's a filter for the $test
			if (!array_key_exists($root, $this->filter_data))
			{
				// Root not found
				return false;
			}
			else
			{
				// Root found, search in the array
				if ($this->method == 'direct')
				{
					// Direct filtering
					return in_array($test, $this->filter_data[$root]);
				}
				else
				{
					// Regex matching
					foreach ($this->filter_data[$root] as $regex)
					{
						if (substr($regex, 0, 1) == '!')
						{
							// Custom Akeeba Backup extension to PCRE notation. If you put a ! before the PCRE, it negates the result of the PCRE.
							if (!preg_match(substr($regex, 1), $test))
							{
								return true;
							}
						}
						else
						{
							// Normal PCRE
							if (preg_match($regex, $test))
							{
								return true;
							}
						}
					}

					// if we're here, no match exists
					return false;
				}
			}
		}
		else
		{
			// -- API-based filters --
			return $this->is_excluded_by_api($test, $root);
		}
	}

	/**
	 * Returns the inclusion filters defined by this class for the requested $object
	 *
	 * @param   string  $object  The object to get inclusions for (dir|db)
	 *
	 * @return    array    The inclusion filters
	 */
	public function &getInclusions($object)
	{
		$dummy = [];

		if (!$this->enabled)
		{
			return $dummy;
		}

		if (($this->subtype != 'inclusion') || ($this->object != $object))
		{
			return $dummy;
		}

		switch ($this->method)
		{
			case 'api':
				return $this->get_inclusions_by_api();
				break;

			case 'direct':
				// Get a local reference of the filter data, if necessary
				if (is_null($this->filter_data))
				{
					$filters           = Factory::getFilters();
					$this->filter_data = $filters->getFilterData($this->filter_name);
				}

				return $this->filter_data;
				break;

			default:
				// regex inclusion is not supported at the moment
				$dummy = [];

				return $dummy;
				break;
		}
	}

	/**
	 * Adds an exclusion filter, or add/replace an inclusion filter
	 *
	 * @param   string  $root  Filter's root
	 * @param   mixed   $test  Exclusion: the filter string. Inclusion: the root definition data
	 *
	 * @return bool True on success
	 */
	public function set($root, $test)
	{
		if (in_array($this->subtype, ['all', 'content', 'children']))
		{
			return $this->setExclusion($root, $test);
		}
		else
		{
			return $this->setInclusion($root, $test);
		}
	}

	/**
	 * Unsets a given filter
	 *
	 * @param   string  $root  Filter's root
	 * @param   string  $test  The filter to remove
	 *
	 * @return bool
	 */
	public function remove($root, $test = null)
	{
		if ($this->subtype == 'inclusion')
		{
			return $this->removeInclusion($root);
		}
		else
		{
			return $this->removeExclusion($root, $test);
		}
	}

	/**
	 * Completely removes all filters off a specific root
	 *
	 * @param   string  $root
	 *
	 * @return bool
	 */
	public function reset($root)
	{
		switch ($this->method)
		{
			default:
			case 'api':
				return false;
				break;

			case 'direct':
			case 'regex':
				// Get a local reference of the filter data, if necessary
				if (is_null($this->filter_data))
				{
					$filters           = Factory::getFilters();
					$this->filter_data = $filters->getFilterData($this->filter_name);
				}
				// Direct filters
				if (array_key_exists($root, $this->filter_data))
				{
					unset($this->filter_data[$root]);
				}
				else
				{
					// Root not found
					return false;
				}
				break;
		}

		$filters = Factory::getFilters();
		$filters->setFilterData($this->filter_name, $this->filter_data);

		return true;
	}

	/**
	 * Toggles a filter
	 *
	 * @param   string  $root        The filter root object
	 * @param   string  $test        The filter string to toggle
	 * @param   bool    $new_status  The new filter status after the operation (true: enabled, false: disabled)
	 *
	 * @return bool True on successful change, false if we failed to change it
	 */
	public function toggle($root, $test, &$new_status)
	{
		// Can't toggle inclusion filters!
		if ($this->subtype == 'inclusion')
		{
			return false;
		}

		$is_set     = $this->isFiltered($test, $root, $this->object, $this->subtype);
		$new_status = !$is_set;
		if ($is_set)
		{
			$status = $this->remove($root, $test);
		}
		else
		{
			$status = $this->set($root, $test);
		}
		if (!$status)
		{
			$new_status = $is_set;
		}

		return $status;
	}

	/**
	 * Does this class has any filters? If it doesn't, its methods are never called by
	 * Akeeba's engine to speed things up.
	 * @return bool
	 */
	public function hasFilters()
	{
		if (!$this->enabled)
		{
			return false;
		}

		switch ($this->method)
		{
			default:
			case 'api':
				// API filters always have data!
				return true;
				break;

			case 'direct':
			case 'regex':
				// Get a local reference of the filter data, if necessary
				if (is_null($this->filter_data))
				{
					$filters           = Factory::getFilters();
					$this->filter_data = $filters->getFilterData($this->filter_name);
				}

				return !empty($this->filter_data);
				break;
		}
	}

	/**
	 * Returns a list of filter strings for the given root. Used by MySQLDump engine.
	 *
	 * @param   string  $root
	 *
	 * @return array
	 */
	public function getFilters($root)
	{
		$dummy = [];

		if (!$this->enabled)
		{
			return $dummy;
		}

		switch ($this->method)
		{
			default:
			case 'api':
				// API filters never have a list
				return $dummy;
				break;

			case 'direct':
			case 'regex':
				// Get a local reference of the filter data, if necessary
				if (is_null($this->filter_data))
				{
					$filters           = Factory::getFilters();
					$this->filter_data = $filters->getFilterData($this->filter_name);
				}

				if (is_null($root))
				{
					// When NULL is passed as the root, we return all roots
					return $this->filter_data;
				}
				elseif (array_key_exists($root, $this->filter_data))
				{
					// The root exists, return its data
					return $this->filter_data[$root];
				}
				else
				{
					// The root doesn't exist, return an empty array
					return $dummy;
				}
				break;
		}
	}

	/**
	 * This method must be overriden by API-type exclusion filters.
	 *
	 * @param   string  $test  The object to test for exclusion
	 * @param   string  $root  The object's root
	 *
	 * @return    bool    Return true if it matches your filters
	 *
	 * @codeCoverageIgnore
	 */
	protected function is_excluded_by_api($test, $root)
	{
		return false;
	}

	/**
	 * This method must be overriden by API-type inclusion filters.
	 *
	 * @return    array    The inclusion filters
	 *
	 * @codeCoverageIgnore
	 */
	protected function &get_inclusions_by_api()
	{
		$dummy = [];

		return $dummy;
	}

	/**
	 * Remove the root prefix from an absolute path
	 *
	 * @param   string  $directory  The absolute path
	 *
	 * @return  string  The translated path, relative to the root directory of the backup job
	 */
	protected function treatDirectory($directory)
	{
		if (!is_object($this->fsTools))
		{
			$this->fsTools = Factory::getFilesystemTools();
		}

		// Get the site's root
		$configuration = Factory::getConfiguration();

		if ($configuration->get('akeeba.platform.override_root', 0))
		{
			$root = $configuration->get('akeeba.platform.newroot', '[SITEROOT]');
		}
		else
		{
			$root = '[SITEROOT]';
		}

		if (stristr($root, '['))
		{
			$root = $this->fsTools->translateStockDirs($root);
		}

		$site_root = $this->fsTools->TrimTrailingSlash($this->fsTools->TranslateWinPath($root));

		$directory = $this->fsTools->TrimTrailingSlash($this->fsTools->TranslateWinPath($directory));

		// Trim site root from beginning of directory
		if (substr($directory, 0, strlen($site_root)) == $site_root)
		{
			$directory = substr($directory, strlen($site_root));

			if (substr($directory, 0, 1) == '/')
			{
				$directory = substr($directory, 1);
			}
		}

		return $directory;
	}

	/**
	 * Sets a filter, for direct and regex exclusion filter types
	 *
	 * @param   string  $root  The filter root object
	 * @param   string  $test  The filter string to set
	 *
	 * @return    bool    True on success
	 *
	 * @codeCoverageIgnore
	 */
	private function setExclusion($root, $test)
	{
		switch ($this->method)
		{
			default:
			case 'api':
				// we can't set new filter elements for API-type filters
				return false;
				break;

			case 'direct':
			case 'regex':
				// Get a local reference of the filter data, if necessary
				if (is_null($this->filter_data))
				{
					$filters           = Factory::getFilters();
					$this->filter_data = $filters->getFilterData($this->filter_name);
				}

				// Direct filters
				if (array_key_exists($root, $this->filter_data))
				{
					if (!in_array($test, $this->filter_data[$root]))
					{
						$this->filter_data[$root][] = $test;
					}
					else
					{
						return false;
					}
				}
				else
				{
					$this->filter_data[$root] = [$test];
				}
				break;
		}

		$filters = Factory::getFilters();
		$filters->setFilterData($this->filter_name, $this->filter_data);

		return true;
	}

	/**
	 * Sets a filter, for direct inclusion filter types
	 *
	 * @param   string  $root  The inclusion filter key (root)
	 * @param   string  $test  The inclusion filter raw data
	 *
	 * @return    bool    True on success
	 *
	 * @codeCoverageIgnore
	 */
	private function setInclusion($root, $test)
	{
		switch ($this->method)
		{
			default:
			case 'api':
			case 'regex':
				// we can't set new filter elements for API or regex type filters
				return false;
				break;

			case 'direct':
				// Get a local reference of the filter data, if necessary
				if (is_null($this->filter_data))
				{
					$filters           = Factory::getFilters();
					$this->filter_data = $filters->getFilterData($this->filter_name);
				}

				$this->filter_data[$root] = $test;
				break;
		}

		$filters = Factory::getFilters();
		$filters->setFilterData($this->filter_name, $this->filter_data);

		return true;
	}

	/**
	 * Remove a key from direct and regex filters
	 *
	 * @param   string  $root  The filter root object
	 * @param   string  $test  The filter string to set
	 *
	 * @return    bool    True on success
	 *
	 * @codeCoverageIgnore
	 */
	private function removeExclusion($root, $test)
	{
		switch ($this->method)
		{
			default:
			case 'api':
				// we can't remove filter elements from API-type filters
				return false;
				break;

			case 'direct':
			case 'regex':
				// Get a local reference of the filter data, if necessary
				if (is_null($this->filter_data))
				{
					$filters           = Factory::getFilters();
					$this->filter_data = $filters->getFilterData($this->filter_name);
				}

				// Direct filters
				if (array_key_exists($root, $this->filter_data))
				{
					if (in_array($test, $this->filter_data[$root]))
					{
						if (count($this->filter_data[$root]) == 1)
						{
							// If it's the only element, remove the entire root key
							unset($this->filter_data[$root]);
						}
						else
						{
							// If there are more elements, remove just the $test value
							$key = array_search($test, $this->filter_data[$root]);
							unset($this->filter_data[$root][$key]);
						}
					}
					else
					{
						// Filter object not found
						return false;
					}
				}
				else
				{
					// Root not found
					return false;
				}
				break;
		}

		$filters = Factory::getFilters();
		$filters->setFilterData($this->filter_name, $this->filter_data);

		return true;
	}

	/**
	 * Remove an inclusion filter
	 *
	 * @param   string  $root  The root of the filter to remove
	 *
	 * @return bool
	 *
	 * @codeCoverageIgnore
	 */
	private function removeInclusion($root)
	{
		switch ($this->method)
		{
			default:
			case 'api':
			case 'regex':
				// we can't remove filter elements from API or regex type filters
				return false;
				break;

			case 'direct':
				// Get a local reference of the filter data, if necessary
				if (is_null($this->filter_data))
				{
					$filters           = Factory::getFilters();
					$this->filter_data = $filters->getFilterData($this->filter_name);
				}

				if (array_key_exists($root, $this->filter_data))
				{
					unset($this->filter_data[$root]);
				}
				else
				{
					// Root not found
					return false;
				}
				break;
		}

		$filters = Factory::getFilters();
		$filters->setFilterData($this->filter_name, $this->filter_data);

		return true;
	}
}
Filter/Multidb.php000060400000002300152456704520010103 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Multiple Database inclusion filter
 */
class Multidb extends Base
{
	public function __construct()
	{
		$this->object  = 'db';
		$this->subtype = 'inclusion';
		$this->method  = 'direct';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Skipfiles.php000060400000002312152456704520010437 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Directory contents (files) exclusion filter
 */
class Skipfiles extends Base
{
	public function __construct()
	{
		$this->object  = 'dir';
		$this->subtype = 'content';
		$this->method  = 'direct';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Tabledata.php000060400000002514152456704520010373 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Database table records exclusion filter
 *
 * This is simple stuff. If a table's on the list, it will backup just its structure, not
 * its contents. Fair and square...
 */
class Tabledata extends Base
{
	public function __construct()
	{
		$this->object  = 'dbobject';
		$this->subtype = 'content';
		$this->method  = 'direct';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Directories.php000060400000002267152456704520010773 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Directory exclusion filter
 */
class Directories extends Base
{
	public function __construct()
	{
		$this->object  = 'dir';
		$this->subtype = 'all';
		$this->method  = 'direct';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Regexfiles.php000060400000002317152456704520010610 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Files exclusion filter based on regular expressions
 */
class Regexfiles extends Base
{
	public function __construct()
	{
		$this->object  = 'file';
		$this->subtype = 'all';
		$this->method  = 'regex';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Extradirs.php000060400000002303152456704520010453 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Extra Directories inclusion filter
 */
class Extradirs extends Base
{
	public function __construct()
	{
		$this->object  = 'dir';
		$this->subtype = 'inclusion';
		$this->method  = 'direct';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Filter/Incremental.php000060400000007547152456704520010766 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use DateTime;
use DateTimeZone;

/**
 * Incremental file filter
 *
 * It will only backup files which are newer than the last backup taken with this profile
 */
class Incremental extends Base
{

	public function __construct()
	{
		$this->object  = 'file';
		$this->subtype = 'all';
		$this->method  = 'api';
	}

	protected function is_excluded_by_api($test, $root)
	{
		static $filter_switch = null;
		static $last_backup = null;

		if (is_null($filter_switch))
		{
			$config        = Factory::getConfiguration();
			$filter_switch = Factory::getEngineParamsProvider()->getScriptingParameter('filter.incremental', 0);
			$filter_switch = ($filter_switch == 1);

			$last_backup = $config->get('volatile.filter.last_backup', null);

			if (is_null($last_backup) && $filter_switch)
			{
				// Get a list of backups on this profile
				$backups = Platform::getInstance()->get_statistics_list([
					'filters' => [
						[
							'field' => 'profile_id',
							'value' => Platform::getInstance()->get_active_profile(),
						],
					],
				]);

				// Find this backup's ID
				$model = Factory::getStatistics();
				$id    = $model->getId();

				if (is_null($id))
				{
					$id = -1;
				}

				// Initialise
				$last_backup = time();
				$now         = $last_backup;

				// Find the last time a successful backup with this profile was made
				if (count($backups))
				{
					foreach ($backups as $backup)
					{
						// Skip the current backup
						if ($backup['id'] == $id)
						{
							continue;
						}

						// Skip non-complete backups
						if ($backup['status'] != 'complete')
						{
							continue;
						}

						$tzUTC      = new DateTimeZone('UTC');
						$dateTime   = new DateTime($backup['backupstart'], $tzUTC);
						$backuptime = $dateTime->getTimestamp();

						$last_backup = $backuptime;
						break;
					}
				}

				if ($last_backup == $now)
				{
					// No suitable backup found; disable this filter
					$config->set('volatile.scripting.incfile.filter.incremental', 0);
					$filter_switch = false;
				}
				else
				{
					// Cache the last backup timestamp
					$config->set('volatile.filter.last_backup', $last_backup);
				}
			}
		}

		if (!$filter_switch)
		{
			return false;
		}

		// Get the filesystem path for $root
		$config   = Factory::getConfiguration();
		$fsroot   = $config->get('volatile.filesystem.current_root', '');
		$ds       = ($fsroot == '') || ($fsroot == '/') ? '' : DIRECTORY_SEPARATOR;
		$filename = $fsroot . $ds . $test;

		// Get the timestamp of the file
		$timestamp = @filemtime($filename);

		// If we could not get this information, include the file in the archive
		if ($timestamp === false)
		{
			return false;
		}

		// Compare it with the last backup timestamp and exclude if it's older than the last backup
		if ($timestamp <= $last_backup)
		{
			//Factory::getLog()->debug("Excluding $filename due to incremental backup restrictions");
			return true;
		}

		// No match? Just include the file!
		return false;
	}

}
Filter/Tablesalwaysskipped.php000060400000003436152456704520012531 0ustar00<?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\Filter
 * @subpackage
 *
 * @copyright   A copyright
 * @license     A "Slug" license name e.g. GPL2
 */

namespace Akeeba\Engine\Filter;

defined('AKEEBAENGINE') || die();

class Tablesalwaysskipped extends Base
{
	public function __construct()
	{
		$this->object  = 'dbobject';
		$this->subtype = 'content';
		$this->method  = 'api';

		parent::__construct();
	}

	/**
	 * This method must be overridden by API-type exclusion filters.
	 *
	 * @param   string  $test  The object to test for exclusion
	 * @param   string  $root  The object's root
	 *
	 * @return  bool  Return true if it matches your filters
	 */
	protected function is_excluded_by_api($test, $root)
	{
		static $alwaysExcludeTables = [
			// Tables from the service connector that shall not be named
			'bf_core_hashes',
			'bf_files',
			'bf_files_last',
			'bf_folders',
			'bf_folders_to_scan',
		];

		// Is it one of the always excluded tables?
		return in_array($test, $alwaysExcludeTables);
	}

}Filter/Files.php000060400000002256152456704520007557 0ustar00<?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\Filter;

defined('AKEEBAENGINE') || die();

/**
 * Files exclusion filter
 */
class Files extends Base
{
	public function __construct()
	{
		$this->object  = 'file';
		$this->subtype = 'all';
		$this->method  = 'direct';

		if (empty($this->filter_name))
		{
			$this->filter_name = strtolower(basename(__FILE__, '.php'));
		}

		parent::__construct();
	}
}
Dump/native.json000060400000005330152456704520007641 0ustar00{
    "_information": {
        "title": "COM_AKEEBA_CONFIG_ENGINE_DUMP_NATIVE_TITLE",
        "description": "COM_AKEEBA_CONFIG_ENGINE_DUMP_NATIVE_DESCRIPTION"
    },
    "engine.dump.divider.common": {
        "default": "0",
        "type": "separator",
        "title": "COM_AKEEBA_CONFIG_DUMP_DIVIDER_COMMON",
        "bold": "1"
    },
    "engine.dump.common.blankoutpass": {
        "default": "0",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_BLANKOUTPASS_TITLE",
        "description": "COM_AKEEBA_CONFIG_BLANKOUTPASS_DESCRIPTION"
    },
    "engine.dump.common.extended_inserts": {
        "default": "1",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_EXTENDEDINSERTS_TITLE",
        "description": "COM_AKEEBA_CONFIG_EXTENDEDINSERTS_DESCRIPTION"
    },
    "engine.dump.common.packet_size": {
        "default": "131072",
        "type": "integer",
        "min": "1",
        "max": "1048576",
        "shortcuts": "16384|32768|65536|131072|262144|524288|1048576",
        "scale": "1024",
        "uom": "KB",
        "title": "COM_AKEEBA_CONFIG_MAXPACKET_TITLE",
        "description": "COM_AKEEBA_CONFIG_MAXPACKET_DESCRIPTION"
    },
    "engine.dump.common.splitsize": {
        "default": "524288",
        "type": "integer",
        "min": "0",
        "max": "10485760",
        "shortcuts": "524288|1048576|2097152|5242880|10485760",
        "scale": "1048576",
        "uom": "MB",
        "title": "COM_AKEEBA_CONFIG_SPLITDBDUMP_TITLE",
        "description": "COM_AKEEBA_CONFIG_SPLITDBDUMP_DESCRIPTION"
    },
    "engine.dump.common.batchsize": {
        "default": "1000",
        "type": "integer",
        "min": "0",
        "max": "100000",
        "shortcuts": "10|20|50|100|200|500|1000",
        "scale": "1",
        "uom": "queries",
        "title": "COM_AKEEBA_CONFIG_BACTHSIZE_TITLE",
        "description": "COM_AKEEBA_CONFIG_BACTHSIZE_DESCRIPTION"
    },
    "engine.dump.divider.mysql": {
        "default": "0",
        "type": "separator",
        "title": "COM_AKEEBA_CONFIG_DUMP_DIVIDER_MYSQL",
        "bold": "1"
    },
    "engine.dump.native.advanced_entitites": {
        "default": "0",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_MYSQL5FEATURES_ENABLE_TITLE",
        "description": "COM_AKEEBA_CONFIG_MYSQL5FEATURES_ENABLE_DESCRIPTION"
    },
    "engine.dump.native.nodependencies": {
        "default": "0",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_NODEPENDENCIES_TITLE",
        "description": "COM_AKEEBA_CONFIG_NODEPENDENCIES_DESCRIPTION"
    },
    "engine.dump.native.nobtree": {
        "default": "1",
        "type": "bool",
        "title": "COM_AKEEBA_CONFIG_MYSQLNOBTREE_TITLE",
        "description": "COM_AKEEBA_CONFIG_MYSQLNOBTREE_TIP"
    }
}Dump/Native.php000060400000012137152456704520007422 0ustar00<?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\Dump;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Base\Exceptions\ErrorException;
use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Dump\Base as DumpBase;
use Akeeba\Engine\Factory;
use RuntimeException;

#[\AllowDynamicProperties]
class Native extends Part
{
	/** @var DumpBase */
	private $_engine = null;

	private $tablePrefix;

	/**
	 * Implements the constructor of the class
	 *
	 * @return  void
	 */
	public function __construct()
	{
		parent::__construct();

		Factory::getLog()->debug(__CLASS__ . " :: New instance");
	}

	/**
	 * Runs the preparation for this part. Should set _isPrepared
	 * to true
	 *
	 * @return  void
	 */
	protected function _prepare()
	{
		Factory::getLog()->debug(__CLASS__ . " :: Processing parameters");

		$options = null;

		// Get the DB connection parameters
		if (is_array($this->_parametersArray))
		{
			$driver   = $this->_parametersArray['driver'] ?? 'mysql';
			$prefix   = $this->_parametersArray['prefix'] ?? '';

			if (($driver == 'mysql') && !function_exists('mysql_connect'))
			{
				$driver = 'mysqli';
			}

			$options = [
				'driver'   => $driver,
				'host'     => $this->_parametersArray['host'] ?? '',
				'port'     => $this->_parametersArray['port'] ?? '',
				'user'     => $this->_parametersArray['user'] ?? ($this->_parametersArray['username'] ?? ''),
				'password' => $this->_parametersArray['password'] ?? '',
				'database' => $this->_parametersArray['database'] ?? '',
				'prefix'   => is_null($prefix) ? '' : $prefix,
			];

			$options['ssl'] = $this->_parametersArray['ssl'] ?? [];
			$options['ssl'] = is_array($options['ssl']) ? $options['ssl'] : [];

			$options['ssl']['enable']             = (bool) ($options['ssl']['enable'] ?? $this->_parametersArray['dbencryption'] ?? false);
			$options['ssl']['cipher']             = ($options['ssl']['cipher'] ?? $this->_parametersArray['dbsslcipher'] ?? null) ?: null;
			$options['ssl']['ca']                 = ($options['ssl']['ca'] ?? $this->_parametersArray['dbsslca'] ?? null) ?: null;
			$options['ssl']['capath']             = ($options['ssl']['capath'] ?? $this->_parametersArray['dbsslcapath'] ?? null) ?: null;
			$options['ssl']['key']                = ($options['ssl']['key'] ?? $this->_parametersArray['dbsslkey'] ?? null) ?: null;
			$options['ssl']['cert']               = ($options['ssl']['cert'] ?? $this->_parametersArray['dbsslcert'] ?? null) ?: null;
			$options['ssl']['verify_server_cert'] = (bool) (($options['ssl']['verify_server_cert'] ?? $this->_parametersArray['dbsslverifyservercert'] ?? false) ?: false);

		}

		$db         = Factory::getDatabase($options);

		if ($db->getErrorNum() > 0)
		{
			$error = $db->getErrorMsg();

			throw new RuntimeException(__CLASS__ . ' :: Database Error: ' . $error);
		}

		$driverType = $db->getDriverType();
		$className  = '\\Akeeba\\Engine\\Dump\\Native\\' . ucfirst($driverType);

		// Check if we have a native dump driver
		if (!class_exists($className, true))
		{
			$this->setState(self::STATE_ERROR);

			throw new ErrorException('Akeeba Engine does not have a native dump engine for ' . $driverType . ' databases');
		}

		Factory::getLog()->debug(__CLASS__ . " :: Instanciating new native database dump engine $className");

		$this->_engine = new $className;

		$this->_engine->setup($this->_parametersArray);

		$this->_engine->callStage('_prepare');
		$this->setState($this->_engine->getState());
		$this->tablePrefix = $this->_engine->getPrefix();
	}

	/**
	 * Runs the finalisation process for this part. Should set
	 * _isFinished to true.
	 *
	 * @return  void
	 */
	protected function _finalize()
	{
		$this->_engine->callStage('_finalize');
		$this->setState($this->_engine->getState());
	}

	/**
	 * Runs the main functionality loop for this part. Upon calling,
	 * should set the _isRunning to true. When it finished, should set
	 * the _hasRan to true. If an error is encountered, setError should
	 * be used.
	 *
	 * @return  void
	 */
	protected function _run()
	{
		$this->_engine->callStage('_run');
		$this->setState($this->_engine->getState());
		$this->setStep($this->_engine->getStep());
		$this->setSubstep($this->_engine->getSubstep());
		$this->partNumber = $this->_engine->partNumber;
	}

	/**
	 * Get the database table prefix
	 *
	 * @return string The database prefix
	 */
	public function getPrefix()
	{
		return $this->tablePrefix;
	}

}
Dump/Base.php000060400000107617152456704520007056 0ustar00<?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\Dump;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Base\Part;
use Akeeba\Engine\Core\Domain\Pack;
use Akeeba\Engine\Driver\Base as DriverBase;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Akeeba\Engine\Util\FileCloseAware;
use Akeeba\Engine\Util\HashTrait;
use Exception;
use RuntimeException;

#[\AllowDynamicProperties]
abstract class Base extends Part
{
	use HashTrait;
	use FileCloseAware;

	// **********************************************************************
	// Configuration parameters
	// **********************************************************************

	/** @var int Current dump file part number */
	public $partNumber = 0;

	/** @var string Prefix to this database */
	protected $prefix = '';

	/** @var string MySQL database server host name or IP address */
	protected $host = '';

	/** @var string MySQL database server port (optional) */
	protected $port = '';

	/** @var string MySQL socket or named pipe (optional) */
	protected $socket = '';

	/** @var string MySQL user name, for authentication */
	protected $username = '';

	/** @var string MySQL password, for authentication */
	protected $password = '';

	/** @var string MySQL database */
	protected $database = '';

	/** @var string The database driver to use */
	protected $driver = '';

	/** @var array|null The MySQL SSL options */
	protected $ssl;

	// **********************************************************************
	// File handling fields
	// **********************************************************************
	/** @var boolean Should I post process quoted values */
	protected $postProcessValues = false;

	/** @var string Absolute path to dump file; must be writable (optional; if left blank it is automatically calculated) */
	protected $dumpFile = '';

	/** @var string Data cache, used to cache data before being written to disk */
	protected $data_cache = '';

	/** @var int */
	protected $largest_query = 0;

	/** @var int Size of the data cache, default 128Kb */
	protected $cache_size = 131072;

	/** @var bool Should I process empty prefixes when creating abstracted names? */
	protected $processEmptyPrefix = true;

	/** @var string Absolute path to the temp file */
	protected $tempFile = '';

	/** @var string Relative path of how the file should be saved in the archive */
	protected $saveAsName = '';

	/** @var array Contains the sorted (by dependencies) list of tables/views to backup */
	protected $tables = [];

	// **********************************************************************
	// Protected fields (data handling)
	// **********************************************************************
	/** @var array Contains the configuration data of the tables */
	protected $tables_data = [];

	/** @var array Maps database table names to their abstracted format */
	protected $table_name_map = [];

	/** @var array Contains the dependencies of tables and views (temporary) */
	protected $dependencies = [];

	/** @var string The next table to backup */
	protected $nextTable;

	/** @var integer The next row of the table to start backing up from */
	protected $nextRange;

	/** @var integer Current table's row count */
	protected $maxRange;

	/** @var bool Use extended INSERTs */
	protected $extendedInserts = false;

	/** @var integer Maximum packet size for extended INSERTs, in bytes */
	protected $packetSize = 0;

	/** @var string Extended INSERT query, while it's being constructed */
	protected $query = '';

	/** @var int Dump part's maximum size */
	protected $partSize = 0;

	/** @var resource Filepointer to the current dump part */
	private $fp = null;

	/**
	 * Should I be using an abstract prefix (#__) for table names?
	 *
	 * @var   bool
	 * @since 9.1.0
	 */
	private $useAbstractPrefix;

	/**
	 * Public constructor.
	 *
	 * @return  void
	 * @since   9.1.0
	 */
	public function __construct()
	{
		$this->useAbstractPrefix =
			Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') !== 'output';

		parent::__construct();
	}


	/**
	 * This method is called when the factory is being serialized and is used to perform necessary cleanup steps.
	 *
	 * @return  void
	 */
	public function _onSerialize()
	{
		$this->closeFile();
	}

	/**
	 * This method is called when the object is destroyed and is used to perform necessary cleanup steps.
	 */
	public function __destruct()
	{
		$this->closeFile();
	}

	/**
	 * Close any open SQL dump (output) file.
	 */
	public function closeFile()
	{
		if (!is_resource($this->fp))
		{
			return;
		}

		Factory::getLog()->debug("Closing SQL dump file.");

		$this->conditionalFileClose($this->fp);
		$this->fp = null;
	}

	/**
	 * Call a specific stage of the dump engine
	 *
	 * @param   string  $stage
	 *
	 * @throws Exception
	 */
	public function callStage($stage)
	{
		switch ($stage)
		{
			case '_prepare':
				$this->_prepare();
				break;

			case '_run':
				$this->_run();
				break;

			case '_finalize':
				$this->_finalize();
				break;
		}
	}

	public function getPrefix(): string
	{
		return $this->prefix ?: '';
	}

	/**
	 * Find where to store the backup files
	 *
	 * @param $partNumber int The SQL part number, default is 0 (.sql)
	 */
	protected function getBackupFilePaths($partNumber = 0)
	{
		Factory::getLog()->debug(__CLASS__ . " :: Getting temporary file");
		$basename       = substr(self::md5(microtime() . random_bytes(8)), random_int(0, 16), 16);
		$this->tempFile = Factory::getTempFiles()->registerTempFile($basename . '.sql');
		Factory::getLog()->debug(__CLASS__ . " :: Temporary file is {$this->tempFile}");

		// Get the base name of the dump file
		$partNumber = intval($partNumber);
		$baseName   = $this->dumpFile;

		if ($partNumber > 0)
		{
			// The file names are in the format dbname.sql, dbname.s01, dbname.s02, etc
			if (strtolower(substr($baseName, -4)) == '.sql')
			{
				$baseName = substr($baseName, 0, -4) . '.s' . sprintf('%02u', $partNumber);
			}
			else
			{
				$baseName = $baseName . '.s' . sprintf('%02u', $partNumber);
			}
		}

		if (empty($this->installerSettings))
		{
			// Fetch the installer settings
			$this->installerSettings = (object) [
				'installerroot' => 'installation',
				'sqlroot'       => 'installation/sql',
				'databasesini'  => 1,
				'readme'        => 1,
				'extrainfo'     => 1,
			];
			$config                  = Factory::getConfiguration();
			$installerKey            = $config->get('akeeba.advanced.embedded_installer');
			$installerDescriptors    = Factory::getEngineParamsProvider()->getInstallerList();

			if (array_key_exists($installerKey, $installerDescriptors))
			{
				// The selected installer exists, use it
				$this->installerSettings = (object) $installerDescriptors[$installerKey];
			}
			elseif (array_key_exists('angie', $installerDescriptors))
			{
				// The selected installer doesn't exist, but ANGIE exists; use that instead
				$this->installerSettings = (object) $installerDescriptors['angie'];
			}
		}

		switch (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal'))
		{
			case 'output':
				// The SQL file will be stored uncompressed in the output directory
				$statistics       = Factory::getStatistics();
				$statRecord       = $statistics->getRecord();
				$this->saveAsName = $statRecord['absolute_path'];
				break;

			case 'normal':
				// The SQL file will be stored in the SQL root of the archive, as
				// specified by the particular embedded installer's settings
				$this->saveAsName = $this->installerSettings->sqlroot . '/' . $baseName;
				break;

			case 'short':
				// The SQL file will be stored on archive's root
				$this->saveAsName = $baseName;
				break;
		}

		if ($partNumber > 0)
		{
			Factory::getLog()->debug("AkeebaDomainDBBackup :: Creating new SQL dump part #$partNumber");
		}

		Factory::getLog()->debug("AkeebaDomainDBBackup :: SQL temp file is " . $this->tempFile);
		Factory::getLog()->debug("AkeebaDomainDBBackup :: SQL file location in archive is " . $this->saveAsName);
	}

	/**
	 * Deletes any leftover files from previous backup attempts
	 *
	 */
	protected function removeOldFiles()
	{
		Factory::getLog()->debug("AkeebaDomainDBBackup :: Deleting leftover files, if any");

		if (file_exists($this->tempFile))
		{
			@unlink($this->tempFile);
		}
	}

	/**
	 * Populates the table arrays with the information for the db entities to back up
	 *
	 * @return  void
	 */
	abstract protected function getTablesToBackup(): void;

	/**
	 * Runs a step of the database dump
	 *
	 * @return  void
	 */
	abstract protected function stepDatabaseDump(): void;

	/**
	 * Return the current database name by querying the database connection object (e.g. SELECT DATABASE() in MySQL)
	 *
	 * @return  string
	 */
	abstract protected function getDatabaseNameFromConnection(): string;

	abstract protected function getAllTables(): array;

	/**
	 * Implements the _prepare abstract method
	 *
	 * @throws Exception
	 */
	protected function _prepare()
	{
		$this->setStep('Initialization');
		$this->setSubstep('');

		// Process parameters, passed to us using the setup() public method
		Factory::getLog()->debug(__CLASS__ . " :: Processing parameters");

		if (is_array($this->_parametersArray))
		{
			$this->parametersArrayToProperties();
		}

		// Try to detect and rectify the wrong database table naming prefix
		$this->workaroundWrongPrefix();

		// Make sure we have self-assigned the first part
		$this->partNumber = 0;

		// Get DB backup only mode
		$configuration = Factory::getConfiguration();

		// Find tables to be included and put them in the $_tables variable
		$this->getTablesToBackup();

		// Find where to store the database backup files
		$this->getBackupFilePaths($this->partNumber);

		// Remove any leftovers
		$this->removeOldFiles();

		// Initialize the extended INSERTs feature
		$this->extendedInserts = ($configuration->get('engine.dump.common.extended_inserts', 0) != 0);
		$this->packetSize      = (int) $configuration->get('engine.dump.common.packet_size', 0);

		if ($this->packetSize == 0)
		{
			$this->extendedInserts = false;
		}

		// Initialize the split dump feature
		$this->partSize = $configuration->get('engine.dump.common.splitsize', 1048576);
		if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'output')
		{
			$this->partSize = 0;
		}
		if (($this->partSize != 0) && ($this->packetSize != 0) && ($this->packetSize > $this->partSize))
		{
			$this->packetSize = floor($this->partSize / 2);
		}

		// Initialize the algorithm
		Factory::getLog()->debug(__CLASS__ . " :: Initializing algorithm for first run");
		$this->nextTable = array_shift($this->tables);

		// If there is no table to back up we are done with the database backup
		if (empty($this->nextTable) && $this->nextTable !== '0')
		{
			$this->setState(self::STATE_POSTRUN);

			return;
		}

		$this->nextRange = 0;
		$this->query     = '';

		// FIX 2.2: First table of extra databases was not being written to disk.
		// This deserved a place in the Bug Fix Hall Of Fame. In subsequent calls to _init, the $fp in
		// _writeline() was not nullified. Therefore, the first dump chunk (that is, the first table's
		// definition and first chunk of its data) were not written to disk. This call causes $fp to be
		// nullified, causing it to be recreated, pointing to the correct file.
		$null = null;
		$this->writeline($null);

		// Finally, mark ourselves "prepared".
		$this->setState(self::STATE_PREPARED);
	}

	/**
	 * Implements the _run() abstract method
	 *
	 * @throws Exception
	 */
	protected function _run()
	{
		// Check if we are already done
		if ($this->getState() == self::STATE_POSTRUN)
		{
			Factory::getLog()->debug(__CLASS__ . " :: Already finished");
			$this->setStep("");
			$this->setSubstep("");

			return;
		}

		// Mark ourselves as still running (we will test if we actually do towards the end ;) )
		$this->setState(self::STATE_RUNNING);

		/**
		 * Resume packing / post-processing part files if necessary.
		 *
		 * @see \Akeeba\Engine\Archiver\BaseArchiver::putDataFromFileIntoArchive
		 *
		 * Sometimes the SQL part file may be bigger than the big file threshold (engine.archiver.common.
		 * big_file_threshold). In this case when we try to add it to the backup archive the archiver engine figures
		 * out it has to be added uncompressed, one chunk (engine.archiver.common.chunk_size) bytes at a time. This
		 * happens in a loop. We read a chunk, push it to the archive, rinse and repeat.
		 *
		 * There are two cases when we might break the loop:
		 *
		 * 1. Not enough free space in the backup archive part and engine.postproc.common.after_part (immediate post-
		 *    processing) is enabled. We break the step to let the backup part be post-processed.
		 *
		 * 2. We ran out of time copying data.
		 *
		 * The following if-blocks deal with these two cases.
		 */
		if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') != 'output')
		{
			$archiver      = Factory::getArchiverEngine();
			$configuration = Factory::getConfiguration();

			// Check whether we need to immediately post-processing a done part
			if (Pack::postProcessDonePartFile($archiver, $configuration))
			{
				return;
			}

			// We had already started putting the DB dump file into the archive but it needs more time
			if ($configuration->get('volatile.engine.archiver.processingfile', false))
			{
				/**
				 * We MUST NOT try to continue adding the file to the backup archive manually. Instead, we have to go
				 * through getNextDumpPart. This method will continue adding the part to the backup archive and when
				 * this is done it will remove the file and create a new one.
				 *
				 * If that method returns false it means that we either hit an error or the archiver didn't have enough
				 * time to add the part to the backup archive. In either case we have to return and let the Engine step.
				 */
				if ($this->getNextDumpPart() === false)
				{
					return;
				}
			}
		}

		$this->stepDatabaseDump();

		$null = null;
		$this->writeline($null);
	}

	/**
	 * Implements the _finalize() abstract method
	 *
	 * @throws Exception
	 */
	protected function _finalize()
	{
		Factory::getLog()->debug("Adding any extra SQL statements imposed by the filters");

		foreach (Factory::getFilters()->getExtraSQL($this->databaseRoot) as $sqlStatement)
		{
			$sqlStatement = trim($sqlStatement) . "\n";

			$this->writeDump($sqlStatement, true);
		}

		// We need this to write out the cached extra SQL statements before closing the file.
		$this->writeDump(null, true);

		// Close the file pointer (otherwise the SQL file is left behind)
		$this->closeFile();

		// If we are not just doing a main db only backup, add the SQL file to the archive
		$finished = true;

		if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') != 'output')
		{
			$archiver      = Factory::getArchiverEngine();
			$configuration = Factory::getConfiguration();

			if ($configuration->get('volatile.engine.archiver.processingfile', false))
			{
				// We had already started archiving the db file, but it needs more time
				Factory::getLog()->debug("Continuing adding the SQL dump to the archive");
				$archiver->addFile(null, null, null);

				$finished = !$configuration->get('volatile.engine.archiver.processingfile', false);
			}
			else
			{
				// We have to add the dump file to the archive
				Factory::getLog()->debug("Adding the final SQL dump to the archive");
				$archiver->addFileRenamed($this->tempFile, $this->saveAsName);

				$finished = !$configuration->get('volatile.engine.archiver.processingfile', false);
			}
		}
		else
		{
			// We just have to move the dump file to its final destination
			Factory::getLog()->debug("Moving the SQL dump to its final location");
			$result = Platform::getInstance()->move($this->tempFile, $this->saveAsName);

			if (!$result)
			{
				Factory::getLog()->debug("Removing temporary file of final SQL dump");
				Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true);

				throw new RuntimeException('Could not move the SQL dump to its final location');
			}
		}

		// Make sure that if the archiver needs more time to process the file we can supply it
		if ($finished)
		{
			Factory::getLog()->debug("Removing temporary file of final SQL dump");
			Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true);

			$this->setState(self::STATE_FINISHED);
		}
	}

	/**
	 * Creates a new dump part
	 *
	 * @return bool
	 * @throws Exception
	 */
	protected function getNextDumpPart()
	{
		// On database dump only mode we mustn't create part files!
		if (Factory::getEngineParamsProvider()->getScriptingParameter('db.saveasname', 'normal') == 'output')
		{
			return false;
		}

		// Is the archiver still processing?
		$configuration   = Factory::getConfiguration();
		$archiver        = Factory::getArchiverEngine();
		$stillProcessing = $configuration->get('volatile.engine.archiver.processingfile', false);

		if ($stillProcessing)
		{
			/**
			 * The archiver is still adding the previous dump part. This means that we are called from the top few lines
			 * of the _run method. We must continue adding the previous dump part.
			 */
			Factory::getLog()->debug("Continuing adding the SQL dump part to the archive");
			$archiver->addFile('', '', '');
		}
		else
		{
			/**
			 * There is no other dump part being processed. Therefore the current SQL dump part is still open. We must
			 * close it and ask the archiver to add it to the backup archive.
			 */
			$this->closeFile();
			Factory::getLog()->debug("Adding the SQL dump part to the archive");
			$archiver->addFileRenamed($this->tempFile, $this->saveAsName);
		}

		// Return false if the file didn't finish getting added to the archive
		if ($configuration->get('volatile.engine.archiver.processingfile', false))
		{
			Factory::getLog()->debug("The SQL dump file has not been processed thoroughly by the archiver. Resuming in the next step.");

			return false;
		}

		/**
		 * If you are here the SQL dump part file is completely added to the backup archive. All we have to do now is
		 * remove it and create a new dump part file.
		 */
		// Remove the old file
		Factory::getLog()->debug("Removing dump part's temporary file");
		Factory::getTempFiles()->unregisterAndDeleteTempFile($this->tempFile, true);

		// Create the new dump part
		$this->partNumber++;
		$this->getBackupFilePaths($this->partNumber);
		$null = null;
		$this->writeline($null);

		return true;
	}

	/**
	 * Creates a new dump part, but only if required to do so
	 *
	 * @return bool
	 * @throws Exception
	 */
	protected function createNewPartIfRequired()
	{
		if ($this->partSize == 0)
		{
			return true;
		}

		$filesize = 0;

		if (@file_exists($this->tempFile))
		{
			$filesize = @filesize($this->tempFile);
		}

		$projectedSize = $filesize + strlen($this->query);

		if ($this->extendedInserts)
		{
			$projectedSize = $filesize + $this->packetSize;
		}

		if ($projectedSize > $this->partSize)
		{
			return $this->getNextDumpPart();
		}

		return true;
	}

	/**
	 * Returns a table's abstract name (replacing the prefix with the magic #__ string)
	 *
	 * @param   string  $tableName  The canonical name, e.g. 'jos_content'
	 *
	 * @return string The abstract name, e.g. '#__content'
	 */
	protected function getAbstract($tableName)
	{
		// Don't return abstract names for non-CMS tables
		if (is_null($this->prefix))
		{
			return $tableName;
		}

		switch ($this->prefix)
		{
			case '':
				if ($this->processEmptyPrefix)
				{
					// This is more of a hack; it assumes all tables are core CMS tables if the prefix is empty.
					return '#__' . $tableName;
				}

				// If $this->processEmptyPrefix (the process_empty_prefix config flag) is false, we don't
				// assume anything.
				return $tableName;

				break;

			default:
				// Normal behaviour for 99% of sites. Start by assuming the table has no prefix, therefore is non-core.
				$tableAbstract = $tableName;

				// If there's a prefix use the abstract name
				if (!empty($this->prefix) && (substr($tableName, 0, strlen($this->prefix)) == $this->prefix))
				{
					$tableAbstract = '#__' . substr($tableName, strlen($this->prefix));
				}

				return $tableAbstract;

				break;
		}
	}

	/**
	 * Writes the SQL dump into the output files. If it fails, it sets the error
	 *
	 * @param   string  $data       Data to write to the dump file. Pass NULL to force flushing to file.
	 * @param   bool    $addMarker  Should I prefix the data with a marker?
	 *
	 * @return  boolean  TRUE on successful write, FALSE otherwise
	 * @throws  Exception
	 */
	protected function writeDump($data, $addMarker = false)
	{
		if (!empty($data))
		{
			if ($addMarker && $this->useAbstractPrefix)
			{
				$this->data_cache .= '/**ABDB**/';
			}
			elseif (!$this->useAbstractPrefix)
			{
				// Replace #__ with the prefix when writing plain .sql files
				$db   = $this->getDB();
				$data = $db->replacePrefix($data) . "\n";
			}

			$this->data_cache .= $data;

			if (strlen($data) > $this->largest_query)
			{
				$this->largest_query = strlen($data);
				Factory::getConfiguration()->set('volatile.database.largest_query', $this->largest_query);
			}
		}

		if ((strlen($this->data_cache) >= $this->cache_size) || (is_null($data) && (!empty($this->data_cache))))
		{
			$this->data_cache = rtrim($this->data_cache, "\n");

			if ($this->useAbstractPrefix && $addMarker && substr($this->data_cache, -10) !== '/**ABDB**/')
			{
				$this->data_cache .= '/**ABDB**/';
			}

			$this->data_cache .= "\n";

			Factory::getLog()->debug("Writing " . strlen($this->data_cache) . " bytes to the dump file");
			$result = $this->writeline($this->data_cache);

			if (!$result)
			{
				$errorMessage = sprintf('Couldn\'t write to the SQL dump file %s; check the temporary directory permissions and make sure you have enough disk space available.', $this->tempFile);
				throw new RuntimeException($errorMessage);
			}

			$this->data_cache = '';
		}

		return true;
	}

	/**
	 * Saves the string in $fileData to the file $backupfile. Returns TRUE. If saving
	 * failed, return value is FALSE.
	 *
	 * @param   string  $fileData  Data to write. Set to null to close the file handle.
	 *
	 * @return boolean TRUE is saving to the file succeeded
	 * @throws Exception
	 */
	protected function writeline(&$fileData)
	{
		if (!is_resource($this->fp))
		{
			$this->fp = @fopen($this->tempFile, 'a');

			if ($this->fp === false)
			{
				throw new RuntimeException('Could not open ' . $this->tempFile . ' for append, in DB dump.');
			}
		}

		if (is_null($fileData))
		{
			$this->conditionalFileClose($this->fp);

			$this->fp = null;

			return true;
		}
		else
		{
			if ($this->fp)
			{
				$ret = fwrite($this->fp, $fileData);
				@clearstatcache();

				// Make sure that all data was written to disk
				return ($ret == strlen($fileData));
			}

			return false;
		}
	}

	/**
	 * Return an instance of DriverBase
	 *
	 * @return DriverBase|bool
	 *
	 * @throws Exception
	 */
	protected function &getDB()
	{
		$ssl     = $this->ssl ?? [];
		$ssl     = is_array($ssl) ? $ssl : [];
		$options = [
			'driver'   => $this->driver,
			'host'     => $this->host,
			'port'     => $this->port,
			'socket'   => $this->socket,
			'user'     => $this->username,
			'password' => $this->password,
			'database' => $this->database,
			'prefix'   => is_null($this->prefix) ? '' : $this->prefix,
			'ssl'      => $ssl,
		];

		$db = Factory::getDatabase($options);

		if ($db->getErrorNum() > 0)
		{
			$error = $db->getErrorMsg();

			throw new RuntimeException(__CLASS__ . ' :: Database Error: ' . $error);
		}

		return $db;
	}

	/**
	 * Returns the database name. If the name was not declared when the object was created we will go through the
	 * getDatabaseNameFromConnection method to populate it.
	 *
	 * @return  string
	 */
	protected function getDatabaseName()
	{
		if (empty($this->database) && $this->database !== '0')
		{
			$this->database = $this->getDatabaseNameFromConnection();
		}

		return $this->database;
	}

	/**
	 * Post process a quoted value before it's written to the database dump.
	 * So far it's only required for SQL Server which has a problem escaping
	 * newline characters...
	 *
	 * @param   string  $value  The quoted value to post-process
	 *
	 * @return  string
	 */
	protected function postProcessQuotedValue($value)
	{
		return $value;
	}

	/**
	 * Returns a preamble for the data dump portion of the SQL backup. This is
	 * used to output commands before the first INSERT INTO statement for a
	 * table when outputting a plain SQL file.
	 *
	 * Practical use: the SET IDENTITY_INSERT sometable ON required for SQL Server
	 *
	 * @param   string   $tableAbstract  Abstract name of the table, e.g. #__foobar
	 * @param   string   $tableName      Real name of the table, e.g. abc_foobar
	 * @param   integer  $maxRange       Row count on this table
	 *
	 * @return  string   The SQL commands you want to be written in the dump file
	 */
	protected function getDataDumpPreamble($tableAbstract, $tableName, $maxRange)
	{
		return '';
	}

	/**
	 * Returns an epilogue for the data dump portion of the SQL backup. This is
	 * used to output commands after the last INSERT INTO statement for a
	 * table when outputting a plain SQL file.
	 *
	 * Practical use: the SET IDENTITY_INSERT sometable OFF required for SQL Server
	 *
	 * @param   string   $tableAbstract  Abstract name of the table, e.g. #__foobar
	 * @param   string   $tableName      Real name of the table, e.g. abc_foobar
	 * @param   integer  $maxRange       Row count on this table
	 *
	 * @return  string   The SQL commands you want to be written in the dump file
	 */
	protected function getDataDumpEpilogue($tableAbstract, $tableName, $maxRange)
	{
		return '';
	}

	/**
	 * Return a list of field names for the INSERT INTO statements. This is only
	 * required for Microsoft SQL Server because without it the SET IDENTITY_INSERT
	 * has no effect.
	 *
	 * @param   array|string  $fieldNames  A list of field names in array format or '*' if it's all fields
	 *
	 * @return  string
	 * @throws Exception
	 */
	protected function getFieldListSQL($fieldNames)
	{
		// If we get a literal '*' we dumped all columns so we don't need to add column names in the INSERT.
		if ($fieldNames === '*')
		{
			return '';
		}

		return '(' . implode(', ', array_map([$this->getDB(), 'qn'], $fieldNames)) . ')';
	}

	/**
	 * Return a list of columns to use in the SELECT query for dumping table data.
	 *
	 * This is used to filter out all generated rows.
	 *
	 * @param   string  $tableAbstract
	 *
	 * @return  string|array  An array of table columns or the string literal '*' to quickly select all columns.
	 *
	 * @see  https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html
	 */
	protected function getSelectColumns($tableAbstract)
	{
		return '*';
	}

	/**
	 * Converts a human formatted size to integer representation of bytes,
	 * e.g. 1M to 1024768
	 *
	 * @param   string  $setting  The value in human readable format, e.g. "1M"
	 *
	 * @return  integer  The value in bytes
	 */
	protected function humanToIntegerBytes($setting)
	{
		$val  = trim($setting);
		$last = strtolower($val[strlen($val) - 1]);

		if (is_numeric($last))
		{
			return $setting;
		}

		switch ($last)
		{
			case 't':
				$val *= 1024;
			case 'g':
				$val *= 1024;
			case 'm':
				$val *= 1024;
			case 'k':
				$val *= 1024;
		}

		return (int) $val;
	}

	/**
	 * Get the PHP memory limit in bytes
	 *
	 * @return int|null  Memory limit in bytes or null if we can't figure it out.
	 */
	protected function getMemoryLimit()
	{
		if (!function_exists('ini_get'))
		{
			return null;
		}

		$memLimit = ini_get("memory_limit");

		if ((is_numeric($memLimit) && ($memLimit < 0)) || !is_numeric($memLimit))
		{
			$memLimit = 0; // 1.2a3 -- Rare case with memory_limit < 0, e.g. -1Mb!
		}

		$memLimit = $this->humanToIntegerBytes($memLimit);

		return $memLimit;
	}

	/**
	 * @return void
	 */
	private function parametersArrayToProperties(): void
	{
		$this->driver             = $this->_parametersArray['driver'] ?? $this->driver;
		$this->host               = $this->_parametersArray['host'] ?? $this->host;
		$this->port               = $this->_parametersArray['port'] ?? $this->port;
		$this->socket             = $this->_parametersArray['socket'] ?? $this->socket;
		$this->username           = $this->_parametersArray['username'] ?? $this->username;
		$this->username           = $this->_parametersArray['user'] ?? $this->username;
		$this->password           = $this->_parametersArray['password'] ?? $this->password;
		$this->database           = $this->_parametersArray['database'] ?? $this->database;
		$this->prefix             = $this->_parametersArray['prefix'] ?? $this->prefix;
		$this->dumpFile           = $this->_parametersArray['dumpFile'] ?? $this->dumpFile;
		$this->processEmptyPrefix = $this->_parametersArray['process_empty_prefix'] ?? $this->processEmptyPrefix;
		$this->ssl                = $this->_parametersArray['ssl'] ?? $this->ssl;
		$this->ssl                = is_array($this->ssl) ? $this->ssl : [];

		$this->ssl['enable']             = (bool) (($this->ssl['enable'] ?? $this->_parametersArray['dbencryption'] ?? false) ?: false);
		$this->ssl['cipher']             = ($this->ssl['cipher'] ?? $this->_parametersArray['dbsslcipher'] ?? null) ?: null;
		$this->ssl['ca']                 = ($this->ssl['ca'] ?? $this->_parametersArray['dbsslca'] ?? null) ?: null;
		$this->ssl['capath']             = ($this->ssl['capath'] ?? $this->_parametersArray['dbsslcapath'] ?? null) ?: null;
		$this->ssl['key']                = ($this->ssl['key'] ?? $this->_parametersArray['dbsslkey'] ?? null) ?: null;
		$this->ssl['cert']               = ($this->ssl['cert'] ?? $this->_parametersArray['dbsslcert'] ?? null) ?: null;
		$this->ssl['verify_server_cert'] = (bool) (($this->ssl['verify_server_cert'] ?? $this->_parametersArray['dbsslverifyservercert'] ?? false) ?: false);
	}

	private function workaroundWrongPrefix(): void
	{
		// Let's see what kind of prefix I have
		$allLowerCasePrefix = strtolower($this->prefix);
		$allUpperCasePrefix = strtoupper($this->prefix);
		$isUpperCasePrefix  = $this->prefix === $allUpperCasePrefix;
		$isLowerCasePrefix  = $this->prefix === $allLowerCasePrefix;
		$isMixedCasePrefix  = !$isUpperCasePrefix && !$isLowerCasePrefix;

		// Log a message
		if ($isUpperCasePrefix)
		{
			Factory::getLog()->info(
				sprintf(
					'You have an all uppercase database prefix (%s). This might cause backup and restoration issues. We are applying automatic mitigations.',
					$this->prefix
				)
			);
		}
		elseif (!$isLowerCasePrefix)
		{
			Factory::getLog()->info(
				sprintf(
					'You have a mixed-case database prefix (%s). This might cause backup and restoration issues. We are applying automatic mitigations.',
					$this->prefix
				)
			);
		}

		// Check if I have any tables with any form of the prefix
		$allTables = $this->getAllTables();

		$hasOriginalTables = array_reduce(
			$allTables,
			function (bool $carry, string $table): bool {
				return $carry || substr($table, 0, strlen($this->prefix)) === $this->prefix;
			},
			false
		);

		$hasLowercaseTables = array_reduce(
			$allTables,
			function (bool $carry, string $table) use ($allLowerCasePrefix): bool {
				return $carry || substr($table, 0, strlen($allLowerCasePrefix)) === $allLowerCasePrefix;
			},
			false
		);

		$hasUppercaseTables = array_reduce(
			$allTables,
			function (bool $carry, string $table) use ($allUpperCasePrefix): bool {
				return $carry || substr($table, 0, strlen($allUpperCasePrefix)) === $allUpperCasePrefix;
			},
			false
		);

		$hasWrongMixedCaseTables = array_reduce(
			$allTables,
			function (bool $carry, string $table) use ($allUpperCasePrefix, $allLowerCasePrefix): bool {
				if ($carry)
				{
					return $carry;
				}

				$prefix = substr($table, 0, strlen($allUpperCasePrefix));

				if ($prefix === $allUpperCasePrefix || $prefix === $allLowerCasePrefix || $prefix === $this->prefix)
				{
					return false;
				}

				return strtolower($prefix) === strtolower($this->prefix);
			},
			false
		);

		// Set up the warning message
		$warningMessage = sprintf(
			'You have database tables whose name starts with a form of the database prefix (%s) which has a different letter case. This WILL cause problems if you restore your site on Windows or macOS. We strongly recommend excluding all tables which do not start with the configured prefix, exactly as shown above (case-sensitive).',
			$this->prefix
		);

		/**
		 * We have tables returned with the original prefix (e.g. fOo_).
		 *
		 * We won't change the prefix, but we have to warn the user if there is a mix of prefixes in the installed
		 * tables.
		 *
		 * We warn when:
		 * - Any kind of prefix, there are tables with a mixed case prefix which doesn't match the configured one.
		 * - Lowercase prefix, there are uppercase tables
		 * - Uppercase prefix, there are lowercase tables
		 * - Mixed case prefix, there are upper- or lowercase tables
		 */
		if ($hasOriginalTables)
		{
			if (
				$hasWrongMixedCaseTables
				|| ($isLowerCasePrefix && $hasUppercaseTables)
				|| ($isUpperCasePrefix && $hasLowercaseTables)
				|| ($isMixedCasePrefix && ($hasLowercaseTables || $hasUppercaseTables))
			)
			{
				Factory::getLog()->warning($warningMessage);
			}

			return;
		}

		/**
		 * No tables with this prefix. Nothing for me to do.
		 *
		 * At this point we have checked if there are any tables with the mixed case prefix, the all lowercase prefix,
		 * and the uppercase prefix. None was found in any of these cases.
		 *
		 * I will not change the prefix to back up. However, if there are tables with the wrong mixed case format of the
		 * prefix I will have to issue a warning.
		 */
		if (!$hasLowercaseTables && !$hasUppercaseTables)
		{
			if ($hasWrongMixedCaseTables)
			{
				Factory::getLog()->warning($warningMessage);
			}

			return;
		}

		if (
			($isLowerCasePrefix && !$hasLowercaseTables && $hasUppercaseTables)
			|| ($isMixedCasePrefix && $hasUppercaseTables)
		)
		{
			Factory::getLog()->info(
				sprintf(
					'Auto-fixing the database prefix: you have configured the database table name prefix %s but we could not find any tables with this prefix. Instead, we found tables with the %s prefix; using that instead.',
					$this->prefix, $allUpperCasePrefix
				)
			);

			$this->_parametersArray['prefix'] = $allUpperCasePrefix;
			$this->prefix                     = $allUpperCasePrefix;
		}
		elseif (
			($isUpperCasePrefix && !$hasUppercaseTables && $hasLowercaseTables)
			|| ($isMixedCasePrefix && $hasLowercaseTables)
		)
		{
			Factory::getLog()->info(
				sprintf(
					'Auto-fixing the database prefix: you have configured the database table name prefix %s but we could not find any tables with this prefix. Instead, we found tables with the %s prefix; using that instead.',
					$this->prefix, $allLowerCasePrefix
				)
			);

			$this->_parametersArray['prefix'] = $allLowerCasePrefix;
			$this->prefix                     = $allLowerCasePrefix;
		}
		else
		{
			Factory::getLog()->warning(
				sprintf(
					'WRONG DATABASE PREFIX. You have configured the database table name prefix %s but we could not find any tables with this prefix, its lowercase (%s) or uppercase (%s) form. THIS WILL CAUSE RESTORATION PROBLEMS. Please rename your tables starting with different forms of the %1$s prefix so that they all start with its lowercase form (%2$s), then retake the backup.',
					$this->prefix, $allLowerCasePrefix, $allUpperCasePrefix
				)
			);

			return;
		}

		if ($hasLowercaseTables && $hasUppercaseTables)
		{
			Factory::getLog()->warning($warningMessage);
		}
	}
}
Dump/Native/Sqlite.php000060400000002521152456704520010657 0ustar00<?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\Dump\Native;

defined('AKEEBAENGINE') || die();

use RuntimeException;

/**
 * Dump class for the "None" database driver (ie no database used by the application)
 */
#[\AllowDynamicProperties]
class Sqlite extends None
{
	public function __construct()
	{
		parent::__construct();

		throw new RuntimeException("Please do not add SQLite databases, they are files. If they are under your site's root they are backed up automatically. Otherwise use the Off-site Directories Inclusion to include them in the backup.");
	}

}
Dump/Native/None.php000060400000003772152456704520010326 0ustar00<?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\Dump\Native;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Dump\Base;
use Akeeba\Engine\Factory;

/**
 * Dump class for the "None" database driver (ie no database used by the application)
 */
#[\AllowDynamicProperties]
class None extends Base
{
	public function __construct()
	{
		parent::__construct();
	}

	/** @inheritDoc */
	protected function getTablesToBackup(): void
	{
	}

	/** @inheritDoc */
	protected function stepDatabaseDump(): void
	{
		Factory::getLog()->info("Reminder: database definitions using the 'None' driver result in no data being backed up.");

		$this->setState(self::STATE_FINISHED);
	}

	/** @inheritDoc */
	protected function getDatabaseNameFromConnection(): string
	{
		return '';
	}

	/** @inheritDoc */
	protected function getAllTables(): array
	{
		return [];
	}

	protected function _run()
	{
		Factory::getLog()->info("Reminder: database definitions using the 'None' driver result in no data being backed up.");

		$this->setState(self::STATE_POSTRUN);
	}

	protected function _finalize()
	{
		Factory::getLog()->info("Reminder: database definitions using the 'None' driver result in no data being backed up.");

		$this->setState(self::STATE_FINISHED);
	}
}
Dump/Native/Mysql.php000060400000212076152456704520010533 0ustar00<?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\Dump\Native;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Driver\QueryException;
use Akeeba\Engine\Dump\Base;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Exception;
use RuntimeException;

/**
 * A generic MySQL database dump class.
 * Now supports views; merge, in-memory, federated, blackhole, etc tables
 * Configuration parameters:
 * host            <string>    MySQL database server host name or IP address
 * port            <string>    MySQL database server port (optional)
 * username        <string>    MySQL user name, for authentication
 * password        <string>    MySQL password, for authentication
 * database        <string>    MySQL database
 * dumpFile        <string>    Absolute path to dump file; must be writable (optional; if left blank it is
 * automatically calculated)
 */
#[\AllowDynamicProperties]
class Mysql extends Base
{
	/**
	 * The primary key structure of the currently backed up table. The keys contained are:
	 * - table        The name of the table being backed up
	 * - field        The name of the primary key field
	 * - value        The last value of the PK field
	 *
	 * @var array
	 */
	protected $table_autoincrement = [
		'table' => null,
		'field' => null,
		'value' => null,
	];

	private $columnListColumnType = [];

	private $columnListSelectColumn = '*';

	private $lastTableColumnType = null;

	private $lastTableSelectColumn = null;

	/**
	 * Implements the constructor of the class
	 *
	 * @return  void
	 */
	public function __construct()
	{
		parent::__construct();

		Factory::getLog()->debug(__CLASS__ . " :: New instance");
	}

	/**
	 * Replaces the table names in the CREATE query with their abstract form. Optionally updates dependencies.
	 *
	 * @param   string  $tableName        The table name the CREATE query is for
	 * @param   string  $tableSql         The CREATE query itself
	 * @param   bool    $withDependecies  Should I update dependencies?
	 *
	 * @return  array [$dependencies, $modifiedSQLQuery] - Dependency information for the table (if $withDependencies)
	 *                and the new CREATE query with all table names replaced with abstract versions.
	 *
	 * @throws  Exception  When we cannot get the DB object
	 */
	public function replaceTableNamesWithAbstracts($tableName, $tableSql, $withDependecies = false)
	{
		// Initialization
		$dependencies = [];
		$tableNameMap = $this->table_name_map;
		$db           = $this->getDB();

		if (!array_key_exists($tableName, $tableNameMap))
		{
			$tableNameMap[$tableName] = $this->getAbstract($tableName);
		}

		foreach ($tableNameMap as $fullName => $abstractName)
		{
			$quotedFullName     = $db->quoteName($fullName);
			$quotedAbstractName = $db->quoteName($abstractName);
			$pos                = strpos($tableSql, $quotedFullName);
			$numReplacements    = 0;

			if ($pos !== false)
			{
				$numReplacements = 1;

				// Do the replacement
				$tableSql = str_replace($quotedFullName, $quotedAbstractName, $tableSql);
			}
			elseif (!is_numeric($fullName))
			{
				$offset                   = 0;
				$fullNameLength           = strlen($fullName);
				$quotedAbstractNameLength = strlen($quotedAbstractName);

				/**
				 * We need to detect the edges of table names. If they are enclosed in backticks it's pretty clear. If they are
				 * not, e.g. in the definitions of TRIGGERs, we need to base our detection on the valid characters for
				 * unquoted MySQL table names per https://dev.mysql.com/doc/refman/5.7/en/identifiers.html
				 */
				[$bareCharRegex, $regexFlags] = $this->getMySQLIdentifierCharacterRegEx();
				$fullCharRegex = "/$bareCharRegex/$regexFlags";

				while (true)
				{
					$pos = strpos($tableSql, $fullName, $offset);

					if ($pos === false)
					{
						break;
					}

					// Skip over table-name-like strings in strings enclosed by single quotes
					$quotePos = strpos($tableSql, "'", $offset);

					if ($quotePos !== false && $quotePos < $pos)
					{
						// The table-like token is inside a string. Find its end.
						while (true)
						{
							$nextPos = strpos($tableSql, "'", $quotePos + 1);

							if ($nextPos === false)
							{
								break;
							}

							$prevChar = $nextPos > 0 ? $tableSql[$nextPos - 1] : null;
							$nextChar = strlen($tableSql) > $nextPos + 1 ? $tableSql[$nextPos + 1] : null;

							// Catch quote escaped as \'
							if ($prevChar === '\\')
							{
								$quotePos = $nextPos;

								continue;
							}

							// Catch quote escaped as ''
							if ($nextChar === "'")
							{
								$quotePos = $nextPos + 1;

								continue;
							}

							$offset = $nextPos + 1;

							continue 2;
						}
					}

					// Skip over table-name-like strings in strings enclosed by double quotes
					$quotePos = strpos($tableSql, '"', $offset);

					if ($quotePos !== false && $quotePos < $pos)
					{
						// The table-like token is inside a string. Find its end.
						while (true)
						{
							$nextPos = strpos($tableSql, '"', $quotePos + 1);

							if ($nextPos === false)
							{
								break;
							}

							$prevChar = $nextPos > 0 ? $tableSql[$nextPos - 1] : null;
							$nextChar = strlen($tableSql) > $nextPos + 1 ? $tableSql[$nextPos + 1] : null;

							// Catch quote escaped as \"
							if ($prevChar === '\\')
							{
								$quotePos = $nextPos;

								continue;
							}

							// Catch quote escaped as ""
							if ($nextChar === '"')
							{
								$quotePos = $nextPos + 1;

								continue;
							}

							$offset = $nextPos + 1;

							continue 2;
						}
					}

					// Catch table-name-like substring inside another table's name
					$previousChar    = ($pos > 0) ? substr($tableSql, $pos - 1, 1) : '';
					$nextChar        = ($pos < (strlen($tableSql) - $fullNameLength)) ? substr($tableSql, $pos + $fullNameLength, 1) : '';
					$prevIsTableChar = $previousChar === '' ? false : preg_match($fullCharRegex, $previousChar);
					$nextIsTableChar = $nextChar === '' ? false : preg_match($fullCharRegex, $nextChar);

					if ($prevIsTableChar || $nextIsTableChar)
					{
						$offset = $pos + 1;

						continue;
					}

					$before = ($pos > 0) ? substr($tableSql, 0, $pos) : '';
					$after  = ($pos < (strlen($tableSql) - $fullNameLength)) ? substr($tableSql, $pos + $fullNameLength) : '';

					$numReplacements++;
					$tableSql = $before . $quotedAbstractName . $after;

					$offset = $pos + $quotedAbstractNameLength;
				}
			}

			if ($withDependecies && $numReplacements && ($fullName != $tableName))
			{
				// Add a reference hit
				$this->dependencies[$fullName][] = $tableName;
				// Add the dependency to this table's metadata
				$dependencies[] = $fullName;
			}
		}

		return [$dependencies, $tableSql];
	}

	/**
	 * Creates a drop query from a CREATE query
	 *
	 * @param   string  $query  The CREATE query to process
	 *
	 * @return  string  The DROP statement
	 */
	protected function createDrop($query)
	{
		$db = $this->getDB();

		// Initialize
		$dropQuery = '';

		// Parse CREATE TABLE commands
		if (substr($query, 0, 12) == 'CREATE TABLE')
		{
			// Try to get the table name
			$restOfQuery = trim(substr($query, 12, strlen($query) - 12)); // Rest of query, after CREATE TABLE

			// Is there a backtick?
			if (substr($restOfQuery, 0, 1) == '`')
			{
				// There is... Good, we'll just find the matching backtick
				$pos       = strpos($restOfQuery, '`', 1);
				$tableName = substr($restOfQuery, 1, $pos - 1);
			}
			else
			{
				// Nope, let's assume the table name ends in the next blank character
				$pos       = strpos($restOfQuery, ' ', 1);
				$tableName = substr($restOfQuery, 0, $pos);
			}

			unset($restOfQuery);

			// Try to drop the table anyway
			$dropQuery = 'DROP TABLE IF EXISTS ' . $db->nameQuote($tableName) . ';';
		}
		// Parse CREATE VIEW commands
		elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, ' VIEW ') !== false))
		{
			// Try to get the view name
			$view_pos    = strpos($query, ' VIEW ');
			$restOfQuery = trim(substr($query, $view_pos + 6)); // Rest of query, after VIEW string

			// Is there a backtick?
			if (substr($restOfQuery, 0, 1) == '`')
			{
				// There is... Good, we'll just find the matching backtick
				$pos       = strpos($restOfQuery, '`', 1);
				$tableName = substr($restOfQuery, 1, $pos - 1);
			}
			else
			{
				// Nope, let's assume the table name ends in the next blank character
				$pos       = strpos($restOfQuery, ' ', 1);
				$tableName = substr($restOfQuery, 0, $pos);
			}

			unset($restOfQuery);

			$dropQuery = 'DROP VIEW IF EXISTS ' . $db->nameQuote($tableName) . ';';
		}
		// CREATE PROCEDURE pre-processing
		elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, 'PROCEDURE ') !== false))
		{
			// Try to get the procedure name
			$entity_keyword = ' PROCEDURE ';
			$entity_pos     = strpos($query, $entity_keyword);
			$restOfQuery    = trim(substr($query, $entity_pos + strlen($entity_keyword))); // Rest of query, after entity key string

			// Is there a backtick?
			if (substr($restOfQuery, 0, 1) == '`')
			{
				// There is... Good, we'll just find the matching backtick
				$pos         = strpos($restOfQuery, '`', 1);
				$entity_name = substr($restOfQuery, 1, $pos - 1);
			}
			else
			{
				// Nope, let's assume the entity name ends in the next blank character
				$pos         = strpos($restOfQuery, ' ', 1);
				$entity_name = substr($restOfQuery, 0, $pos);
			}

			unset($restOfQuery);

			$dropQuery = 'DROP' . $entity_keyword . 'IF EXISTS `' . $entity_name . '`;';
		}
		// CREATE FUNCTION pre-processing
		elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, 'FUNCTION ') !== false))
		{
			// Try to get the procedure name
			$entity_keyword = ' FUNCTION ';
			$entity_pos     = strpos($query, $entity_keyword);
			$restOfQuery    = trim(substr($query, $entity_pos + strlen($entity_keyword))); // Rest of query, after entity key string

			// Is there a backtick?
			if (substr($restOfQuery, 0, 1) == '`')
			{
				// There is... Good, we'll just find the matching backtick
				$pos         = strpos($restOfQuery, '`', 1);
				$entity_name = substr($restOfQuery, 1, $pos - 1);
			}
			else
			{
				// Nope, let's assume the entity name ends in the next blank character
				$pos         = strpos($restOfQuery, ' ', 1);
				$entity_name = substr($restOfQuery, 0, $pos);
			}

			unset($restOfQuery);

			// Try to drop the entity anyway
			$dropQuery = 'DROP' . $entity_keyword . 'IF EXISTS `' . $entity_name . '`;';
		}
		// CREATE TRIGGER pre-processing
		elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, 'TRIGGER ') !== false))
		{
			// Try to get the procedure name
			$entity_keyword = ' TRIGGER ';
			$entity_pos     = strpos($query, $entity_keyword);
			$restOfQuery    = trim(substr($query, $entity_pos + strlen($entity_keyword))); // Rest of query, after entity key string

			// Is there a backtick?
			if (substr($restOfQuery, 0, 1) == '`')
			{
				// There is... Good, we'll just find the matching backtick
				$pos         = strpos($restOfQuery, '`', 1);
				$entity_name = substr($restOfQuery, 1, $pos - 1);
			}
			else
			{
				// Nope, let's assume the entity name ends in the next blank character
				$pos         = strpos($restOfQuery, ' ', 1);
				$entity_name = substr($restOfQuery, 0, $pos);
			}

			unset($restOfQuery);

			// Try to drop the entity anyway
			$dropQuery = 'DROP' . $entity_keyword . 'IF EXISTS `' . $entity_name . '`;';
		}

		return $dropQuery;
	}

	/**
	 * Applies the SQL compatibility setting
	 *
	 * @return  void
	 */
	protected function enforceSQLCompatibility()
	{
		$db = $this->getDB();

		// Try to enforce SQL_BIG_SELECTS option
		try
		{
			$db->setQuery('SET sql_big_selects=1');
			$db->query();
		}
		catch (Exception $e)
		{
			// Do nothing; some versions of MySQL don't allow you to use the BIG_SELECTS option.
		}

		$db->resetErrors();
	}

	/**
	 * Return a list of columns and their data types.
	 *
	 * @param   string  $tableAbstract
	 *
	 * @return  array  An array of table columns and their data types.
	 */
	protected function getColumnTypes($tableAbstract)
	{
		if ($this->lastTableColumnType == $tableAbstract)
		{
			return $this->columnListColumnType;
		}

		$this->lastTableColumnType = $tableAbstract;

		try
		{
			$db = $this->getDB();

			$db->setQuery('SHOW COLUMNS FROM ' . $db->qn($tableAbstract));

			$tableCols = $db->loadAssocList();
		}
		catch (Exception $e)
		{
			return $this->columnListColumnType;
		}

		foreach ($tableCols as $col)
		{
			$typeParts                                 = explode('(', $col['Type'], 2);
			$this->columnListColumnType[$col['Field']] = strtoupper($typeParts[0]);
		}

		return $this->columnListColumnType;
	}

// =============================================================================
// Dependency processing - the Twilight Zone starts here
// =============================================================================

	/**
	 * Return the current database name by querying the database connection object (e.g. SELECT DATABASE() in MySQL)
	 *
	 * @return  string
	 */
	protected function getDatabaseNameFromConnection(): string
	{
		$db = $this->getDB();

		try
		{
			$ret = $db->setQuery('SELECT DATABASE()')->loadResult();
		}
		catch (Exception $e)
		{
			return '';
		}

		return empty($ret) ? '' : $ret;
	}

	/**
	 * Get the default database dump batch size from the configuration
	 *
	 * @return  int
	 */
	protected function getDefaultBatchSize()
	{
		static $batchSize = null;

		if (is_null($batchSize))
		{
			$configuration = Factory::getConfiguration();
			$batchSize     = intval($configuration->get('engine.dump.common.batchsize', 1000));

			if ($batchSize <= 0)
			{
				$batchSize = 1000;
			}
		}

		return $batchSize;
	}

	/**
	 * Get a regular expression and its options for valid characters of an unquoted MySQL identifier.
	 *
	 * This is used wherever we need to detect an arbitrary, unquoted MySQL identifier per
	 * https://dev.mysql.com/doc/refman/5.7/en/identifiers.html
	 *
	 * Also what if Unicode support is not compiled in PCRE? In this case we will fall back to a much simpler regex
	 * which only supports the ASCII subset of the allowed characters. In this case your database dump will be wrong
	 * if you use table names with non-ASCII characters.
	 *
	 * Since the detection is horribly slow we cache its results in an internal static variable.
	 *
	 * @return  array  In the format [$regex, $flags]
	 * @since   7.0.0
	 */
	protected function getMySQLIdentifierCharacterRegEx()
	{
		static $validCharRegEx = null;
		static $unicodeFlag = null;

		if (is_null($validCharRegEx) || is_null($unicodeFlag))
		{
			$noUnicode      = @preg_match('/\p{L}/u', 'σ') !== 1;
			$unicodeFlag    = $noUnicode ? '' : 'u';
			$validCharRegEx = $noUnicode ? '[0-9a-zA-Z$_]' : '[0-9a-zA-Z$_]|[\x{0080}-\x{FFFF}]';
		}

		return [$validCharRegEx, $unicodeFlag];
	}

	/**
	 * Get the optimal row batch size for a given table based on the available memory
	 *
	 * @param   string  $tableAbstract     The abstract table name, e.g. #__foobar
	 * @param   int     $defaultBatchSize  The default row batch size in the application configuration
	 *
	 * @return  int
	 */
	protected function getOptimalBatchSize($tableAbstract, $defaultBatchSize)
	{
		$db = $this->getDB();

		try
		{
			$info = $db->setQuery('SHOW TABLE STATUS LIKE ' . $db->q($tableAbstract))->loadAssoc();
		}
		catch (Exception $e)
		{
			return $defaultBatchSize;
		}

		if (!isset($info['Avg_row_length']) || empty($info['Avg_row_length']))
		{
			return $defaultBatchSize;
		}

		// That's the average row size as reported by MySQL.
		$avgRow = str_replace([',', '.'], ['', ''], $info['Avg_row_length']);
		// The memory available for manipulating data is less than the free memory
		$memoryLimit = $this->getMemoryLimit();
		$memoryLimit = empty($memoryLimit) ? 33554432 : $memoryLimit;
		$usedMemory  = memory_get_usage();
		$memoryLeft  = 0.75 * ($memoryLimit - $usedMemory);
		// The 3.25 factor is empirical and leans on the safe side.
		$maxRows = (int) ($memoryLeft / (3.25 * $avgRow));

		return max(1, min($maxRows, $defaultBatchSize));
	}

	/**
	 * Gets the row count for table $tableAbstract. Also updates the $this->maxRange variable.
	 *
	 * @param   string  $tableAbstract  The abstract name of the table (works with canonical names too, though)
	 *
	 * @return  void
	 *
	 * @throws  QueryException
	 */
	protected function getRowCount($tableAbstract)
	{
		$db = $this->getDB();

		$sql = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->select('COUNT(*)')
			->from($db->nameQuote($tableAbstract));

		$errno = 0;
		$error = '';

		try
		{
			$db->setQuery($sql);
			$this->maxRange = $db->loadResult();

			if (is_null($this->maxRange))
			{
				$errno = $db->getErrorNum();
				$error = $db->getErrorMsg(false);
			}
		}
		catch (Exception $e)
		{
			$this->maxRange = null;
			$errno          = $e->getCode();
			$error          = $e->getMessage();
		}

		if (is_null($this->maxRange))
		{
			Factory::getLog()->warning("Cannot get number of rows of $tableAbstract. MySQL error $errno: $error");

			return;
		}

		Factory::getLog()->debug("Rows on " . $tableAbstract . " : " . $this->maxRange);
	}

	/**
	 * Return a list of columns to use in the SELECT query for dumping table data.
	 *
	 * This is used to filter out all generated rows.
	 *
	 * @param   string  $tableAbstract
	 *
	 * @return  string|array  An array of table columns or the string literal '*' to quickly select all columns.
	 *
	 * @see  https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html
	 */
	protected function getSelectColumns($tableAbstract)
	{
		if ($this->lastTableSelectColumn == $tableAbstract)
		{
			return $this->columnListSelectColumn;
		}

		$this->lastTableSelectColumn = $tableAbstract;

		try
		{
			$db = $this->getDB();

			$db->setQuery('SHOW COLUMNS FROM ' . $db->qn($tableAbstract));

			$tableCols = $db->loadAssocList();
		}
		catch (Exception $e)
		{
			return $this->columnListSelectColumn;
		}

		$totalColumns                 = is_array($tableCols) || $tableCols instanceof \Countable ? count($tableCols) : 0;
		$this->columnListSelectColumn = [];

		$hasInvisibleColumns = false;

		foreach ($tableCols as $col)
		{
			// Skip over generated columns
			$attribs = array_map('strtoupper', empty($col['Extra']) ? [] : explode(' ', $col['Extra']));

			if (in_array('GENERATED', $attribs))
			{
				continue;
			}

			if (in_array('INVISIBLE', $attribs))
			{
				$hasInvisibleColumns = true;
			}

			$this->columnListSelectColumn[] = $col['Field'];
		}

		if (!$hasInvisibleColumns && ($totalColumns == count($this->columnListSelectColumn)))
		{
			$this->columnListSelectColumn = '*';
		}

		return $this->columnListSelectColumn;
	}

	/**
	 * Scans the database for tables to be backed up and sorts them according to
	 * their dependencies on one another. Updates $this->dependencies.
	 *
	 * @return  void
	 */
	protected function getTablesToBackup(): void
	{
		// Makes the MySQL connection compatible with our class
		$this->enforceSQLCompatibility();

		$configuration = Factory::getConfiguration();
		$notracking    = $configuration->get('engine.dump.native.nodependencies', 0);

		// First, get a map of table names <--> abstract names
		$this->get_tables_mapping();

		if ($notracking)
		{
			// Do not process table & view dependencies
			$this->get_tables_data_without_dependencies();
		}
		// Process table & view dependencies (default)
		else
		{
			// Find the type and CREATE command of each table/view in the database
			$this->get_tables_data();

			// Process dependencies and rearrange tables respecting them
			$this->process_dependencies();

			// Remove dependencies array
			$this->dependencies = [];
		}
	}

	/**
	 * Gets the CREATE TABLE command for a given table/view/procedure/function/trigger
	 *
	 * @param   string  $table_abstract  The abstracted name of the entity
	 * @param   string  $table_name      The name of the table
	 * @param   string  $type            The type of the entity to scan. If it's found to differ, the correct type is
	 *                                   returned.
	 * @param   array   $dependencies    The dependencies of this table
	 *
	 * @return  string|null  The CREATE command
	 */
	protected function get_create($table_abstract, $table_name, &$type, &$dependencies)
	{
		$configuration = Factory::getConfiguration();
		$notracking    = $configuration->get('engine.dump.native.nodependencies', 0);

		$db = $this->getDB();

		switch ($type)
		{
			case 'table':
			case 'merge':
			case 'view':
			default:
				$sql = "SHOW CREATE TABLE `$table_abstract`";
				break;

			case 'procedure':
				$sql = "SHOW CREATE PROCEDURE `$table_abstract`";
				break;

			case 'function':
				$sql = "SHOW CREATE FUNCTION `$table_abstract`";
				break;

			case 'trigger':
				$sql = "SHOW CREATE TRIGGER `$table_abstract`";
				break;
		}

		$db->setQuery($sql);

		try
		{
			$temp = $db->loadRowList();
		}
		catch (Exception $e)
		{
			// If the query failed we don't have the necessary SHOW privilege. Log the error and fake an empty reply.
			$entityType = ($type == 'merge') ? 'table' : $type;
			$msg        = $e->getMessage();
			Factory::getLog()->warning("Cannot get the structure of $entityType $table_abstract. Database returned error $msg running $sql  Please check your database privileges. Your database backup may be incomplete.");

			$db->resetErrors();

			$temp = [
				['', '', ''],
			];
		}

		if (in_array($type, ['procedure', 'function', 'trigger']))
		{
			$table_sql = $temp[0][2];

			if (empty($table_sql))
			{
				Factory::getLog()->warning("Cannot get the structure of $type $table_abstract. The database refused to return the CREATE command for this $type. Please check your database privileges. Your database backup may be incomplete.");

				return null;
			}

			// MySQL adds the database name into everything. We have to remove it.
			$dbName    = $db->qn($this->database) . '.`';
			$table_sql = str_replace($dbName, '`', $table_sql);

			// These can contain comment lines, starting with a double dash. Remove them.
			$table_sql = trim($table_sql);

			/**
			 * Remove the definer from the CREATE PROCEDURE/TRIGGER/FUNCTION. For example, MySQL returns this:
			 * CREATE DEFINER=`myuser`@`localhost` PROCEDURE `abc_myProcedure`() ...
			 * If you're restoring on a different machine the definer will probably be invalid, therefore we need to
			 * remove it from the (portable) output.
			 *
			 * Remember, $table_sql may be multiline. Therefore we need to process only the first line and append any
			 * further lines to the CREATE statement.
			 */
			$table_sql = trim($table_sql);
			$lines     = explode("\n", $table_sql);
			$firstLine = array_shift($lines);
			$pattern   = '/^CREATE(.*?) ' . strtoupper($type) . ' (.*)/i';
			$result    = preg_match($pattern, $firstLine, $matches);
			$table_sql = 'CREATE ' . strtoupper($type) . ' ' . $matches[2] . "\n" . implode("\n", $lines);
			$table_sql = trim($table_sql);
		}
		else
		{
			$table_sql = $temp[0][1];
		}
		unset($temp);

		// Smart table type detection
		if (in_array($type, ['table', 'merge', 'view']))
		{
			// Check for CREATE VIEW
			$pattern = '/^CREATE(.*?) VIEW (.*)/i';
			$result  = preg_match($pattern, $table_sql, $matches);

			if ($result === 1)
			{
				// This is a view.
				$type = 'view';

				/**
				 * Newer MySQL versions add the definer and other information in the CREATE VIEW output, e.g.
				 * CREATE ALGORITHM=UNDEFINED DEFINER=`muyser`@`localhost` SQL SECURITY DEFINER VIEW `abc_myview` AS ...
				 * We need to remove that to prevent restoration troubles.
				 */
				$table_sql = 'CREATE VIEW ' . $matches[2];
			}
			else
			{
				// This is a table.
				$type = 'table';

				// # Fix 3.2.1: USING BTREE / USING HASH in indices causes issues migrating from MySQL 5.1+ hosts to
				// MySQL 5.0 hosts
				if ($configuration->get('engine.dump.native.nobtree', 1))
				{
					$table_sql = str_replace(' USING BTREE', ' ', $table_sql);
					$table_sql = str_replace(' USING HASH', ' ', $table_sql);
				}

				// Translate TYPE= to ENGINE=
				$table_sql = str_replace('TYPE=', 'ENGINE=', $table_sql);

				/**
				 * Remove the TABLESPACE option.
				 *
				 * The format of the TABLESPACE table option is:
				 * TABLESPACE tablespace_name [STORAGE {DISK|MEMORY}]
				 * where tablespace_name can be a quoted or unquoted identifier.
				 */
				[$validCharRegEx, $unicodeFlag] = $this->getMySQLIdentifierCharacterRegEx();
				$tablespaceName = "((($validCharRegEx){1,})|(`.*`))";
				$suffix         = 'STORAGE\s{1,}(DISK|MEMORY)';
				$regex          = "#TABLESPACE\s{1,}$tablespaceName\s{0,}($suffix){0,1}#i" . $unicodeFlag;
				$table_sql      = preg_replace($regex, '', $table_sql);

				// Remove table options {DATA|INDEX} DIRECTORY
				$regex     = "#(DATA|INDEX)\s{1,}DIRECTORY\s*=?\s*'.*'#i";
				$table_sql = preg_replace($regex, '', $table_sql);

				// Remove table options ROW_FORMAT=whatever
				$regex     = "#ROW_FORMAT\s*=\s*[A-Z]{1,}#i";
				$table_sql = preg_replace($regex, '', $table_sql);

				// Remove MariaDB MyISAM option PAGE_CHECKSUM
				$regex     = "#PAGE_CHECKSUM\s*=\s*[\d]{1,}#i";
				$table_sql = preg_replace($regex, '', $table_sql);

				// Abstract the names of table constraints and indices
				$regex     = "#(CONSTRAINT|KEY|INDEX)\s{1,}`{$this->prefix}#i";
				$table_sql = preg_replace($regex, '$1 `#__', $table_sql);
			}

			// Is it a VIEW but we don't have SHOW VIEW privileges?
			if (empty($table_sql))
			{
				$type = 'view';
			}
		}

		/**
		 * Replace table name and names of referenced tables with their abstracted forms and populate dependency tables
		 * at the same time.
		 */
		// On DB only backup we don't want any replacing to take place, do we?
		if (!Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1))
		{
			$old_table_sql = $table_sql;
		}

		/**
		 * Replace the table names in the CREATE command with the abstract versions.
		 *
		 * Moreover, it updates the dependency tracking information.
		 *
		 * We have to quote the table name. If we don't we'll get wrong results. Imagine that you have a column whose
		 * name starts with the string literal of the table name itself.
		 *
		 * Example: table `poll`, column `poll_id` would become #__poll, #__poll_id
		 *
		 * By quoting before we make sure this won't happen.
		 */
		[$dependencies, $table_sql] = $this->replaceTableNamesWithAbstracts($table_name, $table_sql, !$notracking);

		// On DB only backup we don't want any replacing to take place, do we?
		if (!Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1))
		{
			$table_sql = $old_table_sql;
		}

		// Add final semicolon and newline character
		$table_sql .= ";\n";

		/**
		 * Views, procedures, functions and triggers may contain the database name followed by the table name, always
		 * quoted e.g. `db`.`table_name`  We need to replace all these instances with just the table name. The only
		 * reliable way to do that is to look for "`db`.`" and replace it with "`"
		 */
		if (in_array($type, ['view', 'procedure', 'function', 'trigger']))
		{
			$dbName      = $db->qn($this->getDatabaseName());
			$dummyQuote  = $db->qn('foo');
			$findWhat    = $dbName . '.' . substr($dummyQuote, 0, 1);
			$replaceWith = substr($dummyQuote, 0, 1);
			$table_sql   = str_replace($findWhat, $replaceWith, $table_sql);
		}

		// Post-process CREATE VIEW
		if ($type == 'view')
		{
			$pos_view = strpos($table_sql, ' VIEW ');

			if ($pos_view > 7)
			{
				// Only post process if there are view properties between the CREATE and VIEW keywords
				$propstring = substr($table_sql, 7, $pos_view - 7); // Properties string
				// Fetch the ALGORITHM={UNDEFINED | MERGE | TEMPTABLE} keyword
				$algostring = '';
				$algo_start = strpos($propstring, 'ALGORITHM=');

				if ($algo_start !== false)
				{
					$algo_end   = strpos($propstring, ' ', $algo_start);
					$algostring = substr($propstring, $algo_start, $algo_end - $algo_start + 1);
				}

				// Create our modified create statement
				$table_sql = 'CREATE OR REPLACE ' . $algostring . substr($table_sql, $pos_view);
			}
		}
		elseif ($type == 'procedure')
		{
			$pos_entity = stripos($table_sql, ' PROCEDURE ');

			if ($pos_entity !== false)
			{
				$table_sql = 'CREATE' . substr($table_sql, $pos_entity);
			}
		}
		elseif ($type == 'function')
		{
			$pos_entity = stripos($table_sql, ' FUNCTION ');

			if ($pos_entity !== false)
			{
				$table_sql = 'CREATE' . substr($table_sql, $pos_entity);
			}
		}
		elseif ($type == 'trigger')
		{
			$pos_entity = stripos($table_sql, ' TRIGGER ');

			if ($pos_entity !== false)
			{
				$table_sql = 'CREATE' . substr($table_sql, $pos_entity);
			}
		}

		return $table_sql;
	}

	/**
	 * Populates the _tables array with the metadata of each table and generates
	 * dependency information for views and merge tables. Updates $this->tables_data.
	 *
	 * @return  void
	 */
	protected function get_tables_data()
	{
		Factory::getLog()->debug(__CLASS__ . " :: Starting CREATE TABLE and dependency scanning");

		// Get a database connection
		$db = $this->getDB();

		Factory::getLog()->debug(__CLASS__ . " :: Got database connection");

		// Reset internal tables
		$this->tables_data  = [];
		$this->dependencies = [];

		// Get a list of tables where their engine type is shown
		$sql = 'SHOW TABLES';
		$db->setQuery($sql);
		$metadata_list = $db->loadRowList();

		Factory::getLog()->debug(__CLASS__ . " :: Got SHOW TABLES");

		// Get filters and filter root
		$registry = Factory::getConfiguration();
		$root     = $registry->get('volatile.database.root', '[SITEDB]');
		$filters  = Factory::getFilters();

		foreach ($metadata_list as $table_metadata)
		{
			// Skip over tables not included in the backup set
			if (!array_key_exists($table_metadata[0], $this->table_name_map))
			{
				continue;
			}

			// Basic information
			$table_name     = $table_metadata[0];
			$table_abstract = $this->table_name_map[$table_metadata[0]];
			$new_entry      = [
				'type'         => 'table',
				'dump_records' => true,
			];

			// Get the CREATE command
			$dependencies              = [];
			$new_entry['create']       = $this->get_create($table_abstract, $table_name, $new_entry['type'], $dependencies);

			if ($new_entry['create'] === null)
			{
				continue;
			}

			$new_entry['dependencies'] = $dependencies;

			if ($new_entry['type'] == 'view')
			{
				$new_entry['dump_records'] = false;
			}
			else
			{
				$new_entry['dump_records'] = true;
			}

			// Scan for the table engine.
			$engine = null; // So that we detect VIEWs correctly

			if ($new_entry['type'] == 'table')
			{
				$engine      = 'MyISAM'; // So that even with MySQL 4 hosts we don't screw this up
				$engine_keys = ['ENGINE=', 'TYPE='];

				foreach ($engine_keys as $engine_key)
				{
					$start_pos = strrpos($new_entry['create'], $engine_key);

					if ($start_pos !== false)
					{
						// Advance the start position just after the position of the ENGINE keyword
						$start_pos += strlen($engine_key);
						// Try to locate the space after the engine type
						$end_pos = stripos($new_entry['create'], ' ', $start_pos);

						if ($end_pos === false)
						{
							// Uh... maybe it ends with ENGINE=EngineType;
							$end_pos = stripos($new_entry['create'], ';', $start_pos);
						}

						if ($end_pos !== false)
						{
							// Grab the string
							$engine = substr($new_entry['create'], $start_pos, $end_pos - $start_pos);

							if (empty($engine))
							{
								Factory::getLog()->debug("*** DEBUG *** $table_name - engine $engine");
								Factory::getLog()->debug($new_entry['create']);
								Factory::getLog()->debug("start $start_pos - end $end_pos");
							}
						}
					}
				}

				$engine = strtoupper($engine);
			}

			switch ($engine)
			{
				/*
				// Views -- They are detected based on their CREATE statement
				case null:
					$new_entry['type'] = 'view';
					$new_entry['dump_records'] = false;
					break;
				*/

				// Merge tables
				case 'MRG_MYISAM':
					$new_entry['type']         = 'merge';
					$new_entry['dump_records'] = false;

					break;

				// Tables whose data we do not back up (memory, federated and can-have-no-data tables)
				case 'MEMORY':
				case 'EXAMPLE':
				case 'BLACKHOLE':
				case 'FEDERATED':
					$new_entry['dump_records'] = false;

					break;

				// Normal tables and VIEWs
				default:
					break;
			}

			// Table Data Filter - skip dumping table contents of filtered out tables
			if ($filters->isFiltered($table_abstract, $root, 'dbobject', 'content'))
			{
				$new_entry['dump_records'] = false;
			}

			$this->tables_data[$table_name] = $new_entry;
		}

		Factory::getLog()->debug(__CLASS__ . " :: Got table list");

		// If we have MySQL > 5.0 add stored procedures, stored functions and triggers
		$enable_entities = $registry->get('engine.dump.native.advanced_entitites', true);

		if ($enable_entities)
		{
			Factory::getLog()->debug(__CLASS__ . " :: Listing MySQL entities");
			// Get a list of procedures
			$sql = 'SHOW PROCEDURE STATUS WHERE `Db`=' . $db->quote($this->database);
			$db->setQuery($sql);

			try
			{
				$metadata_list = $db->loadRowList();
			}
			catch (Exception $e)
			{
				$metadata_list = null;
			}

			if (is_array($metadata_list))
			{
				if (count($metadata_list))
				{
					foreach ($metadata_list as $entity_metadata)
					{
						// Skip over entities not included in the backup set
						if (!array_key_exists($entity_metadata[1], $this->table_name_map))
						{
							continue;
						}

						// Basic information
						$entity_name     = $entity_metadata[1];
						$entity_abstract = $this->table_name_map[$entity_metadata[1]];
						$new_entry       = [
							'type'         => 'procedure',
							'dump_records' => false,
						];

						// There's no point trying to add a non-procedure entity
						if ($entity_metadata[2] != 'PROCEDURE')
						{
							continue;
						}

						$dependencies                    = [];
						$new_entry['create']             = $this->get_create($entity_abstract, $entity_name, $new_entry['type'], $dependencies);

						if ($new_entry['create'] === null)
						{
							continue;
						}

						$new_entry['dependencies']       = $dependencies;
						$this->tables_data[$entity_name] = $new_entry;
					}
				}
			} // foreach

			// Get a list of functions
			$sql = 'SHOW FUNCTION STATUS WHERE `Db`=' . $db->quote($this->database);
			$db->setQuery($sql);

			try
			{
				$metadata_list = $db->loadRowList();
			}
			catch (Exception $e)
			{
				$metadata_list = null;
			}

			if (is_array($metadata_list))
			{
				if (count($metadata_list))
				{
					foreach ($metadata_list as $entity_metadata)
					{
						// Skip over entities not included in the backup set
						if (!array_key_exists($entity_metadata[1], $this->table_name_map))
						{
							continue;
						}

						// Basic information
						$entity_name     = $entity_metadata[1];
						$entity_abstract = $this->table_name_map[$entity_metadata[1]];
						$new_entry       = [
							'type'         => 'function',
							'dump_records' => false,
						];

						// There's no point trying to add a non-function entity
						if ($entity_metadata[2] != 'FUNCTION')
						{
							continue;
						}

						$dependencies                    = [];
						$new_entry['create']             = $this->get_create($entity_abstract, $entity_name, $new_entry['type'], $dependencies);

						if ($new_entry['create'] === null)
						{
							continue;
						}

						$new_entry['dependencies']       = $dependencies;
						$this->tables_data[$entity_name] = $new_entry;
					}
				}
			} // foreach

			// Get a list of triggers
			$sql = 'SHOW TRIGGERS';
			$db->setQuery($sql);

			try
			{
				$metadata_list = $db->loadRowList();
			}
			catch (Exception $e)
			{
				$metadata_list = null;
			}

			if (is_array($metadata_list))
			{
				if (count($metadata_list))
				{
					foreach ($metadata_list as $entity_metadata)
					{
						// Skip over entities not included in the backup set
						if (!array_key_exists($entity_metadata[0], $this->table_name_map))
						{
							continue;
						}

						// Basic information
						$entity_name     = $entity_metadata[0];
						$entity_abstract = $this->table_name_map[$entity_metadata[0]];
						$new_entry       = [
							'type'         => 'trigger',
							'dump_records' => false,
						];

						$dependencies                    = [];
						$new_entry['create']             = $this->get_create($entity_abstract, $entity_name, $new_entry['type'], $dependencies);

						if ($new_entry['create'] === null)
						{
							continue;
						}

						$new_entry['dependencies']       = $dependencies;
						$this->tables_data[$entity_name] = $new_entry;
					}
				}
			} // foreach

			Factory::getLog()->debug(__CLASS__ . " :: Got MySQL entities list");
		}

		/**
		 * // Only store unique values
		 * if(count($dependencies) > 0)
		 * $dependencies = array_unique($dependencies);
		 * /**/
	}

	/**
	 * Populates the _tables array with the metadata of each table.
	 * Updates $this->tables_data and $this->tables.
	 *
	 * @return  void
	 */
	protected function get_tables_data_without_dependencies()
	{
		Factory::getLog()->debug(__CLASS__ . " :: Pushing table data (without dependency tracking)");

		// Reset internal tables
		$this->tables_data  = [];
		$this->dependencies = [];

		// Get filters and filter root
		$registry = Factory::getConfiguration();
		$root     = $registry->get('volatile.database.root', '[SITEDB]');
		$filters  = Factory::getFilters();

		foreach ($this->table_name_map as $table_name => $table_abstract)
		{
			$new_entry = [
				'type'         => 'table',
				'dump_records' => true,
			];

			// Table Data Filter - skip dumping table contents of filtered out tables
			if ($filters->isFiltered($table_abstract, $root, 'dbobject', 'content'))
			{
				$new_entry['dump_records'] = false;
			}

			$this->tables_data[$table_name] = $new_entry;
			$this->tables[]                 = $table_name;
		} // foreach

		Factory::getLog()->debug(__CLASS__ . " :: Got table list");
	}

	/**
	 * Generates a mapping between table names as they're stored in the database
	 * and their abstract representation. Updates $this->table_name_map
	 *
	 * @return  void
	 */
	protected function get_tables_mapping()
	{
		// Get a database connection
		Factory::getLog()->debug(__CLASS__ . " :: Finding tables to include in the backup set");
		$db = $this->getDB();

		// Reset internal tables
		$this->table_name_map = [];

		// Get the list of all database tables
		$sql = "SHOW TABLES";
		$db->setQuery($sql);
		$all_tables = $db->loadResultArray();

		$registry = Factory::getConfiguration();
		$root     = $registry->get('volatile.database.root', '[SITEDB]');

		// If we have filters, make sure the tables pass the filtering
		$filters = Factory::getFilters();

		foreach ($all_tables as $table_name)
		{
			if (substr($table_name, 0, 3) == '#__')
			{
				Factory::getLog()->warning(__CLASS__ . " :: Table $table_name has a prefix of #__. This would cause restoration errors; table skipped.");

				continue;
			}

			if ((strpos($table_name, "\r") !== false) || (strpos($table_name, "\n") !== false))
			{
				$table_name = str_replace(["\r", "\n"], ['\\r', '\\n'], $table_name);
				Factory::getLog()->warning(__CLASS__ . " :: [SECURITY] Table $table_name includes newline characters. Skipping table to protect you against possible MySQL vulnerability CVE-2017-3600 (“Bad Dump”).");

				continue;
			}

			$table_abstract = $this->getAbstract($table_name);

			if (substr($table_abstract, 0, 4) != 'bak_') // Skip backup tables
			{
				// Apply exclusion filters
				if (!$filters->isFiltered($table_abstract, $root, 'dbobject', 'all'))
				{
					Factory::getLog()->info(__CLASS__ . " :: Adding $table_name (internal name $table_abstract)");
					$this->table_name_map[$table_name] = $table_abstract;
				}
				else
				{
					Factory::getLog()->info(__CLASS__ . " :: Skipping $table_name (internal name $table_abstract)");
				}
			}
			else
			{
				Factory::getLog()->info(__CLASS__ . " :: Backup table $table_name automatically skipped.");
			}
		}

		// If we have MySQL > 5.0 add the list of stored procedures, stored functions
		// and triggers, but only if user has allows that and the target compatibility is
		// not MySQL 4! Also, if dependency tracking is disabled, we won't dump triggers,
		// functions and procedures.
		$enable_entities = $registry->get('engine.dump.native.advanced_entitites', true);
		$notracking      = $registry->get('engine.dump.native.nodependencies', 0);

		if (!$enable_entities)
		{
			Factory::getLog()->debug(__CLASS__ . " :: NOT listing stored PROCEDUREs, FUNCTIONs and TRIGGERs (you told me not to)");
		}
		elseif ($notracking != 0)
		{
			Factory::getLog()->debug(__CLASS__ . " :: NOT listing stored PROCEDUREs, FUNCTIONs and TRIGGERs (you have disabled dependency tracking, therefore I can't handle advanced entities)");
		}

		if ($enable_entities && ($notracking == 0))
		{
			// Cache the database name if this is the main site's database

			// 1. Stored procedures
			Factory::getLog()->debug(__CLASS__ . " :: Listing stored PROCEDUREs");
			$sql = "SHOW PROCEDURE STATUS WHERE `Db`=" . $db->quote($this->database);
			$db->setQuery($sql);

			try
			{
				$all_entries = $db->loadResultArray(1);
			}
			catch (Exception $e)
			{
				$all_entries = [];
			}

			// If we have filters, make sure the tables pass the filtering
			if (is_array($all_entries))
			{
				if (count($all_entries))
				{
					foreach ($all_entries as $entity_name)
					{
						if ((strpos($entity_name, "\r") !== false) || (strpos($entity_name, "\n") !== false))
						{
							$entity_name = str_replace(["\r", "\n"], ['\\r', '\\n'], $entity_name);
							Factory::getLog()->warning(__CLASS__ . " :: [SECURITY] Procedure $entity_name includes newline characters. Skipping table to protect you against possible MySQL vulnerability CVE-2017-3600 (“Bad Dump”).");

							continue;
						}

						$entity_abstract = $this->getAbstract($entity_name);

						if (!(substr($entity_abstract, 0, 4) == 'bak_')) // Skip backup entities
						{
							if (!$filters->isFiltered($entity_abstract, $root, 'dbobject', 'all'))
							{
								$this->table_name_map[$entity_name] = $entity_abstract;
							}
						}
					}
				}
			}

			// 2. Stored functions
			Factory::getLog()->debug(__CLASS__ . " :: Listing stored FUNCTIONs");
			$sql = "SHOW FUNCTION STATUS WHERE `Db`=" . $db->quote($this->database);
			$db->setQuery($sql);

			try
			{
				$all_entries = $db->loadResultArray(1);
			}
			catch (Exception $e)
			{
				$all_entries = [];
			}

			// If we have filters, make sure the tables pass the filtering
			if (is_array($all_entries))
			{
				if (count($all_entries))
				{
					foreach ($all_entries as $entity_name)
					{
						if ((strpos($entity_name, "\r") !== false) || (strpos($entity_name, "\n") !== false))
						{
							$entity_name = str_replace(["\r", "\n"], ['\\r', '\\n'], $entity_name);
							Factory::getLog()->warning(__CLASS__ . " :: [SECURITY] Function $entity_name includes newline characters. Skipping table to protect you against possible MySQL vulnerability CVE-2017-3600 (“Bad Dump”).");

							continue;
						}

						$entity_abstract = $this->getAbstract($entity_name);

						if (!(substr($entity_abstract, 0, 4) == 'bak_')) // Skip backup entities
						{
							// Apply exclusion filters if set
							if (!$filters->isFiltered($entity_abstract, $root, 'dbobject', 'all'))
							{
								$this->table_name_map[$entity_name] = $entity_abstract;
							}
						}
					}
				}
			}

			// 3. Triggers
			Factory::getLog()->debug(__CLASS__ . " :: Listing stored TRIGGERs");
			$sql = "SHOW TRIGGERS";
			$db->setQuery($sql);

			try
			{
				$all_entries = $db->loadResultArray();
			}
			catch (Exception $e)
			{
				$all_entries = [];
			}

			// If we have filters, make sure the tables pass the filtering
			if (is_array($all_entries))
			{
				if (count($all_entries))
				{
					foreach ($all_entries as $entity_name)
					{
						if ((strpos($entity_name, "\r") !== false) || (strpos($entity_name, "\n") !== false))
						{
							$entity_name = str_replace(["\r", "\n"], ['\\r', '\\n'], $entity_name);
							Factory::getLog()->warning(__CLASS__ . " :: [SECURITY] Trigger $entity_name includes newline characters. Skipping table to protect you against possible MySQL vulnerability CVE-2017-3600 (“Bad Dump”).");

							continue;
						}

						$entity_abstract = $this->getAbstract($entity_name);

						if (!(substr($entity_abstract, 0, 4) == 'bak_')) // Skip backup entities
						{
							// Apply exclusion filters if set
							if (!$filters->isFiltered($entity_abstract, $root, 'dbobject', 'all'))
							{
								$this->table_name_map[$entity_name] = $entity_abstract;
							}
						}
					}
				}
			}
		} // if MySQL 5

		/**
		 * Store all abstract entity names (tables, views, triggers etc etc ) into a volatile variable, so we can fetch
		 * it later when creating the databases.json file
		 */
		ksort($this->table_name_map);
		$registry->set('volatile.database.table_names', array_values($this->table_name_map));

		/**
		 * IMPORTANT -- DO NOT REMOVE
		 *
		 * We now need to reverse sort the table_name_map. This is of paramount importance in how the
		 * replaceTableNamesWithAbstracts method works. Consider the following case:
		 * foo_test_2 => #__test_2
		 * foo_test_20 => #__test_20
		 * If foo_test_2 comes before foo_test_2 (alpha sort) the CREATE command of foo_test_20 will end up as
		 * CREATE TABLE ``#__test_2`0` (...)
		 * instead of the correct
		 * CREATE TABLE `#__test_20` (...)
		 * That's because the first table replacement done there will be foo_test_2 => `#__test_2`. Ouch.
		 *
		 * By doing a reverse alpha sort on the keys we ENSURE that the longer table names which may be a superset of
		 * another table's name will always end up first on the list.
		 *
		 * In our example the first replacement made is foo_test_20 => `#__test_20`. When we reach the next possible
		 * replacement (foo_test_2) we no longer have the concrete table name foo_test_2 therefore we won't accidentally
		 * break the CREATE command.
		 *
		 * Of course the same replacement problem exists within VIEWs, TRIGGERs, PROCEDUREs and FUNCTIONs. Again, the
		 * reverse alpha sort by concrete table name solves this issue elegantly.
		 */
		krsort($this->table_name_map);
	}

	/**
	 * Process all table dependencies
	 *
	 * @return  void
	 */
	protected function process_dependencies()
	{
		if ((is_array($this->table_name_map) || $this->table_name_map instanceof \Countable ? count($this->table_name_map) : 0) > 0)
		{
			foreach ($this->table_name_map as $table_name => $table_abstract)
			{
				$this->push_table($table_name);
			}
		}

		Factory::getLog()->debug(__CLASS__ . " :: Processed dependencies");
	}

	/**
	 * Pushes a table in the _tables stack, making sure it will appear after
	 * its dependencies and other tables/views depending on it will eventually
	 * appear after it. It's a complicated chicken-and-egg problem. Just make
	 * sure you don't have any bloody circular references!!
	 *
	 * @param   string  $table_name  Canonical name of the table to push
	 * @param   array   $stack       When called recursive, other views/tables previously processed in order to detect
	 *                               *ahem* dependency loops...
	 *
	 * @return  void
	 */
	protected function push_table($table_name, $stack = [], $currentRecursionDepth = 0)
	{
		if (!isset($this->tables_data[$table_name]))
		{
			return;
		}

		// Load information
		$table_data = $this->tables_data[$table_name];

		if (array_key_exists('dependencies', $table_data))
		{
			$referenced = $table_data['dependencies'];
		}
		else
		{
			$referenced = [];
		}

		unset($table_data);

		// Try to find the minimum insert position, so as to appear after the last referenced table
		$insertpos = false;

		if (is_array($referenced) || $referenced instanceof \Countable ? count($referenced) : 0)
		{
			foreach ($referenced as $referenced_table)
			{
				if (is_array($this->tables) || $this->tables instanceof \Countable ? count($this->tables) : 0)
				{
					$newpos = array_search($referenced_table, $this->tables);

					if ($newpos !== false)
					{
						if ($insertpos === false)
						{
							$insertpos = $newpos;
						}
						else
						{
							$insertpos = max($insertpos, $newpos);
						}
					}
				}
			}
		}

		// Add to the _tables array
		if ((is_array($this->tables) || $this->tables instanceof \Countable ? count($this->tables) : 0) && ($insertpos !== false))
		{
			array_splice($this->tables, $insertpos + 1, 0, $table_name);
		}
		else
		{
			$this->tables[] = $table_name;
		}

		// Here's what... Some other table/view might depend on us, so we must appear
		// before it (actually, it must appear after us). So, we scan for such
		// tables/views and relocate them
		if (is_array($this->dependencies) || $this->dependencies instanceof \Countable ? count($this->dependencies) : 0)
		{
			if (array_key_exists($table_name, $this->dependencies))
			{
				foreach ($this->dependencies[$table_name] as $depended_table)
				{
					// First, make sure that either there is no stack, or the
					// depended table doesn't belong it. In any other case, we
					// were fooled to follow an endless dependency loop and we
					// will simply bail out and let the user sort things out.
					if (count($stack) > 0)
					{
						if (in_array($depended_table, $stack))
						{
							continue;
						}
					}

					$my_position     = array_search($table_name, $this->tables);
					$remove_position = array_search($depended_table, $this->tables);

					if (($remove_position !== false) && ($remove_position < $my_position))
					{
						$stack[] = $table_name;
						array_splice($this->tables, $remove_position, 1);

						// Where should I put the other table/view now? Don't tell me.
						// I have to recurse...
						if ($currentRecursionDepth < 19)
						{
							$this->push_table($depended_table, $stack, ++$currentRecursionDepth);
						}
						else
						{
							// We're hitting a circular dependency. We'll add the removed $depended_table
							// in the penultimate position of the table and cross our virtual fingers...
							array_splice($this->tables, (is_array($this->tables) || $this->tables instanceof \Countable ? count($this->tables) : 0) - 1, 0, $depended_table);
						}
					}
				}
			}
		}
	}

	/**
	 * Try to find an auto_increment field for the table being currently backed up and populate the
	 * $this->table_autoincrement table. Updates $this->table_autoincrement.
	 *
	 * @return  void
	 */
	protected function setAutoIncrementInfo()
	{
		$this->table_autoincrement = [
			'table' => $this->nextTable,
			'field' => null,
			'value' => null,
		];

		$db = $this->getDB();

		$query   = 'SHOW COLUMNS FROM ' . $db->qn($this->nextTable) . ' WHERE ' . $db->qn('Extra') . ' = ' .
			$db->q('auto_increment') . ' AND ' . $db->qn('Null') . ' = ' . $db->q('NO');
		$keyInfo = $db->setQuery($query)->loadAssocList();

		if (!empty($keyInfo))
		{
			$row                                = array_shift($keyInfo);
			$this->table_autoincrement['field'] = $row['Field'];
		}
	}

	/**
	 * Performs one more step of dumping database data
	 *
	 * @return  void
	 *
	 * @throws QueryException
	 * @throws Exception
	 */
	protected function stepDatabaseDump(): void
	{
		// Initialize local variables
		$db = $this->getDB();

		if (!is_object($db) || ($db === false))
		{
			throw new RuntimeException(__CLASS__ . '::_run() Could not connect to database?!');
		}

		$outData = ''; // Used for outputting INSERT INTO commands

		$this->enforceSQLCompatibility(); // Apply MySQL compatibility option

		// Touch SQL dump file
		$nada = "";
		$this->writeline($nada);

		// Get this table's information
		$tableName = $this->nextTable;
		$this->setStep($tableName);
		$this->setSubstep('');
		$tableAbstract = trim($this->table_name_map[$tableName]);
		$dump_records  = $this->tables_data[$tableName]['dump_records'];

		// Restore any previously information about the largest query we had to run
		$this->largest_query = Factory::getConfiguration()->get('volatile.database.largest_query', 0);

		// If it is the first run, find number of rows and get the CREATE TABLE command
		if ($this->nextRange == 0)
		{
			$outCreate = '';

			if (is_array($this->tables_data[$tableName]))
			{
				if (array_key_exists('create', $this->tables_data[$tableName]))
				{
					$outCreate = $this->tables_data[$tableName]['create'];
				}
			}

			if (empty($outCreate) && !empty($tableName))
			{
				// The CREATE command wasn't cached. Time to create it. The $type and $dependencies
				// variables will be thrown away.
				$type         = $this->tables_data[$tableName]['type'] ?? 'table';
				$dependencies = [];
				$outCreate    = $this->get_create($tableAbstract, $tableName, $type, $dependencies);
			}

			// Create drop statements if required (the key is defined by the scripting engine)
			if (!empty($outCreate) && Factory::getEngineParamsProvider()->getScriptingParameter('db.dropstatements', 0))
			{
				if (array_key_exists('create', $this->tables_data[$tableName]))
				{
					$dropStatement = $this->createDrop($this->tables_data[$tableName]['create']);
				}
				else
				{
					$type            = 'table';
					$createStatement = $this->get_create($tableAbstract, $tableName, $type, $dependencies);
					$dropStatement   = $this->createDrop($createStatement);
				}

				if (!empty($dropStatement))
				{
					$dropStatement .= "\n";

					if (!$this->writeDump($dropStatement, true))
					{
						return;
					}
				}
			}

			/**
			 * If we have a PROCEDURE, FUNCTION or TRIGGER and we are doing a SQL export meant to be run directly by
			 * MySQL (the scripting db.delimiterstatements flag is set to 1) we need to surround the CREATE statement
			 * with DELIMITER $$ commands.
			 */
			if (
				!empty($outCreate) &&
				(Factory::getEngineParamsProvider()->getScriptingParameter('db.delimiterstatements', 0) == 1)
				&& in_array($this->tables_data[$tableName]['type'], ['trigger', 'function', 'procedure'])
			)
			{
				$outCreate = rtrim($outCreate, ";\n");
				$outCreate = "DELIMITER $$\n$outCreate$$\nDELIMITER ;\n";
			}

			// Write the CREATE command after any DROP command which might be necessary.
			if (!empty($outCreate) && !$this->writeDump($outCreate, true))
			{
				return;
			}

			if (!empty($outCreate) && $dump_records)
			{
				// We are dumping data from a table, get the row count
				$this->getRowCount($tableAbstract);

				// If we can't get the row count we cannot back up this table's data
				if (is_null($this->maxRange))
				{
					$dump_records = false;
				}
			}
			elseif (!$dump_records)
			{
				/**
				 * Do NOT move this line to the if-block below. We need to only log this message on tables which are
				 * filtered, not on tables we simply cannot get the row count information for!
				 */
				Factory::getLog()->info("Skipping dumping data of " . $tableAbstract);
			}

			// The table is either filtered or we cannot get the row count. Either way we should not dump any data.
			if (!$dump_records || empty($outCreate))
			{
				$this->maxRange  = 0;
				$this->nextRange = 1;
				$outData         = '';
				$numRows         = 0;
				$dump_records    = false;
			}

			// Output any data preamble commands, e.g. SET IDENTITY_INSERT for SQL Server
			if ($dump_records && Factory::getEngineParamsProvider()->getScriptingParameter('db.dropstatements', 0))
			{
				Factory::getLog()->debug("Writing data dump preamble for " . $tableAbstract);
				$preamble = $this->getDataDumpPreamble($tableAbstract, $tableName, $this->maxRange);

				if (!empty($preamble))
				{
					if (!$this->writeDump($preamble, true))
					{
						return;
					}
				}
			}

			// Get the table's auto increment information
			if ($dump_records)
			{
				$this->setAutoIncrementInfo();
			}
		}

		// Load the active database root
		$configuration = Factory::getConfiguration();
		$dbRoot        = $configuration->get('volatile.database.root', '[SITEDB]');

		// Get the default and the current (optimal) batch size
		$defaultBatchSize = $this->getDefaultBatchSize();
		$batchSize        = $configuration->get('volatile.database.batchsize', $defaultBatchSize);

		// Check if we have more work to do on this table
		if (($this->nextRange < $this->maxRange))
		{
			$timer = Factory::getTimer();

			// Get the number of rows left to dump from the current table
			$columns         = $this->getSelectColumns($tableAbstract);
			$columnTypes     = $this->getColumnTypes($tableAbstract);
			$columnsForQuery = is_array($columns) ? array_map([$db, 'qn'], $columns) : $columns;
			$sql             = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
				->select($columnsForQuery)
				->from($db->nameQuote($tableAbstract));

			if (!is_null($this->table_autoincrement['field']))
			{
				$sql->order($db->qn($this->table_autoincrement['field']) . ' ASC');
			}

			if ($this->nextRange == 0)
			{
				// Get the optimal batch size for this table and save it to the volatile data
				$batchSize = $this->getOptimalBatchSize($tableAbstract, $defaultBatchSize);
				$configuration->set('volatile.database.batchsize', $batchSize);

				// First run, get a cursor to all records
				$db->setQuery($sql, 0, $batchSize);
				Factory::getLog()->info("Beginning dump of " . $tableAbstract);
				Factory::getLog()->debug("Up to $batchSize records will be read at once.");
			}
			else
			{
				// Subsequent runs, get a cursor to the rest of the records
				$this->setSubstep($this->nextRange . ' / ' . $this->maxRange);

				// If we have an auto_increment value and the table has over $batchsize records use the indexed select instead of a plain limit
				if (!is_null($this->table_autoincrement['field']) && !is_null($this->table_autoincrement['value']))
				{
					Factory::getLog()
						->info("Continuing dump of " . $tableAbstract . " from record #{$this->nextRange} using auto_increment column {$this->table_autoincrement['field']} and value {$this->table_autoincrement['value']}");
					$sql->where($db->qn($this->table_autoincrement['field']) . ' > ' . $db->q($this->table_autoincrement['value']));
					$db->setQuery($sql, 0, $batchSize);
				}
				else
				{
					Factory::getLog()
						->info("Continuing dump of " . $tableAbstract . " from record #{$this->nextRange}");
					$db->setQuery($sql, $this->nextRange, $batchSize);
				}
			}

			$this->query  = '';
			$numRows      = 0;
			$use_abstract = Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1);

			$filters            = Factory::getFilters();
			$mustFilterRows     = $filters->hasFilterType('dbobject', 'children');
			$mustFilterContents = $filters->canFilterDatabaseRowContent();

			try
			{
				$cursor = $db->query();
			}
			catch (Exception $exc)
			{
				// Issue a warning about the failure to dump data
				$errno = $exc->getCode();
				$error = $exc->getMessage();
				Factory::getLog()->warning("Failed dumping $tableAbstract from record #{$this->nextRange}. MySQL error $errno: $error");

				// Reset the database driver's state (we will try to dump other tables anyway)
				$db->resetErrors();
				$cursor = null;

				// Mark this table as done since we are unable to dump it.
				$this->nextRange = $this->maxRange;
			}

			$statsTableAbstract = Platform::getInstance()->tableNameStats;

			while (is_array($myRow = $db->fetchAssoc()) && ($numRows < ($this->maxRange - $this->nextRange)))
			{
				if ($this->createNewPartIfRequired() == false)
				{
					/**
					 * When createNewPartIfRequired returns false it means that we have began adding a SQL part to the
					 * backup archive but it hasn't finished. If we don't return here, the code below will keep adding
					 * data to that dump file. Yes, despite being closed. When you call writeDump the file is reopened.
					 * As a result of writing data of length Y, the file that had a size X now has a size of X + Y. This
					 * means that the loop in BaseArchiver which tries to add it to the archive will never see its End
					 * Of File since we are trying to resume the backup from *beyond* the file position that was
					 * recorded as the file size. The archive can detect a file shrinking but not a file growing!
					 * Therefore we hit an infinite loop a.k.a. runaway backup.
					 */
					return;
				}

				$numRows++;
				$numOfFields = is_array($myRow) || $myRow instanceof \Countable ? count($myRow) : 0;

				// On MS SQL Server there's always a RowNumber pseudocolumn added at the end, screwing up the backup (GRRRR!)
				if ($db->getDriverType() == 'mssql')
				{
					$numOfFields--;
				}

				// If row-level filtering is enabled, please run the filtering
				if ($mustFilterRows)
				{
					$isFiltered = $filters->isFiltered(
						[
							'table' => $tableAbstract,
							'row'   => $myRow,
						],
						$dbRoot,
						'dbobject',
						'children'
					);

					if ($isFiltered)
					{
						// Update the auto_increment value to avoid edge cases when the batch size is one
						if (!is_null($this->table_autoincrement['field']) && isset($myRow[$this->table_autoincrement['field']]))
						{
							$this->table_autoincrement['value'] = $myRow[$this->table_autoincrement['field']];
						}

						continue;
					}
				}

				if ($mustFilterContents)
				{
					$filters->filterDatabaseRowContent($dbRoot, $tableAbstract, $myRow);
				}

				if (
					(!$this->extendedInserts) || // Add header on simple INSERTs, or...
					($this->extendedInserts && empty($this->query)) //...on extended INSERTs if there are no other data, yet
				)
				{
					$newQuery  = true;
					$fieldList = $this->getFieldListSQL($columns);

					if ($numOfFields > 0)
					{
						$this->query = "INSERT INTO " . $db->nameQuote((!$use_abstract ? $tableName : $tableAbstract)) . " {$fieldList} VALUES \n";
					}
				}
				else
				{
					// On other cases, just mark that we should add a comma and start a new VALUES entry
					$newQuery = false;
				}

				$outData = '(';

				// Step through each of the row's values
				$fieldID = 0;

				// Used in running backup fix
				$isCurrentBackupEntry = false;

				// Fix 1.2a - NULL values were being skipped
				if ($numOfFields > 0)
				{
					foreach ($myRow as $fieldName => $value)
					{
						// The ID of the field, used to determine placement of commas
						$fieldID++;

						if ($fieldID > $numOfFields)
						{
							// This is required for SQL Server backups, do NOT remove!
							continue;
						}

						// Fix 2.0: Mark currently running backup as successful in the DB snapshot
						if ($tableAbstract == $statsTableAbstract)
						{
							if ($fieldID == 1)
							{
								// Compare the ID to the currently running
								$statistics           = Factory::getStatistics();
								$isCurrentBackupEntry = ($value == $statistics->getId());
							}
							elseif ($fieldID == 6)
							{
								// Treat the status field
								$value = $isCurrentBackupEntry ? 'complete' : $value;
							}
						}

						// Post-process the value
						if (is_null($value))
						{
							$outData .= "NULL"; // Cope with null values
						}
						else
						{
							// Accommodate for runtime magic quotes
							if (function_exists('get_magic_quotes_runtime'))
							{
								$value = @get_magic_quotes_runtime() ? stripslashes($value) : $value;
							}

							switch ($columnTypes[$fieldName] ?? '')
							{
								// Hex encode spatial data
								case 'GEOMETRY':
								case 'POINT':
								case 'LINESTRING':
								case 'POLYGON':
								case 'MULTIPOINT':
								case 'MULTILINESTRING':
								case 'MULTIPOLYGON':
								case 'GEOMETRYCOLLECTION':
									$hexEncoded = bin2hex($value);
									$value      = "x'$hexEncoded'";
									break;

								default:
									$value = $db->quote($value);
									break;
							}

							if ($this->postProcessValues)
							{
								$value = $this->postProcessQuotedValue($value);
							}

							$outData .= $value;
						}

						if ($fieldID < $numOfFields)
						{
							$outData .= ', ';
						}
					}
				}

				$outData .= ')';

				if ($numOfFields)
				{
					// If it's an existing query and we have extended inserts
					if ($this->extendedInserts && !$newQuery)
					{
						// Check the existing query size
						$query_length = strlen($this->query);
						$data_length  = strlen($outData);

						if (($query_length + $data_length) > $this->packetSize)
						{
							// We are about to exceed the packet size. Write the data so far.
							$this->query .= ";\n";

							if (!$this->writeDump($this->query, true))
							{
								return;
							}

							// Then, start a new query
							$fieldList = $this->getFieldListSQL($columns);

							$this->query = '';
							$this->query = "INSERT INTO " . $db->nameQuote((!$use_abstract ? $tableName : $tableAbstract)) . " {$fieldList} VALUES \n";
							$this->query .= $outData;
						}
						else
						{
							// We have room for more data. Append $outData to the query.
							$this->query .= ",\n";
							$this->query .= $outData;
						}
					}
					// If it's a brand new insert statement in an extended INSERTs set
					elseif ($this->extendedInserts && $newQuery)
					{
						// Append the data to the INSERT statement
						$this->query .= $outData;
						// Let's see the size of the dumped data...
						$query_length = strlen($this->query);

						if ($query_length >= $this->packetSize)
						{
							// This was a BIG query. Write the data to disk.
							$this->query .= ";\n";

							if (!$this->writeDump($this->query, true))
							{
								return;
							}

							// Then, start a new query
							$this->query = '';
						}
					}
					// It's a normal (not extended) INSERT statement
					else
					{
						// Append the data to the INSERT statement
						$this->query .= $outData;
						// Write the data to disk.
						$this->query .= ";\n";

						if (!$this->writeDump($this->query, true))
						{
							return;
						}

						// Then, start a new query
						$this->query = '';
					}
				}

				$outData = '';

				// Update the auto_increment value to avoid edge cases when the batch size is one
				if (!is_null($this->table_autoincrement['field']))
				{
					$this->table_autoincrement['value'] = $myRow[$this->table_autoincrement['field']];
				}

				unset($myRow);

				// Check for imminent timeout
				if ($timer->getTimeLeft() <= 0)
				{
					Factory::getLog()
						->debug("Breaking dump of $tableAbstract after $numRows rows; will continue on next step");

					break;
				}
			}

			$db->freeResult($cursor);

			// Advance the _nextRange pointer
			$this->nextRange += ($numRows != 0) ? $numRows : 1;

			$this->setStep($tableName);
			$this->setSubstep($this->nextRange . ' / ' . $this->maxRange);
		}

		// Finalize any pending query
		// WARNING! If we do not do that now, the query will be emptied in the next operation and all
		// accumulated data will go away...
		if (!empty($this->query))
		{
			$this->query .= ";\n";

			if (!$this->writeDump($this->query, true))
			{
				return;
			}

			$this->query = '';
		}

		// Check for end of table dump (so that it happens inside the same operation)
		if ($this->nextRange >= $this->maxRange)
		{
			// Tell the user we are done with the table
			Factory::getLog()->debug("Done dumping " . $tableAbstract);

			// Output any data preamble commands, e.g. SET IDENTITY_INSERT for SQL Server
			if ($dump_records && Factory::getEngineParamsProvider()->getScriptingParameter('db.dropstatements', 0))
			{
				Factory::getLog()->debug("Writing data dump epilogue for " . $tableAbstract);
				$epilogue = $this->getDataDumpEpilogue($tableAbstract, $tableName, $this->maxRange);

				if (!empty($epilogue))
				{
					if (!$this->writeDump($epilogue, true))
					{
						return;
					}
				}
			}

			if ((is_array($this->tables) || $this->tables instanceof \Countable ? count($this->tables) : 0) == 0)
			{
				// We have finished dumping the database!
				Factory::getLog()->info("End of database detected; flushing the dump buffers...");
				$this->writeDump(null);
				Factory::getLog()->info("Database has been successfully dumped to SQL file(s)");
				$this->setState(self::STATE_POSTRUN);
				$this->setStep('');
				$this->setSubstep('');
				$this->nextTable = '';
				$this->nextRange = 0;

				/**
				 * At the end of the database dump, if any query was longer than 1Mb, let's put a warning file in the
				 * installation folder, but ONLY if the backup is not a SQL-only backup (which has no backup archive).
				 */
				$isSQLOnly = $configuration->get('akeeba.basic.backup_type') == 'dbonly';

				if (!$isSQLOnly && ($this->largest_query >= 1024 * 1024))
				{
					$archive = Factory::getArchiverEngine();
					$archive->addFileVirtual('large_tables_detected', $this->installerSettings->installerroot, $this->largest_query);
				}
			}
			elseif ((is_array($this->tables) || $this->tables instanceof \Countable ? count($this->tables) : 0) != 0)
			{
				// Switch tables
				$this->nextTable = array_shift($this->tables);
				$this->nextRange = 0;
				$this->setStep($this->nextTable);
				$this->setSubstep('');
			}
		}
	}

	/** @inheritDoc */
	protected function getAllTables(): array
	{
		// Get a database connection
		$db = $this->getDB();

		$this->enforceSQLCompatibility();

		$sql = 'SHOW TABLES';
		$db->setQuery($sql);

		return $db->loadColumn() ?: [];
	}
}
Configuration.php000060400000034367152456704520010107 0ustar00<?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;

defined('AKEEBAENGINE') || die();

use DirectoryIterator;
use stdClass;

/**
 * The Akeeba Engine configuration registry class
 */
class Configuration
{
	/**
	 * The currently loaded profile
	 *
	 * @var   integer
	 */
	public $activeProfile = null;

	/**
	 * Default namespace
	 *
	 * @var   string
	 */
	private $defaultNameSpace = 'global';

	/**
	 * Array keys which may contain stock directory definitions
	 *
	 * @var   array
	 */
	private $directory_containing_keys = [
		'akeeba.basic.output_directory',
	];

	/**
	 * Keys whose default values should never be overridden
	 *
	 * @var   array
	 */
	private $protected_nodes = [];

	/** The registry data
	 *
	 * @var   array
	 */
	private $registry = [];

	/**
	 * Constructor
	 *
	 * @return  void
	 */
	public function __construct()
	{
		// Create the default namespace
		$this->makeNameSpace($this->defaultNameSpace);

		// Create a default configuration
		$this->reset();
	}

	/**
	 * Create a namespace
	 *
	 * @param   string  $namespace  Name of the namespace to create
	 *
	 * @return  void
	 */
	public function makeNameSpace($namespace)
	{
		$this->registry[$namespace] = ['data' => new stdClass()];
	}

	/**
	 * Get the list of namespaces
	 *
	 * @return  array  List of namespaces
	 */
	public function getNameSpaces()
	{
		return array_keys($this->registry);
	}

	/**
	 * Get a registry value
	 *
	 * @param   string   $regpath               Registry path (e.g. global.directory.temporary)
	 * @param   mixed    $default               Optional default value
	 * @param   boolean  $process_special_vars  Optional. If true (default), it processes special variables, e.g.
	 *                                          [SITEROOT] in folder names
	 *
	 * @return  mixed  Value of entry or null
	 */
	public function get($regpath, $default = null, $process_special_vars = true)
	{
		// Cache the platform-specific stock directories
		static $stock_directories = [];

		if (empty($stock_directories))
		{
			$stock_directories = Platform::getInstance()->get_stock_directories();
		}

		$result = $default;

		// Explode the registry path into an array
		if ($nodes = explode('.', $regpath))
		{
			// Get the namespace
			$count = count($nodes);

			if ($count < 2)
			{
				$namespace = $this->defaultNameSpace;
				$nodes[1]  = $nodes[0];
			}
			else
			{
				$namespace = $nodes[0];
			}

			if (isset($this->registry[$namespace]))
			{
				$ns        = $this->registry[$namespace]['data'];
				$pathNodes = $count - 1;

				for ($i = 1; $i < $pathNodes; $i++)
				{
					if ((isset($ns->{$nodes[$i]})))
					{
						$ns = $ns->{$nodes[$i]};
					}
				}

				if (isset($ns->{$nodes[$i]}))
				{
					$result = $ns->{$nodes[$i]};
				}
			}
		}

		// Post-process certain directory-containing variables
		if ($process_special_vars && in_array($regpath, $this->directory_containing_keys))
		{
			if (!empty($stock_directories))
			{
				foreach ($stock_directories as $tag => $content)
				{
					$result = str_replace($tag, $content, $result);
				}
			}
		}

		return $result;
	}

	/**
	 * Set a registry value
	 *
	 * @param   string  $regpath               Registry Path (e.g. global.directory.temporary)
	 * @param   mixed   $value                 Value of entry
	 * @param   bool    $process_special_vars  Optional. If true (default), it processes special variables, e.g.
	 *                                         [SITEROOT] in folder names
	 *
	 * @return  mixed  Value of old value or boolean false if operation failed
	 */
	public function set($regpath, $value, $process_special_vars = true)
	{
		// Cache the platform-specific stock directories
		static $stock_directories = [];

		if (empty($stock_directories))
		{
			$stock_directories = Platform::getInstance()->get_stock_directories();
		}

		if (in_array($regpath, $this->protected_nodes))
		{
			return $this->get($regpath);
		}

		// Explode the registry path into an array
		$nodes = explode('.', $regpath);

		// Get the namespace
		$count = count($nodes);

		if ($count < 2)
		{
			$namespace = $this->defaultNameSpace;
		}
		else
		{
			$namespace = array_shift($nodes);
			$count--;
		}

		if (!isset($this->registry[$namespace]))
		{
			$this->makeNameSpace($namespace);
		}

		$ns = $this->registry[$namespace]['data'];

		$pathNodes = $count - 1;

		if ($pathNodes < 0)
		{
			$pathNodes = 0;
		}

		for ($i = 0; $i < $pathNodes; $i++)
		{
			// If any node along the registry path does not exist, create it
			if (!isset($ns->{$nodes[$i]}))
			{
				$ns->{$nodes[$i]} = new stdClass();
			}
			$ns = $ns->{$nodes[$i]};
		}

		// Set the new values
		if (is_string($value))
		{
			if (substr($value, 0, 10) == '###json###')
			{
				$value = json_decode(substr($value, 10));
			}
		}

		// Post-process certain directory-containing variables
		if ($process_special_vars && in_array($regpath, $this->directory_containing_keys))
		{
			if (!empty($stock_directories))
			{
				$data = $value;

				foreach ($stock_directories as $tag => $content)
				{
					$data = str_replace($tag, $content, $data);
				}

				$ns->{$nodes[$i]} = $data;

				return $ns->{$nodes[$i]};
			}
		}

		// This is executed if any of the previous two if's is false
		if (empty($nodes[$i]))
		{
			return false;
		}

		$ns->{$nodes[$i]} = $value;

		return $ns->{$nodes[$i]};
	}

	/**
	 * Unset (remove) a registry value
	 *
	 * @param   string  $regpath  Registry Path (e.g. global.directory.temporary)
	 *
	 * @return  boolean  True if the node was removed
	 */
	public function remove($regpath)
	{
		// Explode the registry path into an array
		$nodes = explode('.', $regpath);

		// Get the namespace
		$count = count($nodes);

		if ($count < 2)
		{
			$namespace = $this->defaultNameSpace;
		}
		else
		{
			$namespace = array_shift($nodes);
			$count--;
		}

		if (!isset($this->registry[$namespace]))
		{
			$this->makeNameSpace($namespace);
		}

		$ns = $this->registry[$namespace]['data'];

		$pathNodes = $count - 1;

		if ($pathNodes < 0)
		{
			$pathNodes = 0;
		}

		for ($i = 0; $i < $pathNodes; $i++)
		{
			// If any node along the registry path does not exist, return false
			if (!isset($ns->{$nodes[$i]}))
			{
				return false;
			}

			$ns = $ns->{$nodes[$i]};
		}

		unset($ns->{$nodes[$i]});

		return true;
	}

	/**
	 * Resets the registry to the default values
	 */
	public function reset()
	{
		// Load the Akeeba Engine INI files
		$root_path = __DIR__;

		$paths = [
			$root_path . '/Core',
			$root_path . '/Archiver',
			$root_path . '/Dump',
			$root_path . '/Scan',
			$root_path . '/Writer',
			$root_path . '/Proc',
			$root_path . '/Platform/Filter/Stack',
			$root_path . '/Filter/Stack',
		];

		$platform_paths = Platform::getInstance()->getPlatformDirectories();

		foreach ($platform_paths as $p)
		{
			$paths[] = $p . '/Filter/Stack';
			$paths[] = $p . '/Config';
		}

		foreach ($paths as $root)
		{
			if (!(is_dir($root) || is_link($root)))
			{
				continue;
			}

			if (!is_readable($root))
			{
				continue;
			}

			$di = new DirectoryIterator($root);

			/** @var DirectoryIterator $file */
			foreach ($di as $file)
			{
				if (!$file->isFile())
				{
					continue;
				}

				if ($file->getExtension() == 'json')
				{
					$this->mergeEngineJSON($file->getRealPath());
				}
			}
		}
	}

	/**
	 * Merges an associative array of key/value pairs into the registry.
	 * If noOverride is set, only non set or null values will be applied.
	 *
	 * @param   array  $array                 An associative array. Its keys are registry paths.
	 * @param   bool   $noOverride            [optional] Do not override pre-set values.
	 * @param   bool   $process_special_vars  Optional. If true (default), it processes special variables, e.g.
	 *                                        [SITEROOT] in folder names
	 */
	public function mergeArray($array, $noOverride = false, $process_special_vars = true)
	{
		if (!$noOverride)
		{
			foreach ($array as $key => $value)
			{
				$this->set($key, $value, $process_special_vars);
			}
		}
		else
		{
			foreach ($array as $key => $value)
			{
				if (is_null($this->get($key, null)))
				{
					$this->set($key, $value, $process_special_vars);
				}
			}
		}
	}

	/**
	 * Merges a JSON file into the registry. Its top level keys are registry paths,
	 * child keys are appended to the section-defined paths and then set equal to the
	 * values. If noOverride is set, only non set or null values will be applied.
	 * Top level keys beginning with an underscore will be ignored.
	 *
	 * @param   string   $jsonPath    The full path to the INI file to load
	 * @param   boolean  $noOverride  [optional] Do not override pre-set values.
	 *
	 * @return  boolean  True on success
	 */
	public function mergeJSON($jsonPath, $noOverride = false)
	{
		if (!file_exists($jsonPath))
		{
			return false;
		}

		$rawData  = file_get_contents($jsonPath);
		$jsonData = empty($rawData) ? [] : json_decode($rawData, true);

		foreach ($jsonData as $rootkey => $rootvalue)
		{
			if (!is_array($rootvalue))
			{
				if (!$noOverride)
				{
					$this->set($rootkey, $rootvalue);
				}
				elseif (is_null($this->get($rootkey, null)))
				{
					$this->set($rootkey, $rootvalue);
				}
			}
			elseif (substr($rootkey, 0, 1) != '_')
			{
				foreach ($rootvalue as $key => $value)
				{
					if (!$noOverride)
					{
						$this->set($rootkey . '.' . $key, $rootvalue);
					}
					elseif (is_null($this->get($rootkey . '.' . $key, null)))
					{
						$this->set($rootkey . '.' . $key, $rootvalue);
					}
				}
			}
		}

		return true;
	}

	/**
	 * Merges an engine JSON file to the configuration. Each top level key defines a full
	 * registry path (section.subsection.key). It searches each top level key for the
	 * child key named "default" and merges its value to the configuration. The other keys
	 * are simply ignored.
	 *
	 * @param   string  $jsonPath    The absolute path to an JSON file
	 * @param   bool    $noOverride  [optional] If true, values from the JSON file will not override the configuration
	 *
	 * @return  boolean  True on success
	 */
	public function mergeEngineJSON($jsonPath, $noOverride = false)
	{
		if (!file_exists($jsonPath))
		{
			return false;
		}

		$rawData  = file_get_contents($jsonPath);
		$jsonData = empty($rawData) ? [] : json_decode($rawData, true);

		foreach ($jsonData ?? [] as $section => $nodes)
		{
			if (is_array($nodes))
			{
				if (substr($section, 0, 1) != '_')
				{
					// Is this a protected node?
					$protected = false;

					if (array_key_exists('protected', $nodes))
					{
						$protected = $nodes['protected'];
					}

					// If overrides are allowed, unprotect until we can set the value
					if (!$noOverride)
					{
						if (in_array($section, $this->protected_nodes))
						{
							$pnk = array_search($section, $this->protected_nodes);
							unset($this->protected_nodes[$pnk]);
						}
					}

					if (array_key_exists('remove', $nodes))
					{
						// Remove a node if it has "remove" set
						$this->remove($section);
					}
					elseif (isset($nodes['default']))
					{
						if (!$noOverride)
						{
							// Update the default value if No Override is set
							$this->set($section, $nodes['default']);
						}
						elseif (is_null($this->get($section, null)))
						{
							// Set the default value if it does not exist
							$this->set($section, $nodes['default']);
						}
					}

					// Finally, if it's a protected node, enable the protection
					if ($protected)
					{
						$this->protected_nodes[] = $section;
					}
					else
					{
						$idx = array_search($section, $this->protected_nodes);

						if ($idx !== false)
						{
							unset($this->protected_nodes[$idx]);
						}
					}
				}
			}
		}

		return true;
	}

	/**
	 * Exports the current registry snapshot as a JSON-encoded object. Each namespace is a property of the top-level
	 * JSON object.
	 *
	 * @return  string  The JSON-encoded representation of the registry
	 *
	 * @since   6.4.1
	 */
	public function exportAsJSON()
	{
		$forJSON    = [];
		$namespaces = $this->getNameSpaces();

		foreach ($namespaces as $namespace)
		{
			if ($namespace == 'volatile')
			{
				continue;
			}

			$forJSON[$namespace] = $this->registry[$namespace]['data'];
		}

		return json_encode($forJSON, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_FORCE_OBJECT | JSON_PRETTY_PRINT);

	}

	/**
	 * Sets the protection status for a specific configuration key
	 *
	 * @param   string|array  $node     The node to protect/unprotect
	 * @param   boolean       $protect  True to protect, false to unprotect
	 *
	 * @return  void
	 */
	public function setKeyProtection($node, $protect = false)
	{
		if (is_array($node))
		{
			foreach ($node as $k)
			{
				$this->setKeyProtection($k, $protect);
			}
		}
		elseif (is_string($node))
		{
			if (is_array($this->protected_nodes))
			{
				$protected = in_array($node, $this->protected_nodes);
			}
			else
			{
				$this->protected_nodes = [];
				$protected             = false;
			}

			if ($protect)
			{
				if (!$protected)
				{
					$this->protected_nodes[] = $node;
				}
			}
			else
			{
				if ($protected)
				{
					$pnk = array_search($node, $this->protected_nodes);
					unset($this->protected_nodes[$pnk]);
				}
			}
		}
	}

	/**
	 * Returns a list of protected keys
	 *
	 * @return  array
	 */
	public function getProtectedKeys()
	{
		return $this->protected_nodes;
	}

	/**
	 * Resets the protected keys
	 *
	 * @return  void
	 */
	public function resetProtectedKeys()
	{
		$this->protected_nodes = [];
	}

	/**
	 * Sets the protected keys
	 *
	 * @param   array  $keys  A list of keys to protect
	 *
	 * @return  void
	 */
	public function setProtectedKeys($keys)
	{
		$this->protected_nodes = $keys;
	}
}
Core/04.quota.json000060400000004436152456704520007717 0ustar00{
    "_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"
    }
}Core/05.tuning.json000060400000006434152456704520010073 0ustar00{
    "_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"
    }
}Core/Database.php000060400000007112152456704520007660 0ustar00<?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]);
	}
}
Core/scripting.json000060400000007351152456704520010345 0ustar00{
    "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"
}Core/Timer.php000060400000014251152456704520007236 0ustar00<?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);
	}
}
Core/Domain/Db.php000060400000025254152456704520007717 0ustar00<?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'] = '';
			}
		}
	}
}
Core/Domain/Pack.php000060400000106550152456704520010247 0ustar00<?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;
		}
	}
}
Core/Domain/Init.php000060400000035070152456704520010272 0ustar00<?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]';
	}
}
Core/Domain/Installer.php000060400000015041152456704520011320 0ustar00<?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;
	}
}
Core/Domain/Finalizer/RemoveTemporaryFiles.php000060400000002702152456704520015431 0ustar00<?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;
	}
}Core/Domain/Finalizer/FinalizerInterface.php000060400000003171152456704520015053 0ustar00<?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();
}Core/Domain/Finalizer/AbstractQuotaManagement.php000060400000033735152456704520016072 0ustar00<?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);
	}
}Core/Domain/Finalizer/RemoteQuotas.php000060400000016136152456704520013744 0ustar00<?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;
	}

}Core/Domain/Finalizer/LocalQuotas.php000060400000012313152456704520013534 0ustar00<?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;
	}
}Core/Domain/Finalizer/ObsoleteRecordsQuotas.php000060400000007310152456704520015601 0ustar00<?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;
	}
}Core/Domain/Finalizer/UpdateFileSizes.php000060400000005156152456704520014354 0ustar00<?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;
	}
}Core/Domain/Finalizer/AbstractFinalizer.php000060400000004772152456704520014726 0ustar00<?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);
	}
}Core/Domain/Finalizer/MailAdministrators.php000060400000021610152456704520015113 0ustar00<?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();
    }
}Core/Domain/Finalizer/UpdateStatistics.php000060400000004661152456704520014611 0ustar00<?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;
	}
}Core/Domain/Finalizer/PostProcessing.php000060400000023331152456704520014271 0ustar00<?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;
	}
}Core/Domain/Finalizer/UploadKickstart.php000060400000006001152456704520014406 0ustar00<?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;
	}
}Core/Domain/Finalization.php000060400000017171152456704520012020 0ustar00<?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('');
		}
	}
}
Core/Kettenrad.php000060400000061166152456704520010106 0ustar00<?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;
}
Core/02.advanced.json000060400000002341152456704520010322 0ustar00{
    "_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"
    }
}Core/Filters.php000060400000025162152456704520007571 0ustar00<?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 = [];
	}
}
Core/01.basic.json000060400000002711152456704520007636 0ustar00{
    "_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"
    }
}