#!/usr/bin/php
<?php
/**
 * Command-line code generation utility to automate administrator tasks.
 *
 * Shell dispatcher class
 *
 * PHP versions 4 and 5
 *
 * CakePHP(tm) :  Rapid Development Framework <http://www.cakephp.org/>
 * Copyright 2005-2008, Cake Software Foundation, Inc.
 *                                                              1785 E. Sahara Avenue, Suite 490-204
 *                                                              Las Vegas, Nevada 89104
 * Modified for PostfixAdmin by Valkum 2011
 * Modified for PostfixAdmin by Christian Boltz 2011-2013
 *
 * Copyright 2010
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @filesource
 * @copyright           Copyright 2005-2008, Cake Software Foundation, Inc.
 * @link                                http://postfixadmin.sourceforge.net/ Postfixadmin on Sourceforge
 * @package                     postfixadmin
 * @subpackage          -
 * @since                       -
 * @version                     $Revision$
 * @modifiedby          $LastChangedBy$
 * @lastmodified        $Date$
 * @license                     http://www.opensource.org/licenses/mit-license.php The MIT License
 */


class PostfixAdmin {
    /**
     * Version
     *
     * @var string
     */
    public $version ='0.2';

    /**
     * Standard input stream.
     *
     * @var filehandle
     */
    public $stdin;

    /**
     * Standard output stream.
     *
     * @var filehandle
     */
    public $stdout;

    /**
     * Standard error stream.
     *
     * @var filehandle
     */
    public $stderr;

    /**
     * Contains command switches parsed from the command line.
     *
     * @var array
     */
    public $params = array();

    /**
     * Contains arguments parsed from the command line.
     *
     * @var array
     */
    public $args = array();

    /**
     * The file name of the shell that was invoked.
     *
     * @var string
     */
    public $shell = null;

    /**
     * The class name of the shell that was invoked.
     *
     * @var string
     */
    public $shellClass = null;

    /**
     * The command called if public methods are available.
     *
     * @var string
     */
    public $shellCommand = null;

    /**
     * The name of the shell in camelized.
     *
     * @var string
     */
    public $shellName = null;

    /**
     * Constructor
     *
     * @param array $args the argv.
     */
    public function __construct($args = array()) {
        set_time_limit(0);
        $this->__initConstants();
        $this->parseParams($args);
        $this->__initEnvironment();
    }

    /**
     * Defines core configuration.
     */
    private function __initConstants() {
        ini_set('display_errors', '1');
        ini_set('error_reporting', E_ALL);
        ini_set('html_errors', false);
        ini_set('implicit_flush', true);
        ini_set('max_execution_time', 0);

        define('DS', DIRECTORY_SEPARATOR);
        define('CORE_INCLUDE_PATH', dirname(__FILE__));
        define('CORE_PATH', dirname(CORE_INCLUDE_PATH) ); # CORE_INCLUDE_PATH/../

        if(!defined('POSTFIXADMIN')) { # already defined if called from setup.php
            define('POSTFIXADMIN', 1); # checked in included files
        }
    }

    /**
     * Defines current working environment.
     */
    private function __initEnvironment() {
        $this->stdin = fopen('php://stdin', 'r');
        $this->stdout = fopen('php://stdout', 'w');
        $this->stderr = fopen('php://stderr', 'w');

        if (!$this->__bootstrap()) {
            $this->stderr("");
            $this->stderr("Unable to load.");
            $this->stderr("\tMake sure /config.inc.php exists in " . PATH);
            exit(1);
        }


        if (basename(__FILE__) !=  basename($this->args[0])) {
            $this->stderr('Warning: the dispatcher may have been loaded incorrectly, which could lead to unexpected results...');
            if ($this->getInput('Continue anyway?', array('y', 'n'), 'y') == 'n') {
                exit(1);
            }
        }

        $this->shiftArgs();
    }

    /**
     * Initializes the environment and loads the Cake core.
     *
     * @return boolean Success.
     */
    private function __bootstrap() {
        if ($this->params['webroot'] != '' ) {
            define('PATH', $this->params['webroot'] );
        } else {
            define('PATH', CORE_PATH);
        }

        if (!file_exists(PATH)) {
            $this->stderr( PATH . " don't exists");
            return false;
        }

        # make sure global variables fron functions.inc.php end up in the global namespace, instead of being local to this function
        global $version, $min_db_version;

        if (!require_once(PATH . '/common.php')) {
            $this->stderr("Failed to load " . PATH . '/common.php');
            return false;
        }
        return true;
    }

