<?php
/*********************************************************************
    class.upgrader.php

    osTicket Upgrader

    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2013 osTicket
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/

require_once INCLUDE_DIR.'class.setup.php';
require_once INCLUDE_DIR.'class.migrater.php';

class Upgrader {
    function __construct($prefix, $basedir) {
        global $ost;

        $this->streams = array();
        foreach (DatabaseMigrater::getUpgradeStreams($basedir) as $stream=>$hash) {
            $signature = $ost->getConfig()->getSchemaSignature($stream);
            $this->streams[$stream] = new StreamUpgrader($signature, $hash, $stream,
                $prefix, $basedir.$stream.'/', $this);
        }

        //Init persistent state of upgrade.
        $this->state = &$_SESSION['ost_upgrader']['state'];

        $this->mode = &$_SESSION['ost_upgrader']['mode'];

        $this->current = &$_SESSION['ost_upgrader']['stream'];
        if (!$this->current || $this->getCurrentStream()->isFinished()) {
            $streams = array_keys($this->streams);
            do {
                $this->current = array_shift($streams);
            } while ($this->current && $this->getCurrentStream()->isFinished());
        }
    }

    function getCurrentStream() {
        return $this->streams[$this->current];
    }

    function isUpgradable() {
        if ($this->isAborted())
            return false;

        foreach ($this->streams as $s)
            if (!$s->isUpgradable())
                return false;

        return true;
    }

    function isAborted() {
        return !strcasecmp($this->getState(), 'aborted');
    }

    function abort($msg, $debug=false) {
        if ($this->getCurrentStream())
            $this->getCurrentStream()->abort($msg, $debug);
    }

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

    function setState($state) {
        $this->state = $state;
        if ($state == 'done') {
            ModelMeta::flushModelCache();
            $this->createUpgradedTicket();
        }
    }

    function createUpgradedTicket() {
        global $cfg;

        $i18n = new Internationalization();
        $vars = $i18n->getTemplate('templates/ticket/upgraded.yaml')->getData();
        $vars['deptId'] = $cfg->getDefaultDeptId();

        //Create a ticket to make the system warm and happy.
        $errors = array();
        Ticket::create($vars, $errors, 'api', false, false);
    }

    function getMode() {
        return $this->mode;
    }

    function setMode($mode) {
        $this->mode = $mode;
    }

    function upgrade() {
        if (!$this->current)
            return true;

        return $this->getCurrentStream()->upgrade();
    }

    function __call($what, $args) {
        if ($this->getCurrentStream()) {
            $callable = array($this->getCurrentStream(), $what);
            if (!is_callable($callable))
                throw new Exception('InternalError: Upgrader method not callable: '
                    . $what);
            return call_user_func_array($callable, $args);
        }
    }
}

/**
 * Updates a single database stream. In the classical sense, osTicket only
 * maintained a single database update stream. In that model, this
 * represents upgrading that single stream. In multi-stream mode,
 * customizations and plugins are supported to have their own respective
 * database update streams. The Upgrader class is used to coordinate updates
 * for all the streams, whereas the work to upgrade each stream is done in
 * this class
 */
class StreamUpgrader extends SetupWizard {

    var $prefix;
    var $sqldir;
    var $signature;

    var $state;
    var $mode;

    var $phash;

    /**
     * Parameters:
     * schema_signature - (string<hash-hex>) Current database-reflected (via
     *      config table) version of the stream
     * target - (stream<hash-hex>) Current stream tip, as reflected by
     *      streams/<stream>.sig
     * stream - (string) Name of the stream (folder)
     * prefix - (string) Database table prefix
     * sqldir - (string<path>) Path of sql patches
     * upgrader - (Upgrader) Parent coordinator of parallel stream updates
     */
    function __construct($schema_signature, $target, $stream, $prefix, $sqldir, $upgrader) {

        $this->signature = $schema_signature;
        $this->target = $target;
        $this->prefix = $prefix;
        $this->sqldir = $sqldir;
        $this->errors = array();
        $this->mode = 'ajax'; //
        $this->upgrader = $upgrader;
        $this->name = $stream;

        //Disable time limit if - safe mode is set.
        if(!ini_get('safe_mode'))
            set_time_limit(0);

        //Init the task Manager.
        if(!isset($_SESSION['ost_upgrader'][$this->getShash()]))
            $_SESSION['ost_upgrader']['task'] = array();

        //Tasks to perform - saved on the session.
        $this->phash = &$_SESSION['ost_upgrader']['phash'];

        //Database migrater
        $this->migrater = null;
    }

