<?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();

/**
 * Integrated restoration Model
 *
 */
class AkeebaModelSrprestores extends FOFModel
{
	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 = AEPlatform::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'];
		AEPlatform::getInstance()->load_configuration($profile_id);

		$path = $data['absolute_path'];
		$exists = @file_exists($path);
		if(!$exists)
		{
			// Let's try figuring out an alternative path
			$config = AEFactory::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 AEArchiverDirectftp();
				$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__;
		}

	}
}