<?php
defined('WikyBlog') or die("Not an entry point...");

global $langA;
$langA['database_backup'] = 'Database Backup';
$langA['backup'] = 'Backup';
$langA['backup_now'] = 'Backup Now';
$langA['recover'] = 'Recover';
$langA['table'] = 'Table';
$langA['tables'] = 'Tables';
$langA['create'] = 'Create';
$langA['download'] = 'Download';
$langA['start_recovery'] = 'Start Recovery';
$langA['continue_recovery'] = 'Continue Recovery';
$langA['info'] = 'Info';
$langA['DOWNLOAD_MESSAGE'] = '<a %s>Initiate the download</a> of your backup file here.';


if( !isAdmin() ){
	global $page;
	$page->contentA['Admin Only'] = 'You must be an administrator to access this page.';
	return;
}

class dbBackup{
	var $tables = array();
	var $useTable = false;
	
	var $maxTime = 8;
	var $startNum = 100;
	var $safeMode = false;
	var $compress = false;
	var $backupCounts = array();
	var $handle = false;
	var $lineCount = 0;
	var $recCounts = array();
	
	function dbBackup(){
		global $page,$pageOwner,$dbObject,$langA,$rootDir,$wbWritable,$wbUniq;
		
		if( isset($wbWritable) && ($wbWritable === false) ){
			message('The backup script requires the ability to write files to your server. If php is running in safe mode, WikyBlog must be installed with ftp values.');
			return;
		}
			
		if( ini_get('safe_mode') ){
			$this->safeMode = true;
		}
		
		//
		//	setup
		//
		$page->regLink($langA['database_backup'],'/Admin/'.$pageOwner['username'].'/Backup');
		$page->autoForm = true;

		$this->setTables();
		
		$this->backupFile = $rootDir.'/userfiles/backup.'.$wbUniq.'.tmp';
		$this->recoverFile = $rootDir.'/userfiles/recover.txt';
		
		if(function_exists('gzopen') && function_exists('ob_get_level') ){
			$this->compress = true;
		}

		
		//
		//	GRANTS
		//
		includeFile('admin/CheckPrivs.php');
		if( !checkPrivs::isGrantedCheck(array('ALTER')) ){
			message('<strong>Warning:</strong> The MySQL user you supplied at installation needs the <a href="http://dev.mysql.com/doc/refman/4.1/en/privileges-provided.html" target="_blank">FILE and ALTER privilege</a> to be able to perform backups and recoveries. You can find the username being used by looking in your /wiki.php file.');
		}
		
		//
		//	Download
		//
		if( $page->userCmd == 'download'){
			$this->downloadNow();
		}
		
		
		
		ob_start();
		$this->options();
		$page->contentA[$langA['database_backup']] = wb::get_clean();
	}
	
