Skip to content
Snippets Groups Projects
class.upgrader.php 13.3 KiB
Newer Older
<?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:
**********************************************************************/

Peter Rotich's avatar
Peter Rotich committed
require_once INCLUDE_DIR.'class.setup.php';
require_once INCLUDE_DIR.'class.migrater.php';
class Upgrader {
    function Upgrader($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');
    }

Jared Hancock's avatar
Jared Hancock committed
    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')
            $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 check_prereq() {
        if ($this->getCurrentStream())
            return $this->getCurrentStream()->check_prereq();
    }
    function check_php() {
        if ($this->getCurrentStream())
            return $this->getCurrentStream()->check_php();
    }
    function check_mysql() {
        if ($this->getCurrentStream())
            return $this->getCurrentStream()->check_mysql();
    }

Peter Rotich's avatar
Peter Rotich committed

    function getTask() {
        if($this->getCurrentStream())
            return $this->getCurrentStream()->getTask();
    }

Jared Hancock's avatar
Jared Hancock committed
    function doTask() {
        return $this->getCurrentStream()->doTask();
    }

    function getErrors() {
Jared Hancock's avatar
Jared Hancock committed
        if ($this->getCurrentStream())
            return $this->getCurrentStream()->getErrors();
    }

    function getNextAction() {
        if ($this->getCurrentStream())
            return $this->getCurrentStream()->getNextAction();
    }

    function getNextVersion() {
        return $this->getCurrentStream()->getNextVersion();
    }

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

    function getSHash() {
        if ($this->getCurrentStream())
            return $this->getCurrentStream()->getSHash();
    }
}

/**
 * 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;

Jared Hancock's avatar
Jared Hancock committed
    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 StreamUpgrader($schema_signature, $target, $stream, $prefix, $sqldir, $upgrader) {

        $this->signature = $schema_signature;
        $this->target = $target;
        $this->prefix = $prefix;
        $this->sqldir = $sqldir;
        $this->errors = array();
        $this->upgrader = $upgrader;
        $this->name = $stream;
Peter Rotich's avatar
Peter Rotich committed
        //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()]))
Jared Hancock's avatar
Jared Hancock committed
            $_SESSION['ost_upgrader']['task'] = array();

        //Tasks to perform - saved on the session.
Jared Hancock's avatar
Jared Hancock committed
        $this->phash = &$_SESSION['ost_upgrader']['phash'];
        //Database migrater
        $this->migrater = null;
    }

    function onError($error) {
        global $ost, $thisstaff;
        $subject = '['.$this->name.']: 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.
Peter Rotich's avatar
Peter Rotich committed
            Mailer::sendmail($thisstaff->getEmail(), $subject, $error, sprintf('"osTicket Alerts"<%s>', $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);
    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
Peter Rotich's avatar
Peter Rotich committed
        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 getNextAction() {

        $action='Upgrade osTicket to '.$this->getVersion();
Jared Hancock's avatar
Jared Hancock committed
        if($task=$this->getTask()) {
            $action = $task->getDescription() .' ('.$task->getStatus().')';
        } elseif($this->isUpgradable() && ($nextversion = $this->getNextVersion())) {
            $action = "Upgrade to $nextversion";
        }

        return '['.$this->name.'] '.$action;
Jared Hancock's avatar
Jared Hancock committed
    function getPendingTask() {

        $pending=array();
Jared Hancock's avatar
Jared Hancock committed
        if ($task=$this->getTask())
            return ($task->isFinished()) ? 1 : 0;
Jared Hancock's avatar
Jared Hancock committed
        return false;
Jared Hancock's avatar
Jared Hancock committed
    function getTask() {
Peter Rotich's avatar
Peter Rotich committed
        global $ost;

Jared Hancock's avatar
Jared Hancock committed
        $task_file = $this->getSQLDir() . "{$this->phash}.task.php";
        if (!file_exists($task_file))
Jared Hancock's avatar
Jared Hancock committed
        if (!isset($this->task)) {
            $class = (include $task_file);
            if (!is_string($class) || !class_exists($class))
                return $ost->logError("Bogus migration task", "{$this->phash}:{$class}") ;
Jared Hancock's avatar
Jared Hancock committed
            $this->task = new $class();
            if (isset($_SESSION['ost_upgrader']['task'][$this->phash]))
                $this->task->wakeup($_SESSION['ost_upgrader']['task'][$this->phash]);
        }
        return $this->task;
Jared Hancock's avatar
Jared Hancock committed
    function doTask() {
Jared Hancock's avatar
Jared Hancock committed
        if(!($task = $this->getTask()))
            return false; //Nothing to do.
Jared Hancock's avatar
Jared Hancock committed
                sprintf('Upgrader - %s (task pending).', $this->getShash()),
                sprintf('The %s task reports there is work to do',
                    get_class($task))
Jared Hancock's avatar
Jared Hancock committed
        if(!($max_time = ini_get('max_execution_time')))
            $max_time = 30; //Default to 30 sec batches.
Jared Hancock's avatar
Jared Hancock committed
        $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);
Jared Hancock's avatar
Jared Hancock committed
        return false;
    function upgrade() {
Jared Hancock's avatar
Jared Hancock committed
        if($this->getPendingTask() || !($patches=$this->getPatches()))
Peter Rotich's avatar
Peter Rotich committed
        $start_time = Misc::micro_time();
        if(!($max_time = ini_get('max_execution_time')))
            $max_time = 300; //Apache/IIS defaults.

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

            unset($_SESSION['ost_upgrader'][$this->getShash()]);
Peter Rotich's avatar
Peter Rotich committed

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

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

            $this->log("Upgrader - $shash applied", $logMsg);
            $this->signature = $shash; //Update signature to the *new* HEAD
Jared Hancock's avatar
Jared Hancock committed
            $this->phash = $phash;
Jared Hancock's avatar
Jared Hancock committed
            //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))
Peter Rotich's avatar
Peter Rotich committed
                    break;
Jared Hancock's avatar
Jared Hancock committed
                else
                    // Apply the next patch
                    continue;
            //We have work to do... set the tasks and break.
Peter Rotich's avatar
Peter Rotich committed
            $_SESSION['ost_upgrader'][$shash]['state'] = 'upgrade';
            break;
        }

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

Peter Rotich's avatar
Peter Rotich committed
        return true;
    function log($title, $message, $level=LOG_DEBUG) {
        // Never alert the admin, and force the write to the database
        $ost->log($level, $title, $message, false, true);
    }
    /************* TASKS **********************/
    function cleanup() {
Jared Hancock's avatar
Jared Hancock committed
        $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("Upgrader - {$this->phash} cleanup",
                "Applied cleanup script {$file}");
        $this->log('Upgrader', sprintf("%s: Unable to process cleanup file",
Peter Rotich's avatar
Peter Rotich committed
                        $this->phash));