<?php
/**
 * @package    solo
 * @copyright  Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license    GNU GPL version 3 or later
 */

namespace Solo\Model;

use Akeeba\Engine\Archiver\Directftp;
use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;
use Awf\Html\Select;
use Awf\Mvc\Model;
use Awf\Session\Randval;
use Awf\Text\Text;
use Solo\Helper\Escape;

class Restore extends Model
{
	/** @var   array  The backup record being restored */
	private $data = array();

	/** @var   string  The extension of the archive being restored */
	private $extension = null;

	/** @var   string  Absolute path to the archive being restored */
	private $path = null;

	/** @var   string  Random password, used to secure the restoration */
	public $password;

	/**
	 * Validates the data passed to the request.
	 *
	 * @return  void
	 *
	 * @throws  \RuntimeException
	 */
	function validateRequest()
	{
		// Is this a valid backup entry?
		$id = $this->getState('id', 0);
		$profile_id = $this->getState('profileid', 0);

		if (empty($id) && ($profile_id <= 0))
		{
			throw new \RuntimeException(Text::_('COM_AKEEBA_RESTORE_ERROR_INVALID_RECORD'), 500);
		}

		if (empty($id))
		{
			$id = $this->getLatestBackupForProfile($profile_id);
		}

		$data = Platform::getInstance()->get_statistics($id);

		if (empty($data))
		{
			throw new \RuntimeException(Text::_('COM_AKEEBA_RESTORE_ERROR_INVALID_RECORD'), 500);
		}

		if ($data['status'] != 'complete')
		{
			throw new \RuntimeException(Text::_('COM_AKEEBA_RESTORE_ERROR_INVALID_RECORD'), 500);
		}

		// Load the profile ID (so that we can find out the output directory)
		$profile_id = $data['profile_id'];
		Platform::getInstance()->load_configuration($profile_id);

		$path = $data['absolute_path'];
		$exists = @file_exists($path);

		if (!$exists)
		{
			// Let's try figuring out an alternative path
			$config = Factory::getConfiguration();
			$path = $config->get('akeeba.basic.output_directory', '') . '/' . $data['archivename'];
			$exists = @file_exists($path);
		}

		if (!$exists)
		{
			throw new \RuntimeException(Text::_('COM_AKEEBA_RESTORE_ERROR_ARCHIVE_MISSING'), 500);
		}

		$filename = basename($path);
		$lastDot = strrpos($filename, '.');
		$extension = strtoupper(substr($filename, $lastDot + 1));

		if (!in_array($extension, array('JPS', 'JPA', 'ZIP')))
		{
			throw new \RuntimeException(Text::_('COM_AKEEBA_RESTORE_ERROR_INVALID_TYPE'), 500);
		}

		$this->data = $data;
		$this->path = $path;
		$this->extension = $extension;

		$this->setState('extension', $extension);
	}

	/**
	 * Finds the latest backup for a given backup profile with an "OK" status (the archive file exists on your server).
	 * If none is found a RuntimeException is thrown.
	 *
	 * This method uses the code from the Transfers model for DRY reasons.
	 *
	 * @param   int  $profileID  The profile in which to locate the latest valid backup
	 *
	 * @return  int
	 *
	 * @throws  \RuntimeException
	 *
	 * @since   5.3.0
	 */
	public function getLatestBackupForProfile($profileID)
	{
		/** @var Transfers $transferModel */
		$transferModel = new Transfers($this->container);
		$latestBackup  = $transferModel->getLatestBackupInformation($profileID);

		if (empty($latestBackup))
		{
			throw new \RuntimeException(Text::sprintf('COM_AKEEBA_RESTORE_ERROR_NO_LATEST', $profileID));
		}

		return $latestBackup['id'];
	}

