<?php

require_once(SG_BACKUP_PATH.'SGIBackupDelegate.php');
require_once(SG_LIB_PATH.'SGDBState.php');
require_once(SG_LIB_PATH.'SGMysqldump.php');
require_once(SG_LIB_PATH.'SGCharsetHandler.php');
@include_once(SG_LIB_PATH.'SGMigrate.php');

class SGBackupDatabase implements SGIMysqldumpDelegate
{
	private $sgdb = null;
	private $backupFilePath = '';
	private $delegate = null;
	private $cancelled = false;
	private $nextProgressUpdate = 0;
	private $actionStartTs = 0;
	private $totalRowCount = 0;
	private $currentRowCount = 0;
	private $warningsFound = false;
	private $pendingStorageUploads = array();
	private $state = null;
	private $reloadStartTs = null;
	private $progressUpdateInterval;
	private $migrationAvailable = null;
	private $oldDbPrefix = null;
	private $backedUpTables = null;
	private $newTableNames = null;
	private $migrateObj = null;
	private $charsetHanlder = null;

	public function __construct()
	{
		$this->sgdb = SGDatabase::getInstance();
		$this->progressUpdateInterval = SGConfig::get('SG_ACTION_PROGRESS_UPDATE_INTERVAL');
	}

	public function setDelegate(SGIBackupDelegate $delegate)
	{
		$this->delegate = $delegate;
	}

	public function setFilePath($filePath)
	{
		$this->backupFilePath = $filePath;
	}

	public function isMigrationAvailable()
	{
		if ($this->migrationAvailable === null) {
			$this->migrationAvailable = SGBoot::isFeatureAvailable('BACKUP_WITH_MIGRATION');
		}

		return $this->migrationAvailable;
	}

	public function getOldDbPrefix()
	{
		if ($this->oldDbPrefix === null) {
			$this->oldDbPrefix = SGConfig::get('SG_OLD_DB_PREFIX');
		}

		return $this->oldDbPrefix;
	}

	public function getBackedUpTables()
	{
		if ($this->backedUpTables === null) {
			$tableNames = SGConfig::get('SG_BACKUPED_TABLES');
			if ($tableNames) {
				$tableNames = json_decode($tableNames, true);
			}
			else {
				$tableNames = array();
			}
			$this->backedUpTables = $tableNames;
		}

		return $this->backedUpTables;
	}

	public function getNewTableNames()
	{
		if ($this->newTableNames === null) {
			$oldDbPrefix = $this->getOldDbPrefix();
			$tableNames = $this->getBackedUpTables();

			$newTableNames = array();
			foreach ($tableNames as $tableName) {
				$newTableNames[] = str_replace($oldDbPrefix, SG_ENV_DB_PREFIX, $tableName);
			}
			$this->newTableNames = $newTableNames;
		}

		return $this->newTableNames;
	}

	public function getMigrateObj()
	{
		if ($this->migrateObj === null) {
			$this->migrateObj = new SGMigrate();
		}

		return $this->migrateObj;
	}

	public function getCharsetHandler()
	{
		if ($this->charsetHanlder === null) {
			$this->charsetHanlder = new SGCharsetHandler();
		}

		return $this->charsetHanlder;
	}

	public function didFindWarnings()
	{
		return $this->warningsFound;
	}

	public function setPendingStorageUploads($pendingStorageUploads)
	{
		$this->pendingStorageUploads = $pendingStorageUploads;
	}

	public function saveStateData($offset, $cursor, $inprogress, $lineSize, $backedUpTables)
	{
		$sgDBState = $this->getState();
		$token = $this->delegate->getToken();

		$sgDBState->setLineSize($lineSize);
		$sgDBState->setNumberOfEntries($this->totalRowCount);
		$sgDBState->setAction(SG_STATE_ACTION_EXPORTING_SQL);
		$sgDBState->setInprogress($inprogress);
		$sgDBState->setCursor($cursor);
		$sgDBState->setProgressCursor($this->currentRowCount);
		$sgDBState->setOffset($offset);
		$sgDBState->setToken($token);
		$sgDBState->setProgress($this->nextProgressUpdate);
		$sgDBState->setWarningsFound($this->warningsFound);
		$sgDBState->setPendingStorageUploads($this->pendingStorageUploads);
		$sgDBState->setBackedUpTables($backedUpTables);
		$sgDBState->save();
	}