	function options(){
		global $page,$langA,$wbAdminUser,$wbTablePrefix,$dbObject;
		
		$dbObject->downloadBackup = true;
		switch($page->userCmd){
			
			case 'recover':
				$this->startRecovery();
			return;
			case wbStrtolower($langA['start_recovery']):
				$this->recover(true);
				$this->recoverInstruct();
			return;
			case wbStrtolower($langA['continue_recovery']):
				$this->recover();
				$this->recoverInstruct();
			return;
			
			
			
			case wbStrtolower($langA['backup_now']):
				$this->backupInit();
				$this->backup();
				if( !$this->useTable ){
					$this->downloadNow();
				}else{
					$dbObject->downloadBackup = false;
				}
			return;
				
			case wbStrtolower($langA['continue']):
				$this->backup();
				$dbObject->downloadBackup = false;
				if( $this->useTable ){
					return;
				}else{
					$url = wbLinks::getUrl('Backup?cmd=download');
					// echo '<iframe style="border: 0px none ; width: 0px; height: 0px;" src="'.$url.'"></iframe>';

					$url = 'href="'.$url.'"';
					//$link = 'You can now <a href="'.wbLinks::getUrl('Backup?cmd=download').'">'.$langA['download'].'</a> your backup file.';
					message('DOWNLOAD_MESSAGE',$url);
				}
			break;
		}
		
		echo '<p>';
		echo 'This utility will allow you to download an entire copy of your site\'s database and, in the event of data loss, recover your database from these files.';
		echo '</p>';
		
		$this->backupForm();
		
		$this->recoverInstruct();
		
		echo '<h3>Remember</h3>';
		echo '<ul>';
		echo '<li>Sensitive data is stored in the backup files and should not be stored on a public server.</li>';
		echo '<li>If you have to re-install WikyBlog, you\'ll need to re-install the same version with the same admin username and mysql table prefix. These values are currently "<tt>'.$wbAdminUser.'</tt>" and "<tt>'.$wbTablePrefix.'</tt>" respectively.</li>';
		echo '<li>Recovery will overwrite any data left in your database.</li>';
		echo '</ul>';
	}
	function setTables(){
		global $wbTablePrefix;
		includeFile('installDB.php');
		$this->database = getDatabaseArray($wbTablePrefix);
	}
	

	
	//////////////////////////////////////////////////////////////////////////////////////////
	//
	//		Recovery
	//
	function startRecovery(){
		global $langA;
		
		$this->recoveryVars();
		$this->recoverForm();
		echo '<br/>';
		echo '<input type="submit" name="cmd" value="'.$langA['start_recovery'].'" />';
		$this->recoverInstruct();
	}
		
	function recoveryVars(){
		global $page,$langA,$wbAdminUser,$wbTablePrefix,$dbname,$packageVersion;
		$page->displayTitle = $langA['recover'];
		
		if( !file_exists($this->recoverFile) ){
			message('The recover file doesn\'t exist.');
			return false;
		}
		
		$this->handleRec = fopen($this->recoverFile,'r');
		$i = 0;
		$code = '';
		while($ln = fgets($this->handleRec) ){
			$i++;
			$test = trim($ln);
			if( empty($test) ){
				break;
			}
			$code .= $ln;
			if( $i > 10 ){
				break;
			}
		}
		
		
		//get the last line of the recover file..
		$seek = min(filesize($this->recoverFile),20000);
		fseek($this->handleRec,-($seek),SEEK_END);
		$prevPos = $nextPos = ftell($this->handleRec);
		while($ln = fgets($this->handleRec) ){
			$lastLine = $ln;
			$prevPos = $nextPos;
			$nextPos = ftell($this->handleRec);
		}
		$this->recLength = $prevPos;
		rewind($this->handleRec);
		$data = unserialize($lastLine);
		$this->recTables =& $data['tables'];
		$this->recCounts =& $data['counts'];
		
		$done = true;
		if( $data['adminUser'] != $wbAdminUser){
			$done = false;
			message('The admininstrator\'s username must be the same for the backup.txt file and the current installation. The backup.txt file\'s admin user is <tt>'.$data['adminUser'].'</tt>');
		}
		if( $data['tablePrefix'] != $wbTablePrefix){
			message('The MySQL table prefix must be the same for the backup.txt file and the current installation. The backup.txt file\'s table prefix is <tt>'.$data['tablePrefix'].'</tt>');
			$done = false;
		}
		if( $data['version'] != $packageVersion){
			message('<strong>Warning:</strong> The version of this installation (<tt>V'.$packageVersion.'</tt>) does not match the version of the backup file (<tt>V'.$data['version'].'</tt>). If the database structure of these two versions does not match, your data will not be recovered correctly. We recommend installing version <tt>'.$data['version'].'</tt> before continuing.');
		}
		return $done;
	}
	