    /**
     * Dispatches a CLI request
     */
    public function dispatch() {
        $CONF = Config::read('all');

        check_db_version(); # ensure the database layout is up to date

        if (!isset($this->args[0])) {
            $this->help();
            return;
        }

        $this->shell = $this->args[0];
        $this->shiftArgs();
        $this->shellName = ucfirst($this->shell);
        $this->shellClass = $this->shellName . 'Handler';


        if ($this->shell == 'help') {
            $this->help();
            return;
        }
        # TODO: move shells/shell.php to model/ to enable autoloading
        if (!class_exists('Shell')) {
            require CORE_INCLUDE_PATH . DS . "shells" . DS . 'shell.php';
        }
        $command = 'help'; # not the worst default ;-)
        if (isset($this->args[0])) {
            $command = $this->args[0];
        }

        $this->shellCommand = $command;
        $this->shellClass = 'Cli' . ucfirst($command);

        if (ucfirst($command) == 'Add' || ucfirst($command) == 'Update') {
            $this->shellClass = 'CliEdit';
        }

        if (!class_exists($this->shellClass)) {
            $this->stderr('Unknown task ' . $this->shellCommand);
            return;
        }

        $shell = new $this->shellClass($this);

        $shell->handler_to_use = ucfirst($this->shell) . 'Handler';

        if (!class_exists($shell->handler_to_use)) {
            $this->stderr('Unknown module ' . $this->shell);
            return;
        }

        $task = ucfirst($command);

        $shell->new = 0;
        if ($task == 'Add') {
            $shell->new = 1;
        }

        # TODO: add a way to Cli* to signal if the selected handler is supported (for example, not all *Handler support changing the password)

        if (strtolower(get_parent_class($shell)) == 'shell') {
            $shell->initialize();

            $handler = new $shell->handler_to_use;
            if (in_array($task, $handler->taskNames)) {
                $this->shiftArgs();
                $shell->startup();

                if (isset($this->args[0]) && $this->args[0] == 'help') {
                    if (method_exists($shell, 'help')) {
                        $shell->help();
                        exit();
                    } else {
                        $this->help();
                    }
                }

                $shell->execute();
                return;
            }
        }

        $classMethods = get_class_methods($shell);

        $privateMethod = $missingCommand = false;
        if ((in_array($command, $classMethods) || in_array(strtolower($command), $classMethods)) && strpos($command, '_', 0) === 0) {
            $privateMethod = true;
        }

        if (!in_array($command, $classMethods) && !in_array(strtolower($command), $classMethods)) {
            $missingCommand = true;
        }

        $protectedCommands = array(
            'initialize','in','out','err','hr',
            'createfile', 'isdir','copydir','object','tostring',
            'requestaction','log','cakeerror', 'shelldispatcher',
            '__initconstants','__initenvironment','__construct',
            'dispatch','__bootstrap','getinput','stdout','stderr','parseparams','shiftargs'
        );

        if (in_array(strtolower($command), $protectedCommands)) {
            $missingCommand = true;
        }

        if ($missingCommand && method_exists($shell, 'main')) {
            $shell->startup();
            $shell->main();
        } elseif (!$privateMethod && method_exists($shell, $command)) {
            $this->shiftArgs();
            $shell->startup();
            $shell->{$command}();
        } else {
            $this->stderr("Unknown {$this->shellName} command '$command'.\nFor usage, try 'postfixadmin-cli {$this->shell} help'.\n\n");
        }
    }

    /**
     * Prompts the user for input, and returns it.
     *
     * @param string $prompt Prompt text.
     * @param mixed $options Array or string of options.
     * @param string $default Default input value.
     * @return Either the default value, or the user-provided input.
     */
    public function getInput($prompt, $options = null, $default = null) {
        if (!is_array($options)) {
            $print_options = '';
        } else {
            $print_options = '(' . implode('/', $options) . ')';
        }

        if ($default == null) {
            $this->stdout($prompt . " $print_options \n" . '> ', false);
        } else {
            $this->stdout($prompt . " $print_options \n" . "[$default] > ", false);
        }
        $result = fgets($this->stdin);

        if ($result === false){
            exit(1);
        }
        $result = trim($result);

        if ($default != null && empty($result)) {
            return $default;
        }
        return $result;
    }