    function check_prereq() {
        return (parent::check_prereq() && $this->check_mysql_version());
    }
    function onError($error) {
        global $ost, $thisstaff;

        $subject = '['.$this->name.']: '._S('Upgrader Error');
        $ost->logError($subject, $error);
        $this->setError($error);
        $this->upgrader->setState('aborted');

        //Alert staff upgrading the system - if the email is not same as admin's
        // admin gets alerted on error log (above)
        if(!$thisstaff || !strcasecmp($thisstaff->getEmail(), $ost->getConfig()->getAdminEmail()))
            return;

        $email=null;
        if(!($email=$ost->getConfig()->getAlertEmail()))
            $email=$ost->getConfig()->getDefaultEmail(); //will take the default email.

        if($email) {
            $email->sendAlert($thisstaff->getEmail(), $subject, $error);
        } else {//no luck - try the system mail.
            Mailer::sendmail($thisstaff->getEmail(), $subject, $error,
                '"'._S('osTicket Alerts')."\" <{$thisstaff->getEmail()}>");
        }

    }

    function isUpgradable() {
        return $this->getNextPatch();
    }

    function getSchemaSignature() {
        return $this->signature;
    }

    function getShash() {
        return  substr($this->getSchemaSignature(), 0, 8);
    }

    function getTablePrefix() {
        return $this->prefix;
    }

    function getSQLDir() {
        return $this->sqldir;
    }

    function getMigrater() {
        if(!$this->migrater)
            $this->migrater = new DatabaseMigrater($this->signature, $this->target, $this->sqldir);

        return  $this->migrater;
    }

    function getPatches() {
        $patches = array();
        if($this->getMigrater())
            $patches = $this->getMigrater()->getPatches();

        return $patches;
    }

    function getNextPatch() {
        return (($p=$this->getPatches()) && count($p)) ? $p[0] : false;
    }

    function getNextVersion() {
        if(!$patch=$this->getNextPatch())
            return __('(Latest)');

        $info = $this->readPatchInfo($patch);
        return $info['version'];
    }

    function isFinished() {
        # TODO: 1. Check if current and target hashes match,
        #       2. Any pending tasks
        return !($this->getNextPatch() || $this->getPendingTask());
    }

    function readPatchInfo($patch) {
        $info = $matches = $matches2 = array();
        if (preg_match(':/\*\*(.*)\*/:s', file_get_contents($patch), $matches)) {
            if (preg_match_all('/@([\w\d_-]+)\s+(.*)$/m', $matches[0],
                        $matches2, PREG_SET_ORDER))
                foreach ($matches2 as $match)
                    $info[$match[1]] = $match[2];
        }
        if (!isset($info['version']))
            $info['version'] = substr(basename($patch), 9, 8);
        return $info;
    }

    function getUpgradeSummary() {
        $summary = '';
        foreach ($this->getPatches() as $p) {
            $info = $this->readPatchInfo($p);
            $summary .= '<div class="patch">' . $info['version'];
            if (isset($info['title']))
                $summary .= ': <span class="patch-title">'.$info['title']
                    .'</span>';
            $summary .= '</div>';
        }
        return $summary;
    }

    function getNextAction() {

        $action=sprintf(__('Upgrade osTicket to %s'), $this->getVersion());
        if($task=$this->getTask()) {
            $action = $task->getDescription() .' ('.$task->getStatus().')';
        } elseif($this->isUpgradable() && ($nextversion = $this->getNextVersion())) {
            $action = sprintf(__("Upgrade to %s"),$nextversion);
        }

        return '['.$this->name.'] '.$action;
    }

    function getPendingTask() {

        $pending=array();
        if (($task=$this->getTask()) && ($task instanceof MigrationTask))
            return ($task->isFinished()) ? 1 : 0;

        return false;
    }