	function initRecover(){
		global $wbTablePrefix;
		includeFile('installDB.php');
		$alterDb = new dbAlter();
		if( $alterDb->go($wbTablePrefix,$this->recTables) ){
			return true;
		}
		return false;
	}
	function recoverForm(){
		global $pageOwner,$langA;
		
		if( empty($this->recCounts) ){
			$this->recoveryVars();
		}
		
		$i = 0;
		$classes[] = ' class="tableRowOdd" ';
		$classes[] = ' class="tableRowEven" ';

		echo '<table class="tableRows">';
		echo '<tr><th>';
		echo $langA['table'];
		echo '</th>';
		echo '<th>';
		echo $langA['info'];
		echo '</th>';
		echo '</tr>';
		
		$exists = true;
		foreach($this->recCounts as $table => $len){
			echo '<tr'.$classes[($i%2)].'>';
			echo '<td>';
			echo $table;
			echo '</td>';
			echo '<td style="text-align:right !important;">';
			if( $len <= 0 ){
				echo '-nothing to recover-';
			}elseif( $exists ){
				echo number_format($len).' rows';
			}
			echo '</td>';
			echo '</tr>';
			$i++;
		}
		echo '<tr><td>';
		echo $langA['total'];
		echo '</td>';
		echo '<td style="text-align:right !important">';
		echo number_format(array_sum($this->recCounts)).' rows';
		
		echo '</td></tr></table>';
	}
		
	function recover($init=false){
		global $pageOwner,$langA,$wbTablePrefix;
		
		if( !$this->recoveryVars() ){
			return false;
		}
		if( $init && !$this->initRecover()){
			return false;
		}
		

		if( isset($_POST['position']) && is_numeric($_POST['position']) ){
			fseek($this->handleRec,$_POST['position'],SEEK_SET);
		}
		if( isset($_POST['count']) && is_numeric($_POST['count']) ){
			$this->lineCount = $_POST['count'];
		}
		
		foreach($this->recTables as $table => $null){
			$query = 'ALTER TABLE `'.$wbTablePrefix.$table.'` DISABLE KEYS ';
			wbDB::runQuery($query);
		}
		
		if( $this->safeMode ){
			$this->recoverPortion();
		}else{
			do{
				$this->recoverPortion();
				set_time_limit(15);
			}while( !$this->recFinished() );
		}
		
		foreach($this->recTables as $table => $null){
			$query = 'ALTER TABLE `'.$wbTablePrefix.$table.'` ENABLE KEYS ';
			wbDB::runQuery($query);
		}
		$this->recoverForm();
		
		if( !$this->recFinished() ){
			echo '<br/>';
			message(number_format($this->lineCount).' rows have been recovered out of a total of of '.number_format(array_sum($this->recCounts)).'.');
			echo '<input type="hidden" name="count" value="'.$this->lineCount.'" />';
			echo '<input type="hidden" name="position" value="'.ftell($this->handleRec).'" />';
			echo '<input type="submit" name="cmd" value="'.$langA['continue_recovery'].'" />';
			
		}else{
			message('All '.number_format($this->lineCount).' rows have been recovered.');
		}
		
		return true;
	}
	
	function recFinished(){
		if( ftell($this->handleRec) >= $this->recLength){
			return true;
		}
		return false;
	}
	
	function recoverPortion(){
		$duration = 0;
		
		$startTime = microtime();
		while(($duration < $this->maxTime)  && ($ln = fgets($this->handleRec)) && !$this->recFinished() ){
			
			$ln = trim($ln);
			if( empty($ln) ){
				continue;
			}
			if( strpos($ln,'INSERT') !== 0){
				//message('hmm: '.wbHtmlspecialchars($ln));
				continue;
			}
			$ln = 'REPLACE '.substr($ln,6);
			wbDB::runQuery($ln);
			$this->lineCount++;
			$duration = microtime_diff($startTime, microtime());
		}
		
		//
		//	So the user isn't logged out
		//
		$query = 'SELECT `sid` FROM '.$wbTables['users'].' WHERE `username` = "'.wbDB::escape($pageOwner['username']).'" ';
		$result = wbDB::runQuery($query);
		$row = mysql_fetch_assoc($result);
		
		if( isset($_COOKIE['remember']) ){
			$_SESSION['remember'] = $_COOKIE['remember'];
		}
		if( $row['sid'] != $_COOKIE['wbsid2'] ){
			wbSession::siteCookie('wbsid2',$row['sid']);
			global $sessionCheckSum;
			$sessionCheckSum = true;
		}		
		
	}

