From 330ef8ac62e30f38dc50250c610dfd0d144d358e Mon Sep 17 00:00:00 2001
From: Jared Hancock <jared@osticket.com>
Date: Tue, 1 Jan 2013 23:40:40 -0600
Subject: [PATCH] Add management meta script

Which will allow for a modular approach to command-line scripting of the
osTicket system. Hopefully, it will alleviate the need to copy+paste code
between scripts and otherwise duplicate functionality between locations. It
will also allow for easier administration of osTicket by shipping scripts to
do things like database maintenance, statistics exports (collectd), data
exports, etc.

The first example script include the capability to unpack the osTicket
installation/upgrade tarball to the destination install path with optionally
placing the include folder in a different location. The script will monkey
patch main.inc.php so that the INCLUDE_DIR define will point to the
installed location of the include/ folder.
---
 setup/scripts/manage.php               |  60 +++++++
 setup/scripts/modules/class.module.php | 214 +++++++++++++++++++++++++
 setup/scripts/modules/unpack.php       | 158 ++++++++++++++++++
 3 files changed, 432 insertions(+)
 create mode 100755 setup/scripts/manage.php
 create mode 100644 setup/scripts/modules/class.module.php
 create mode 100644 setup/scripts/modules/unpack.php

diff --git a/setup/scripts/manage.php b/setup/scripts/manage.php
new file mode 100755
index 000000000..c73831829
--- /dev/null
+++ b/setup/scripts/manage.php
@@ -0,0 +1,60 @@
+#!/usr/bin/env php
+<?php
+
+require_once "modules/class.module.php";
+
+class Manager extends Module {
+    var $prologue =
+        "Manage one or more osTicket installations";
+
+    var $arguments = array(
+        'action' => "Action to be managed"
+    );
+
+    var $usage = '$script action [options] [arguments]';
+
+    var $autohelp = false;
+
+    function showHelp() {
+        foreach (glob(dirname(__file__).'/modules/*.php') as $script)
+            include_once $script;
+
+        global $registered_modules;
+        $this->epilog = 
+            "Currently available modules follow. Use 'manage.php <module>
+            --help' for usage regarding each respective module:";
+
+        parent::showHelp();
+        
+        echo "\n";
+        foreach ($registered_modules as $name=>$mod)
+            echo str_pad($name, 20) . $mod->prologue . "\n";
+    }
+
+    function run() {
+        if ($this->getOption('help') && !$this->getArgument('action'))
+            $this->showHelp();
+
+        else {
+            $action = $this->getArgument('action');
+
+            global $argv;
+            foreach ($argv as $idx=>$val)
+                if ($val == $action)
+                    unset($argv[$idx]);
+
+            include_once dirname(__file__) . '/modules/' . $action . '.php';
+            $module = Module::getInstance($action);
+            $module->run();
+        }
+    }
+}
+
+if (php_sapi_name() != "cli")
+    die("Management only supported from command-line\n");
+
+$manager = new Manager();
+$manager->parseOptions();
+$manager->run();
+
+?>
diff --git a/setup/scripts/modules/class.module.php b/setup/scripts/modules/class.module.php
new file mode 100644
index 000000000..de30f1632
--- /dev/null
+++ b/setup/scripts/modules/class.module.php
@@ -0,0 +1,214 @@
+<?php
+
+class Option {
+    
+    var $default = false;
+
+    function Option() {
+        call_user_func_array(array($this, "__construct"), func_get_args());
+    }
+
+    function __construct($options=false) {
+        list($this->short, $this->long) = array_slice($options, 0, 2);
+        $this->help = (isset($options['help'])) ? $options['help'] : "";
+        $this->action = (isset($options['action'])) ? $options['action']
+            : "store";
+        $this->dest = (isset($options['dest'])) ? $options['dest']
+            : substr($this->long, 2);
+        $this->type = (isset($options['type'])) ? $options['type']
+            : 'string';
+        $this->const = (isset($options['const'])) ? $options['const']
+            : null;
+        $this->default = (isset($options['default'])) ? $options['default']
+            : null;
+        $this->metavar = (isset($options['metavar'])) ? $options['metavar']
+            : 'var';
+    }
+
+    function hasArg() {
+        return $this->action != 'store_true'
+            && $this->action != 'store_false';
+    }
+
+    function handleValue(&$destination, $args) {
+        $nargs = 0;
+        $value = array_shift($args);
+        if ($value[0] == '-')
+            $value = null;
+        elseif ($value)
+            $nargs = 1;
+        switch ($this->action) {
+            case 'store_true':
+                $value = true;
+                break;
+            case 'store_false':
+                $value = false;
+                break;
+            case 'store_const':
+                $value = $this->const;
+                break;
+            case 'store':
+            default:
+                if ($this->type == 'int')
+                    $value = (int)$value;
+                break;
+        }
+        $destination[$this->dest] = $value;
+        return $nargs;
+    }
+
+    function toString() {
+        $short = explode(':', $this->short);
+        $long = explode(':', $this->long);
+        if ($this->nargs == '?')
+            $switches = sprintf('    %s [%3$s], %s[=%3$s]', $short[0],
+                $long[0], $this->metavar);
+        elseif ($this->hasArg())
+            $switches = sprintf('    %s %3$s, %s=%3$s', $short[0], $long[0],
+                $this->metavar);
+        else
+            $switches = sprintf("    %s, %s", $short[0], $long[0]);
+        $help = preg_replace('/\s+/', ' ', $this->help);
+        if (strlen($switches) > 24)
+            $help = "\n" . str_repeat(" ", 24) . $help;
+        else
+            $switches = str_pad($switches, 24);
+        $help = wordwrap($help, 54, "\n" . str_repeat(" ", 24));
+        return $switches . $help;
+    }
+}
+
+class Module {
+
+    var $options = array();
+    var $arguments = array();
+    var $prologue = "";
+    var $epilog = "";
+    var $usage = '$script [options] $args [arguments]';
+    var $autohelp = true;
+
+    function Module() {
+        call_user_func_array(array($this, '__construct'), func_get_args());
+    }
+
+    function __construct() {
+        $this->options['help'] = array("-h","--help",
+            'action'=>'store_true',
+            'help'=>"Display this help message");
+        foreach ($this->options as &$opt)
+            $opt = new Option($opt);
+    }
+
+    function showHelp() {
+        if ($this->prologue)
+            echo $this->prologue . "\n\n";
+
+        echo "Usage:\n";
+        global $argv;
+        echo "    " . str_replace(
+                array('$script', '$args'),
+                array($argv[0], implode(' ', array_keys($this->arguments))),
+            $this->usage) . "\n";
+
+        ksort($this->options);
+        if ($this->options) {
+            echo "\nOptions:\n";
+            foreach ($this->options as $name=>$opt)
+                echo $opt->toString() . "\n";
+        }
+
+        if ($this->arguments) {
+            echo "\nArguments:\n";
+            foreach ($this->arguments as $name=>$help)
+                echo $name . "\n    " . wordwrap(
+                    preg_replace('/\s+/', ' ', $help), 76, "\n    ");
+        }
+
+        if ($this->epilog) {
+            echo "\n\n";
+            $epilog = preg_replace('/\s+/', ' ', $this->epilog);
+            echo wordwrap($epilog, 76, "\n");
+        }
+
+        echo "\n";
+    }
+
+    function getOption($name, $default=false) {
+        $this->parseOptions();
+        if (isset($this->_options[$name]))
+            return $this->_options[$name];
+        else
+            return $default;
+    }
+
+    function getArgument($name, $default=false) {
+        $this->parseOptions();
+        foreach (array_keys($this->arguments) as $idx=>$arg)
+            if ($arg == $name && isset($this->_args[$idx]))
+                return $this->_args[$idx];
+        return $default;
+    }
+
+    function parseOptions() {
+        if (is_array($this->_options))
+            return;
+
+        global $argv;
+        list($this->_options, $this->_args) =
+            $this->parseArgs(array_slice($argv, 1));
+
+        foreach (array_keys($this->arguments) as $idx=>$name)
+            if (!isset($this->_args[$idx]))
+                $this->optionError($name . " is a required argument");
+
+        if ($this->autohelp && $this->getOption('help')) {
+            $this->showHelp();
+            die();
+        }
+    }
+
+    function optionError($error) {
+        echo "Error: " . $error . "\n\n";
+        $this->showHelp();
+        die();
+    }
+
+    /* abstract */ function run() {
+    }
+
+    /* static */ function register($action, $class) {
+        global $registered_modules;
+        $registered_modules[$action] = new $class();
+    }
+
+    /* static */ function getInstance($action) {
+        global $registered_modules;
+        return $registered_modules[$action];
+    }
+
+    function parseArgs($argv) {
+        $options = $args = array();
+        $argv = array_slice($argv, 0);
+        while ($arg = array_shift($argv)) {
+            if (strpos($arg, '=') !== false) {
+                list($arg, $value) = explode('=', $arg, 2);
+                array_unshift($argv, $value);
+            }
+            $found = false;
+            foreach ($this->options as $opt) {
+                if ($opt->short == $arg || $opt->long == $arg) {
+                    if ($opt->handleValue($options, $argv))
+                        array_shift($argv);
+                    $found = true;
+                }
+            }
+            if (!$found && $arg[0] != '-')
+                $args[] = $arg;
+        }
+        return array($options, $args);
+    }
+}
+
+$registered_modules = array();
+
+?>
diff --git a/setup/scripts/modules/unpack.php b/setup/scripts/modules/unpack.php
new file mode 100644
index 000000000..0f6aed8ab
--- /dev/null
+++ b/setup/scripts/modules/unpack.php
@@ -0,0 +1,158 @@
+<?php
+
+require_once dirname(__file__) . "/class.module.php";
+
+class Unpacker extends Module {
+    
+    var $prologue = "Unpacks osTicket into target install path";
+
+    var $epilog =
+        "Copies an unpacked osticket tarball or zipfile into a production
+         location, optionally placing the include/ folder in a separate 
+         location if requested";
+
+    var $options = array(
+        'include' => array('-i','--include', 'metavar'=>'path', 'help'=>
+            "The include/ folder, which contains the bulk of osTicket's source
+             code can be located outside of the install path. This is recommended
+             for better security. If you would like to install the include/
+             folder somewhere else, give the path here. Note that the full
+             path is assumed, so give path/to/include/ to unpack the source
+             code in that folder. The folder will be automatically created if
+             it doesn't already exist."
+        ),
+    );
+
+    var $arguments = array(
+        'install-path' =>
+            "The destination for osTicket to reside. Use the --include
+             option to specify destination of the include/ folder, if the
+             administrator should chose to locate it separate from the
+             main installation path.",
+    );
+
+    function find_upload_folder() {
+        # Hop up to the root folder
+        $start = dirname(__file__);
+        for (;;) {
+            if (is_dir($start . '/upload')) break;
+            $start .= '/..';
+        }
+        return realpath($start.'/upload');
+    }
+
+    function change_include_dir($include_path) {
+        # Read the main.inc.php script
+        $main_inc_php = $this->destination . '/main.inc.php';
+        $lines = explode("\n", file_get_contents($main_inc_php));
+        # Try and use ROOT_PATH
+        if (strpos($include_path, $this->destination) === 0)
+            $include_path = "ROOT_PATH . '" .
+                str_replace($this->destination, '', $include_path) . "'";
+        else
+            $include_path = "'$include_path'";
+        # Find the line that defines INCLUDE_DIR
+        foreach ($lines as &$line) {
+            if (preg_match("/(\s*)define\s*\(\s*'INCLUDE_DIR'/", $line, $match)) {
+                # Replace the definition with the new locatin
+                $line = $match[1] . "define('INCLUDE_DIR', "
+                    . $include_path
+                    . "); // Set by installer";
+                break;
+            }
+        }
+        if (!file_put_contents($main_inc_php, implode("\n", $lines)))
+            die("Unable to configure location of INCLUDE_DIR in main.inc.php\n");
+    }
+
+    function exclude($pattern, $match) {
+        if (!$pattern) {
+            return false;
+        } elseif (is_array($pattern)) {
+            foreach ($pattern as $p)
+                if (fnmatch($p, $match))
+                    return true;
+        } else {
+            return fnmatch($pattern, $match);
+        }
+        return false;
+    }
+
+    function unpackage($folder, $destination, $recurse=true, $exclude=false) {
+        foreach (glob($folder, GLOB_BRACE|GLOB_NOSORT) as $file) {
+            if ($this->exclude($exclude, $file))
+                continue;
+            if (is_file($file)) {
+                if (!is_dir($destination))
+                    mkdir($destination, 0751, true);
+                copy($file, $destination . '/' . basename($file));
+            }
+        }
+        if ($recurse) {
+            foreach (glob(dirname($folder).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
+                if ($this->exclude($exclude, $dir))
+                    continue;
+                $this->unpackage(
+                    dirname($folder).'/'.basename($dir).'/'.basename($folder),
+                    $destination.'/'.basename($dir),
+                    $recurse - 1, $exclude);
+            }
+        }
+    }
+
+    function get_include_dir() {
+        $main_inc_php = $this->destination . '/main.inc.php';
+        $lines = preg_grep("/define\s*\(\s*'INCLUDE_DIR'/",
+            explode("\n", file_get_contents($main_inc_php)));
+
+        // NOTE: that this won't work for crafty folks who have a define or some
+        //       variable in the value of their include path
+        if (!defined('ROOT_DIR')) define('ROOT_DIR', $this->destination . '/');
+        foreach ($lines as $line)
+            eval($line);
+
+        return INCLUDE_DIR;
+    }
+
+    function run() {
+        $this->destination = $this->getArgument('install-path');
+        if (!is_dir($this->destination))
+            if (!mkdir($this->destination, 0751, true))
+                $this->die("Destination path does not exist and cannot be created");
+
+        # Determine if this is an upgrade, and if so, where the include/
+        # folder is currently located
+        $upgrade = file_exists("{$this->destination}/main.inc.php");
+
+        # Locate the upload folder
+        $upload = $this->find_upload_folder();
+
+        # Unpack the upload folder to the destination, except the include folder
+        if ($upgrade)
+            # Get the current value of the INCLUDE_DIR before overwriting
+            # main.inc.php
+            $include = $this->get_include_dir();
+        $this->unpackage("$upload/*", $this->destination, -1, "*include");
+
+        if (!$upgrade) {
+            if ($this->getOption('include')) {
+                $location = $this->getOption('include');
+                if (!is_dir("$location/"))
+                    if (!mkdir("$location/", 0751, true))
+                        die("Unable to create folder for include/ files\n");
+                $this->unpackage("$upload/include/*", $location, -1);
+                $this->change_include_dir($location);
+            }
+            else
+                $this->unpackage("$upload/include/*", "{$this->destination}/include", -1);
+        }
+        else {
+            $this->unpackage("$upload/include/*", $include, -1);
+            # Change the new main.inc.php to reflect the location of the
+            # include/ directory
+            $this->change_include_dir($include);
+        }
+    }
+}
+
+Module::register('unpack', 'Unpacker');
-- 
GitLab