<?php
/**
 * Akeeba Engine
 * The modular PHP5 site backup engine
 *
 * @copyright Copyright (c)2006-2015 Nicholas K. Dionysopoulos
 * @license   GNU GPL version 3 or, at your option, any later version
 * @package   akeebaengine
 *
 */

namespace Akeeba\Engine;

// Protection against direct access
defined('AKEEBAENGINE') or die();

use Akeeba\Engine\Core\Database;
use Psr\Log\LogLevel;

// Try to kill errors display
if (function_exists('ini_set') && !defined('AKEEBADEBUG'))
{
	ini_set('display_errors', false);
}

define('AKEEBA_CACERT_PEM', __DIR__ . '/cacert.pem');

// 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 classes
 */
class Factory
{
	/** @var   array  A list of instantiated objects which will persist after serialisation / unserialisation */
	protected $objectList = array();

	/** @var   array  A list of instantiated objects which will NOT persist after serialisation / unserialisation */
	protected $temporaryObjectList = array();

	/** Private constructor makes sure we can't directly instantiate the class */
	private function __construct()
	{
	}

	/**
	 * The magic __sleep method is called before serialising this object. We need to return an array with the properties
	 * which will be serialised.
	 *
	 * @return array
	 */
	function __sleep()
	{
		return array('objectList');
	}

	/**
	 * Gets a single, internally used instance of the Factory
	 *
	 * @param   string $serialized_data [optional] Serialized data to spawn the instance from
	 *
	 * @return  Factory  A reference to the unique Factory object instance
	 */
	public static function &getInstance($serialized_data = null)
	{
		static $myInstance;

		if (!is_object($myInstance) || !is_null($serialized_data))
		{
			if (!is_null($serialized_data))
			{
				$myInstance = unserialize($serialized_data);
			}
			else
			{
				$myInstance = new self();
			}
		}

		return $myInstance;
	}

	/**
	 * Internal function which instantiates an object of a class named $class_name.
	 *
	 * @param   string $class_name
	 *
	 * @return  object
	 */
	protected static function &getObjectInstance($class_name)
	{
		$self = self::getInstance();

		if (!isset($self->objectList[$class_name]))
		{
			if (!class_exists($class_name, true))
			{
				$self->objectList[$class_name] = false;
			}
			else
			{
				$self->objectList[$class_name] = new $class_name;
			}
		}

		return $self->objectList[$class_name];
	}

	/**
	 * Internal function which removes the object of the class named $class_name
	 *
	 * @param  string $class_name
	 *
	 * @return void
	 */
	protected static function unsetObjectInstance($class_name)
	{
		$self = self::getInstance();

		if (isset($self->objectList[$class_name]))
		{
			$self->objectList[$class_name] = null;
			unset($self->objectList[$class_name]);
		}
	}

	/**
	 * Internal function which instantiates an object of a class named $class_name. This is a temporary instance which
	 * will not survive serialisation and subsequent unserialisation.
	 *
	 * @param   string $class_name
	 *
	 * @return  object
	 */
	protected static function &getTempObjectInstance($class_name)
	{
		$self = self::getInstance();

		if (!isset($self->temporaryObjectList[$class_name]))
		{
			if (!class_exists($class_name, true))
			{
				$self->temporaryObjectList[$class_name] = false;
			}
			else
			{
				$self->temporaryObjectList[$class_name] = new $class_name;
			}
		}

		return $self->temporaryObjectList[$class_name];
	}

	/**
	 * Internal function which removes the object of the class named $class_name. This is a temporary instance which
	 * will not survive serialisation and subsequent unserialisation.
	 *
	 * @param  string $class_name
	 *
	 * @return void
	 */
	protected static function unsetTempObjectInstance($class_name)
	{
		$self = self::getInstance();

		if (isset($self->temporaryObjectList[$class_name]))
		{
			$self->temporaryObjectList[$class_name] = null;
			unset($self->temporaryObjectList[$class_name]);
		}
	}