	function recoverInstruct(){
		global $pageOwner;
		$link =  wbLinks::local('/Admin/'.$pageOwner['username'].'/Backup?cmd=recover','recover script.');

		echo '<h3>Recovery Instructions</h3>';
		echo '<ol>';
		echo '<li>Copy your most recent backup.txt file to <tt style="white-space:nowrap">'.$this->recoverFile.'</tt>.</li>';
		echo '<li>Run the '.$link.'</li>';
		echo '<li>Delete the backup.txt and temp.txt files from your server.</li>';
		echo '</ol>';
	}
	
	
	//////////////////////////////////////////////////////////////////////////////////////////
	//
	//		Backup
	//
	function backupForm(){
		global $langA;
		
		$i = 0;
		$classes[] = ' class="tableRowOdd" ';
		$classes[] = ' class="tableRowEven" ';
		
		echo '<table class="tableRows">';
		echo '<tr><th colspan="6">';
		echo $langA['tables'];
		echo '</th></tr>';
		$temp = array_keys($this->database);
		
		if( $this->useTable){
			$doneTable = true;
		}else{
			$doneTable = false;
		}
		
		while(count($temp) > 0){
			$table = array_shift($temp);
			
			if( ($i%3) == 0){
				echo '<tr'.$classes[($i%2)].'>';
			}
			echo '<td>';
			echo $table;
			echo '</td><td>';
			
			if($this->useTable == $table){
				echo '<img alt="" height="16" width="16" src="'.wbLinks::getDir('/imgs/icons/hourglass.gif').'" />';
				echo '<input type="hidden" name="tables[]" value="'.wbHtmlspecialchars($table).'" />';
				$doneTable = false;
			}elseif( $doneTable ){
				echo '<img alt="" height="16" width="16" src="'.wbLinks::getDir('/imgs/icons/tick.gif').'" />';
				echo '<input type="hidden" name="tables[]" value="'.wbHtmlspecialchars($table).'" />';
			}else{
				echo '<input type="checkbox" name="tables[]" value="'.wbHtmlspecialchars($table).'" checked="checked" />';
			}
			echo '</td>';
			
			if( ($i%3) == 2){
				echo '</tr>';
			}
			$i++;
		}
		if( ($i%3) !== 0){
			while( ($i%3) !== 0){
				echo '<td colspan="2"></td>';
				$i++;
			}
			echo '</tr>';
		}
			
		echo '<tr>';
		echo '<td colspan="6" style="text-align:right">';
		if( $this->useTable ){
			echo '<input type="submit" name="cmd" value="'.$langA['continue'].'" />';
			echo '<input type="hidden" name="counts" value="'.wbHtmlspecialchars(serialize($this->backupCounts)).'" />';
			echo '<input type="hidden" name="num" readonly="readonly" value="'.wbHtmlspecialchars($_POST['num']).'" />';
			echo '<input type="hidden" name="offset" readonly="readonly" value="'.wbHtmlspecialchars($_POST['offset']).'" />';
			echo '<input type="hidden" name="useTable" readonly="readonly" value="'.wbHtmlspecialchars($this->useTable).'" />';
		}else{
			echo '<input type="submit" name="cmd" value="'.$langA['backup_now'].'" />'; //onclick="this.type=\'button\';"this.value=\' &nbsp; &nbsp; . . . &nbsp; &nbsp; \';
		}
		echo '</td></tr>';
		echo '</table>';

	}
	