	public function getState()
	{
		return $this->delegate->getState();
	}

	public function shouldReload()
	{
		$currentTime = time();

		if (($currentTime - $this->reloadStartTs) >= SG_RELOAD_TIMEOUT) {
			return true;
		}

		return false;
	}

	public function reload()
	{
		$this->delegate->reload();
	}

	public function backup($filePath)
	{
		$this->reloadStartTs = time();
		$this->state = $this->delegate->getState();

		if ($this->state && $this->state->getAction() == SG_STATE_ACTION_PREPARING_STATE_FILE) {
			$this->actionStartTs = time();
			SGBackupLog::writeAction('backup database', SG_BACKUP_LOG_POS_START);
			$this->resetBackupProgress();
		}
		else {
			$this->totalRowCount = $this->state->getNumberOfEntries();
			$this->currentRowCount = $this->state->getProgressCursor();
		}

		$this->backupFilePath = $filePath;

		$this->export();

		SGBackupLog::writeAction('backup database', SG_BACKUP_LOG_POS_END);
		SGBackupLog::write('backup database total duration: '.backupGuardFormattedDuration($this->actionStartTs, time()));
	}

	public function restore($filePath)
	{
		$this->reloadStartTs = time();
		$this->backupFilePath = $filePath;

		$sgDBState = $this->getState();
		if ($sgDBState && $sgDBState->getType() == SG_STATE_TYPE_DB) {
			if ($sgDBState->getAction() != SG_STATE_ACTION_RESTORING_DATABASE) {
				SGBackupLog::writeAction('restore database', SG_BACKUP_LOG_POS_START);
				$this->actionStartTs = time();
				//prepare for restore (reset variables)
				$this->resetRestoreProgress();
			}
			//import all db tables
			$this->import();
		}

		//run migration logic
		if ($this->isMigrationAvailable()) {
			if ($sgDBState->getAction() != SG_STATE_ACTION_MIGRATING_DATABASE) {
				$this->delegate->prepareMigrateStateFile();
			}

			$this->processMigration();
		}

		//external restore file doesn't have the wordpress functions
		//so we cannot do anything here
		//it will finalize the restore for itself
		if (!SGExternalRestore::isEnabled()) {
			$this->finalizeRestore();
		}

		SGBackupLog::writeAction('restore database', SG_BACKUP_LOG_POS_END);
		SGBackupLog::write('restore database total duration: '.backupGuardFormattedDuration($this->actionStartTs, time()));
	}

	private function processMigration()
	{
		$sgMigrateState = $this->getState();
		if ($sgMigrateState && $sgMigrateState->getAction() != SG_STATE_ACTION_MIGRATING_DATABASE) {
			SGBackupLog::writeAction('migration', SG_BACKUP_LOG_POS_START);
		}

		$sgMigrate = new SGMigrate($this->sgdb);
		$sgMigrate->setDelegate($this);

		$tables = $this->getTables();

		$oldSiteUrl = SGConfig::get('SG_OLD_SITE_URL');

		// Find and replace old urls with new ones
		$sgMigrate->migrate($oldSiteUrl, SG_SITE_URL, $tables);

		// Find and replace old db prefixes with new ones
		$sgMigrate->migrateDBPrefix();

		$isMultisite = backupGuardIsMultisite();
		if ($isMultisite) {
			$tables = explode(',', SG_MULTISITE_TABLES_TO_MIGRATE);

			$oldPath = SGConfig::get('SG_MULTISITE_OLD_PATH');
			$newPath = PATH_CURRENT_SITE;
			$newDomain = DOMAIN_CURRENT_SITE;

			$sgMigrate->migrateMultisite($newDomain, $newPath, $oldPath, $tables);
		}

		SGBackupLog::writeAction('migration', SG_BACKUP_LOG_POS_END);
	}

