<?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 (isset($_GET['ping']) && !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;
$results = $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__))
);
$results = $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);
}
}