	// ========================================================================
	// Public factory interface
	// ========================================================================

	/**
	 * Gets a serialized snapshot of the Factory for safekeeping (hibernate)
	 *
	 * @return  string  The serialized snapshot of the Factory
	 */
	public static function serialize()
	{
		// Call _onSerialize in all classes known to the factory
		$self = self::getInstance();

		if (!empty($self->objectList))
		{
			foreach ($self->objectList as $class_name => $object)
			{
				$o = $self->objectList[$class_name];

				if (method_exists($o, '_onSerialize'))
				{
					call_user_func(array($o, '_onSerialize'));
				}
			}
		}

		// Serialize the factory
		return serialize(self::getInstance());
	}

	/**
	 * Regenerates the full Factory state from a serialized snapshot (resume)
	 *
	 * @param string $serialized_data The serialized snapshot to resume from
	 */
	public static function unserialize($serialized_data)
	{
		self::getInstance($serialized_data);
	}

	/**
	 * Reset the internal factory state, freeing all previously created objects
	 */
	public static function nuke()
	{
		$self = self::getInstance();

		foreach ($self->objectList as $key => $object)
		{
			$self->objectList[$key] = null;
		}

		$self->objectList = array();
	}

	/**
	 * Saves the engine state to temporary storage
	 *
	 * @param string $tag      The backup origin to save. Leave empty to get from already loaded Kettenrad instance.
	 * @param string $backupId The backup ID to save. Leave empty to get from already loaded Kettenrad instance.
	 *
	 * @return  void
	 */
	public static function saveState($tag = null, $backupId = null)
	{
		$kettenrad = self::getKettenrad();

		if (empty($tag))
		{
			$tag = $kettenrad->getTag();
		}

		if (empty($backupId))
		{
			$backupId = $kettenrad->getBackupId();
		}

		$saveTag = $tag . (empty($backupId) ? '' : ('.' . $backupId));

		$ret = $kettenrad->getStatusArray();

		if ($ret['HasRun'] == 1)
		{
			Factory::getLog()->log(LogLevel::DEBUG, "Will not save a finished Kettenrad instance");
		}
		else
		{
			Factory::getLog()->log(LogLevel::DEBUG, "Saving Kettenrad instance $tag");

			// Save a Factory snapshot
			self::getFactoryStorage()->set(self::serialize(), $saveTag);
		}
	}