	public function finalizeRestore()
	{
		if (SG_ENV_ADAPTER != SG_ENV_WORDPRESS) {
			return;
		}

		//recreate current user (to be able to login with it)
		$this->restoreCurrentUser();

		//setting the following options will tell WordPress that the db is already updated
		global $wp_db_version;
		update_option('db_version', $wp_db_version);
		update_option('db_upgraded', true);

		//fix invalid upload path inserted in db
		update_option("upload_path", "");
	}

	private function export()
	{
		if ($this->state && $this->state->getAction() == SG_STATE_ACTION_PREPARING_STATE_FILE) {
			if (!$this->isWritable($this->backupFilePath)) {
				throw new SGExceptionForbidden('Permission denied. File is not writable: '.$this->backupFilePath);
			}
		}

		$customTablesToExclude = str_replace(' ', '', SGConfig::get('SG_TABLES_TO_EXCLUDE'));
		$tablesToExclude = explode(',', SGConfig::get('SG_BACKUP_DATABASE_EXCLUDE').','.$customTablesToExclude);

		$tablesToBackup = $this->state->getTablesToBackup() ? explode(',', $this->state->getTablesToBackup()) : array();
		$dump = new SGMysqldump($this->sgdb, SG_DB_NAME, 'mysql', array(
			'exclude-tables'=>$tablesToExclude,
			'include-tables'=>$tablesToBackup,
			'skip-dump-date'=>true,
			'skip-comments'=>true,
			'skip-tz-utz'=>true,
			'add-drop-table'=>true,
			'no-autocommit'=>false,
			'single-transaction'=>false,
			'lock-tables'=>false,
			'default-character-set'=>SG_DB_CHARSET,
			'add-locks'=>false
		));

		$dump->setDelegate($this);

		$dump->start($this->backupFilePath);
	}

	private function prepareQueryToExec($query)
	{
		$query = $this->replaceInvalidCharacters($query);
		$query = $this->replaceInvalidEngineTypeInQuery($query);

		if ($this->isMigrationAvailable()) {
			$tableNames = $this->getBackedUpTables();
			$newTableNames = $this->getNewTableNames();
			$query = $this->getMigrateObj()->replaceValuesInQuery($tableNames, $newTableNames, $query);
		}

		$query = $this->getCharsetHandler()->replaceInvalidCharsets($query);

		$query = rtrim(trim($query), "/*SGEnd*/");

		return $query;
	}

	private function replaceInvalidEngineTypeInQuery($query)
	{
		if (version_compare(SG_MYSQL_VERSION, '5.1', '>=')) {
			return str_replace("TYPE=InnoDB", "ENGINE=InnoDB", $query);
		}
		else {
			return str_replace("ENGINE=InnoDB", "TYPE=InnoDB", $query);
		}
	}

	private function replaceInvalidCharacters($str)
	{
		return $str;//preg_replace('/\x00/', '', $str);;
	}

	private function getDatabaseHeaders()
	{
		return "/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;/*SGEnd*/".PHP_EOL.
		"/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;/*SGEnd*/".PHP_EOL.
		"/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;/*SGEnd*/".PHP_EOL.
		"/*!40101 SET NAMES ".SG_DB_CHARSET." */;/*SGEnd*/".PHP_EOL.
		"/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;/*SGEnd*/".PHP_EOL.
		"/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;/*SGEnd*/".PHP_EOL.
		"/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;/*SGEnd*/".PHP_EOL.
		"/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;/*SGEnd*/".PHP_EOL;
	}

