<?php
/**
 * @package   AkeebaBackup
 * @copyright Copyright (c)2009-2014 Nicholas K. Dionysopoulos
 * @license   GNU General Public License version 3, or later
 *
 * @since     3.3
 */

// Protect from unauthorized access
defined('_JEXEC') or die();

use Akeeba\Engine\Factory;
use Akeeba\Engine\Platform;

/**
 * Integrated restoration Model
 *
 */
class AkeebaModelSrprestores extends F0FModel
{
	private $data;

	private $extension;

	private $path;

	public $id;

	public $info;

	/**
	 * Sets the list of IDs from the request data
	 */
	public function setIDsFromRequest()
	{
		// Get the ID or list of IDs from the request or the configuration
		$cid = $this->input->get('cid', null, 'array');
		$id  = $this->input->get('id', 0, 'int');

		if (is_array($cid) && !empty($cid))
		{
			$this->setIds($cid);
		}
		else
		{
			if (empty($id))
			{
				$this->setId($id);
			}
		}

		return $this;
	}

	/**
	 * Validates the data passed to the request.
	 *
	 * @return mixed True if all is OK, an error string if something is wrong
	 */
	function validateRequest()
	{
		// Does the ID exist?
		$this->setIDsFromRequest();
		$id = $this->getId();
		if (empty($id))
		{
			return JText::_('RESTORE_ERROR_INVALID_RECORD');
		}

		// Is this a valid backup entry?
		$data = Platform::getInstance()->get_statistics($id);
		if (empty($data))
		{
			return JText::_('RESTORE_ERROR_INVALID_RECORD');
		}

		// Is this a complete backup?
		if ($data['status'] != 'complete')
		{
			return JText::_('RESTORE_ERROR_INVALID_RECORD');
		}

		// Is it a restoration point backup?
		if ($data['tag'] != 'restorepoint')
		{
			return JText::_('RESTORE_ERROR_NOT_AN_SRP');
		}

		$rawDataParts        = explode("\n", $data['comment']);
		$this->info          = json_decode($rawDataParts[1]);
		$this->info->srpdate = $data['backupstart'];

		// 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)
		{
			return JText::_('RESTORE_ERROR_ARCHIVE_MISSING');
		}

		$filename  = basename($path);
		$lastdot   = strrpos($filename, '.');
		$extension = strtoupper(substr($filename, $lastdot + 1));
		if ( !in_array($extension, array('JPA', 'ZIP')))
		{
			return JText::_('RESTORE_ERROR_INVALID_TYPE');
		}

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

		return true;
	}

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

		// Get the absolute path to site's root
		$siteroot = JPATH_SITE;

		$restoration_setup = array(
			'kickstart.enabled'              => false,
			'kickstart.tuning.max_exec_time' => '5',
			'kickstart.tuning.run_time_bias' => '75',
			'kickstart.tuning.min_exec_time' => '0',
			'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'
		);

		if ($procengine == 'ftp')
		{
			$ftp_host = $this->getState('ftp_host', '');
			$ftp_port = $this->getState('ftp_port', '21');
			$ftp_user = $this->getState('ftp_user', '');
			$ftp_pass = $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', '');

			$extraOptions = array(
				'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
			);

			$restoration_setup = array_merge($restoration_setup, $extraOptions);
		}

		$json = json_encode($restoration_setup);