	/**
	 * Loads the engine state from the storage (if it exists)
	 *
	 * @param string $tag      The backup origin to load
	 * @param string $backupId The backup ID to load
	 *
	 * @return  void
	 */
	public static function loadState($tag = null, $backupId = null)
	{
		if (is_null($tag) && defined('AKEEBA_BACKUP_ORIGIN'))
		{
			$tag = AKEEBA_BACKUP_ORIGIN;
		}

		if (is_null($backupId) && defined('AKEEBA_BACKUP_ID'))
		{
			$tag = AKEEBA_BACKUP_ID;
		}

		$loadTag = $tag . (empty($backupId) ? '' : ('.' . $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 = self::getConfiguration();

		if (empty($config->activeProfile))
		{
			// Only bother loading a configuration if none has been already loaded
			$statList = Platform::getInstance()->get_statistics_list(array(
					'filters'  => array(
						array('field' => 'tag', 'value' => $tag)
					), 'order' => array(
						'by' => 'id', 'order' => 'DESC'
					)
				)
			);

			if (is_array($statList))
			{
				$stat = array_pop($statList);
				$profile = $stat['profile_id'];

				Platform::getInstance()->load_configuration($profile);
			}
		}

		Factory::getLog()->open($loadTag);
		Factory::getLog()->log(LogLevel::DEBUG, "Kettenrad :: Attempting to load from database ($tag) [$loadTag]");

		$serialized_factory = self::getFactoryStorage()->get($loadTag);

		if ($serialized_factory !== false)
		{
			Factory::getLog()->log(LogLevel::DEBUG, " -- Loaded stored Akeeba Factory ($tag) [$loadTag]");
			self::unserialize($serialized_factory);
		}
		else
		{
			// There is no serialized factory. Nuke the in-memory factory.
			Factory::getLog()->log(LogLevel::DEBUG, " -- Stored Akeeba Factory ($tag) [$loadTag] not found - hard reset");
			self::nuke();
			Platform::getInstance()->load_configuration();
		}

		unset($serialized_factory);
	}

	/**
	 * Resets the engine state, wiping out any pending backups and/or stale
	 * temporary data.
	 *
	 * @param array $config Configuration parameters for the reset operation
	 */
	public static function resetState($config = array())
	{
		$default_config = array(
			'global' => true, // Reset all origins when true
			'log'    => false, // Log our actions
			'maxrun' => 180, // Consider "pending" backups as failed after this many seconds
		);

		$config = (object)array_merge($default_config, $config);

		// Pause logging if so desired
		if (!$config->log)
		{
			Factory::getLog()->pause();
		}

		$originTag = null;

		if (!$config->global)
		{
			// If we're not resetting globally, get a list of running backups per tag
			$originTag = Platform::getInstance()->get_backup_origin();
		}

		// Cache the factory before proceeding
		$factory = self::serialize();

		$runningList = Platform::getInstance()->get_running_backups($originTag);

		// Origins we have to clean
		$origins = array(
			Platform::getInstance()->get_backup_origin()
		);

		// 1. Detect failed backups
		if (is_array($runningList) && !empty($runningList))
		{
			// The current timestamp
			$now = time();

			// Mark running backups as failed
			foreach ($runningList as $running)
			{
				if (empty($originTag))
				{
					// Check the timestamp of the log file to decide if it's stuck,
					// but only if a tag is not set
					$tstamp = Factory::getLog()->getLastTimestamp($running['origin']);

					if (!is_null($tstamp))
					{
						// We can only check the timestamp if it's returned. If not, we assume the backup is stale
						$difference = abs($now - $tstamp);

						// Backups less than maxrun seconds old are not considered stale (default: 3 minutes)
						if ($difference < $config->maxrun)
						{
							continue;
						}
					}
				}

				$filenames = Factory::getStatistics()->get_all_filenames($running, false);
				$totalSize = 0;

				// Process if there are files to delete...
				if (!is_null($filenames))
				{
					// Delete the failed backup's archive, if exists
					foreach ($filenames as $failedArchive)
					{
						if (file_exists($failedArchive))
						{
							$totalSize += (int)@filesize($failedArchive);
							Platform::getInstance()->unlink($failedArchive);
						}
					}
				}

				// Mark the backup failed
				if (!$running['total_size'])
				{
					$running['total_size'] = $totalSize;
				}

				$running['status'] = 'fail';
				$running['multipart'] = 0;
				$dummy = null;
				Platform::getInstance()->set_or_update_statistics($running['id'], $running, $dummy);

				$backupId = isset($running['backupid']) ? ('.' . $running['backupid']) : '';

				$origins[] = $running['origin'] . $backupId;
			}
		}

		if (!empty($origins))
		{
			$origins = array_unique($origins);

			foreach ($origins as $originTag)
			{
				self::loadState($originTag);
				// Remove temporary files
				Factory::getTempFiles()->deleteTempFiles();
				// Delete any stale temporary data
				self::getFactoryStorage()->reset($originTag);
			}
		}

		// Reload the factory
		self::unserialize($factory);
		unset($factory);

		// Unpause logging if it was previously paused
		if (!$config->log)
		{
			Factory::getLog()->unpause();
		}
	}

	// ========================================================================
	// Core objects which are part of the engine state
	// ========================================================================

	/**
	 * Returns an Akeeba Configuration object
	 *
	 * @return  \Akeeba\Engine\Configuration  The Akeeba Configuration object
	 */
	public static function &getConfiguration()
	{
		return self::getObjectInstance('\\Akeeba\\Engine\\Configuration');
	}

	/**
	 * Returns a statistics object, used to track current backup's progress
	 *
	 * @return  \Akeeba\Engine\Util\Statistics
	 */
	public static function &getStatistics()
	{
		return self::getObjectInstance('\\Akeeba\\Engine\\Util\\Statistics');
	}

	/**
	 * Returns the currently configured archiver engine
	 *
	 * @param   bool $reset Should I try to forcible create a new instance?
	 *
	 * @return  \Akeeba\Engine\Archiver\Base
	 */
	public static function &getArchiverEngine($reset = false)
	{
		static $class_name;

		if ($reset)
		{
			$class_name = null;
		}

		if (empty($class_name))
		{
			$registry = self::getConfiguration();
			$engine = $registry->get('akeeba.advanced.archiver_engine');
			$class_name = '\\Akeeba\\Engine\\Archiver\\' . ucfirst($engine);

			if (!class_exists($class_name, true))
			{
				$class_name = '\\Akeeba\\Engine\\Archiver\\Jpa';
			}
		}

		if ($reset)
		{
			self::unsetObjectInstance($class_name);
		}

		return self::getObjectInstance($class_name);
	}

	/**
	 * Returns the currently configured dump engine
	 *
	 * @param   boolean $reset Should I try to forcible create a new instance?
	 *
	 * @return  \Akeeba\Engine\Dump\Base
	 */
	public static function &getDumpEngine($reset = false)
	{
		static $class_name;

		if (empty($class_name))
		{
			$registry = self::getConfiguration();
			$engine = $registry->get('akeeba.advanced.dump_engine');
			$class_name = '\\Akeeba\\Engine\\Dump\\' . ucfirst($engine);

			if (!class_exists($class_name, true))
			{
				$class_name = '\\Akeeba\\Engine\\Dump\\Native';
			}
		}

		if ($reset)
		{
			self::unsetObjectInstance($class_name);
		}

		return self::getObjectInstance($class_name);
	}

	/**
	 * Returns the filesystem scanner engine instance
	 *
	 * @param   bool $reset Should I try to forcible create a new instance?
	 *
	 * @return  \Akeeba\Engine\Scan\Base  The scanner engine
	 */
	public static function &getScanEngine($reset = false)
	{
		static $class_name;

		if ($reset)
		{
			$class_name = null;
		}

		if (empty($class_name))
		{
			$registry = self::getConfiguration();
			$engine = $registry->get('akeeba.advanced.scan_engine');
			$class_name = '\\Akeeba\\Engine\\Scan\\' . ucfirst($engine);

			if (!class_exists($class_name, true))
			{
				$class_name = '\\Akeeba\\Engine\\Scan\\Large';
			}
		}

		if ($reset)
		{
			self::unsetObjectInstance($class_name);
		}

		return self::getObjectInstance($class_name);
	}

	/**
	 * Returns the current post-processing engine. If no class is specified we
	 * return the post-processing engine configured in akeeba.advanced.postproc_engine
	 *
	 * @param   string $engine The name of the post-processing class to forcibly return
	 *
	 * @return  \Akeeba\Engine\Postproc\Base
	 */
	public static function &getPostprocEngine($engine = null)
	{
		$class_name = '\\Akeeba\\Engine\\Postproc\\Fake';

		if (!is_null($engine))
		{
			$class_name = '\\Akeeba\\Engine\\Postproc\\' . ucfirst($engine);
		}

		if (is_null($engine) || !class_exists($class_name, true))
		{
			$registry = self::getConfiguration();
			$engine = $registry->get('akeeba.advanced.postproc_engine');
			$class_name = '\\Akeeba\\Engine\\Postproc\\' . ucfirst($engine);

			if (!class_exists($class_name, true))
			{
				$class_name = '\\Akeeba\\Engine\\Postproc\\None';
			}
		}

		return self::getObjectInstance($class_name);
	}

	/**
	 * Returns an instance of the Filters feature class
	 *
	 * @return  \Akeeba\Engine\Core\Filters  The Filters feature class' object instance
	 */
	public static function &getFilters()
	{
		return self::getObjectInstance('\\Akeeba\\Engine\\Core\\Filters');
	}

	/**
	 * Returns an instance of the specified filter group class. Do note that it does not
	 * work with platform filter classes. They are handled internally by AECoreFilters.
	 *
	 * @param   string $filter_name The filter class to load, without AEFilter prefix
	 *
	 * @return  \Akeeba\Engine\Filter\Base  The filter class' object instance
	 */
	public static function &getFilterObject($filter_name)
	{
		return self::getObjectInstance('\\Akeeba\\Engine\\Filter\\' . ucfirst($filter_name));
	}

	/**
	 * Loads an engine domain class and returns its associated object
	 *
	 * @param   string $domain_name The name of the domain, e.g. installer for AECoreDomainInstaller
	 *
	 * @return  \Akeeba\Engine\Base\Part
	 */
	public static function &getDomainObject($domain_name)
	{
		return self::getObjectInstance('\\Akeeba\\Engine\\Core\\Domain\\' . ucfirst($domain_name));
	}

	/**
	 * Returns a database connection object. It's an alias of AECoreDatabase::getDatabase()
	 *
	 * @param   array $options Options to use when instantiating the database connection
	 *
	 * @return  \Akeeba\Engine\Driver\Base
	 */
	public static function &getDatabase($options = null)
	{
		if (is_null($options))
		{
			$options = Platform::getInstance()->get_platform_database_options();
		}

		return Database::getDatabase($options);
	}

	/**
	 * Returns a database connection object. It's an alias of AECoreDatabase::getDatabase()
	 *
	 * @param   array $options Options to use when instantiating the database connection
	 *
	 * @return  \Akeeba\Engine\Driver\Base
	 */
	public static function unsetDatabase($options = null)
	{
		if (is_null($options))
		{
			$options = Platform::getInstance()->get_platform_database_options();
		}

		$db = Database::getDatabase($options);
		$db->close();

		Database::unsetDatabase($options);
	}

	/**
	 * Get the a reference to the Akeeba Engine's timer
	 *
	 * @return  \Akeeba\Engine\Core\Timer
	 */
	public static function &getTimer()
	{
		return self::getObjectInstance('\\Akeeba\\Engine\\Core\\Timer');
	}

	/**
	 * Get a reference to Akeeba Engine's main controller called Kettenrad
	 *
	 * @return  \Akeeba\Engine\Core\Kettenrad
	 */
	public static function &getKettenrad()
	{
		return self::getObjectInstance('\\Akeeba\\Engine\\Core\\Kettenrad');
	}

	// ========================================================================
	// Temporary objects which are not part of the engine state
	// ========================================================================

	/**
	 * Returns an instance of the factory storage class (formerly Tempvars)
	 *
	 * @return \Akeeba\Engine\Util\FactoryStorage
	 */
	public static function &getFactoryStorage()
	{
		return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\FactoryStorage');
	}

	/**
	 * Returns an instance of the encryption class
	 *
	 * @return \Akeeba\Engine\Util\Encrypt
	 */
	public static function &getEncryption()
	{
		return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\Encrypt');
	}

	/**
	 * Returns an instance of the CRC32 calculations class
	 *
	 * @return \Akeeba\Engine\Util\CRC32
	 */
	public static function &getCRC32Calculator()
	{
		return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\CRC32');
	}

	/**
	 * Returns an instance of the crypto-safe random value generator class
	 *
	 * @return \Akeeba\Engine\Util\RandomValue
	 */
	public static function &getRandval()
	{
		return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\RandomValue');
	}

	/**
	 * Returns an instance of the filesystem tools class
	 *
	 * @return \Akeeba\Engine\Util\FileSystem
	 */
	public static function &getFilesystemTools()
	{
		return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\FileSystem');
	}

	/**
	 * Returns an instance of the filesystem tools class
	 *
	 * @return \Akeeba\Engine\Util\FileLister
	 */
	public static function &getFileLister()
	{
		return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\FileLister');
	}

	/**
	 * Returns an instance of the engine parameters provider which provides information on scripting, GUI configuration
	 * elements and engine parts
	 *
	 * @return \Akeeba\Engine\Util\EngineParameters
	 */
	public static function &getEngineParamsProvider()
	{
		return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\EngineParameters');
	}

	/**
	 * Returns an instance of the log object
	 *
	 * @return \Akeeba\Engine\Util\Logger
	 */
	public static function &getLog()
	{
		return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\Logger');
	}

	/**
	 * Returns an instance of the configuration checks object
	 *
	 * @return \Akeeba\Engine\Util\ConfigurationCheck
	 */
	public static function &getConfigurationChecks()
	{
		return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\ConfigurationCheck');
	}

	/**
	 * Returns an instance of the secure settings handling object
	 *
	 * @return \Akeeba\Engine\Util\SecureSettings
	 */
	public static function &getSecureSettings()
	{
		return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\SecureSettings');
	}

	/**
	 * Returns an instance of the secure settings handling object
	 *
	 * @return \Akeeba\Engine\Util\TemporaryFiles
	 */
	public static function &getTempFiles()
	{
		return self::getTempObjectInstance('\\Akeeba\\Engine\\Util\\TemporaryFiles');
	}

	// ========================================================================
	// Handy functions
	// ========================================================================

	/**
	 * Returns the absolute path to Akeeba Engine's installation
	 *
	 * @return  string
	 */
	public static function getAkeebaRoot()
	{
		static $root = null;

		if (empty($root))
		{
			$root = __DIR__;
		}

		return $root;
	}

	// ========================================================================
	// Used in unit testing
	// ========================================================================

	/**
	 * Force an object instance which will survive serialisation. This is supposed to be used only in Unit Tests.
	 *
	 * @param   string      $class_name The class name used internally by the Factory
	 * @param   object|null $object     The object to push. Use null to unset the object
	 *
	 * @return  void
	 *
	 * @throws  \Exception  when you try using it outside of Unit Tests
	 */
	public static function forceObjectInstance($class_name, $object)
	{
		if (!interface_exists('PHPUnit_Exception', false))
		{
			$method = __METHOD__;
			throw new \Exception("You can only use $method in Unit Tests", 500);
		}

		$self = self::getInstance();

		if (is_null($object) && isset($self->objectList[$class_name]))
		{
			unset($self->objectList[$class_name]);
			return;
		}

		$self->objectList[$class_name] = $object;
	}

	/**
	 * Force an object instance which will not survive serialisation. This is supposed to be used only in Unit Tests.
	 *
	 * @param   string      $class_name The class name used internally by the Factory
	 * @param   object|null $object     The object to push. Use null to unset the object
	 *
	 * @return  void
	 *
	 * @throws  \Exception  when you try using it outside of Unit Tests
	 */
	public static function forceTempObjectInstance($class_name, $object)
	{
		if (!interface_exists('PHPUnit_Exception', false))
		{
			$method = __METHOD__;
			throw new \Exception("You can only use $method in Unit Tests", 500);
		}

		$self = self::getInstance();

		if (is_null($object) && isset($self->temporaryObjectList[$class_name]))
		{
			unset($self->temporaryObjectList[$class_name]);
			return;
		}

		$self->temporaryObjectList[$class_name] = $object;
	}
}

// Define and register the timeout trap
function AkeebaTimeoutTrap()
{
	if (connection_status() >= 2)
	{
		Factory::getLog()->log(LogLevel::ERROR, 'Akeeba Engine has timed out');
	}
}

register_shutdown_function("\\Akeeba\\Engine\\AkeebaTimeoutTrap");