controller should go into the main repo, it's already horde 4 ready
authorChuck Hagenbuch <chuck@horde.org>
Sun, 9 Nov 2008 20:36:46 +0000 (15:36 -0500)
committerChuck Hagenbuch <chuck@horde.org>
Sun, 9 Nov 2008 20:36:46 +0000 (15:36 -0500)
framework/Controller/lib/Horde/Controller/Base.php [new file with mode: 0644]
framework/Controller/lib/Horde/Controller/Dispatcher.php [new file with mode: 0644]
framework/Controller/lib/Horde/Controller/Exception.php [new file with mode: 0644]
framework/Controller/lib/Horde/Controller/Request/Base.php [new file with mode: 0644]
framework/Controller/lib/Horde/Controller/Request/Http.php [new file with mode: 0644]
framework/Controller/lib/Horde/Controller/Request/Mock.php [new file with mode: 0644]
framework/Controller/lib/Horde/Controller/Response/Base.php [new file with mode: 0644]
framework/Controller/lib/Horde/Controller/Response/Http.php [new file with mode: 0644]
framework/Controller/lib/Horde/Controller/Response/Mock.php [new file with mode: 0644]
framework/Controller/package.xml [new file with mode: 0644]
framework/Controller/test/Horde/Controller/AllTests.php [new file with mode: 0644]

diff --git a/framework/Controller/lib/Horde/Controller/Base.php b/framework/Controller/lib/Horde/Controller/Base.php
new file mode 100644 (file)
index 0000000..d56a4d6
--- /dev/null
@@ -0,0 +1,417 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ */
+
+/**
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ */
+abstract class Horde_Controller_Base
+{
+    /**
+     * Params is the list of variables set through routes.
+     * @var Horde_Support_Array
+     */
+    protected $params;
+
+    /**
+     * Have we performed a render on this controller
+     * @var boolean
+     */
+    protected $_performedRender = false;
+
+    /**
+     * Have we performed a redirect on this controller
+     * @var boolean
+     */
+    protected $_performedRedirect = false;
+
+    /**
+     * The request object we are processing
+     * @var Horde_Controller_Request_Base
+     * @todo Assign default value.
+     */
+    protected $_request;
+
+    /**
+     * The response object we are returning
+     * @var Horde_Controller_Response
+     * @todo Assign default value.
+     */
+    protected $_response;
+
+    /**
+     * The current action being performed
+     * @var string
+     * @todo Assign default value.
+     */
+    protected $_action;
+
+    /**
+     * Normal methods available as action requests.
+     * @var array
+     */
+    protected $_actionMethods = array();
+
+    /** @var string */
+    protected $_viewsDir = '';
+
+    /**
+     * New controller instance
+     */
+    public function __construct($options)
+    {
+        foreach ($options as $key => $val) {
+            $this->{'_' . $key} = $val;
+        }
+    }
+
+    /**
+     * Lazy loading of resources: view, ...
+     *
+     * @param string $name Property to load
+     */
+    public function __get($name)
+    {
+        switch ($name) {
+        case '_view':
+            $this->_view = new Horde_View;
+            return $this->_view;
+        }
+    }
+
+    /**
+     * Process the {@link Horde_Controller_Request_Base} and return
+     * the {@link Horde_Controller_Response}. This is the method that is called
+     * for every request to be processed. It then determines which action to call
+     * based on the parameters set within the {@link Horde_Controller_Request_Base}
+     * object.
+     *
+     * <code>
+     *  <?php
+     *  ...
+     *  $request  = new Horde_Controller_Request_Base();
+     *  $response = new Horde_Controller_Response_Base();
+     *
+     *  $response = $controller->process($request, $response);
+     *  ...
+     *  ?>
+     * </code>
+     *
+     * @param   Horde_Controller_Request_Base   $request
+     * @param   Horde_Controller_Response_Base  $response
+     * @return  Horde_Controller_Response_Base
+     */
+    public function process(Horde_Controller_Request_Base $request, Horde_Controller_Response_Base $response)
+    {
+        $this->_request = $request;
+        $this->_response = $response;
+
+        $this->_initParams();
+
+        $this->_shortName = str_replace('Controller', '', $this->params[':controller']);
+
+        try {
+            // templates
+            $this->_initActionMethods();
+            $this->_initViewPaths();
+            $this->_initViewHelpers();
+
+            // Initialize application logic used through all actions
+            $this->_initializeApplication();
+            if ($this->_performed()) {
+                return $this->_response;
+            }
+
+            // Initialize sub-controller logic used through all actions
+            if (is_callable(array($this, '_initialize'))) {
+                $this->_initialize();
+            }
+
+            // pre filters
+
+            // execute action & save any changes to sessionData
+            $this->{$this->_action}();
+
+            // post filters
+
+            // render default if we haven't performed an action yet
+            if (!$this->_performed()) {
+                $this->render();
+            }
+        } catch (Exception $e) {
+            // error handling
+        }
+
+        return $this->_response;
+    }
+
+    /**
+     * Render the response to the user. Actions are automatically rendered if no other
+     * action is specified.
+     *
+     * <code>
+     *  <?php
+     *  ...
+     *  $this->render(array('text'    => 'some text to render'));
+     *  $this->render(array('action'  => 'actionName'));
+     *  $this->render(array('nothing' => 1));
+     *  ...
+     *  ?>
+     * </code>
+     *
+     * @see renderText()
+     * @see renderAction()
+     * @see renderNothing()
+     *
+     * @param array $options
+     *
+     * @throws  Horde_Controller_Exception
+     */
+    protected function render($options = array())
+    {
+        // should not render/redirect more than once.
+        if ($this->_performed()) {
+            throw new Horde_Controller_Exception("Double render error: \"$this->_action\"");
+        }
+
+        // validate options
+
+        // set response status
+        if ($status = $options['status']) {
+            $header = $this->interpretStatus($status);
+            $this->_response->setStatus($header);
+        }
+
+        // set response location
+        if ($location = $options['location']) {
+            $url = $this->urlFor($location);
+            $this->_response->setHeader("Location: $url", $replace=true);
+        }
+
+        // render text
+        if ($text = $options['text']) {
+            $this->renderText($text);
+
+        // render xml
+        } elseif ($xml = $options['xml']) {
+            $this->_response->setContentType('application/xml');
+
+            if (is_object($xml) && method_exists($xml, 'toXml')) {
+                $xml = $xml->toXml();
+            }
+
+            $this->renderText($xml);
+
+        // render template file
+        } elseif (!empty($options['action'])) {
+            $this->renderAction($options['action']);
+
+        // render empty body
+        } elseif (!empty($options['nothing'])) {
+            $this->renderText('');
+
+        // render default template
+        } else {
+            $this->renderAction($this->_action);
+        }
+    }
+
+    /**
+     * Render text directly to the screen without using a template
+     *
+     * <code>
+     *  <?php
+     *  ...
+     *  $this->renderText('some text to render to the screen');
+     *  ...
+     *  ?>
+     * </code>
+     *
+     * @param   string  $text
+     */
+    protected function renderText($text)
+    {
+        $this->_response->setBody($text);
+        $this->_performedRender = true;
+    }
+
+    /**
+     * The name of the action method will render by default.
+     *
+     * render 'listDocuments' template file
+     * <code>
+     *  <?php
+     *  ...
+     *  $this->renderAction('listDocuments');
+     *  ...
+     *  ?>
+     * </code>
+     *
+     * @param   string  $name
+     */
+    protected function renderAction($name)
+    {
+        // current url
+        $this->_view->currentUrl = $this->_request->getUri();
+
+        // copy instance variables
+        foreach (get_object_vars($this) as $key => $value) {
+            $this->_view->$key = $value;
+        }
+
+        // add suffix
+        if ($this->_actionConflict) {
+            $name = str_replace('Action', '', $name);
+        }
+        if (strpos($name, '.') === false) {
+            $name .= '.html.php';
+        }
+
+        // prepend this controller's "short name" only if the action was
+        // specified without a controller "short name".
+        // e.g. index           -> Shortname/index
+        //      Shortname/index -> Shortname/index
+        if (strpos($name, '/') === false) {
+            // $name = $this->_shortName . '/' . $name;
+        }
+
+        if ($this->_useLayout) {
+            $this->_view->contentForLayout = $this->_view->render($name);
+            $text = $this->_view->render($this->_layoutName);
+        } else {
+            $text = $this->_view->render($name);
+        }
+        $this->renderText($text);
+    }
+
+    /**
+     * Render blank content. This can be used anytime you want to send a 200 OK
+     * response back to the user, but don't need to actually render any content.
+     * This is mostly useful for ajax requests.
+     *
+     * <code>
+     *  <?php
+     *  ...
+     *  $this->renderNothing();
+     *  ...
+     *  ?>
+     * </code>
+     */
+    protected function renderNothing()
+    {
+        $this->renderText('');
+    }
+
+    /**
+     * Check if a render or redirect has been performed
+     * @return  boolean
+     */
+    protected function _performed()
+    {
+        return $this->_performedRender || $this->_performedRedirect;
+    }
+
+    /**
+     * Each variable set through routing {@link Horde_Routes_Mapper} is
+     * availabie in controllers using the $params array.
+     *
+     * The controller also has access to GET/POST arrays using $params
+     *
+     * The action method to be performed is stored in $this->params[':action'] key
+     */
+    protected function _initParams()
+    {
+        $this->params = new Horde_Support_Array($this->_request->getParameters());
+        $this->_action = $this->params->get(':action');
+    }
+
+    /**
+     * Set the list of public actions that are available for this Controller.
+     * Subclasses can remove methods from being publicly called by calling
+     * {@link hideAction()}.
+     *
+     * @throws  Horde_Controller_Exception
+     */
+    protected function _initActionMethods()
+    {
+        // Perform reflection to get the list of public methods
+        $reflect = new ReflectionClass($this);
+        $methods = $reflect->getMethods();
+        foreach ($methods as $m) {
+            if ($m->isPublic() && !$m->isConstructor() && !$m->isDestructor()  &&
+                $m->getName() != 'process' && substr($m->getName(), 0, 1) != '_') {
+                $this->_actionMethods[$m->getName()] = 1;
+            }
+        }
+
+        // try action suffix.
+        if (!isset($this->_actionMethods[$this->_action]) &&
+            isset($this->_actionMethods[$this->_action.'Action'])) {
+            $this->_actionConflict = true;
+            $this->_action = $this->_action.'Action';
+        }
+        // action isn't set, but there is a methodMissing() catchall method
+        if (!isset($this->_actionMethods[$this->_action]) &&
+            isset($this->_actionMethods['methodMissing'])) {
+            $this->_action = 'methodMissing';
+
+        // make sure we have an action set, and that there is no methodMissing() method
+        } elseif (!isset($this->_actionMethods[$this->_action]) &&
+                  !isset($this->_actionMethods['methodMissing'])) {
+            $msg = 'Missing action: '.get_class($this)."::".$this->_action;
+            throw new Horde_Controller_Exception($msg);
+        }
+    }
+
+    /**
+     * Initialize the view paths where the templates reside for this controller.
+     * These are added in FIFO order, so if we do $this->renderAction('foo'),
+     * in the BarController, the order it will search these directories will be:
+     *  1. /views/Bar/foo.html
+     *  2. /views/shared/foo.html
+     *  3. /views/layouts/foo.html
+     *  4. /views/foo.html (the default)
+     *
+     * We can specify a directory to look in instead of relying on the default order
+     * by doing $this->renderAction('shared/foo').
+     */
+    protected function _initViewPaths()
+    {
+        $this->_view->addTemplatePath($this->_viewsDir . 'layouts');
+        $this->_view->addTemplatePath($this->_viewsDir . 'shared');
+        $this->_view->addTemplatePath($this->_viewsDir . $this->_shortName);
+    }
+
+    /**
+     * Initialize the default helpers for use in the views
+     */
+    protected function _initViewHelpers()
+    {
+        $controllerHelper = $this->_shortName . 'Helper';
+        if (class_exists($controllerHelper)) {
+            new $controllerHelper($this->_view);
+        }
+    }
+
+    /**
+     * This gets called before action is performed in a controller.
+     * Override method in subclass to setup filters/helpers
+     */
+    protected function _initializeApplication(){
+    }
+
+}
diff --git a/framework/Controller/lib/Horde/Controller/Dispatcher.php b/framework/Controller/lib/Horde/Controller/Dispatcher.php
new file mode 100644 (file)
index 0000000..2091140
--- /dev/null
@@ -0,0 +1,233 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ */
+
+/**
+ * Dispatch a request to the appropriate controller and execute the response.
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ */
+class Horde_Controller_Dispatcher
+{
+    /** @var Horde_Routes_Mapper */
+    protected $_mapper;
+
+    /** @var Horde_Log_Logger */
+    protected $_logger;
+
+    /** @var Horde_Support_Inflector */
+    protected $_inflector;
+
+    /** @var string */
+    protected $_controllerDir = '';
+
+    /** @var string */
+    protected $_viewsDir = '';
+
+    /**
+     * Class constructor.
+     */
+    public function __construct($context)
+    {
+        if (!isset($context['mapper']) || ! $context['mapper'] instanceof Horde_Routes_Mapper) {
+            throw new Horde_Controller_Exception('Mapper object missing from Dispatcher constructor');
+        }
+
+        foreach ($context as $key => $val) {
+            $this->{'_' . $key} = $val;
+        }
+
+        // Make sure controller directory, if set, ends in a /.
+        if ($this->_controllerDir && substr($this->_controllerDir, -1) != '/') {
+            $this->_controllerDir .= '/';
+        }
+
+        // Make sure views directory, if set, ends in a /.
+        if ($this->_viewsDir && substr($this->_viewsDir, -1) != '/') {
+            $this->_viewsDir .= '/';
+        }
+
+        // Make sure we have an inflector
+        if (!$this->_inflector) {
+            $this->_inflector = new Horde_Support_Inflector;
+        }
+    }
+
+    /**
+     * Get the route utilities for this dispatcher and its mapper.
+     *
+     * @return  Horde_Routes_Utils
+     */
+    public function getRouteUtils()
+    {
+        return $this->_mapper->utils;
+    }
+
+    /**
+     * Dispatch the request to the correct controller.
+     *
+     * @param Horde_Controller_Request_Base $request
+     */
+    public function dispatch(Horde_Controller_Request_Base $request, $response = null)
+    {
+        $t = new Horde_Support_Timer;
+        $t->push();
+
+        if (! $response instanceof Horde_Controller_Response_Base) {
+            // $response = new Horde_Controller_Response_Http;
+            $response = new Horde_Controller_Response_Base;
+        }
+
+        // Recognize routes and process request
+        $controller = $this->recognize($request);
+        $response = $controller->process($request, $response);
+
+        // Send response and log request
+        $time = $t->pop();
+        $this->_logRequest($request, $time);
+        $response->send();
+    }
+
+    /**
+     * Check if request path matches any Routes to get the controller
+     *
+     * @return  Horde_Controller_Base
+     * @throws  Horde_Controller_Exception
+     */
+    public function recognize($request)
+    {
+        $path = $request->getPath();
+        if (substr($path, 0, 1) != '/') {
+            $path = '/' . $path;
+        }
+
+        $matchdata = $this->_mapper->match($path);
+        if ($matchdata) {
+            $hash = $this->formatMatchdata($matchdata);
+        }
+
+        if (empty($hash) || !isset($hash[':controller'])) {
+            $msg = 'No routes match the path: "' . $request->getPath() . '"';
+            throw new Horde_Controller_Exception($msg);
+        }
+
+        $request->setPathParams($hash);
+
+        // try to load the class
+        $controllerName = $hash[':controller'];
+        if (!class_exists($controllerName, false)) {
+            $path = $this->_controllerDir . $controllerName . '.php';
+            if (file_exists($path)) {
+                require $path;
+            } else {
+                $msg = "The Controller \"$controllerName\" does not exist at " . $path;
+                throw new Horde_Controller_Exception($msg);
+            }
+        }
+
+        $options = array(
+            'viewsDir' => $this->_viewsDir,
+        );
+        return new $controllerName($options);
+    }
+
+    /**
+     * Take the $matchdata returned by a Horde_Routes_Mapper match and add
+     * in :controller and :action that are used by the rest of the framework.
+     *
+     * Format controller names: my_stuff => MyStuffController
+     * Format action names:     action_name => actionName
+     *
+     * @param   array   $matchdata
+     * @return  mixed   false | array
+     */
+    public function formatMatchdata($matchdata)
+    {
+        $ret = array();
+        foreach ($matchdata as $key => $val) {
+            if ($key == 'controller') {
+                $ret[':controller'] = $this->_inflector->camelize($val) . 'Controller';
+            } elseif ($key == 'action') {
+                $ret[':action'] = $this->_inflector->camelize($val, 'lower');
+            }
+
+            $ret[$key] = $val;
+        }
+        return !empty($ret) && isset($ret['controller']) ? $ret : false;
+    }
+
+    /**
+     * Log the http request
+     *
+     * @todo - get total query times
+     *
+     * @param   Horde_Controller_Request_Base $request
+     * @param   int $totalTime
+     */
+    protected function _logRequest(Horde_Controller_Request_Base $request, $totalTime)
+    {
+        if (!is_callable(array($this->_logger, 'info'))) {
+            return;
+        }
+
+        $queryTime  = 0; // total time to execute queries
+        $queryCount = 0; // total queries performed
+        $phpTime = $totalTime - $queryTime;
+
+        // embed user info in log
+        $uri    = $request->getUri();
+        $method = $request->getMethod();
+
+        $paramStr = 'PARAMS=' . $this->_formatLogParams($request->getAllParams());
+
+        $msg = "$method $uri $totalTime ms (DB=$queryTime [$queryCount] PHP=$phpTime) $paramStr";
+        $msg = wordwrap($msg, 80, "\n\t  ", 1);
+
+        $this->_logger->info($msg);
+    }
+
+    /**
+     * Formats the request parameters as a "key => value, key => value, ..." string
+     * for the log file.
+     *
+     * @param array $params
+     * @return string
+     */
+    protected function _formatLogParams($params)
+    {
+        $paramStr = '{';
+        $count = 0;
+        foreach ($params as $key => $value) {
+            if ($key != 'controller'  && $key != 'action' &&
+                $key != ':controller' && $key != ':action') {
+                if ($count++ > 0) { $paramStr .= ', '; }
+
+                $paramStr .= $key.' => ';
+
+                if (is_array($value)) {
+                    $paramStr .= $this->_formatLogParams($value);
+                } elseif (is_object($value)) {
+                    $paramStr .= get_class($value);
+                } else {
+                    $paramStr .= $value;
+                }
+            }
+        }
+        return $paramStr . '}';
+    }
+
+}
diff --git a/framework/Controller/lib/Horde/Controller/Exception.php b/framework/Controller/lib/Horde/Controller/Exception.php
new file mode 100644 (file)
index 0000000..e62cf5e
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ */
+
+/**
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ */
+class Horde_Controller_Exception extends Exception
+{
+}
diff --git a/framework/Controller/lib/Horde/Controller/Request/Base.php b/framework/Controller/lib/Horde/Controller/Request/Base.php
new file mode 100644 (file)
index 0000000..1dd34e9
--- /dev/null
@@ -0,0 +1,378 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage Request
+ *
+ * http://pythonpaste.org/webob/
+ * http://usrportage.de/archives/875-Dojo-and-the-Zend-Framework.html
+ * http://framework.zend.com/manual/en/zend.filter.input.html
+ */
+
+/**
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage Request
+ */
+class Horde_Controller_Request_Base
+{
+    /**
+     * Request timestamp
+     *
+     * @var integer
+     */
+    protected $_timestamp;
+
+    /**
+     * The SAPI
+     *
+     * @var string
+     */
+    protected $_sapi;
+
+    /**
+     * Unique id per request.
+     * @var string
+     */
+    protected $_requestId;
+
+    protected $_server;
+    protected $_env;
+
+    protected $_body;
+
+    /**
+     */
+    public function __construct($options = array())
+    {
+        $this->_initRequestId();
+
+        $this->_server  = isset($options['server'])  ? $options['server']  : $_SERVER;
+        $this->_env     = isset($options['env'])     ? $options['env']     : $_ENV;
+
+        if (isset($_SERVER['REQUEST_TIME'])) {
+            $this->_timestamp = $_SERVER['REQUEST_TIME'];
+        } else {
+            $this->_timestamp = time();
+        }
+
+        $this->_sapi = php_sapi_name();
+    }
+
+    /**
+     * Get server variable with the specified $name
+     *
+     * @param   string  $name
+     * @return  string
+     */
+    public function getServer($name)
+    {
+        if ($name == 'SCRIPT_NAME' && strncmp($this->_sapi, 'cgi', 3) === 0) {
+            $name = 'SCRIPT_URL';
+        }
+        return isset($this->_server[$name]) ? $this->_server[$name] : null;
+    }
+
+    /**
+     * Get environment variable with the specified $name
+     *
+     * @param   string  $name
+     * @return  string
+     */
+    public function getEnv($name)
+    {
+        return isset($this->_env[$name]) ? $this->_env[$name] : null;
+    }
+
+    /**
+     * Get a combination of all parameters. We have to do
+     * some wacky loops to make sure that nested values in one
+     * param list don't overwrite other nested values
+     *
+     * @return  array
+     */
+    public function getParameters()
+    {
+        $allParams = array();
+        $paramArrays = array($this->_pathParams, $this->_formattedRequestParams);
+
+        foreach ($paramArrays as $params) {
+            foreach ((array)$params as $key => $value) {
+                if (!is_array($value) || !isset($allParams[$key])) {
+                    $allParams[$key] = $value;
+                } else {
+                    $allParams[$key] = array_merge($allParams[$key], $value);
+                }
+            }
+        }
+        return $allParams;
+    }
+
+    /**
+     * Get entire list of parameters set by {@link Horde_Controller_Route_Path} for
+     * the current request
+     *
+     * @return  array
+     */
+    public function getPathParams()
+    {
+        return $this->_pathParams;
+    }
+
+    /**
+     * When the {@link Horde_Controller_Dispatcher} determines the
+     * correct {@link Horde_Controller_Route_Path} to match the url, it uses the
+     * Routing object data to set appropriate variables so that they can be passed
+     * to the Controller object.
+     *
+     * @param   array   $params
+     */
+    public function setPathParams($params)
+    {
+        $this->_pathParams = !empty($params) ? $params : array();
+    }
+
+    /**
+     * Get the unique ID generated for this request
+     * @see     _initRequestId()
+     * @return  string
+     */
+    public function getRequestId()
+    {
+        return $this->_requestId;
+    }
+
+    /**
+     * The request body
+     *
+     * @return  string
+     */
+    public function getBody()
+    {
+        if (!isset($this->_body)) {
+            $this->_body = file_get_contents("php://input");
+        }
+        return $this->_body;
+    }
+
+    /**
+     * Return the request content length
+     *
+     * @return  int
+     */
+    public function getContentLength()
+    {
+        return strlen($this->getBody());
+    }
+
+    /**
+     * Get rid of register_globals variables.
+     *
+     * @author Richard Heyes
+     * @author Stefan Esser
+     * @url http://www.phpguru.org/article.php?ne_id=60
+     */
+    public function reverseRegisterGlobals()
+    {
+        if (ini_get('register_globals')) {
+            // Variables that shouldn't be unset
+            $noUnset = array(
+                'GLOBALS',
+                '_GET',
+                '_POST',
+                '_COOKIE',
+                '_REQUEST',
+                '_SERVER',
+                '_ENV',
+                '_FILES',
+            );
+
+            $input = array_merge(
+                $_GET,
+                $_POST,
+                $_COOKIE,
+                $_SERVER,
+                $_ENV,
+                $_FILES,
+                isset($_SESSION) ? $_SESSION : array()
+            );
+
+            foreach ($input as $k => $v) {
+                if (!in_array($k, $noUnset) && isset($GLOBALS[$k])) {
+                    unset($GLOBALS[$k]);
+                }
+            }
+        }
+    }
+
+    /**
+     * @author Ilia Alshanetsky <ilia@php.net>
+     */
+    public function reverseMagicQuotes()
+    {
+        set_magic_quotes_runtime(0);
+        if (get_magic_quotes_gpc()) {
+            $input = array(&$_GET, &$_POST, &$_REQUEST, &$_COOKIE, &$_ENV, &$_SERVER);
+
+            while (list($k, $v) = each($input)) {
+                foreach ($v as $key => $val) {
+                    if (!is_array($val)) {
+                        $key = stripslashes($key);
+                        $input[$k][$key] = stripslashes($val);
+                        continue;
+                    }
+                    $input[] =& $input[$k][$key];
+                }
+            }
+
+            unset($input);
+        }
+    }
+
+    /**
+     * Turn this request into a URL-encoded query string.
+     */
+    public function __toString()
+    {
+        return http_build_query($this);
+    }
+
+    public function getPath()
+    {
+    }
+
+    /**
+     * Uniquely identify each request from others. This aids in threading
+     *  related log requests during troubleshooting on a busy server
+     */
+    private function _initRequestId()
+    {
+        $this->_requestId = (string)new Horde_Support_Uuid;
+    }
+
+    /**
+     * The default locale (eg. en-us) the application uses.
+     *
+     * @var      string
+     * @access   private
+     */
+    var $_defaultLocale = 'en-us';
+
+    /**
+     * The locales (eg. en-us, fi_fi, se_se etc) the application
+     * supports.
+     *
+     * @var      array
+     * @access   private
+     */
+    var $_supportedLocales = NULL;
+
+    /**
+     * Gets the used character encoding.
+     *
+     * Returns the name of the character encoding used in the body of
+     * this request.
+     *
+     * @todo     implement this method
+     * @return   string  the used character encoding
+     * @access   public
+     */
+    function getCharacterEncoding()
+    {
+        // XXX: what to do with this?
+    }
+
+    /**
+     * Gets the default locale for the application.
+     *
+     * @return   string  the default locale
+     * @access   public
+     */
+    function getDefaultLocale()
+    {
+        return $this->_defaultLocale;
+    }
+
+    /**
+     * Gets the supported locales for the application.
+     *
+     * @return   array      the supported locales
+     * @access   public
+     */
+    function getSupportedLocales()
+    {
+        return $this->_supportedLocales;
+    }
+
+    /**
+     * Deduces the clients preferred locale.
+     *
+     * You might want to override this method if you want to do more
+     * sophisticated decisions. It gets the supported locales and the
+     * default locale from the class attributes file and tries to find a
+     * match. If no match is found it uses the default locale. The
+     * locale is always changed into lowercase.
+     *
+     * @return   string  the locale
+     * @access   public
+     */
+    function getLocale()
+    {
+        require_once('HTTP.php');
+
+        if ($this->_supportedLocales == NULL) {
+            return $this->_defaultLocale;
+        } else {
+            return strtolower(HTTP::negotiateLanguage(  $this->_supportedLocales,
+                                                        $this->_defaultLocale   ));
+        }
+    }
+
+    /**
+     * Sets the default locale for the application.
+     *
+     * Create an instance of <code>Ismo_Core_Request</code> manually and
+     * set the default locale with this method. Then add it as the
+     * application's request class with
+     * <code>Ismo_Core_Application::setRequest()</code>.
+     *
+     * @param   string  $locale     the default locale
+     * @access  public
+     */
+    function setDefaultLocale($locale)
+    {
+        $this->_defaultLocale = str_replace('_', '-', $locale);
+    }
+
+    /**
+     * Sets the locales supported by the application.
+     *
+     * Create an instance of <code>Ismo_Core_Request</code> manually and
+     * set the supported locales with this method. Then add it as the
+     * application's request class with
+     * <code>Ismo_Core_Application::setRequest()</code>.
+     *
+     * @param   array   $locales    the locales
+     * @access  public
+     */
+    function setSupportedLocales($locales)
+    {
+        if (is_array($locales)) {
+            foreach ($locales as $n => $locale) {
+                 $this->_supportedLocales[ str_replace('_', '-', $locale) ] = true;
+            }
+        }
+    }
+
+}
diff --git a/framework/Controller/lib/Horde/Controller/Request/Http.php b/framework/Controller/lib/Horde/Controller/Request/Http.php
new file mode 100644 (file)
index 0000000..8ac0264
--- /dev/null
@@ -0,0 +1,594 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage Request
+ */
+
+/**
+ * Represents an HTTP request to the server. This class handles all
+ * headers/cookies/session data so that it all has one point of entry for being
+ * written/retrieved.
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage Request
+ */
+class Horde_Controller_Request_Http extends Horde_Controller_Request_Base
+{
+    /**
+     * All the headers.
+     * @var array
+     */
+    protected $_headers = null;
+
+    /**
+     * PHPSESSID
+     * @var string
+     */
+    protected $_sessionId;
+
+    // superglobal arrays
+    protected $_get;
+    protected $_post;
+    protected $_files;
+    protected $_request;
+
+    // cookie/session info
+    protected $_cookie;
+    protected $_session;
+
+    protected $_contentType;
+    protected $_accepts;
+    protected $_format;
+    protected $_method;
+    protected $_remoteIp;
+    protected $_port;
+    protected $_https;
+    protected $_isAjax;
+
+    protected $_domain;
+    protected $_uri;
+    protected $_pathParams;
+
+    /*##########################################################################
+    # Construct/Destruct
+    ##########################################################################*/
+
+    /**
+     * Request is populated with all the superglobals from page request if
+     * data is not passed in.
+     *
+     * @param   array   $options  Associative array with all superglobals
+     */
+    public function __construct($options = array())
+    {
+        $this->_initSessionData();
+
+        // superglobal data if not passed in thru constructor
+        $this->_get     = isset($options['get'])     ? $options['get']     : $_GET;
+        $this->_post    = isset($options['post'])    ? $options['post']    : $_POST;
+        $this->_cookie  = isset($options['cookie'])  ? $options['cookie']  : $_COOKIE;
+        $this->_request = isset($options['request']) ? $options['request'] : $_REQUEST;
+
+        parent::__construct($options);
+
+        $this->_pathParams = array();
+        // $this->_formattedRequestParams = $this->_parseFormattedRequestParameters();
+
+        // use FileUpload object to store files
+        $this->_setFilesSuperglobals();
+
+        // disable all superglobal data to force us to use correct way
+        //@TODO
+        //$_GET = $_POST = $_FILES = $_COOKIE = $_REQUEST = $_SERVER = array();
+
+        $this->_domain   = $this->getServer('SERVER_NAME');
+        $this->_uri      = trim($this->getServer('REQUEST_URI'), '/');
+        $this->_method   = $this->getServer('REQUEST_METHOD');
+        // @TODO look at HTTP_X_FORWARDED_FOR, handling multiple addresses: http://weblogs.asp.net/james_crowley/archive/2007/06/19/gotcha-http-x-forwarded-for-returns-multiple-ip-addresses.aspx
+        $this->_remoteIp = $this->getServer('REMOTE_ADDR');
+        $this->_port     = $this->getServer('SERVER_PORT');
+        $this->_https    = $this->getServer('HTTPS');
+        $this->_isAjax   = $this->getServer('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest';
+    }
+
+
+    /*##########################################################################
+    # Public Methods
+    ##########################################################################*/
+
+    /**
+     * Get the http request method:
+     *  eg. GET, POST, PUT, DELETE
+     *
+     * @return  string
+     */
+    public function getMethod()
+    {
+        $methods = array('GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'OPTIONS');
+
+        if ($this->_method == 'POST') {
+            $params = $this->getParameters();
+            if (isset($params['_method'])) {
+                $faked = strtoupper($params['_method']);
+                if (in_array($faked, $methods)) return $faked;
+            }
+        }
+
+        return $this->_method;
+    }
+
+    /**
+     * Get list of all superglobals to pass into a different request
+     *
+     * @return  array
+     */
+    public function getGlobals()
+    {
+        return array('get'     => $this->_get,
+                     'post'    => $this->_post,
+                     'cookie'  => $this->_cookie,
+                     'session' => $this->_session,
+                     'files'   => $this->_files,
+                     'request' => $this->_request,
+                     'server'  => $this->_server,
+                     'env'     => $this->_env);
+    }
+
+    /**
+     * Get the domain for the current request
+     * eg. https://www.maintainable.com/articles/show/123
+     *     $domain is -> www.maintainable.com
+     *
+     * @return  string
+     */
+    public function getDomain()
+    {
+        return $this->_domain;
+    }
+
+    /**
+     * Get the host for the current request
+     * eg. http://www.maintainable.com:3000/articles/show/123
+     *     $host is -> http://www.maintainablesoftware.com:3000
+     *
+     * @param   boolean $usePort
+     * @return  string
+     */
+    public function getHost($usePort = false)
+    {
+        $scheme = 'http' . ($this->_https == 'on' ? 's' : null);
+        $port   = $usePort && !empty($this->_port) && $this->_port != '80' ? ':' . $this->_port : null;
+        return "{$scheme}://{$this->_domain}$port";
+    }
+
+    /**
+     * @todo    add ssl support
+     * @return  string
+     */
+    public function getProtocol()
+    {
+        // return $this->getServer('SERVER_PROTOCOL');
+        return 'http://';
+    }
+
+    /**
+     * Get the uri for the current request
+     * eg. https://www.maintainable.com/articles/show/123?page=1
+     *     $uri is -> articles/show/123?page=1
+     *
+     * @return  string
+     */
+    public function getUri()
+    {
+        return $this->_uri;
+    }
+
+    /**
+     * Get the path from the URI. (strip get params)
+     * eg. https://www.maintainable.com/articles/show/123?page=1
+     *     $path is -> articles/show/123
+     *
+     * @return  string
+     */
+    public function getPath()
+    {
+        $path = $this->_uri;
+        if (strstr($path, '?')) {
+            $path = trim(substr($path, 0, strpos($path, '?')), '/');
+        }
+        return $path;
+    }
+
+    public function getContentType()
+    {
+        if (!isset($this->_contentType)) {
+            $type = $this->getServer('CONTENT_TYPE');
+            // strip parameters from content-type like "; charset=UTF-8"
+            if (is_string($type)) {
+                if (preg_match('/^([^,\;]*)/', $type, $matches)) {
+                    $type = $matches[1];
+                }
+            }
+
+            // $this->_contentType = Horde_Controller_Mime_Type::lookup($type);
+        }
+        return $this->_contentType;
+    }
+
+    /**
+     * @return  array
+     */
+    public function getAccepts()
+    {
+        if (!isset($this->_accepts)) {
+            $accept = $this->getServer('HTTP_ACCEPT');
+            if (empty($accept)) {
+                $types = array();
+                $contentType = $this->getContentType();
+                if ($contentType) { $types[] = $contentType; }
+                $types[] = Horde_Controller_Mime_Type::lookupByExtension('all');
+                $accepts = $types;
+            } else {
+                $accepts = Horde_Controller_Mime_Type::parse($accept);
+            }
+            $this->_accepts = $accepts;
+        }
+        return $this->_accepts;
+    }
+
+
+    /**
+     * Returns the Mime type for the format used in the request. If there is no
+     * format available, the first of the
+     *
+     * @return  string
+     */
+    public function getFormat()
+    {
+        if (!isset($this->_format)) {
+            $params = $this->getParameters();
+            if (isset($params['format'])) {
+                $this->_format = Horde_Controller_Mime_Type::lookupByExtension($params['format']);
+            } else {
+                $this->_format = current($this->getAccepts());
+            }
+        }
+        return $this->_format;
+    }
+
+    /**
+     * Get the remote Ip address as a dotted decimal string.
+     *
+     * @return  string
+     */
+    public function getRemoteIp()
+    {
+        return $this->_remoteIp;
+    }
+
+    /**
+     * Get cookie value from specified $name OR get All when $name isn't passed in
+     *
+     * @param   string  $name
+     * @param   string  $default
+     * @return  string
+     */
+    public function getCookie($name=null, $default=null)
+    {
+        if (isset($name)) {
+            return isset($this->_cookie[$name]) ? $this->_cookie[$name] : $default;
+        } else {
+            return $this->_cookie;
+        }
+    }
+
+    /**
+     * Get session value from session data by $name or ALL when $name isn't passed in
+     *
+     * @param   string  $name
+     * @param   string  $default
+     * @return  mixed
+     */
+    public function getSession($name=null, $default=null)
+    {
+        if (isset($name)) {
+            return isset($this->_session[$name]) ? $this->_session[$name] : $default;
+        } else {
+            return $this->_session;
+        }
+    }
+
+    /**
+     * Get a combination of all parameters. We have to do
+     * some wacky loops to make sure that nested values in one
+     * param list don't overwrite other nested values
+     *
+     * @return  array
+     */
+    public function getParameters()
+    {
+        $allParams = array();
+        $paramArrays = array($this->_pathParams, /*$this->_formattedRequestParams, */
+                             $this->_get, $this->_post, $this->_files);
+
+        foreach ($paramArrays as $params) {
+            foreach ((array)$params as $key => $value) {
+                if (!is_array($value) || !isset($allParams[$key])) {
+                    $allParams[$key] = $value;
+                } else {
+                    $allParams[$key] = array_merge($allParams[$key], $value);
+                }
+            }
+        }
+        return $allParams;
+    }
+
+    /**
+     * Get entire list of $_GET parameters
+     * @return  array
+     */
+    public function getGetParams()
+    {
+        return $this->_get;
+    }
+
+    /**
+     * Get entire list of $_POST parameters
+     *
+     * @return  array
+     */
+    public function getPostParams()
+    {
+        return $this->_post;
+    }
+
+    /**
+     * Get entire list of $_FILES parameters
+     *
+     * @return  array
+     */
+    public function getFilesParams()
+    {
+        return $this->_files;
+    }
+
+    /**
+     * Get the session ID of this request (PHPSESSID)
+     * @see    _initSession()
+     * @return string
+     */
+    public function getSessionId()
+    {
+        return $this->_sessionId;
+    }
+
+    /*##########################################################################
+    # Modifiers
+    ##########################################################################*/
+
+    /**
+     * Set the uri and parse it for useful info
+     *
+     * @param   string  $uri
+     */
+    public function setUri($uri)
+    {
+        $this->_uri = trim($uri, '/');
+    }
+
+    /**
+     * Set the session array.
+     *
+     * @param   string  $name
+     * @param   mixed   $value
+     */
+    public function setSession($name, $value=null)
+    {
+        if (is_array($name)) {
+            $this->_session = $name;
+        } else {
+            $this->_session[$name] = $value;
+        }
+    }
+
+
+    /*##########################################################################
+    # Private Methods
+    ##########################################################################*/
+
+    /**
+     * Start up default session storage, and get stored data.
+     *
+     * @todo    further investigate session_cache_limiter() on ie6 (see below)
+     * @todo    implement active record session store
+     */
+    protected function _initSessionData()
+    {
+        $this->_sessionId = session_id();
+
+        if (! strlen($this->_sessionId)) {
+            // internet explorer 6 will ignore the filename/content-type during
+            // sendfile over ssl unless session_cache_limiter('public') is set
+            // http://joseph.randomnetworks.com/archives/2004/10/01/making-ie-accept-file-downloads/
+            $agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
+            if (strpos($agent, 'MSIE') !== false) {
+                session_cache_limiter("public");
+            }
+
+            session_start();
+            $this->_sessionId = session_id();
+        }
+
+        // Important: Setting "$this->_session = $_SESSION" does NOT work.
+        $this->_session = array();
+        if (is_array($_SESSION)) {
+            foreach ($_SESSION as $key => $value) {
+                $this->_session[$key] = $value;
+            }
+        }
+    }
+
+    /**
+     * Initialize the File upload information
+     */
+    protected function _setFilesSuperglobals()
+    {
+        if (empty($_FILES)) {
+            $this->_files = array();
+            return;
+        }
+        $_FILES = array_map(array($this, '_fixNestedFiles'), $_FILES);
+
+        // create FileUpload object of of the file options
+        foreach ((array)$_FILES as $name => $options) {
+            if (isset($options['tmp_name'])) {
+                $this->_files[$name] = new Horde_Controller_FileUpload($options);
+            } else {
+                foreach ($options as $attr => $data) {
+                    $this->_files[$name][$attr] = new Horde_Controller_FileUpload($data);
+                }
+            }
+        }
+    }
+
+    /**
+     * fix $_FILES superglobal array. (PHP mungles data when we use brackets)
+     *
+     * @link http://www.shauninman.com/archive/2006/11/30/fixing_the_files_superglobal
+     * @param   array   $group
+     */
+    protected function _fixNestedFiles($group)
+    {
+        // only rearrange nested files
+        if (!is_array($group['tmp_name'])) { return $group; }
+
+        foreach ($group as $property => $arr) {
+            foreach ($arr as $item => $value) {
+                $result[$item][$property] = $value;
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Gets the value of header.
+     *
+     * Returns the value of the specified request header.
+     *
+     * @param    string  $name   the name of the header
+     * @return   string          the value of the specified header
+     * @access   public
+     */
+    function getHeader($name)
+    {
+        if ($this->_headers == NULL) {
+            $this->_headers = $this->_getAllHeaders();
+        }
+
+        if (isset($this->_headers[$name])) {
+            return $this->_headers[$name];
+        }
+        return NULL;
+    }
+
+    /**
+     * Gets all the header names.
+     *
+     * Returns an array of all the header names this request
+     * contains.
+     *
+     * @return   array   all the available headers as strings
+     * @access   public
+     */
+    function getHeaderNames()
+    {
+        if ($this->_headers == NULL) {
+            $this->_headers = $this->_getAllHeaders();
+        }
+        return array_keys($this->_headers);
+    }
+
+    /**
+     * Gets all the headers.
+     *
+     * Returns an associative array of all the header names and values of this
+     * request.
+     *
+     * @return   array   containing all the headers
+     * @access   public
+     */
+    function getHeaders()
+    {
+        if ($this->_headers == NULL) {
+            $this->_headers = $this->_getAllHeaders();
+        }
+        return $this->_headers;
+    }
+
+    /**
+     * Returns all HTTP_* headers.
+     *
+     * Returns all the HTTP_* headers. Works both if PHP is an apache
+     * module and if it's running as a CGI.
+     *
+     * @return   array    the headers' names and values
+     * @access   private
+     */
+    function _getAllHeaders()
+    {
+        if (function_exists('getallheaders')) {
+            return getallheaders();
+        }
+
+        reset($_SERVER);
+        $result = array();
+        array_walk($_SERVER, array($this, '_getAllHeadersHelper'), $result);
+
+        // map so that the variables gotten from the environment when
+        // running as CGI have the same names as when PHP is an apache
+        // module
+        $map = array (
+            'HTTP_ACCEPT'           =>  'Accept',
+            'HTTP_ACCEPT_CHARSET'   =>  'Accept-Charset',
+            'HTTP_ACCEPT_ENCODING'  =>  'Accept-Encoding',
+            'HTTP_ACCEPT_LANGUAGE'  =>  'Accept-Language',
+            'HTTP_CONNECTION'       =>  'Connection',
+            'HTTP_HOST'             =>  'Host',
+            'HTTP_KEEP_ALIVE'       =>  'Keep-Alive',
+            'HTTP_USER_AGENT'       =>  'User-Agent' );
+
+        $mapped_result = array();
+        foreach ($result as $k => $v) {
+            $mapped_result[$map[$k]] = $v;
+        }
+
+        return $mapped_result;
+    }
+
+    /**
+     * Helper function for _getallheaders.
+     *
+     * For use with array_walk.
+     */
+    protected function _getAllHeadersHelper($value, $key, &$result)
+    {
+        $header_name = substr($key, 0, 5);
+        if ($header_name == 'HTTP_') {
+            $result[$key] = $value;
+        }
+    }
+
+}
diff --git a/framework/Controller/lib/Horde/Controller/Request/Mock.php b/framework/Controller/lib/Horde/Controller/Request/Mock.php
new file mode 100644 (file)
index 0000000..6d97f4b
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage Request
+ */
+
+/**
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage Request
+ */
+class Horde_Controller_Request_Mock extends Horde_Controller_Request_Base
+{
+}
diff --git a/framework/Controller/lib/Horde/Controller/Response/Base.php b/framework/Controller/lib/Horde/Controller/Response/Base.php
new file mode 100644 (file)
index 0000000..9f88dfb
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage Response
+ */
+
+/**
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage Response
+ */
+class Horde_Controller_Response_Base
+{
+    /**
+     */
+    public function send()
+    {
+        echo $this->_body;
+    }
+
+    public function setBody($body)
+    {
+        $this->_body = $body;
+    }
+
+}
diff --git a/framework/Controller/lib/Horde/Controller/Response/Http.php b/framework/Controller/lib/Horde/Controller/Response/Http.php
new file mode 100644 (file)
index 0000000..bea6bcd
--- /dev/null
@@ -0,0 +1,224 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage Response
+ */
+
+/**
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage Response
+ */
+class Horde_Controller_Response_Http extends Horde_Controller_Response_Base
+{
+    /**
+     * Adds the specified cookie to the response.
+     *
+     * This method can be called multiple times to set more than one cookie or
+     * to modify an already set one. Returns true if the adding was successful,
+     * false otherwise.
+     *
+     * @param    Ismo_Core_Cookie   $cookie   the cookie object to add
+     * @return   boolean                true if the adding was successful,
+     *                                  false otherwise
+     * @access   public
+     */
+    function addCookie($cookie)
+    {
+        if (get_class($cookie) == 'ismo_core_cookie' ||
+            get_parent_class($cookie) == 'ismo_core_cookie') {
+            $secure = 0;
+            if ($cookie->isSecure()) {
+                $secure = 1;
+            }
+
+            setcookie(  $cookie->getName(),
+                        $cookie->getValue(),
+                        $cookie->getExpire(),
+                        $cookie->getPath(),
+                        $cookie->getDomain(),
+                        $secure );
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Deletes the specified cookie from the response.
+     *
+     * @param    IsmoCookie   $cookie  the cookie object to delete
+     * @access   public
+     */
+    function deleteCookie($cookie)
+    {
+        if (get_class($cookie) == 'ismo_core_cookie' ||
+            get_parent_class($cookie) == 'ismo_core_cookie') {
+            $secure = 0;
+            if ($cookie->isSecure()) {
+                $secure = 1;
+            }
+
+            // set the expiration date to one hour ago
+            $cookie->setExpire(time() - 3600);
+
+            setcookie(  $cookie->getName(),
+                        $cookie->getValue(),
+                        $cookie->getExpire(),
+                        $cookie->getPath(),
+                        $cookie->getDomain(),
+                        $secure );
+        }
+    }
+
+    /**
+     * Adds a response header with the given name and value.
+     *
+     * This method allows response headers to have multiple values. Returns true
+     * if the header could be added, false otherwise. False will be returned
+     * f.g. when the headers have already been sent.  The replace parameter
+     * indicates if an already existing header with the same name should be
+     * replaced or not.
+     *
+     * @param    string  $name      the name of the header
+     * @param    string  $value     the value of the header
+     * @param    boolean $replace   should the header be replaced or not
+     * @return   boolean            true if the header could be set, false
+     *                              otherwise
+     * @access   public
+     */
+    function addHeader($name, $value, $replace)
+    {
+        if (headers_sent()) {
+            return false;
+        }
+
+        header("$name: $value", $replace);
+        return true;
+    }
+
+    /**
+     * Sends an error response to the client using the specified status code.
+     *
+     * Sends an error response to the client using the specified status code.
+     * This will create a page that looks like an HTML-formatted server error
+     * page containing the specifed message (if any), setting the content type
+     * to "text/html", leaving cookies and other headers unmodified.
+     *
+     * If the headers have already been sent this method returns <b>false</b>
+     * otherwise <b>true</b>. After this method the response should be
+     * considered commited, i.e.  both headers and data have been sent to the
+     * client.
+     *
+     * @todo                    decide what the error page should look like
+     * @param    string  $code  the status code to use
+     * @param    string  $msg   the message to show
+     * @return   boolean        <b>true</b> if the error response could be
+     *                          send, <b>false</b> otherwise (if the headers
+     *                          have already been sent)
+     * @access   public
+     */
+    function sendError($code, $msg = NULL)
+    {
+        if (headers_sent()) {
+            return false;
+        }
+
+        header('HTTP/1.0 '.$code);
+
+        // @todo what kind of HTML page should it be?
+        ?>
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
+<html><head>
+<title><?= $code ?></title>
+</head><body>
+<h1><?= $code ?></h1>
+<?php
+    if ($msg != NULL)
+    {
+        echo $msg;
+    }
+?>
+<hr>
+</body></html>
+<?php
+
+        return true;
+    }
+
+    /**
+     * Sends a temporary redirect respones to the client using the specifed
+     * redirect URL.
+     *
+     * If the headers have already been sent this method returns <b>false</b>
+     * otherwise <b>true</b>. After this method the response should be
+     * considered commited.
+     *
+     * Examples:
+     * <code>
+     *   $u = new Ismo_Core_Url("http://a.b.c");
+     *   $response->sendRedirect($u);
+     * </code>
+     * Redirects the browser to http://a.b.c using an Ismo_Core_Url instance.
+     *
+     * <code>
+     *   $response->sendRedirect("http://d.e.f");
+     * </code>
+     * Redirects the browser to http://d.e.f using a string.
+     *
+     * @param    mixed   $location  url to redirect to, this can either be an
+     *                              Ismo_Core_Url instance or a string
+     * @return   boolean            <b>false</b> if the headers have already
+     *                              been sent, <b>true</b> otherwise
+     * @access   public
+     */
+    function sendRedirect($location)
+    {
+        if (headers_sent()) {
+            return false;
+        }
+
+        if (get_class($location) == 'ismo_core_url') {
+            $location = $location->toString(false);
+        }
+
+        /* so that it works correctly for IE */
+        header('HTTP/1.1 301 Moved Permanently');
+        header('Location: ' . $location);
+        header('Connection: close');
+
+        return true;
+    }
+
+    /**
+     * Sets the status code for this request.
+     *
+     * Sets the status code for this response. This method is used to set the
+     * return status code when there is no error (for example, for the status
+     * codes SC_OK or SC_MOVED_TEMPORARILY). If there is an error, and the
+     * caller wishes to provide a message for the response, the sendError()
+     * method should be used instead.
+     *
+     * @param    string $code    the status code to set
+     * @access   public
+     * @see      sendError()
+     */
+    function setStatus($code)
+    {
+        header('HTTP/1.0 ' . $code);
+    }
+
+}
diff --git a/framework/Controller/lib/Horde/Controller/Response/Mock.php b/framework/Controller/lib/Horde/Controller/Response/Mock.php
new file mode 100644 (file)
index 0000000..7f57f79
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage Response
+ */
+
+/**
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage Response
+ */
+class Horde_Controller_Response_Mock extends Horde_Controller_Response_Base
+{
+}
diff --git a/framework/Controller/package.xml b/framework/Controller/package.xml
new file mode 100644 (file)
index 0000000..d76628a
--- /dev/null
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Controller</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Controller libraries</summary>
+ <description>This package provides the controller part of an MVC system for Horde.
+ </description>
+ <lead>
+  <name>Mike Naberezny</name>
+  <user>mnaberez</user>
+  <email>mike@naberezny.com</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2008-09-24</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://opensource.org/licenses/bsd-license.php">BSD</license>
+ <notes>* Initial release.</notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Controller">
+      <dir name="Request">
+       <file name="Base.php" role="php" />
+       <file name="Http.php" role="php" />
+       <file name="Mock.php" role="php" />
+      </dir> <!-- /lib/Horde/Controller/Requeset -->
+      <dir name="Response">
+       <file name="Base.php" role="php" />
+       <file name="Http.php" role="php" />
+       <file name="Mock.php" role="php" />
+      </dir> <!-- /lib/Horde/Controller/Response -->
+      <file name="Base.php" role="php" />
+      <file name="Dispatcher.php" role="php" />
+      <file name="Exception.php" role="php" />
+     </dir> <!-- /lib/Horde/Controller -->
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.5.0</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Controller/Base.php" as="Horde/Controller/Base.php" />
+   <install name="lib/Horde/Controller/Dispatcher.php" as="Horde/Controller/Dispatcher.php" />
+   <install name="lib/Horde/Controller/Exception.php" as="Horde/Controller/Exception.php" />
+   <install name="lib/Horde/Controller/Request/Base.php" as="Horde/Controller/Request/Base.php" />
+   <install name="lib/Horde/Controller/Request/Http.php" as="Horde/Controller/Request/Http.php" />
+   <install name="lib/Horde/Controller/Request/Mock.php" as="Horde/Controller/Request/Mock.php" />
+   <install name="lib/Horde/Controller/Response/Base.php" as="Horde/Controller/Response/Base.php" />
+   <install name="lib/Horde/Controller/Response/Http.php" as="Horde/Controller/Response/Http.php" />
+   <install name="lib/Horde/Controller/Response/Mock.php" as="Horde/Controller/Response/Mock.php" />
+  </filelist>
+ </phprelease>
+</package>
diff --git a/framework/Controller/test/Horde/Controller/AllTests.php b/framework/Controller/test/Horde/Controller/AllTests.php
new file mode 100644 (file)
index 0000000..8d442ce
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Copyright 2007 Maintainable Software, LLC
+ * Copyright 2008 The Horde Project (http://www.horde.org/)
+ *
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage UnitTests
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Horde_Controller_AllTests::main');
+}
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+/**
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @author     Derek DeVries <derek@maintainable.com>
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @license    http://opensource.org/licenses/bsd-license.php
+ * @category   Horde
+ * @package    Horde_Controller
+ * @subpackage UnitTests
+ */
+class Horde_Controller_AllTests {
+
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        set_include_path(dirname(__FILE__) . '/../../../lib' . PATH_SEPARATOR . get_include_path());
+        if (!spl_autoload_functions()) {
+            spl_autoload_register(create_function('$class', '$filename = str_replace(array(\'::\', \'_\'), \'/\', $class); include "$filename.php";'));
+        }
+
+        $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Horde_Controller');
+
+        $basedir = dirname(__FILE__);
+        $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) {
+            if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+                $pathname = $file->getPathname();
+                require $pathname;
+
+                $class = str_replace(DIRECTORY_SEPARATOR, '_',
+                                     preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname));
+                $suite->addTestSuite('Horde_Controller_' . $class);
+            }
+        }
+
+        return $suite;
+    }
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Controller_AllTests::main') {
+    Horde_Controller_AllTests::main();
+}