		$session = JFactory::getSession();
		$session->set('restoration_setup', $json, 'akeeba');
	}

	public function getRestorationParameters()
	{
		$session = JFactory::getSession();
		$json    = $session->get('restoration_setup', null, 'akeeba');

		if ( !empty($json))
		{
			return json_decode($json, true);
		}
		else
		{
			return array();
		}
	}

	function getFTPParams()
	{
		$config = JFactory::getConfig();
		if (version_compare(JVERSION, '3.0', 'ge'))
		{
			return array(
				'procengine' => $config->get('ftp_enable', 0) ? 'ftp' : 'direct',
				'ftp_host'   => $config->get('ftp_host', 'localhost'),
				'ftp_port'   => $config->get('ftp_port', '21'),
				'ftp_user'   => $config->get('ftp_user', ''),
				'ftp_pass'   => $config->get('ftp_pass', ''),
				'ftp_root'   => $config->get('ftp_root', ''),
				'tempdir'    => $config->get('tmp_path', '')
			);
		}
		else
		{
			return array(
				'procengine' => $config->getValue('config.ftp_enable', 0) ? 'ftp' : 'direct',
				'ftp_host'   => $config->getValue('config.ftp_host', 'localhost'),
				'ftp_port'   => $config->getValue('config.ftp_port', '21'),
				'ftp_user'   => $config->getValue('config.ftp_user', ''),
				'ftp_pass'   => $config->getValue('config.ftp_pass', ''),
				'ftp_root'   => $config->getValue('config.ftp_root', ''),
				'tempdir'    => $config->getValue('config.tmp_path', '')
			);
		}
	}

	function getExtractionModes()
	{
		$options   = array();
		$options[] = JHTML::_('select.option', 'direct', JText::_('RESTORE_LABEL_EXTRACTIONMETHOD_DIRECT'));
		$options[] = JHTML::_('select.option', 'ftp', JText::_('RESTORE_LABEL_EXTRACTIONMETHOD_FTP'));

		return $options;
	}

	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 \Akeeba\Engine\Archiver\Directftp();
				$test->initialize('', $config);
				$errors = $test->getError();
				if (empty($errors))
				{
					$result = true;
				}
				else
				{
					$result = $errors;
				}
				break;

			case 'restoreFilesPing':
			case 'restoreFilesStart':
			case 'restoreFilesStep':
			case 'restoreFilesFinalize':
				global $restoration_setup;
				$restoration_setup = $this->getRestorationParameters();

				define('KICKSTART', 1);

				include_once(JPATH_COMPONENT_ADMINISTRATOR . '/restore.php');
				akeebaTinyHackForRestorationObserver();

				masterSetup();


				$retArray = array(
					'status'  => true,
					'message' => null
				);

				switch ($ajax)
				{
					case 'restoreFilesPing':
						// ping task - realy does nothing!
						$timer = AKFactory::getTimer();
						$timer->enforce_min_exec_time();
						break;

					case 'restoreFilesStart':
						AKFactory::nuke(); // Reset the factory

					case 'restoreFilesStep':
						$config = JFactory::getConfig();
						if (version_compare(JVERSION, '3.0', 'ge'))
						{
							$tmp_path = $config->get('tmp_path', '');
						}
						else
						{
							$tmp_path = $config->getValue('tmp_path', '');
						}
						$override = array(
							'rename_dirs' => array('sql' => rtrim($tmp_path, '/\\') . '/sql')
						);
						$engine   = AKFactory::getUnarchiver($override); // Get the engine
						$observer = new RestorationObserver(); // Create a new observer
						$engine->attach($observer); // Attach the observer
						$engine->tick();
						$ret = $engine->getStatusArray();

						if ($ret['Error'] != '')
						{
							$retArray['status']  = false;
							$retArray['done']    = true;
							$retArray['message'] = $ret['Error'];
						}
						elseif ( !$ret['HasRun'])
						{
							$retArray['files']    = $observer->filesProcessed;
							$retArray['bytesIn']  = $observer->compressedTotal;
							$retArray['bytesOut'] = $observer->uncompressedTotal;
							$retArray['status']   = true;
							$retArray['done']     = true;
						}
						else
						{
							$retArray['files']    = $observer->filesProcessed;
							$retArray['bytesIn']  = $observer->compressedTotal;
							$retArray['bytesOut'] = $observer->uncompressedTotal;
							$retArray['status']   = true;
							$retArray['done']     = false;
							$retArray['factory']  = AKFactory::serialize();
						}
						break;

					case 'restoreFilesFinalize':
						$root = AKFactory::get('kickstart.setup.destdir');
						// Remove the sql dump directory
						$config = JFactory::getConfig();
						if (version_compare(JVERSION, '3.0', 'ge'))
						{
							$tmp_path = $config->get('tmp_path', '');
						}
						else
						{
							$tmp_path = $config->getValue('tmp_path', '');
						}
						$sqldir = rtrim($tmp_path, '/\\') . '/sql';
						recursive_remove_directory($sqldir);
						break;
				}

				return $retArray;

				break;

			case 'dbRestoreStart':
				$this->dbRestorationInit();
			case 'dbRestore':
				$result = $this->dbRestore();
				break;

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

		return $result;
	}

	private function dbRestorationInit()
	{
		// Get the path to SQL files
		$config = JFactory::getConfig();
		if (version_compare(JVERSION, '3.0', 'ge'))
		{
			$path = rtrim($config->get('tmp_path', ''), '/\\') . '/sql';
		}
		else
		{
			$path = rtrim($config->getValue('tmp_path', ''), '/\\') . '/sql';
		}
		$sqlfile = 'site.sql';

		// Fallback for SRPs created with older versions of Akeeba Backup
		if ( !file_exists($path . '/' . $sqlfile))
		{
			$sqlfile = 'joomla.sql';
		}

		$totalSize = 0;
		$runSize   = 0;
		$partsMap  = array();
		$found     = true;
		$index     = 0;

		while ($found)
		{
			if ($index == 0)
			{
				$basename = $sqlfile;
			}
			else
			{
				$basename = substr($sqlfile, 0, -4) . '.s' . sprintf('%02u', $index);
			}

			$file = $path . '/' . $basename;
			if ( !file_exists($file))
			{
				$found = false;
				break;
			}

			$filesize = @filesize($file);
			$totalSize += intval($filesize);
			$partsMap[] = $file;

			$index++;
		}

		$parts = count($partsMap);

		$dbInfo = (object)array(
			'totalSize' => $totalSize,
			'runSize'   => $runSize,
			'parts'     => $parts,
			'partsMap'  => $partsMap,
			'curpart'   => 0,
			'foffset'   => 0,
			'file'      => null,
			'start'     => 0
		);

		$session = JFactory::getSession();
		$session->clear('restoration_setup', 'akeeba');
		$session->set('dbinfo', $dbInfo, 'akeeba');
	}

	private function dbGetInfo()
	{
		$session = JFactory::getSession();

		return $session->get('dbinfo', (object)array(), 'akeeba');
	}

	private function dbSetInfo($dbInfo)
	{
		$session = JFactory::getSession();
		$session->set('dbinfo', $dbInfo, 'akeeba');
	}

	private function dbGetNextFile()
	{
		$dbInfo = $this->dbGetInfo();

		if ($dbInfo->curpart >= ($dbInfo->parts - 1))
		{
			return false;
		}

		$dbInfo->curpart++;
		$dbInfo->foffset = 0;

		$this->dbSetInfo($dbInfo);

		return $this->dbOpenFile();
	}

	private function dbOpenFile()
	{
		$dbInfo = $this->dbGetInfo();

		$filename = $dbInfo->partsMap[$dbInfo->curpart];

		if ( !$dbInfo->file = @fopen($filename, "rt"))
		{
			$this->setError("Could not open SQL dump file $filename");

			return false;
		}
		else
		{
			// Get the file size
			if (fseek($dbInfo->file, 0, SEEK_END) == 0)
			{
				$filesize = ftell($dbInfo->file);
			}
			else
			{
				$this->setError("Could not determine the file size of the SQL dump file");

				return false;
			}
		}

		// Check start and foffset are numeric values
		if ( !is_numeric($dbInfo->start) || !is_numeric($dbInfo->foffset))
		{
			$this->setError("Invalid parameters");

			return false;
		}

		$dbInfo->start   = floor($dbInfo->start);
		$dbInfo->foffset = floor($dbInfo->foffset);

		// Check $foffset upon $filesize
		if ($dbInfo->foffset > $filesize)
		{
			$this->setError("File offset is after EOF");

			return false;
		}

		// Set file pointer to $foffset
		if (fseek($dbInfo->file, $dbInfo->foffset) != 0)
		{
			$this->setError("Can not seek to specified file offset");

			return false;
		}

		$this->dbSetInfo($dbInfo);

		return true;
	}

	private function getTimeRemaining($reset = false, $newmax = 5)
	{
		static $start = 0;
		static $maxAllowed = 5;

		if ($reset)
		{
			$start      = microtime(true);
			$maxAllowed = $newmax;
		}

		return $maxAllowed - (microtime(true) - $start);
	}

	private function dbRestore()
	{
		$dbInfo = $this->dbGetInfo();

		define('DATA_CHUNK_LENGTH', 65536); // How many bytes to read per step
		define('MAX_QUERY_LINES', 300); // How many lines may be considered to be one query (except text lines)

		$skipMySQLErrorNumbers = array(
			1262,
			1263,
			1264,
			1265, // "Data truncated" warning
			1266,
			1287,
			1299
			// , 1406	// "Data too long" error
		);

		$comment = array('#', '-- ', '---', '/*!');

		$this->getTimeRemaining(true, 3);

		$file = false;

		if ( !$this->dbOpenFile())
		{
			return array('percent' => 0, 'message' => '', 'error' => $this->getError(), 'done' => 0);
		}

		$db = JFactory::getDBO();
		$db->setQuery('SET FOREIGN_KEY_CHECKS = 0');
		$db->execute();

		$query                = "";
		$queries              = 0;
		$dbInfo->totalqueries = empty($dbInfo->totalqueries) ? 0 : $dbInfo->totalqueries;
		$dbInfo->linenumber   = $dbInfo->start;
		$totalsizeread        = 0;
		$linenumber           = 0;

		while ($this->getTimeRemaining() > 0)
		{
			// Read one line (1 line = 1 query)
			$query = "";
			while ( !feof($dbInfo->file) && (strpos($query, "\n") === false))
			{
				$query .= fgets($dbInfo->file, DATA_CHUNK_LENGTH);
			}

			// An empty query is EOF. Are we done or should I skip to the next file?
			if (empty($query) || ($query === false))
			{
				if ($dbInfo->curpart >= ($dbInfo->parts - 1))
				{
					break;
				}
				else
				{
					// Register the bytes read
					$current_foffset = @ftell($dbInfo->file);
					$dbInfo->runSize += $current_foffset - $dbInfo->foffset;
					// Get the next file
					if ( !$this->dbGetNextFile())
					{
						return array('percent' => 0, 'message' => '', 'error' => $this->getError(), 'done' => 0);
					}
					// Rerun the fetcher
					continue;
				}
			}

			if (substr($query, -1) != "\n")
			{
				// WTF? We read more data than we should?! Roll back the file
				$rollback = strlen($query) - strpos($query, "\n");
				fseek($dbInfo->file, -$rollback, SEEK_CUR);
				// And chop the line
				$query = substr($query, 0, $rollback);
			}

			// Handle DOS linebreaks
			$query = str_replace("\r\n", "\n", $query);
			$query = str_replace("\r", "\n", $query);

			// Skip comments and blank lines only if NOT in parents
			$skipline = false;
			reset($comment);
			foreach ($comment as $comment_value)
			{
				if (trim($query) == "" || strpos($query, $comment_value) === 0)
				{
					$skipline = true;
					break;
				}
			}
			if ($skipline)
			{
				$linenumber++;
				continue;
			}

			$query = trim($query, " \n");
			$query = rtrim($query, ';');

			// CREATE TABLE query pre-processing
			$replaceAll     = false;
			$changeEncoding = false;
			if (substr($query, 0, 12) == 'CREATE TABLE')
			{
				// Yes, 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, 1, $pos - 1);
				}
				unset($restOfQuery);

				// Try to drop the table anyway
				$dropQuery = 'DROP TABLE IF EXISTS `' . $tableName . '`;';
				$db->setQuery(trim($dropQuery), false);
				if ( !$db->execute())
				{
					$this->setError($db->getError());

					return array('percent' => 0, 'message' => '', 'error' => $this->getError(), 'done' => 0);
				}
			}
			else
				// CREATE VIEW query pre-processing
				// In any case, drop the view before attempting to create it. (Views can't be renamed)
				if ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, ' VIEW ') !== false))
				{
					// Yes, 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, 1, $pos - 1);
					}
					unset($restOfQuery);

					// Try to drop the view anyway
					$dropQuery = 'DROP VIEW IF EXISTS `' . $tableName . '`;';
					$db->setQuery(trim($dropQuery), false);
					if ( !$db->execute())
					{
						$this->setError($db->getError());

						return array('percent' => 0, 'message' => '', 'error' => $this->getError(), 'done' => 0);
					}
				}

			if ( !empty($query))
			{
				$db->setQuery(trim($query));
				if ( !$db->execute())
				{
					// Skip over errors we can safely ignore...
					if (in_array($db->errno, $skipMySQLErrorNumbers))
					{
						continue;
					}

					$this->setError($db->getError());

					return array('percent' => 0, 'message' => '', 'error' => $this->getError(), 'done' => 0);
				}
			}

			$totalsizeread += strlen($query);
			$dbInfo->totalqueries++;
			$queries++;
			$query = "";
			$linenumber++;
		}

		// Get the current file position
		$current_foffset = ftell($dbInfo->file);
		if ($current_foffset === false)
		{
			if ($dbInfo->file)
			{
				fclose($dbInfo->file);
			}
			$this->setError("Can not read file pointer");

			return array('percent' => 0, 'message' => '', 'error' => $this->getError(), 'done' => 0);
		}
		else
		{
			$dbInfo->runSize += $current_foffset - $dbInfo->foffset;
			$dbInfo->foffset = $current_foffset;
		}

		// Return statistics
		if ($dbInfo->totalSize < 1)
		{
			$pct_done = 0;
		}
		else
		{
			$pct_done = ceil($dbInfo->runSize / $dbInfo->totalSize * 100);
		}
		$bytes_done  = $dbInfo->runSize;
		$bytes_tota  = $dbInfo->totalSize;
		$bytes_togo  = $dbInfo->totalSize - $dbInfo->runSize;
		$kbytes_done = round($bytes_done / 1024, 2);
		$kbytes_tota = round($bytes_tota / 1024, 2);

		// Check for global EOF
		if (($dbInfo->curpart >= ($dbInfo->parts - 1)) && feof($dbInfo->file))
		{
			$bytes_togo = 0;
		}

		if ($bytes_togo == 0)
		{
			// Clear stored variables if we're finished
			$lines_togo   = '0';
			$lines_tota   = $linenumber - 1;
			$queries_togo = '0';
			$queries_tota = $dbInfo->totalqueries;
		}
		else
		{
			$this->dbSetInfo($dbInfo);
		}

		// Close the file
		if ($dbInfo->file)
		{
			fclose($dbInfo->file);
		}

		// Return meaningful data to AJAX
		$ret = array(
			'percent' => $pct_done,
			'message' => 'Restored ' . sprintf('%1.2f', $kbytes_done) . ' of ' . sprintf('%1.2f', $kbytes_tota) . ' Kilobytes (' .
				sprintf('%02.1f', $pct_done) . ' %)',
			'error'   => '',
			'done'    => ($bytes_togo == 0) ? '1' : '0'
		);

		return $ret;
	}
}

function akeebaTinyHackForRestorationObserver()
{
	// The observer class, used to report number of files and bytes processed
	class RestorationObserver extends AKAbstractPartObserver
	{
		public $compressedTotal = 0;

		public $uncompressedTotal = 0;

		public $filesProcessed = 0;

		public function update($object, $message)
		{
			if ( !is_object($message))
			{
				return;
			}

			if ( !array_key_exists('type', get_object_vars($message)))
			{
				return;
			}

			if ($message->type == 'startfile')
			{
				$this->filesProcessed++;
				$this->compressedTotal += $message->content->compressed;
				$this->uncompressedTotal += $message->content->uncompressed;
			}
		}

		public function __toString()
		{
			return __CLASS__;
		}
	}
}