	private function import()
	{
		$fileHandle = @fopen($this->backupFilePath, 'r');
		if (!is_resource($fileHandle)) {
			throw new SGExceptionForbidden('Could not open file: '.$this->backupFilePath);
		}

		$importQuery = '';
		$sgDBState = $this->getState();
		if ($sgDBState && $sgDBState->getAction() == SG_STATE_ACTION_RESTORING_DATABASE) {
			$offset = $sgDBState->getOffset();
			fseek($fileHandle, $offset);

			$this->totalRowCount = $sgDBState->getNumberOfEntries();
			$this->currentRowCount = $sgDBState->getProgressCursor();
			$this->nextProgressUpdate = $sgDBState->getProgress();
			$this->warningsFound = $sgDBState->getWarningsFound();

			$importQuery = $this->getDatabaseHeaders();
		}

		while (($row = @fgets($fileHandle)) !== false) {
			$importQuery .= $row;
			$trimmedRow = trim($row);

			if (strpos($trimmedRow, 'CREATE TABLE') !== false) {
				$strLength = strlen($trimmedRow);
				$strCtLength =  strlen('CREATE TABLE ');
				$length = $strLength - $strCtLength - 2;
				$tableName = substr($trimmedRow, $strCtLength, $length);

				SGBackupLog::write('Importing table: '.$tableName);
			}

			if ($trimmedRow && substr($trimmedRow, -9) == "/*SGEnd*/") {
				$queries = explode("/*SGEnd*/".PHP_EOL, $importQuery);
				foreach ($queries as $query) {
					if (!$query) {
						continue;
					}

					$importQuery = $this->prepareQueryToExec($query);

					SGPing::update();
					$res = $this->sgdb->execRaw($importQuery);
					if ($res === false) {
						//continue restoring database if any query fails
						//we will just show a warning inside the log

						if (isset($tableName)) {
							$this->warn('Could not import table: '.$tableName);
						}

						$this->warn('Error: '.$this->sgdb->getLastError());
					}

					$shouldReload = $this->shouldReload();
					$isReloadEnabled = backupGuardIsReloadEnabled();
					if ($shouldReload && $isReloadEnabled && SGExternalRestore::isEnabled()) {
						$offset = ftell($fileHandle);
						$token = $this->delegate->getToken();

						$sgDBState = $this->getState();

						$sgDBState->setToken($token);
						$sgDBState->setOffset($offset);
						$sgDBState->setProgress($this->nextProgressUpdate);
						$sgDBState->setWarningsFound($this->warningsFound);
						$sgDBState->setNumberOfEntries($this->totalRowCount);
						$sgDBState->setProgressCursor($this->currentRowCount);
						$sgDBState->setActionId($this->delegate->getActionId());
						$sgDBState->setAction(SG_STATE_ACTION_RESTORING_DATABASE);

						$sgDBState->save();

						SGPing::update();
						@fclose($fileHandle);

						$this->reload();
					}
				}

				$importQuery = '';
			}

			$this->currentRowCount++;
			SGPing::update();
			$this->updateProgress();
		}

		@fclose($fileHandle);
	}

	public function saveCurrentUser()
	{
		if (SG_ENV_ADAPTER != SG_ENV_WORDPRESS) {
			return;
		}

		$user = wp_get_current_user();

		$currentUser = serialize(array(
			'login' => $user->user_login,
			'pass' => $user->user_pass,
			'email' => $user->user_email,
		));

		SGConfig::set('SG_CURRENT_USER', $currentUser);
	}

	private function restoreCurrentUser()
	{
		$currentUser = SGConfig::get('SG_CURRENT_USER');
		$user = unserialize($currentUser);

		//erase user data from the config table
		SGConfig::set('SG_CURRENT_USER', '');

		//if a user is found, it means it's cache, because we have dropped wp_users already
		$cachedUser = get_user_by('login', $user['login']);
		if ($cachedUser) {
			clean_user_cache($cachedUser); //delete user from cache
		}

		//create a user (it will be a subscriber)
		$id = wp_create_user($user['login'], $user['pass'], $user['email']);
		if (is_wp_error($id)) {
			SGBackupLog::write('User not recreated: '.$id->get_error_message());
			return false; //user was not created for some reason
		}

		//get the newly created user
		$newUser = get_user_by('id', $id);

		//remove its role of subscriber
		$newUser->remove_role('subscriber');
		$isMultisite = backupGuardIsMultisite();

		if ($isMultisite) {
			// add super adminn role
			grant_super_admin($id);
		}
		else {
			//add admin role
			$newUser->add_role('administrator');
		}

		//update password to set the correct (old) password
		$this->sgdb->query(
			'UPDATE '.SG_ENV_DB_PREFIX.'users SET user_pass=%s WHERE ID=%d',
			array(
				$user['pass'],
				$id
			)
		);

		//clean cache, so new password can take effect
		clean_user_cache($newUser);

		SGBackupLog::write('User recreated: '.$user['login']);
	}

