<?php
/**
* @version	$Id: backup_helper.php 16513 2017-01-20 14:10:53Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2011 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

defined('FULL_PATH') or die('restricted access!');

class BackupHelper extends kBase {

	const SQL_ERROR_DURING_RESTORE = -3;

	const FAILED_READING_BACKUP_FILE = -1;

	/**
	 * Backup 50 records per step
	 */
	const BACKUP_PER_STEP = 50;

	/**
	 * Restore 200 lines per step
	 */
	const RESTORE_PER_STEP = 200;

	/**
	 * Path, where backups are stored
	 *
	 * @var string
	 */
	protected $path = '';

	public function __construct()
	{
		parent::__construct();

		$this->path = $this->Application->ConfigValue('Backup_Path');
	}

	/**
	 * Backup all data
	 *
	 * @return bool
	 */
	function initBackup()
	{
		/** @var FileHelper $file_helper */
		$file_helper = $this->Application->recallObject('FileHelper');

		if (!$file_helper->CheckFolder($this->path) || !is_writable($this->path)) {
			$this->Application->SetVar('error_msg', $this->Application->Phrase('la_Text_backup_access'));

			return false;
		}

		$tables = $this->_getBackupTables();

		$backup_progress = Array (
			'table_num' => 0,
			'table_names' => $tables,
			'table_count' => count($tables),
			'record_count' => 0,
			'file_name' => $this->getBackupFile( adodb_mktime() ),
		);

		$this->Application->RemoveVar('adm.backupcomplete_filename');
		$this->Application->RemoveVar('adm.backupcomplete_filesize');

		$fp = fopen($backup_progress['file_name'], 'a');

		// write down module versions, used during backup
		foreach ($this->Application->ModuleInfo as $module_name => $module_info) {
			fwrite($fp, '# ' . $module_name . ' Version: ' . $module_info['Version'] . ";\n");
		}

		fwrite($fp, "#------------------------------------------\n\n");

		// write down table structure
		$out = Array ();

		foreach ($tables as $table) {
			$out[] = $this->_getCreateTableSql($table);
		}

		fwrite($fp, implode("\n", $out) . "\n");
		fclose($fp);

		$this->Application->StoreVar('adm.backup_status', serialize($backup_progress));

		return true;
	}

	/**
	 * Returns tables, that can be backed up
	 *
	 * @return Array
	 * @access protected
	 */
	protected function _getBackupTables()
	{
		$ret = Array ();
		$tables = $this->Conn->GetCol('SHOW TABLES LIKE "' . TABLE_PREFIX . '%"');

		foreach ($tables as $table) {
			if ( strpos($table, 'ses_') === false ) {
				$ret[] = $table;
			}
		}

		return $ret;
	}

	public function performBackup()
	{
		$backup_progress = unserialize($this->Application->RecallVar('adm.backup_status'));
		$current_table = $backup_progress['table_names'][ $backup_progress['table_num'] ];

		// get records
		$a_records = $this->_getInsertIntoSql($current_table, $backup_progress['record_count'], self::BACKUP_PER_STEP);

		if ( $a_records['num'] < self::BACKUP_PER_STEP ) {
			$backup_progress['table_num']++;
			$backup_progress['record_count'] = 0;
		}
		else {
			$backup_progress['record_count'] += self::BACKUP_PER_STEP;
		}

		if ( $a_records['sql'] ) {
			$fp = fopen($backup_progress['file_name'], 'a');
			fwrite($fp, $a_records['sql']);
			fclose($fp);
		}

		$percent = ($backup_progress['table_num'] / $backup_progress['table_count']) * 100;

		if ( $percent >= 100 ) {
			$percent = 100;
			$this->Application->StoreVar('adm.backupcomplete_filename', $backup_progress['file_name']);
			$this->Application->StoreVar('adm.backupcomplete_filesize', round(filesize($backup_progress['file_name']) / 1024 / 1024, 2)); // Mbytes
		}
		else {
			$this->Application->StoreVar('adm.backup_status', serialize($backup_progress));
		}

		return round($percent);

	}

	/**
	 * Returns sql, that will insert given amount of data into given table
	 *
	 * @param string $table
	 * @param int $start
	 * @param int $limit
	 * @return Array
	 * @access protected
	 */
	protected function _getInsertIntoSql($table, $start, $limit)
	{
		$sql = 'SELECT *
				FROM ' . $table . '
				' . $this->Conn->getLimitClause($start, $limit);
	    $a_data = $this->Conn->Query($sql);

		if ( !$a_data ) {
			return Array ('num' => 0, 'sql' => '',);
		}

	    $ret = '';
		$fields_sql = $this->_getFieldsSql( $a_data[0] );

		foreach ($a_data AS $a_row) {
			$values_sql = '';

			foreach ($a_row as $field_value) {
				$values_sql .= $this->Conn->qstr($field_value) . ',';
			}

			$values_sql = substr($values_sql, 0, -1);

			$sql = 'INSERT INTO ' . $table . ' (' . $fields_sql . ') VALUES (' . $values_sql . ');';
			$sql = str_replace("\n", "\\n", $sql);
			$sql = str_replace("\r", "\\r", $sql);
			$ret .= $sql . "\n";
		}

		if ( strlen(TABLE_PREFIX) ) {
			$ret = str_replace('INSERT INTO ' . TABLE_PREFIX, 'INSERT INTO ', $ret);
		}

		return Array ('num' => count($a_data), 'sql' => $ret);
	}

	/**
	 * Builds field list part of INSERT INTO statement based on given fields
	 *
	 * @param Array $row
	 * @return string
	 */
	protected function _getFieldsSql($row)
	{
		$ret = '';
		$a_fields = array_keys($row);

	    foreach ($a_fields AS $field_name) {
			$ret .= '`' . $field_name . '`,';
	    }

		return substr($ret, 0, -1);
	}

	/**
	 * Returns sql, that will create given table
	 *
	 * @param string $table
	 * @param string $crlf
	 * @return string
	 */
	protected function _getCreateTableSql($table, $crlf = "\n")
	{
		$this->Conn->Query('SET SQL_QUOTE_SHOW_CREATE = 0');
	    $tmp_res = $this->Conn->Query('SHOW CREATE TABLE ' . $table);

		if ( !is_array($tmp_res) || !isset($tmp_res[0]) ) {
			return '';
		}

		// add drop table & create table
		$schema_create = 'DROP TABLE IF EXISTS ' . $table . ';' . $crlf;
		$schema_create .= '# --------------------------------------------------------' . $crlf;
		$schema_create .= str_replace("\n", $crlf, $tmp_res[0]['Create Table']);

		// remove table prefix from dump
		if ( strlen(TABLE_PREFIX) ) {
			$schema_create = str_replace('DROP TABLE IF EXISTS ' . TABLE_PREFIX, 'DROP TABLE ', $schema_create);
			$schema_create = str_replace('CREATE TABLE ' . TABLE_PREFIX, 'CREATE TABLE ', $schema_create);
		}

		// remove any table properties (e.g. auto-increment, collation, etc)
		$last_brace_pos = strrpos($schema_create, ')');

		if ( $last_brace_pos !== false ) {
			$schema_create = substr($schema_create, 0, $last_brace_pos + 1);
		}

		$schema_create .= "\n# --------------------------------------------------------\n";

	    return $schema_create;
	}

	public function initRestore()
	{
		$file = $this->getBackupFile();

		$restoreProgress = Array (
			'file_pos' => 0,
			'file_name' => $file,
			'file_size' => filesize($file),
		);

		$this->Application->RemoveVar('adm.restore_success');
		$this->Application->StoreVar('adm.restore_status', serialize($restoreProgress));
	}

	/**
	 * Restores part of backup file
	 *
	 * @return int
	 * @access public
	 */
	public function performRestore()
	{
		$restore_progress = unserialize($this->Application->RecallVar('adm.restore_status'));
		$filename = $restore_progress['file_name'];
		$file_offset = $restore_progress['file_pos'];
		$size = filesize($filename);

		if ( $file_offset > $size ) {
			return self::FAILED_READING_BACKUP_FILE;
		}

		$fp = fopen($filename, "r");
		if ( !$fp ) {
			return self::FAILED_READING_BACKUP_FILE;
		}

		if ( $file_offset > 0 ) {
			fseek($fp, $file_offset);
		}
		else {
			$end_of_sql = FALSE;
			$sql = "";

			while ( !feof($fp) && !$end_of_sql ) {
				$line = fgets($fp);

				if ( substr($line, 0, 11) == "INSERT INTO" ) {
					$end_of_sql = TRUE;
				}
				else {
					$sql .= $line;
					$file_offset = ftell($fp) - strlen($line);
				}
			}

			if ( strlen($sql) ) {
				$error = $this->runSchemaText($sql);

				if ( $error != '' ) {
					$this->Application->StoreVar('adm.restore_error', $error);

					return self::SQL_ERROR_DURING_RESTORE;
				}
			}

			fseek($fp, $file_offset);
		}

		$lines_read = 0;
		$all_sqls = Array ();

		while ( $lines_read < self::RESTORE_PER_STEP && !feof($fp) ) {
			$sql = fgets($fp);

			if ( strlen($sql) ) {
				$all_sqls[] = $sql;
				$lines_read++;
			}
		}

		$file_offset = !feof($fp) ? ftell($fp) : $size;

		fclose($fp);

		if ( count($all_sqls) > 0 ) {
			$error = $this->runSQLText($all_sqls);

			if ( $error != '' ) {
				$this->Application->StoreVar('adm.restore_error', $error);

				return self::SQL_ERROR_DURING_RESTORE;
			}
		}

		$restore_progress['file_pos'] = $file_offset;
		$this->Application->StoreVar('adm.restore_status', serialize($restore_progress));

		return round($file_offset / $size * 100);
	}

	/**
	 * Adds table prefix to given sql set
	 *
	 * @param string $sql
	 * @return void
	 * @access protected
	 */
	protected function addTablePrefix(&$sql)
	{
		$table_prefix = 'restore' . TABLE_PREFIX;

		if ( strlen($table_prefix) > 0 ) {
			$replacements = Array ('INSERT INTO ', 'UPDATE ', 'ALTER TABLE ', 'DELETE FROM ', 'REPLACE INTO ');

			foreach ($replacements as $replacement) {
				$sql = str_replace($replacement, $replacement . $table_prefix, $sql);
			}
		}

		$sql = str_replace('CREATE TABLE ', 'CREATE TABLE IF NOT EXISTS ' . $table_prefix, $sql);
		$sql = str_replace('DROP TABLE ', 'DROP TABLE IF EXISTS ' . $table_prefix, $sql);
	}

	/**
	 * Run given schema sqls and return error, if any
	 *
	 * @param $sql
	 * @return string
	 * @access protected
	 */
	protected function runSchemaText($sql)
	{
		$this->addTablePrefix($sql);

		$commands = explode("# --------------------------------------------------------", $sql);

		if ( count($commands) > 0 ) {
			$commands = array_map('trim', $commands);

			foreach ($commands as $cmd) {
				if ( strlen($cmd) > 0 ) {
					$this->Conn->Query($cmd);

					if ( $this->Conn->hasError() ) {
						return $this->Conn->getErrorMsg() . " COMMAND:<PRE>$cmd</PRE>";
					}
				}
			}
		}

		return '';
	}

	/**
	 * Runs given sqls and return error message, if any
	 *
	 * @param $all_sqls
	 * @return string
	 * @access protected
	 */
	protected function runSQLText($all_sqls)
	{
		$line = 0;

		while ( $line < count($all_sqls) ) {
			$sql = $all_sqls[$line];

			if ( strlen(trim($sql)) > 0 && substr($sql, 0, 1) != "#" ) {
				$this->addTablePrefix($sql);
				$sql = trim($sql);

				if ( strlen($sql) > 0 ) {
					$this->Conn->Query($sql);

					if ( $this->Conn->hasError() ) {
						return $this->Conn->getErrorMsg() . " COMMAND:<PRE>$sql</PRE>";
					}
				}
			}

			$line++;
		}

		return '';
	}

	/**
	 * Replaces current tables with restored tables
	 *
	 * @return void
	 * @access public
	 */
	public function replaceRestoredFiles()
	{
		// gather restored table names
		$tables = $this->Conn->GetCol('SHOW TABLES');
		$mask_restore_table = '/^restore' . TABLE_PREFIX . '(.*)$/';

		foreach ($tables as $table) {
			if ( preg_match($mask_restore_table, $table) ) {
				$old_table = substr($table, 7);

				$this->Conn->Query('DROP TABLE IF EXISTS ' . $old_table);
				$this->Conn->Query('CREATE TABLE ' . $old_table . ' LIKE ' . $table);
				$this->Conn->Query('INSERT INTO ' . $old_table . ' SELECT * FROM ' . $table);
				$this->Conn->Query('DROP TABLE ' . $table);
			}
		}
	}

	/**
	 * Deletes current backup file
	 *
	 * @return void
	 * @access public
	 */
	public function delete()
	{
		@unlink($this->getBackupFile());
	}

	/**
	 * Returns path to current backup file
	 *
	 * @param int|null $backup_date
	 * @return string
	 * @access protected
	 */
	protected function getBackupFile($backup_date = null)
	{
		if ( !isset($backup_date) ) {
			$backup_date = $this->Application->GetVar('backupdate');
		}

		return $this->path . '/dump' . $backup_date . '.txt';
	}

	/**
	 * Returns list of backup files, available for restore
	 *
	 * @return Array
	 * @access public
	 */
	public function getBackupFiles()
	{
		/** @var FileHelper $file_helper */
		$file_helper = $this->Application->recallObject('FileHelper');

		$ret = Array ();
		$backup_path = $this->Application->ConfigValue('Backup_Path');
		$file_helper->CheckFolder($backup_path);
		$backup_files = glob($backup_path . DIRECTORY_SEPARATOR . 'dump*.txt');

		if ( !$backup_files ) {
			return Array ();
		}

		foreach ($backup_files as $backup_file) {
			$ret[] = Array (
				'filedate' => preg_replace('/^dump([\d]+)\.txt$/', '\\1', basename($backup_file)),
				'filesize' => filesize($backup_file));
		}

		rsort($ret);

		return $ret;
	}
}