	/**
	 * Creates the restoration.php file with the restoration engine parameters
	 *
	 * @return  void
	 *
	 * @throws  \RuntimeException
	 */
	function createRestorationFile()
	{
		// Get a password
		$randVal = new Randval(new \Awf\Utils\Phpfunc());
		$this->password = base64_encode($randVal->generate(32));
		$this->setState('password', $this->password);

		// Do we have to use FTP?
		$procEngine = $this->getState('procengine', 'direct');

		// Get the absolute path to site's root
		$configuration = Factory::getConfiguration();
		if ($configuration->get('akeeba.platform.override_root', 0))
		{
			$siteRoot = $configuration->get('akeeba.platform.newroot', '[SITEROOT]');
		}
		else
		{
			$siteRoot = '[SITEROOT]';
		}

		if (stristr($siteRoot, '['))
		{
			$siteRoot = Factory::getFilesystemTools()->translateStockDirs($siteRoot);
		}

		if (empty($siteRoot))
		{
			throw new \RuntimeException(Text::_('SOLO_RESTORE_ERR_NOSITEROOT'), 500);
		}

		// Get the JPS password
		$password = Escape::escapeJS($this->getState('jps_key'));

		// Get min / max execution time
		$min_exec = $this->getState('min_exec', 0, 'int');
		$max_exec = $this->getState('max_exec', 5, 'int');
		$bias 	  = 75;

		// Used for tests
		if (defined('AKEEBA_TESTS_SLOW_RESTORATION'))
		{
			$max_exec = 2;
			$min_exec = 1;
			$bias	  = 75;
		}

		$data = "<?php\ndefined('_AKEEBA_RESTORATION') or die();\n";
		$data .= '$restoration_setup = array(' . "\n";
		$data .= <<<ENDDATA
	'kickstart.security.password' => '{$this->password}',
	'kickstart.tuning.max_exec_time' => '$max_exec',
	'kickstart.tuning.run_time_bias' => '$bias',
	'kickstart.tuning.min_exec_time' => '$min_exec',
	'kickstart.procengine' => '$procEngine',
	'kickstart.setup.sourcefile' => '{$this->path}',
	'kickstart.setup.destdir' => '$siteRoot',
	'kickstart.setup.restoreperms' => '0',
	'kickstart.setup.filetype' => '{$this->extension}',
	'kickstart.setup.dryrun' => '0',
	'kickstart.jps.password' => '$password'
ENDDATA;

		/**
		 * Should I enable the “Delete everything before extraction” option?
		 *
		 * This requires TWO conditions to be true:
		 *
		 * 1. The application-level configuration option showDeleteOnRestore was enabled to show the option to the user
		 * 2. The user has enabled this option (the Controller sets it in the zapbefore model variable)
		 */
		$shownDeleteOnRestore = $this->container->appConfig->get('showDeleteOnRestore', 0) == 1;

		if ($shownDeleteOnRestore && ($this->getState('zapbefore', 0, 'int') == 1))
		{
			$data .= ",\n\t'kickstart.setup.zapbefore' => '1'";
		}

		// If we're using the FTP or Hybrid engine we need to set up the FTP parameters
		if (in_array($procEngine, array('ftp', 'hybrid')))
		{
			$ftp_host = $this->getState('ftp_host', '');
			$ftp_port = $this->getState('ftp_port', '21');
			$ftp_user = $this->getState('ftp_user', '');
			$ftp_pass = addcslashes($this->getState('ftp_pass', ''), "'\\");
			$ftp_root = $this->getState('ftp_root', '');
			$ftp_ssl = $this->getState('ftp_ssl', 0);
			$ftp_pasv = $this->getState('ftp_root', 1);
			$tempdir = $this->getState('tmp_path', '');
			$data .= <<<ENDDATA
	,
	'kickstart.ftp.ssl' => '$ftp_ssl',
	'kickstart.ftp.passive' => '$ftp_pasv',
	'kickstart.ftp.host' => '$ftp_host',
	'kickstart.ftp.port' => '$ftp_port',
	'kickstart.ftp.user' => '$ftp_user',
	'kickstart.ftp.pass' => '$ftp_pass',
	'kickstart.ftp.dir' => '$ftp_root',
	'kickstart.ftp.tempdir' => '$tempdir'
ENDDATA;
		}

		$data .= ');';

		// Remove the old file, if it's there...
		$configPath = APATH_BASE . '/restoration.php';

		$fs = $this->container->fileSystem;
		clearstatcache(true, $configPath);

		if (@file_exists($configPath))
		{
			$fs->delete($configPath);
		}

		// Write new file
		$fs->write($configPath, $data);

		// Clear opcode caches for the generated .php file
		if (function_exists('opcache_invalidate'))
		{
			opcache_invalidate($configPath);
		}

		if (function_exists('apc_compile_file'))
		{
			apc_compile_file($configPath);
		}

		if (function_exists('wincache_refresh_if_changed'))
		{
			wincache_refresh_if_changed(array($configPath));
		}

		if (function_exists('xcache_asm'))
		{
			xcache_asm($configPath);
		}
	}

	/**
	 * Returns the default FTP parameters
	 *
	 * @return  array
	 */
	function getFTPParams()
	{
		$config = $this->container->appConfig;

		return array(
			'procengine' => $config->get('fs.driver', 'file') == 'ftp' ? 'hybrid' : 'direct',
			'ftp_host'   => $config->get('fs.host', 'localhost'),
			'ftp_port'   => $config->get('fs.port', '21'),
			'ftp_user'   => $config->get('fs.username', ''),
			'ftp_pass'   => $config->get('fs.password', ''),
			'ftp_root'   => $config->get('fs.directory', ''),
			'tempdir'    => APATH_BASE . '/tmp'
		);
	}

	/**
	 * Gets the options for the extraction mode drop-down
	 *
	 * @return  array
	 */
	function getExtractionModes()
	{
		$options = array();
		$options[] = Select::option('hybrid',	Text::_('COM_AKEEBA_RESTORE_LABEL_EXTRACTIONMETHOD_HYBRID'));
		$options[] = Select::option('direct',	Text::_('COM_AKEEBA_RESTORE_LABEL_EXTRACTIONMETHOD_DIRECT'));
		$options[] = Select::option('ftp',		Text::_('COM_AKEEBA_RESTORE_LABEL_EXTRACTIONMETHOD_FTP'));

		return $options;
	}

	/**
	 * AJAX request proxy
	 *
	 * @return  boolean
	 */
	function doAjax()
	{
		$ajax = $this->getState('ajax');

		switch ($ajax)
		{
			// FTP Connection test for DirectFTP
			case 'testftp':
				// Grab request parameters
				$config = array(
					'host'    => $this->input->get('host', '', 'none', 2),
					'port'    => $this->input->get('port', 21, 'int'),
					'user'    => $this->input->get('user', '', 'none', 2),
					'pass'    => $this->input->get('pass', '', 'none', 2),
					'initdir' => $this->input->get('initdir', '', 'none', 2),
					'usessl'  => $this->input->get('usessl', 'cmd') == 'true',
					'passive' => $this->input->get('passive', 'cmd') == 'true'
				);

				// Perform the FTP connection test
				$test = new Directftp();
				$test->initialize('', $config);
				$errors = $test->getError();

				if (empty($errors))
				{
					$result = true;
				}
				else
				{
					$result = $errors;
				}

				break;

			// Unrecognized AJAX task
			default:
				$result = false;

				break;
		}

		return $result;
	}
} 