	//empty existing backup file
	function backupInit(){
		global $rootDir;
		wbData::loadFileFunctions();
		
		if( !file_exists($this->backupFile) ){
			saveFile($this->backupFile,$contents);
			wbChmod($this->backupFile,0660);
		}
		
		if( $this->compress ){
			$this->handle = gzopen($this->backupFile,'wb');
		}else{
			$this->handle = fopen($this->backupFile,'wb');
		}
		
		//
		//index file
		//
		$tmpFile = $rootDir.'/userfiles/index.html';
		if( !file_exists($tmpFile) ){
			$contents = '';
			saveFile($tmpFile,$contents);
		}
	}
	
	function backupFinish(){
		global $rootDir,$wbAdminUser,$wbTablePrefix,$packageVersion;
		
		$data = array();
		$data['adminUser'] = $wbAdminUser;
		$data['tablePrefix'] = $wbTablePrefix;
		$data['version'] = $packageVersion;
		$data['tables'] =& $this->database;
		$data['counts'] =& $this->backupCounts;
		$contents = "\n\n".serialize($data);
		
		if( $this->compress ){
			gzwrite($this->handle,$contents);
		}else{
			fwrite($this->handle,$contents);
		}
	}
	
	function backupStart(){
		if( empty($_POST['tables']) ){
			message('Please select tables to backup.');
			return false;
		}
		
		//whichTables
		foreach($_POST['tables'] as $i => $table){
			if( !isset($this->database[$table]) ){
				continue;
			}
			$this->tables[] = $table;
		}
		
		//useTable
		if( isset($_POST['useTable']) && isset($this->database[$_POST['useTable']]) ){
			$this->useTable = $_POST['useTable'];
		}else{
			reset($this->tables);
			$this->useTable = current($this->tables);
		}
		
		//query values
		if( !isset($_POST['offset']) ){
			$_POST['offset'] = 0;
		}
		$_POST['offset'] = (int)$_POST['offset'];
		
		if( !isset($_POST['num']) ){
			$_POST['num'] = $this->startNum;
		}
		$_POST['num'] = (int)$_POST['num'];
		
		if( isset($_POST['count']) ){
			$this->backupCounts = unserialize($_POST['counts']);
		}
		
		header('wb-ping: Pong');
		return true;
	}
	
	
	//files created by mysql are always world writable
	function backup(){
		global $page,$rootDir,$wbUniq,$wbTables,$langA,$wbTablePrefix,$wbCompress,$pageOwner;
		
		if( !$this->backupStart() ){
			return;
		}
		
		//
		//	file preparation
		//
		if( !$this->handle ){
			if( $this->compress ){
				$this->handle = gzopen($this->backupFile,'ab');
			}else{
				$this->handle = fopen($this->backupFile,'ab');
			}
		}
		
		$startTime = microtime();
		if( $this->safeMode ){
			do{
				$this->backupTable();
				$duration = microtime_diff($startTime, microtime());
			}while( $this->useTable && ($duration < $this->maxTime) );
			
		}else{
			do{
				$this->backupTable();
				set_time_limit(15);
			}while( $this->useTable );
		}
		if( !$this->useTable ){
			$this->backupFinish();
		}else{
			$this->backupForm();
		}
	}
	
	function downloadNow(){
		
		clearstatcache();
		if( !file_exists($this->backupFile) || (filesize($this->backupFile) < 10)  ){
			message('It appears your backup file has already been erased, run the backup script again to download your backup file.');
			return;
		}
		if($this->handle ){
			if( $this->compress ){
				gzclose($this->handle);
			}else{
				fclose($this->handle);
			}
		}
		
		if( $this->compress ){
			//sending the compressed data only seems to work if we end all the buffers
			while( ob_get_level() > 0){
				ob_end_clean();
			}
			header('Content-type: application/x-gzip');
			header('Content-Disposition: attachment; filename=backup.txt.gz');
			
		}else{
			//and sending the uncompressed data only seems to work if we keep the buffers
			header('Content-type: text/plain; charset=UTF-8');
			header('Content-Disposition: attachment; filename=backup.txt');
			
		}
		
		// $size = filesize($this->backupFile);
		// header('Content-Length: '.$size);
		
		readfile($this->backupFile); //rewind and passthru weren't working properly
		
		$this->deleteBackup();
		
		die();
	}
	
	function deleteBackup(){
		$fp = fopen($this->backupFile,'wb');
		fclose($fp);
		unlink($this->backupFile);
	}
		
	function backupTable(){	
		global $wbTablePrefix;
		//
		//	database info
		//
		$data = $this->database[$this->useTable];
		
		$query = 'SHOW INDEX FROM `'.$wbTablePrefix.$this->useTable.'`';
		$result = wbDB::runQuery($query);
		$tempA = array();
		while($row = mysql_fetch_assoc($result)){
			$tempA[$row['Key_name']]['Non_unique'] = $row['Non_unique'];
			$tempA[$row['Key_name']]['cols'][(int)$row['Seq_in_index']] = '`'.$row['Column_name'].'`';
			if( !empty($row['Sub_part']) ){
				$tempA[$row['Key_name']]['cols'][(int)$row['Seq_in_index']] .= '('.$row['Sub_part'].')';
			}
			$tempA[$row['Key_name']]['Index_type'] = $row['Index_type'];
		}
		if( isset($tempA['PRIMARY']) ){
			$orderBy = ' ORDER BY '.implode(', ',$tempA['PRIMARY']['cols']);
		}else{
			trigger_error('The primary key was not specified for '.$this->useTable);
			return;
		}
		
		$query = 'SELECT COUNT(*) as `count` ';
		$query .= ' FROM `'.$wbTablePrefix.$this->useTable.'` ';
		$result = wbDB::runQuery($query);
		$row = mysql_fetch_assoc($result);
		$totalRows = $row['count'];
		
		//
		//	Build query
		//
		$query = 'SELECT CONCAT("INSERT INTO `'.$wbTablePrefix.$this->useTable.'` SET" ';
		
		$comma = '';
		foreach($data['columns'] as $column => $sql){
			$query .= $comma;
			$query .= ', " `'.$column.'` = " ';
			
			$query .= ', REPLACE(REPLACE(QUOTE(`'.$column.'` ),"\n","\\\n"),"\r","\\\r") ';
			$comma = ', "," ';
		}
		$query .= ',";") as `sql` ';
		$query .= ' FROM `'.$wbTablePrefix.$this->useTable.'` ';
		$query .= $orderBy;
		$query .= ' LIMIT '.$_POST['num'].' OFFSET '.$_POST['offset'];
		//
		//	run query and save results
		//
		$startTime = microtime();
		$result = wbDB::runQuery($query);
		$numRows = mysql_num_rows($result);
		while( $row = mysql_fetch_assoc($result) ){
			if( $this->compress ){
				gzwrite($this->handle,$row['sql']."\n");
			}else{
				fwrite($this->handle,$row['sql']."\n");
			}
		}
		if( $totalRows == ($_POST['offset']+$numRows)){
			$this->backupCounts[$this->useTable] = $totalRows;
			$_POST['offset'] = 0;
			$_POST['num'] = $this->startNum;
			$this->nextTable();
		}else{
			$_POST['offset'] = ($_POST['offset']+$numRows);
			$duration = microtime_diff($startTime, microtime());
			$timePerRow = $duration/$numRows;
			$_POST['num'] = floor($this->maxTime/$timePerRow);
		}
		
	}
	
	function nextTable(){
		$next = false;
		foreach($this->tables as $table){
			if( $next ){
				$this->useTable = $table;
				return;
			}
			if( $table == $this->useTable){
				$next = true;
			}
		}
		$this->useTable = false;
	}
		
}

new dbBackup();