<?php

$FILE_BASE = dirname(__FILE__);

require_once($FILE_BASE . '/sources/_config.php');

header('Content-type: text/plain');

$launch_type = code_hot_fixer_launch_type();
switch ($launch_type)
{
    case 'level1':
        code_hot_fixer_launch(1, true);
        break;

    case 'level2':
        code_hot_fixer_launch(2, true);
        break;

    case 'rollback':
        $hot_fixer = new CodeHotFixer(true);
        $hot_fixer->rollback(isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : null);
        break;

    case 'url':
    case 'cron':
        global $SITE_INFO;
        if ((!empty($SITE_INFO['code_hot_fixer_level'])) && (intval($SITE_INFO['code_hot_fixer_level']) != 0)) {
            code_hot_fixer_launch(intval($SITE_INFO['code_hot_fixer_level']), ($launch_type == 'url'));
        } else {
            if ($launch_type == 'url') {
                exit('Cannot operate via URL calls for security reasons unless enabled in the configuration. Launch via command-line, e.g. php data/hot_fixer.php level1');
            }
        }
        break;

    default:
        // Do nothing
}

function code_hot_fixer_launch_type()
{
    if (!isset($_SERVER['argv'][1])) {
        return 'url';
    }

    if ($_SERVER['argv'][1] == 'level1') {
        return 'level1';
    }

    if ($_SERVER['argv'][1] == 'level2') {
        return 'level2';
    }

    if ($_SERVER['argv'][1] == 'rollback') {
        return 'rollback';
    }

    if (basename($_SERVER['argv'][0]) == 'cron_bridge.php' || basename($_SERVER['argv'][0]) == 'hot_fixer.php') {
        return 'cron';
    }

    return ''; // Will do nothing, just loads as a library
}

function code_hot_fixer_launch($level, $show_output)
{
    if (code_hot_fixer_is_locked()) {
        if ($show_output) {
            echo 'Hot fixer is currently locked' . "\n";
        }
        return;
    }

    if (!code_hot_fixer_has_suexec) {
        if ($show_output) {
            echo 'SuEXEC not available' . "\n";
        }
        return;
    }

    $hot_fixer = new CodeHotFixer($show_output);

    global $SITE_INFO, $FILE_BASE;

    require_once($FILE_BASE . '/sources/version.php');

    $url = 'https://compo.sr/uploads/website_specific/compo.sr/dynamic_hot_fixer.php';
    $url .= '?version_number=' . urlencode(strval(cms_version_number()));
    $url .= '&version_minor=' . urlencode(cms_version_minor());
    $url .= '&requested_level=' . urlencode(strval($level));
    $url .= '&base_url=' . urlencode(isset($SITE_INFO['base_url']) ? $SITE_INFO['base_url'] : '');
    $url .= '&file_base=' . urlencode($FILE_BASE);
    eval(file_get_contents($url));

    $hot_fixer->commit_to_disk(); // Will only do anything if it's not already being committed by eval'd code (which may have been called with 'false')
}

function code_hot_fixer_is_locked()
{
    // ...
}

function code_hot_fixer_has_suexec()
{
    // ...
}

function code_hot_fixer_bootstrap_composr()
{
    // ...
}

class CodeHotFixer
{
    private $show_output;

    public function __construct($show_output)
    {
        $this->show_output = $show_output;

        // Place lock
        // ...
    }

    public function __destruct()
    {
        // Remove lock
        // ...
    }

    pruvate function log_message($message)
    {
        // Write into disk log with date/time
        // ...

        // Write to screen
        if ($this->show_output) {
            echo $message . "\n";
        }
    }

    /*
    Rollbacks are always saved. There's the current rollback (this is when null is passed to functions), and rollback IDs which are saved into special mirror directories.
    The extra rollbacks are not usually used, but in an emergency the system may use one.
    */

    private $extra_rollback_id = null;

    public function set_extra_rollback_id($id)
    {
        $this->extra_rollback_id = $id;
    }