	public function warn($message)
	{
		$this->warningsFound = true;
		SGBackupLog::writeWarning($message);
	}

	public function didExportRow()
	{
		$this->currentRowCount++;
		SGPing::update();

		if ($this->updateProgress())
		{
			if ($this->delegate && $this->delegate->isCancelled())
			{
				$this->cancelled = true;
				return;
			}
		}

		if (SGBoot::isFeatureAvailable('BACKGROUND_MODE') && $this->delegate->isBackgroundMode())
		{
			SGBackgroundMode::next();
		}
	}

	public function cancel()
	{
		@unlink($this->filePath);
	}

	private function resetBackupProgress()
	{
		$this->totalRowCount = 0;
		$this->currentRowCount = 0;
		$tableNames = $this->getTables();
		foreach ($tableNames as $table)
		{
			$this->totalRowCount += $this->getTableRowsCount($table);
		}
		$this->nextProgressUpdate = $this->progressUpdateInterval;
		SGBackupLog::write('Total tables to backup: '.count($tableNames));
		SGBackupLog::write('Total rows to backup: '.$this->totalRowCount);
	}

	private function resetRestoreProgress()
	{
		$this->totalRowCount = $this->getFileLinesCount($this->backupFilePath);
		$this->currentRowCount = 0;
		$this->progressUpdateInterval = SGConfig::get('SG_ACTION_PROGRESS_UPDATE_INTERVAL');
		$this->nextProgressUpdate = $this->progressUpdateInterval;
	}

	private function getTables()
	{
		$tableNames = array();
		$tables = $this->sgdb->query('SHOW TABLES FROM `'.SG_DB_NAME.'`');
		if (!$tables)
		{
			throw new SGExceptionDatabaseError('Could not get tables of database: '.SG_DB_NAME);
		}
		foreach ($tables as $table)
		{
			$tableName = $table['Tables_in_'.SG_DB_NAME];
			$tablesToExclude = explode(',', SGConfig::get('SG_BACKUP_DATABASE_EXCLUDE'));
			if (in_array($tableName, $tablesToExclude))
			{
				continue;
			}
			$tableNames[] = $tableName;
		}
		return $tableNames;
	}

	private function getTableRowsCount($tableName)
	{
		$count = 0;
		$tableRowsNum = $this->sgdb->query('SELECT COUNT(*) AS total FROM '.$tableName);
		$count = @$tableRowsNum[0]['total'];
		return $count;
	}

	private function getFileLinesCount($filePath)
	{
		$fileHandle = @fopen($filePath, 'rb');
		if (!is_resource($fileHandle))
		{
			throw new SGExceptionForbidden('Could not open file: '.$filePath);
		}

		$linecount = 0;
		while (!feof($fileHandle))
		{
			$linecount += substr_count(fread($fileHandle, 8192), "\n");
		}

		@fclose($fileHandle);
		return $linecount;
	}

	private function updateProgress()
	{
		$progress = round($this->currentRowCount*100.0/$this->totalRowCount);

		if ($progress>=$this->nextProgressUpdate)
		{
			$this->nextProgressUpdate += $this->progressUpdateInterval;

			if ($this->delegate)
			{
				$this->delegate->didUpdateProgress($progress);
			}

			return true;
		}

		return false;
	}

	/* Helper Functions */

	private function isWritable($filePath)
	{
		if (!file_exists($filePath))
		{
			$fp = @fopen($filePath, 'wb');
			if (!$fp)
			{
				throw new SGExceptionForbidden('Could not open file: '.$filePath);
			}
			@fclose($fp);
		}
		return is_writable($filePath);
	}
}
