Skip to content
Snippets Groups Projects
deploy.php 11 KiB
Newer Older
Jared Hancock's avatar
Jared Hancock committed
<?php
require_once dirname(__file__) . "/unpack.php";

class Deployment extends Unpacker {
    var $prologue = "Deploys osTicket into target install path";

    var $epilog =
        "Deployment is used from the continuous development model. If you
        are following the upstream git repo, then you can use the deploy
        script to deploy changes made by you or upstream development to your
        installation target";

    function __construct() {
        $this->options['dry-run'] = array('-t','--dry-run',
            'action'=>'store_true',
            'help'=>'Don\'t actually deploy new code. Just show the files
                that would be copied');
        $this->options['setup'] = array('-s','--setup',
            'action'=>'store_true',
            'help'=>'Deploy the setup folder. Useful for deploying for new
                installations.');
        $this->options['clean'] = array('-C','--clean',
            'action'=>'store_true',
            'help'=>'Remove files from the destination that are no longer
                included in this repository');
        $this->options['git'] = array('-g','--git',
            'action'=>'store_true',
            'help'=>'Use `git ls-files -s` as files source. Eliminates
                possibility of deploying untracked files');
        $this->options['force'] = array('-f', '--force',
            'action'=>'store_true',
            'help'=>'Deploy all files, even if they have not changed');
        # super(*args);
        call_user_func_array(array('parent', '__construct'), func_get_args());
    }

Jared Hancock's avatar
Jared Hancock committed
    function find_root_folder() {
        # Hop up to the root folder of this repo
        $start = dirname(__file__);
        for (;;) {
            if (is_file($start . '/main.inc.php')) break;
            $start .= '/..';
        }
        return self::realpath($start);
    /**
     * Removes files from the deployment location that no longer exist in
     * the local repository
     */
    function clean($local, $destination, $root, $recurse=0, $exclude=false) {
        $dryrun = $this->getOption('dry-run', false);
        $verbose = $dryrun || $this->getOption('verbose');
        $destination = rtrim($destination, '/') . '/';
        $contents = glob($destination.'{,.}*', GLOB_BRACE|GLOB_NOSORT);
        foreach ($contents as $i=>$file) {
            $relative = str_replace($root, "", $file);
            if ($this->exclude($exclude, $relative))
                continue;
            if (is_file($file)) {
                $ltarget = $local . '/' . basename($file);
                if (is_file($ltarget))
                    continue;
                if ($verbose)
                    $this->stdout->write("(delete): $file\n");
                if (!$dryrun)
                    unlink($file);
                unset($contents[$i]);
            }
            elseif (in_array(basename($file), array('.','..'))) {
                // Doesn't indicate that the folder has contents
                unset($contents[$i]);
            }
        }
        if ($recurse) {
            $folders = glob(dirname($destination).'/'.basename($destination).'/*',
                GLOB_BRACE|GLOB_ONLYDIR|GLOB_NOSORT);
            foreach ($folders as $dir) {
                if (in_array(basename($dir), array('.','..')))
                    continue;
                $relative = str_replace($root, "", $dir);
                if ($this->exclude($exclude, "$relative/"))
                    continue;
                $this->clean(
                    $local.'/'.basename($dir),
                    $destination.basename($dir),
                    $root, $recurse - 1, $exclude);
            }
        }
        if (!$contents || !glob($destination.'{,.}*', GLOB_BRACE|GLOB_NOSORT)) {
            if ($verbose)
                $this->stdout->write("(delete-folder): $destination\n");
            if (!$dryrun)
                rmdir($destination);
        }
    }

    function writeManifest($root) {
        $lines = array();
        foreach ($this->manifest as $F=>$H)
            $lines[] = "$H $F";

        return file_put_contents($this->include_path.'/.MANIFEST', implode("\n", $lines));
    }

    function hashContents($file) {
        $md5 = md5($file);
        $sha1 = sha1($file);
        return substr($md5, -20) . substr($sha1, -20);
    }

    function getEditedContents($src) {
        static $short = false;
        static $version = false;

        if (substr($src, -4) != '.php')

        if (!$short) {
            $hash = exec('git rev-parse HEAD');
            $short = substr($hash, 0, 7);
        }

        if (!$version)
JediKev's avatar
JediKev committed
            $version = exec('git describe');

        if (!$short || !$version)

        $source = file_get_contents($src);
        $original = crc32($source);
        $source = preg_replace(':<script(.*) src="([^"]+)\.js"></script>:',
            '<script$1 src="$2.js?'.$short.'"></script>',
            $source);
        $source = preg_replace(':<link(.*) href="([^"]+)\.css"([^/>]*)/?>:', # <?php
            '<link$1 href="$2.css?'.$short.'"$3/>',
            $source);
        // Set THIS_VERSION
        $source = preg_replace("/^(\s*)define\s*\(\s*'THIS_VERSION'.*$/m",
            "$1define('THIS_VERSION', '".$version."'); // Set by installer",
        // Set GIT_VERSION
        $source = preg_replace("/^(\s*)define\s*\(\s*'GIT_VERSION'.*$/m",
            "$1define('GIT_VERSION', '".$short."'); // Set by installer",
            $source);
        // Disable error display
        $source = preg_replace("/^(\s*)ini_set\s*\(\s*'(display_errors|display_startup_errors)'.*$/m",
            "$1ini_set('$2', '0'); // Set by installer",
            $source);

        // return FALSE if the edited contents do not differ from the
        // original contents
        return $original != crc32($source) ? $source : false;
    function isChanged($source, $hash=false) {
        $local = str_replace($this->source.'/', '', $source);
        $hash = $hash ?: $this->hashFile($source);
        list($shash, $flag) = explode(':', $this->readManifest($local));
        return ($flag === 'rewrite') ? $flag : $shash != $hash;
    }

    function copyFile($source, $dest, $hash=false, $mode=0644, $contents=false) {
        $contents = $contents ?: $this->getEditedContents($source);
        if ($contents === false)
            // Regular file
            return parent::copyFile($source, $dest, $hash, $mode);

        if (!file_put_contents($dest, $contents))
            $this->fail($dest.": Unable to apply rewrite rules");
        $this->updateManifest($source, "$hash:rewrite");
        return chmod($dest, $mode);
    function unpackage($folder, $destination, $recurse=0, $exclude=false) {
        $use_git = $this->getOption('git', false);
        if (!$use_git)
            return parent::unpackage($folder, $destination, $recurse, $exclude);
        // Attempt to read from git using `git ls-files` for deployment
        if (substr($destination, -1) !== '/')
            $destination .= '/';
        $source = $this->source;
        if (substr($source, -1) != '/')
            $source .= '/';
        $local = str_replace(array($source, '{,.}*'), array('',''), $folder);

        $pipes = array();
        $patterns = array();
        foreach ((array) $exclude as $x) {
            $patterns[] = str_replace($source, '', $x);
        }
        $X = implode(' --exclude-per-directory=', $patterns);
        chdir($source.$local);
            "git ls-files -zs --exclude-standard --exclude-per-directory=$X -- .",
            array(1 => array('pipe', 'w')),
            $pipes
        ))) {
            return parent::unpackage($folder, $destination, $recurse, $exclude);
        }

        $dryrun = $this->getOption('dry-run', false);
        $verbose = $this->getOption('verbose') || $dryrun;
        $force = $this->getOption('force');
        while ($line = stream_get_line($pipes[1], 255, "\x00")) {
            list($mode, $hash, , $path) = preg_split('/\s+/', $line);
            $src = $source.$local.$path;
            if ($this->exclude($exclude, $src))
            if (!$force && false === ($flag = $this->isChanged($src, $hash)))
                continue;
            $dst = $destination.$path;
            if ($verbose) {
                $msg = $dst;
                if (is_string($flag))
                    $msg = "$msg ({$flag})";
                $this->stdout->write("$msg\n");
            }
            if ($dryrun)
                continue;
            if (!is_dir(dirname($dst)))
                mkdir(dirname($dst), 0755, true);
            $this->copyFile($src, $dst, $hash, octdec($mode));
Jared Hancock's avatar
Jared Hancock committed
    function run($args, $options) {
        $this->destination = $args['install-path'];
        if (!is_dir($this->destination))
            if (!@mkdir($this->destination, 0751, true))
                die("Destination path does not exist and cannot be created");
        $this->destination = self::realpath($this->destination).'/';
Jared Hancock's avatar
Jared Hancock committed

        # Determine if this is an upgrade, and if so, where the include/
        # folder is currently located
        $upgrade = file_exists("{$this->destination}/main.inc.php");

        # Get the current value of the INCLUDE_DIR before overwriting
Jared Hancock's avatar
Jared Hancock committed
        $include = ($upgrade) ? $this->get_include_dir()
            : ($options['include'] ? $options['include']
                : rtrim($this->destination, '/')."/include");
        $this->include_path = $include = rtrim($include, '/').'/';
Jared Hancock's avatar
Jared Hancock committed

        # Locate the upload folder
        $root = $this->source = $this->find_root_folder();
Dmitry Gorelenkov's avatar
Dmitry Gorelenkov committed
        $rootPattern = str_replace("\\","\\\\", $root); //need for windows case
        # Prime the manifest system
        $this->readManifest($this->destination.'/.MANIFEST');

        $exclusions = array("$rootPattern/include/*", "$rootPattern/.git*",
            "*.sw[a-z]","*.md", "*.txt");
        if (!$options['setup'])
            $exclusions[] = "$rootPattern/setup/*";
Jared Hancock's avatar
Jared Hancock committed
        # Unpack everything but the include/ folder
Jared Hancock's avatar
Jared Hancock committed
        $this->unpackage("$root/{,.}*", $this->destination, -1,
            $exclusions);
Jared Hancock's avatar
Jared Hancock committed
        # Unpack the include folder
Jared Hancock's avatar
Jared Hancock committed
        $this->unpackage("$root/include/{,.}*", $include, -1,
            array("*/include/ost-config.php", "*.sw[a-z]"));
        if (!$options['dry-run']) {
            if ($include != "{$this->destination}/include/")
                $this->change_include_dir($include);
        }

        if ($options['clean']) {
            // Clean everything but include folder first
            $local_include = str_replace($this->destination, "", $include);
            $this->clean($root, $this->destination, $this->destination, -1,
                array($local_include, "setup/"));
            $this->clean("$root/include", $include, $include, -1,
                array("ost-config.php","settings.php","plugins/",
        if (!$options['dry-run'])
            $this->writeManifest($this->destination);
Jared Hancock's avatar
Jared Hancock committed
    }
}

Module::register('deploy', 'Deployment');
?>