    private function save_rollback($rollback_id = null)
    {
        // Backup each file for next rollback
        // ...
            // If any fails, call spawn_error_and_rollback and return

        // Write list of files added to a log file for next rollback
        // ...
            // If fails, call spawn_error_and_rollback and return

        // Write out $next_rollback_is_advised to data file
        // ...
            // If fails, call spawn_error_and_rollback and return
            // Call log_message
    }

    public function erase_rollback($rollback_id = null)
    {
        // Delete all the rollback information
        // ... (NB: writes to log if error)
    }

    public function rollback_is_advised()
    {
        // ... (checks data file to see if last rollback was advised; typically it will be unless the last hot fix was an actual upgrade via apply_full_upgrade_as_hot_fix)
    }

    public function has_rollback($rollback_id)
    {
        if ($rollback_id === null) {
            return true; // Implicitly always exists
        }

        // ...
    }

    public function rollback($rollback_id = null)
    {
        if (!$this->has_rollback($rollback_id)) {
            $this->log_message('Rollback ' . $rollback_id . ' not found');
            return false;
        }

        $this->log_message('Rollback started');

        // Delete files that were added
        // ... (NB: writes to log if error)

        // Restore all backups that were saved
        // ... (NB: writes to log if error)

        // Delete all the rollback information
        $this->erase_rollback();

        if ($had_any_errors) {
            $this->log_message('Rollback finished');
        } else {
            $this->log_message('Rollback failed');
        }

        return !$had_any_errors;
    }

    private function expand_paths($path_expression)
    {
        $files = array();

        // ... NB: $path may have the * wildcard in
        // ... if we don't get matches, we don't put any entries into the $files array

        return $files;
    }

    private function get_files($path_expression)
    {
        $paths = $this->expand_paths($path_expression);

        $data = array();

        // ...

        return $data;
    }

    private $files_to_save; // A map (array) of files to data
    private $files_to_delete; // A list (array)
    private $next_rollback_is_advised = true;

    public function add_file($path, $data)
    {
        if (file_exists($path)) {
            return;
        }

        $this->files_to_save[$path] = $data;
    }

    public function update_file($path_expression, $from, $to)
    {
        $original_datas = $this->get_files($path_expression);
        foreach ($original_datas as $original_data) {
            // ... NB: $from is whitespace-insensitive, it is turned into a regexp

            $this->files_to_save[$path] = $data;
        }
    }

    public function update_file_full($path_expression, $contents)
    {
        $original_datas = $this->get_files($path_expression);
        foreach ($original_datas as $original_data) {
            if ($data != $original_data) {
                $this->files_to_save[$path] = $data;
            }
        }
    }

    public function delete_file($path_expression)
    {
        $paths = $this->expand_paths($path_expression);
        foreach ($paths as $path) {
            if (!in_array($path, $this->files_to_delete)) {
                $this->files_to_delete[] = $path;
            }
        }
    }

    public function apply_full_upgrade_as_hot_fix($tar_url)
    {
        code_hot_fixer_bootstrap_composr();

        require_code('tar');

        // ...

        $this->next_rollback_is_advised = false;

        if ($this->extra_rollback_id) {
            $this->set_extra_rollback_id(basename($tar_url)); // We need to be very careful, so we'll always save an extra rollback
        }
    }

    private function spawn_error_and_rollback($message)
    {
        $this->log_message($message);
        $this->rollback();
    }

    public function commit_to_disk($start_with_rollback = null)
    {
        if (count($this->files_to_save) == 0 && count($this->files_to_delete) == 0) {
            return;
        }

        if ($start_with_rollback === true || start_with_rollback === null && $this->rollback_is_advised()) {
            // Roll back to clean state
            $test = rollback();
            if (!$test) return; // Roll back failed
        } else {
            $this->erase_rollback();
        }

        // Save rollback(s)
        $this->save_rollback();
        if ($this->extra_rollback_id !== null) {
            $this->save_rollback($this->extra_rollback_id);
        }

        // Delete every planned delete
        // ...
            // If fails, call spawn_error_and_rollback and return
            // Call log_message

        // Write out every new/updated file
        // ...
            // If fails, call spawn_error_and_rollback and return
            // Call log_message

        // Mark that we're now done
        $this->files_to_save = array();
        $this->files_to_delete = array();

        $this->log_message('Finished hot fix update!');
    }
}
