<?php
ini_set('error_log', dirname(__FILE__) . '/error_log');
ini_set('display_errors', 'off');
ini_set('log_errors', 'on');
error_reporting(E_ALL);

error_log( time() . ': ' . __FILE__ . ' startup' . PHP_EOL, 3, dirname( __FILE__ ) . '/error_log' );

if (!empty($_GET['ping'])) {
	echo 'Pong';
	exit;
}

set_time_limit( 15 * 60 );
$upgrader = new Upgrade($_POST);

$upgrader->execute();

exit;

/**
 * Pre Installer Payload Script
 *
 * This is called via curl. The settings are passed in the headers and the process is run
 * on the remote server.
 *
 * @subpackage Lib.assets
 *
 * @copyright SimpleScripts.com, 8 May, 2012
 * @author
 **/

/**
 * Define DocBlock
 **/

class Upgrade {

/**
 * Debug Storage
 *
 * @var array $debug
 */
	public $debug = array();

/**
 * Settings
 *
 * @var array $settings
 */
	public $settings = array(
		'filename' => null,
		'extract_package' => null,
		'directory_created' => false,
		'backup_dir' => '.ss_backup',
		'file_ext' => 'tgz',
	);

/**
 * Class Constructor
 *
 * The $_POST will be sent to this method and merged into the $settings defaults.
 *
 * @author
 **/
	public function __construct($settings = null) {
		if (!$settings) {
			return false;
		}
		$this->debug['setup'][] = 'Configuring settings.';
		$this->settings = array_merge($this->settings, $settings);
		$this->settings['os'] = strtolower(substr(PHP_OS, 0, 3));
		$this->settings['passthru'] = function_exists('passthru') ? true : false;
		$this->settings['root_directory'] = dirname(__FILE__);
	}

/**
 * Does stuff badly.
 *
 * @return [type] [description]
 */
	public function execute() {
		//check for backup dir and create it if it doesnt exists. New in v3
		if (!is_dir($this->settings['backup_dir'])) {
			$this->debug['setup'][] = 'Creating backup directory.';
			if (!@mkdir($this->settings['backup_dir'])) {
				$this->error['backupDirectory'] = 'the backup directory could not be created.';
				$this->error['extra'] = $this->settings['backup_dir'];
				$this->errorDie();
			}
		}

		//Run the backup process
		if ($this->settings['backup_dir']) {
			$this->debug['process'][] = 'Backing up current installation';
			$this->backup();
			$this->debug['process'][] = 'Backup successfully created';
		}

		$this->debug['setup'][] = 'Setting full file path.';
		$this->settings['path_to_file'] = dirname(__FILE__) . '/' . $this->settings['filename'];

		$this->debug['status'] = 'success';
		echo json_encode($this->debug);
	}

/**
 * mkdir replacement
 *
 * Recursive directory creation fails if at any point an
 * implicit is_dir fails when exploring the tree.  The problem
 * being, that is_dir sometimes fails on CIFS (Windows Samba
 * mounts) when there are a large number of inodes.  When that
 * returns false, mkdir tries to create that directory in the
 * path, but the mkdir also fails because the directory does
 * in fact exist.  chdir, stat, file_exists, etc., also all
 * fail.  But if you try to create a directory inside of the
 * unreadable directory it will succeed - so we need to find
 * our way past that if we can before reporting failure.
 *
 * @param  string $dir
 * @param  octet  $mode
 * @param  bool   $recursive
 * @return bool
 **/
	public function mkdir($dir, $mode = 0777, $recursive = true) {
		if ( @mkdir( $dir, $mode, $recursive ) ) {
			return true;
		} else {
			// Looks like we failed.  Chances are the first place we
			// couldn't read was the site root itself, so try to make
			// the first directory under that as a starting point.

			// First, standardize paths.
			$root = implode( DIRECTORY_SEPARATOR, preg_split( '{[/\\\\]}', $this->settings['root_directory'] ) );
			$path = implode( DIRECTORY_SEPARATOR, preg_split( '{[/\\\\]}', $dir ) );

			// Find only those path elements which immediately follow
			// the root, and see if we can create that directory first.
			if ( !strpos( $path, $root ) === 0 ) {
				// Doesn't look like one is in the other, bail out.
				return false;
			}

			// Get the first path element following the site root.
			$pathParts = array_filter(
				preg_split(
					'{[/\\\\]}',
					substr( $path, strlen( $root ) )
				)
			);
			if ( !$pathParts || !count( $pathParts ) ) {
				false;
			}

			$firstPath = $root . ( !preg_match( '{[/\\\\]$}', $root ) ? DIRECTORY_SEPARATOR : '' ) . array_shift( $pathParts );
			// This attempt must be false, or it will fail just like the
			// initial one above.
			if ( @mkdir( $firstPath, $mode, false ) ) {
				if ( $recursive ) {
					if ( count( $pathParts ) ) {
						// Should be OK to resume trying from here.
						return @mkdir( $path, $mode, $recursive );
					} else {
						// Created all there is to create, return true.
						return true;
					}
				} elseif ( implode( '', $pathParts ) ) {
					// Gave us a recursive path but were told non-recursive?
					return false;
				} else {
					return true;
				}
			}
		}
		return false;
	}

/**
 * Backup
 *
 * Backup the user account(File backup implied. Database backup if the database is present and small enough)
 *
 *
 * @return void
 * @author
 **/
	public function backup() {
		if ($this->settings['backup_database']) {
			if (!$this->backupDatabase()) {
				$this->error['backup'] = 'No database backup file created. The backup process appears to have failed';
			}
		}
		if (!$this->backupUserFiles()) {
			$this->error['backup'] = 'No backup file created. The backup process appears to have failed';
		}
		if (!$this->backupProcessCleanup()) {
			$this->error['backup'] = 'Backup cleanup process failed.';
		}
	}
/**
 * Backup User Files
 *
 * Backup the users current files in the protected .ss_backup directory which is created if it doesnt exist
 *
 * @return void
 * @author
 **/
	public function backupUserFiles() {
		$options = null;
		if ($this->settings['file_ext'] == 'tar') {
			$options = '';
		} elseif ($this->settings['file_ext'] == 'tgz') {
			$options = '-z';
		} else {
			$this->error['incompatibleExtension'] = 'extension not compatible with backup process.';
			$this->error['extra'] = dirname(__FILE__) . '/' . $this->settings['filename'];
			$this->errorDie();
		}

		//protect the backup dir
		file_put_contents($this->settings['backup_dir'] . '/.htaccess', 'deny from all');

		$backupFileName = $this->settings['backup_dir'] . DIRECTORY_SEPARATOR . 'ss_backup-' . (int)gmdate('U') . '.tgz';

		//Pass the backup file back to the daemon
		$this->debug['backup_filename'] = $backupFileName;
		$command = sprintf(
			'tar --no-same-owner --exclude=.ss_backup -c %s -f %s -C %s/ .',
			$options,
			escapeshellarg($backupFileName),
			escapeshellarg(dirname(__FILE__))
		);

		$this->settings['command'] = $command;
		$result = $this->runCommand($command, 600);

		if (!file_exists($backupFileName)) {
			$this->debug['error'][] = "Command to create asset backup failed.";
			$this->errorDie();
		}

		$this->settings['list'] = preg_split("/\n/", $results);
		return true;
	}

/**
 * Backup User Database
 *
 * Backup the user database if there is one
 *
 * @return void
 * @author
 **/
	public function backupDatabase() {
		$outputFile = dirname(__FILE__) . '/.database_backup.sql';

		$backupCommand = sprintf(
			'mysqldump --user=%s --host=%s -p %s > %s',
			escapeshellarg($this->settings['ss_dbuser']),
			escapeshellarg($this->settings['ss_dbhost']),
			escapeshellarg($this->settings['ss_dbname']),
			escapeshellarg($outputFile)
		);

		$inputToBackupCommand = sprintf(
			"%s\n",
			$this->settings['ss_dbpass']
		);

		$this->debug['database_backup_command'] = $backupCommand;
		$this->debug['process'][] = 'Running database backup command';
		$result = $this->runCommand($backupCommand, 600, $inputToBackupCommand);

		// Legacy fallback.  Maybe we'd hit this on Windows?
		if ($result === false) {
			$this->debug['process'][] = 'Running backup command failed; dropping back to manual backup.';

			$host = $this->settings['ss_dbhost'];
			$user = $this->settings['ss_dbuser'];
			$pass = $this->settings['ss_dbpass'];
			$name = $this->settings['ss_dbname'];

			$tables = '*';

			$link = mysql_connect($host,$user,$pass);

			mysql_select_db($name,$link);

			if ($tables == '*') {
				$tables = array();
				$result = mysql_query('SHOW TABLES');
				while ($row = mysql_fetch_row($result)) {
					$tables[] = $row[0];
				}
			} else {
				$tables = is_array($tables) ? $tables : explode(',',$tables);
			}

			foreach ($tables as $table) {
				$result = mysql_query('SELECT * FROM ' . $table);
				$numFields = mysql_num_fields($result);
				$return .= 'DROP TABLE IF EXISTS ' . $table . ';';
				$row2 = mysql_fetch_row(mysql_query('SHOW CREATE TABLE ' . $table));
				$return .= "\n\n" . $row2[1] . ";\n\n";
				for ($i = 0; $i < $numFields; $i++) {
					while ($row = mysql_fetch_row($result)) {
						$return .= 'INSERT INTO ' . $table . ' VALUES(';
						for ($j = 0; $j < $numFields; $j++) {
							$row[$j] = addslashes($row[$j]);
							$row[$j] = preg_replace("/\n/", "/\\n/", $row[$j]);
							if (isset($row[$j])) {
								$return .= '"' . $row[$j] . '"';
							} else {
								$return .= '""';
							}
							if ($j < ($numFields - 1)) {
								$return .= ',';
							}
						}
						$return .= ");\n";
					}
				}
				$return .= "\n\n\n";
			}

			$handle = fopen($outputFile,'w+');
			fwrite($handle,$return);
			fclose($handle);
		}

		return true;
	}

/**
 * Backup Process Cleanup
 *
 * Cleanup after the backup process is run. Nuke the db thats created in the install directory
 *
 * @return void
 * @author
 **/
	public function backupProcessCleanup() {
		$command = sprintf(
			'rm %s/.database_backup.sql',
			escapeshellarg(dirname(__FILE__))
		);
		$result = $this->runCommand($command);
		$this->settings['list'] = preg_split("/\n/", $results);
		return true;
	}

/**
 * Random String
 *
 * Generate a random string
 * @param string $ length The length of the random string to generate
 * @return void
 * @author
 **/
	public function randomString($length) {
		$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

		$size = strlen( $chars );
		for ( $i = 0; $i < $length; $i++ ) {
			$str .= $chars[rand( 0, $size - 1 )];
		}
		return $str;
	}

/**
 * Run Command
 *
 * Run the specified command using passthru to get and return the results.
 *
 * @param string $command The command to run on the server.
 * @return string $output The raw output of the command.
 **/
	public function runCommand($command = null, $ttl = 600, $input = '') {
		if (!$command) {
			return false;
		}

		$descriptorSpec = Array(
			Array('pipe', 'r'),
			Array('pipe', 'w'),
			Array('pipe', 'w')
		);

		$pipes = Array();
		$stdout = '';
		$stderr = '';

		$process = proc_open($command, $descriptorSpec, $pipes);

		if (!is_resource($process)) {
			$this->debug[] = 'Failed to open process for command ' . $command;
			return false;
		}

		// Keep reads from hanging
		stream_set_blocking($pipes[0], 0);
		stream_set_blocking($pipes[1], 0);
		stream_set_blocking($pipes[2], 0);

		$loops = 0;

		if ($input) {
			sleep(1);
			fwrite($pipes[0], $input);
		}

		while (1) {
			$loops += 1;

			// This used to be fancy with stream_select, but it turns out that
			// stream_select doesn't block even when I want it to.  :/
			sleep(1);

			while ($moreStdout = stream_get_contents($pipes[1])) {
				$stdout .= $moreStdout;
			}

			while ($moreStderr = stream_get_contents($pipes[2])) {
				$stderr .= $moreStderr;
			}

			$status = proc_get_status($process);

			if (!$status['running']) {
				break;
			}

			if ($loops > $ttl) {
				$this->debug[] = "terminating with 15 due to timeout at $loops loops";
				proc_terminate($process, 15);
				$status['exitcode'] = '15';
			}

			if ($loops > 5 + $ttl) {
				$this->debug[] = "terminating with 9 due to timeout at $loops loops";
				proc_terminate($process, 9);
				$status['exitcode'] = '9';
			}
		}

		$exitCode = $status['exitcode'];

		$this->debug[] = compact('command', 'stdout', 'stderr', 'exitCode', 'loops');

		if ($exitCode) {
			return false;
		}

		return $stdout;
	}

/**
 * Error
 *
 * Call an error to pass back to the caller.
 *
 * @return void
 *
 **/
	public function errorDie() {
		$this->error['status'] = 'error';
		$this->error['debug'] = $this->debug;
		$this->error = json_encode($this->error);
		die($this->error);
	}
}