    function getTask() {
        global $ost;

        $task_file = $this->getSQLDir() . "{$this->phash}.task.php";
        if (!file_exists($task_file))
            return null;

        if (!isset($this->task)) {
            $class = (include $task_file);
            if (!is_string($class) || !class_exists($class))
                return $ost->logError("Bogus migration task",
                        "{$this->phash}:{$class}"); //FIXME: This can cause crash
            $this->task = new $class();
            if (isset($_SESSION['ost_upgrader']['task'][$this->phash]))
                $this->task->wakeup($_SESSION['ost_upgrader']['task'][$this->phash]);
        }
        return $this->task;
    }

    function doTask() {

        if(!($task = $this->getTask()))
            return false; //Nothing to do.

        $this->log(
                sprintf(_S('Upgrader - %s (task pending).'), $this->getShash()),
                sprintf(_S('The %s task reports there is work to do'),
                    get_class($task))
                );
        if(!($max_time = ini_get('max_execution_time')))
            $max_time = 30; //Default to 30 sec batches.

        // Drop any model meta cache to ensure model changes do not cause
        // crashes
        ModelMeta::flushModelCache();

        $task->run($max_time);
        if (!$task->isFinished()) {
            $_SESSION['ost_upgrader']['task'][$this->phash] = $task->sleep();
            return true;
        }
        // Run the cleanup script, if any, and destroy the task's session
        // data
        $this->cleanup();
        unset($_SESSION['ost_upgrader']['task'][$this->phash]);
        $this->phash = null;
        unset($this->task);
        return false;
    }

    function upgrade() {
        global $ost;

        if($this->getPendingTask() || !($patches=$this->getPatches()))
            return false;

        $start_time = Misc::micro_time();
        if(!($max_time = ini_get('max_execution_time')))
            $max_time = 300; //Apache/IIS defaults.

        // Drop any model meta cache to ensure model changes do not cause
        // crashes
        ModelMeta::flushModelCache();

        // Apply up to five patches at a time
        foreach (array_slice($patches, 0, 5) as $patch) {
            //TODO: check time used vs. max execution - break if need be
            if (!$this->load_sql_file($patch, $this->getTablePrefix()))
                return false;

            //clear previous patch info -
            unset($_SESSION['ost_upgrader'][$this->getShash()]);

            $phash = substr(basename($patch), 0, 17);
            $shash = substr($phash, 9, 8);

            //Log the patch info
            $logMsg = sprintf(_S("Patch %s applied successfully"), $phash);
            if(($info = $this->readPatchInfo($patch)) && $info['version'])
                $logMsg.= ' ('.$info['version'].') ';

            $this->log(sprintf(_S("Upgrader - %s applied"), $shash), $logMsg);
            $this->signature = $shash; //Update signature to the *new* HEAD
            $this->phash = $phash;

            //Break IF elapsed time is greater than 80% max time allowed.
            if (!($task=$this->getTask())) {
                $this->cleanup();
                if (($elapsedtime=(Misc::micro_time()-$start_time))
                        && $max_time && $elapsedtime>($max_time*0.80))
                    break;
                else
                    // Apply the next patch
                    continue;
            }

            //We have work to do... set the tasks and break.
            $_SESSION['ost_upgrader'][$shash]['state'] = 'upgrade';
            break;
        }

        //Reset the migrater
        $this->migrater = null;

        return true;
    }

    function log($title, $message, $level=LOG_DEBUG) {
        global $ost;
        // Never alert the admin, and force the write to the database
        $ost->log($level, $title, $message, false, true);
    }

    /************* TASKS **********************/
    function cleanup() {
        $file = $this->getSQLDir().$this->phash.'.cleanup.sql';

        if(!file_exists($file)) //No cleanup script.
            return 0;

        //We have a cleanup script  ::XXX: Don't abort on error?
        if($this->load_sql_file($file, $this->getTablePrefix(), false, true)) {
            $this->log(sprintf(_S("Upgrader - %s cleanup"), $this->phash),
                sprintf(_S("Applied cleanup script %s"), $file));
            return 0;
        }

        $this->log(_S('Upgrader'), sprintf(_S("%s: Unable to process cleanup file"),
                        $this->phash));
        return 0;
    }
}
?>