    /**
     * Outputs to the stdout filehandle.
     *
     * @param string $string String to output.
     * @param boolean $newline If true, the outputs gets an added newline.
     */
    public function stdout($string, $newline = true) {
        if ($newline) {
            fwrite($this->stdout, $string . "\n");
        } else {
            fwrite($this->stdout, $string);
        }
    }

    /**
     * Outputs to the stderr filehandle.
     *
     * @param string $string Error text to output.
     */
    public function stderr($string) {
        fwrite($this->stderr, 'Error: '. $string . "\n");
    }

    /**
     * Parses command line options
     *
     * @param array $params Parameters to parse
     */
    public function parseParams($params) {
        $this->__parseParams($params);

        $defaults = array('webroot' => CORE_PATH);

        $params = array_merge($defaults, array_intersect_key($this->params, $defaults));

        $isWin = array_filter(array_map('strpos', $params, array('\\')));

        $params = str_replace('\\', '/', $params);


        if (!empty($matches[0]) || !empty($isWin)) {
            $params = str_replace('/', '\\', $params);
        }

        $this->params = array_merge($this->params, $params);
    }

    /**
     * Helper for recursively paraing params
     *
     * @return array params
     */
    private function __parseParams($params) {
        $count = count($params);
        for ($i = 0; $i < $count; $i++) {
            if (isset($params[$i])) {
                if ($params[$i] != '' && $params[$i]{0} === '-') {
                    $key = substr($params[$i], 1);
                    $this->params[$key] = true;
                    unset($params[$i]);
                    if (isset($params[++$i])) {
                        # TODO: ideally we should know if a parameter can / must have a value instead of whitelisting known valid values starting with '-' (probably only bool doesn't need a value)
                        if ($params[$i]{0} !== '-' or $params[$i] != '-1') {
                            $this->params[$key] = $params[$i];
                            unset($params[$i]);
                        } else {
                            $i--;
                            $this->__parseParams($params);
                        }
                    }
                } else {
                    $this->args[] = $params[$i];
                    unset($params[$i]);
                }
            }
        }
    }

    /**
     * Removes first argument and shifts other arguments up
     *
     * @return boolean False if there are no arguments
     */
    public function shiftArgs() {
        if (empty($this->args)) {
            return false;
        }
        unset($this->args[0]);
        $this->args = array_values($this->args);
        return true;
    }

    /**
     * prints help message and exits.
     */
    public function help() {
        $this->stdout("\nWelcome to Postfixadmin-CLI v" . $this->version);
        $this->stdout("---------------------------------------------------------------");
        $this->stdout("Usage:");
        $this->stdout("    postfixadmin-cli <module> <task> [--option value --option2 value]");
        $this->stdout("");
        $this->stdout("Available modules:");

        $modules = explode(',','admin,domain,mailbox,alias,aliasdomain,fetchmail');
        foreach ($modules as $module) {
            $this->stdout("    $module");
        }
        $this->stdout("");
        $this->stdout("Most modules support the following tasks:");
        $this->stdout("    view      View an item");
        $this->stdout("    add       Add an item");
        $this->stdout("    update    Update an item");
        $this->stdout("    delete    Delete an item");
        $this->stdout("    scheme    Print database scheme (useful for developers only)");
        $this->stdout("    help      Print help output");
        $this->stdout("");
        $this->stdout("");
        $this->stdout("For module-specific help, see:");
        $this->stdout("");
        $this->stdout("    postfixadmin-cli <module> help");
        $this->stdout("        print a detailed list of available commands");
        $this->stdout("");
        $this->stdout("    postfixadmin-cli <module> <task> help");
        $this->stdout("        print a list of available options.");
        $this->stdout("");

        exit();

    }
}


define ("POSTFIXADMIN_CLI", 1);

$dispatcher = new PostfixAdmin($argv);

$CONF = Config::read('all');

$dispatcher->dispatch();

/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */
