From: Chuck Hagenbuch Date: Sun, 9 Nov 2008 20:36:46 +0000 (-0500) Subject: controller should go into the main repo, it's already horde 4 ready X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=dbc9c537f55dc8b7dd616fb2424c7253f04057dc;p=horde.git controller should go into the main repo, it's already horde 4 ready --- diff --git a/framework/Controller/lib/Horde/Controller/Base.php b/framework/Controller/lib/Horde/Controller/Base.php new file mode 100644 index 000000000..d56a4d61c --- /dev/null +++ b/framework/Controller/lib/Horde/Controller/Base.php @@ -0,0 +1,417 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_Controller + */ + +/** + * @author Mike Naberezny + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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. + * + * + * process($request, $response); + * ... + * ?> + * + * + * @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. + * + * + * render(array('text' => 'some text to render')); + * $this->render(array('action' => 'actionName')); + * $this->render(array('nothing' => 1)); + * ... + * ?> + * + * + * @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 + * + * + * renderText('some text to render to the screen'); + * ... + * ?> + * + * + * @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 + * + * renderAction('listDocuments'); + * ... + * ?> + * + * + * @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. + * + * + * renderNothing(); + * ... + * ?> + * + */ + 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 index 000000000..209114039 --- /dev/null +++ b/framework/Controller/lib/Horde/Controller/Dispatcher.php @@ -0,0 +1,233 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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 + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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 index 000000000..e62cf5ef2 --- /dev/null +++ b/framework/Controller/lib/Horde/Controller/Exception.php @@ -0,0 +1,24 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_Controller + */ + +/** + * @author Mike Naberezny + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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 index 000000000..1dd34e918 --- /dev/null +++ b/framework/Controller/lib/Horde/Controller/Request/Base.php @@ -0,0 +1,378 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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 + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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 + */ + 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 Ismo_Core_Request manually and + * set the default locale with this method. Then add it as the + * application's request class with + * Ismo_Core_Application::setRequest(). + * + * @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 Ismo_Core_Request manually and + * set the supported locales with this method. Then add it as the + * application's request class with + * Ismo_Core_Application::setRequest(). + * + * @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 index 000000000..8ac026431 --- /dev/null +++ b/framework/Controller/lib/Horde/Controller/Request/Http.php @@ -0,0 +1,594 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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 + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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 index 000000000..6d97f4b01 --- /dev/null +++ b/framework/Controller/lib/Horde/Controller/Request/Mock.php @@ -0,0 +1,26 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_Controller + * @subpackage Request + */ + +/** + * @author Mike Naberezny + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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 index 000000000..9f88dfb21 --- /dev/null +++ b/framework/Controller/lib/Horde/Controller/Response/Base.php @@ -0,0 +1,38 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_Controller + * @subpackage Response + */ + +/** + * @author Mike Naberezny + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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 index 000000000..bea6bcd98 --- /dev/null +++ b/framework/Controller/lib/Horde/Controller/Response/Http.php @@ -0,0 +1,224 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_Controller + * @subpackage Response + */ + +/** + * @author Mike Naberezny + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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 false + * otherwise true. 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 true if the error response could be + * send, false 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? + ?> + + +<?= $code ?> + +

+ +
+ +false + * otherwise true. After this method the response should be + * considered commited. + * + * Examples: + * + * $u = new Ismo_Core_Url("http://a.b.c"); + * $response->sendRedirect($u); + * + * Redirects the browser to http://a.b.c using an Ismo_Core_Url instance. + * + * + * $response->sendRedirect("http://d.e.f"); + * + * 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 false if the headers have already + * been sent, true 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 index 000000000..7f57f7903 --- /dev/null +++ b/framework/Controller/lib/Horde/Controller/Response/Mock.php @@ -0,0 +1,26 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @license http://opensource.org/licenses/bsd-license.php + * @category Horde + * @package Horde_Controller + * @subpackage Response + */ + +/** + * @author Mike Naberezny + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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 index 000000000..d76628a57 --- /dev/null +++ b/framework/Controller/package.xml @@ -0,0 +1,80 @@ + + + Controller + pear.horde.org + Horde Controller libraries + This package provides the controller part of an MVC system for Horde. + + + Mike Naberezny + mnaberez + mike@naberezny.com + yes + + + Chuck Hagenbuch + chuck + chuck@horde.org + yes + + 2008-09-24 + + 0.1.0 + 0.1.0 + + + beta + beta + + BSD + * Initial release. + + + + + + + + + + + + + + + + + + + + + + + + + + + 5.2.0 + + + 1.5.0 + + + + + + + + + + + + + + + + + diff --git a/framework/Controller/test/Horde/Controller/AllTests.php b/framework/Controller/test/Horde/Controller/AllTests.php new file mode 100644 index 000000000..8d442cebd --- /dev/null +++ b/framework/Controller/test/Horde/Controller/AllTests.php @@ -0,0 +1,68 @@ + + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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 + * @author Derek DeVries + * @author Chuck Hagenbuch + * @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(); +}