# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. /** * The version of the trails library. */ define('TRAILS_VERSION', '0.6.9'); /** * The Dispatcher is used to map an incoming HTTP request to a Controller * producing a response which is then rendered. To initialize an instance of * class Trails_Dispatcher you have to give three configuration settings: * * trails_root - the absolute file path to a directory containing the * applications controllers, views etc. * trails_uri - the URI to which routes to mapped Controller/Actions * are appended * default_controller - the route to a controller, that is used if no * controller is given, that is the route is equal to '/' * * After instantiation of a dispatcher you have to call method #dispatch with * the request uri to be mapped to a controller/action pair. * * @package trails * * @author mlunzena * @copyright (c) Authors * @version $Id: trails.php 7001 2008-04-04 11:20:27Z mlunzena $ */ class Trails_Dispatcher { # TODO (mlunzena) Konfiguration muss anders geschehen /** * This is the absolute file path to the trails application directory. * * @access public * @var string */ public $trails_root; /** * This is the URI to which routes to controller/actions are appended. * * @access public * @var string */ public $trails_uri; /** * This variable contains the route to the default controller. * * @access public * @var string */ public $default_controller; /** * Constructor. * * @param string absolute file path to a directory containing the * applications controllers, views etc. * @param string the URI to which routes to mapped Controller/Actions * are appended * @param string the route to a controller, that is used if no * controller is given, that is the route is equal to '/' * * @return void */ function __construct($trails_root, $trails_uri, $default_controller) { $this->trails_root = $trails_root; $this->trails_uri = $trails_uri; $this->default_controller = $default_controller; } /** * Maps a string to a response which is then rendered. * * @param string The requested URI. * * @return void */ function dispatch($uri) { # E_USER_ERROR|E_USER_WARNING|E_USER_NOTICE|E_RECOVERABLE_ERROR = 5888 $old_handler = set_error_handler(array($this, 'error_handler'), 5888); ob_start(); $level = ob_get_level(); $this->map_uri_to_response($this->clean_request_uri((string) $uri))->output(); while (ob_get_level() >= $level) { ob_end_flush(); } if (isset($old_handler)) { set_error_handler($old_handler); } } /** * Maps an URI to a response by figuring out first what controller to * instantiate, then delegating the unconsumed part of the URI to the * controller who returns an appropriate response object or throws a * Trails_Exception. * * @param string the URI string * * @return mixed a response object */ function map_uri_to_response($uri) { try { list($controller_path, $unconsumed) = '' === $uri ? $this->default_route() : $this->parse($uri); $controller = $this->load_controller($controller_path); $response = $controller->perform($unconsumed); } catch (Exception $e) { $response = isset($controller) ? $controller->rescue($e) : $this->trails_error($e); } return $response; } /** * * @return array an array containing the default controller and an * empty unconsumed route */ function default_route() { if (!$this->file_exists($this->default_controller . '.php')) { throw new Trails_MissingFile( "Default controller '{$this->default_controller}' not found'"); } return array($this->default_controller, ''); } function trails_error($exception) { ob_clean(); # show details for local requests $detailed = @$_SERVER['REMOTE_ADDR'] === '127.0.0.1'; $body = sprintf('Trails Error'. '

%s

%s
', htmlentities($exception->__toString()), $detailed ? htmlentities($exception->getTraceAsString()) : ''); if ($exception instanceof Trails_Exception) { $response = new Trails_Response($body, $exception->headers, $exception->getCode(), $exception->getMessage()); } else { $response = new Trails_Response($body, array(), 500, $exception->getMessage()); } return $response; } /** * Clean up URI string by removing the query part and leading slashes. * * @param string an URI string * * @return string the cleaned string */ function clean_request_uri($uri) { if (FALSE !== ($pos = strpos($uri, '?'))) { $uri = substr($uri, 0, $pos); } return ltrim($uri, '/'); } /** * * * @param type * @param type * * @return type */ function parse($unconsumed, $controller = NULL) { list($head, $tail) = $this->split_on_first_slash($unconsumed); if (!preg_match('/^\w+$/', $head)) { throw new Trails_RoutingError("No route matches '$head'"); } $controller = (isset($controller) ? $controller . '/' : '') . $head; if ($this->file_exists($controller . '.php')) { return array($controller, $tail); } else if ($this->file_exists($controller)) { return $this->parse($tail, $controller); } throw new Trails_RoutingError("No route matches '$head'"); } function split_on_first_slash($str) { preg_match(":([^/]*)(/+)?(.*):", $str, $matches); return array($matches[1], $matches[3]); } function file_exists($path) { return file_exists("{$this->trails_root}/controllers/$path"); } /** * Loads the controller file for a given controller path and return an * instance of that controller. If an error occures, an exception will be * thrown. * * @param string the relative controller path * * @return TrailsController an instance of that controller */ function load_controller($controller) { require_once "{$this->trails_root}/controllers/{$controller}.php"; $class = Trails_Inflector::camelize($controller) . 'Controller'; if (!class_exists($class)) { throw new Trails_UnknownController("Controller missing: '$class'"); } return new $class($this); } /** * This method transforms E_USER_* and E_RECOVERABLE_ERROR to * Trails_Exceptions. * * @param integer the level of the error raised * @param string the error message * @param string the filename that the error was raised in * @param integer the line number the error was raised at * @param array an array of every variable that existed in the scope the * error was triggered in * * @throws Trails_Exception * * @return void */ function error_handler($errno, $string, $file, $line, $context) { if (!(5888 & $errno)) { return false; } throw new Trails_Exception(500, $string); } } /** * This class represents a response returned by a controller that was asked to * perform for a given request. A Trails_Response contains the body, status and * additional headers which can be renderer back to the client. * * @package trails * * @author mlunzena * @copyright (c) Authors * @version $Id: trails.php 7001 2008-04-04 11:20:27Z mlunzena $ */ class Trails_Response { /** * @ignore */ public $body = '', $status, $reason, $headers = array(); /** * Constructor. * * @param string the body of the response defaulting to '' * @param array an array of additional headers defaulting to an * empty array * @param integer the status code of the response defaulting to a * regular 200 * @param string the descriptional reason for a status code defaulting to * the standard reason phrases defined in RFC 2616 * * @return void */ function __construct($body = '', $headers = array(), $status = NULL, $reason = NULL) { $this->set_body($body); $this->headers = $headers; if (isset($status)) { $this->set_status($status, $reason); } } /** * Sets the body of the response. * * @param string the body * * @return mixed this response object. Useful for cascading method calls. */ function set_body($body) { $this->body = $body; return $this; } /** * Sets the status code and an optional custom reason. If none is given, the * standard reason phrase as of RFC 2616 is used. * * @param integer the status code * @param string the custom reason, defaulting to the one given in RFC 2616 * * @return mixed this response object. Useful for cascading method calls. */ function set_status($status, $reason = NULL) { $this->status = $status; $this->reason = isset($reason) ? $reason : self::get_reason($status); return $this; } /** * Returns the reason phrase of this response according to RFC2616. * * @param int the response's status * * @return string the reason phrase for this response's status */ public static function get_reason($status) { $reason = array( 100 => 'Continue', 'Switching Protocols', 200 => 'OK', 'Created', 'Accepted', 'Non-Authoritative Information', 'No Content', 'Reset Content', 'Partial Content', 300 => 'Multiple Choices', 'Moved Permanently', 'Found', 'See Other', 'Not Modified', 'Use Proxy', '(Unused)', 'Temporary Redirect', 400 => 'Bad Request', 'Unauthorized', 'Payment Required','Forbidden', 'Not Found', 'Method Not Allowed', 'Not Acceptable', 'Proxy Authentication Required', 'Request Timeout', 'Conflict', 'Gone', 'Length Required', 'Precondition Failed', 'Request Entity Too Large', 'Request-URI Too Long', 'Unsupported Media Type', 'Requested Range Not Satisfiable', 'Expectation Failed', 500 => 'Internal Server Error', 'Not Implemented', 'Bad Gateway', 'Service Unavailable', 'Gateway Timeout', 'HTTP Version Not Supported'); return isset($reason[$status]) ? $reason[$status] : ''; } /** * Adds an additional header to the response. * * @param string the left hand key part * @param string the right hand value part * * @return mixed this response object. Useful for cascading method calls. */ function add_header($key, $value) { $this->headers[$key] = $value; return $this; } /** * Outputs this response to the client using "echo" and "header". * * @return void */ function output() { if (isset($this->status)) { $this->send_header(sprintf('HTTP/1.1 %d %s', $this->status, $this->reason), TRUE, $this->status); } foreach ($this->headers as $k => $v) { $this->send_header("$k: $v"); } echo $this->body; } /** * Internally used function to actually send headers * * @param string the HTTP header * @param bool optional; TRUE if previously sent header should be * replaced - FALSE otherwise (default) * @param integer optional; the HTTP response code * * @return void */ function send_header($header, $replace = FALSE, $status = NULL) { if (isset($status)) { header($header, $replace, $status); } else { header($header, $replace); } } } /** * A Trails_Controller is responsible for matching the unconsumed part of an URI * to an action using the left over words as arguments for that action. The * action is then mapped to method of the controller instance which is called * with the just mentioned arguments. That method can send the #render_action, * #render_template, #render_text, #render_nothing or #redirect method. * Otherwise the #render_action is called with the current action as argument. * If the action method sets instance variables during performing, they will be * be used as attributes for the flexi-template opened by #render_action or * #render_template. A controller's response's body is populated with the output * of the #render_* methods. The action methods can add additional headers or * change the status of that response. * * @package trails * * @author mlunzena * @copyright (c) Authors * @version $Id: trails.php 7001 2008-04-04 11:20:27Z mlunzena $ */ class Trails_Controller { /** * @ignore */ protected $dispatcher, $response, $performed, $layout; /** * Constructor. * * @param mixed the dispatcher who creates this instance * * @return void */ function __construct($dispatcher) { $this->dispatcher = $dispatcher; $this->erase_response(); } /** * Resets the response of the controller * * @return void */ function erase_response() { $this->performed = FALSE; $this->response = new Trails_Response(); } /** * Return this controller's response * * @return mixed the controller's response */ function get_response() { return $this->response; } /** * This method extracts an action string and further arguments from it's * parameter. The action string is mapped to a method being called afterwards * using the said arguments. That method is called and a response object is * generated, populated and sent back to the dispatcher. * * @param type * * @return type */ function perform($unconsumed) { list($action, $args, $format) = $this->extract_action_and_args($unconsumed); $this->format = isset($format) ? $format : 'html'; $before_filter_result = $this->before_filter($action, $args); # send action to controller # TODO (mlunzena) shouldn't the after filter be triggered too? if (!(FALSE === $before_filter_result || $this->performed)) { $callable = $this->map_action($action); if (is_callable($callable)) { call_user_func_array($callable, $args); } else { $this->does_not_understand($action, $args); } if (!$this->performed) { $this->render_action($action); } $this->after_filter($action, $args); } return $this->response; } /** * Extracts action and args from a string. * * @param string the processed string * * @return array an array with two elements - a string containing the * action and an array of strings representing the args */ function extract_action_and_args($string) { if ('' === $string) { return $this->default_action_and_args(); } // find optional file extension $format = NULL; if (preg_match('/^(.*[^\/.])\.(\w+)$/', $string, $matches)) { list($_, $string, $format) = $matches; } // TODO this should possibly remove empty tokens $args = explode('/', $string); $action = array_shift($args); return array($action, $args, $format); } /** * Return the default action and arguments * * @return an array containing the action, an array of args and the format * */ function default_action_and_args() { return array('index', array(), NULL); } /** * Maps the action to an actual method name. * * @param string the action * * @return string the mapped method name */ function map_action($action) { return array(&$this, $action . '_action'); } /** * Callback function being called before an action is executed. If this * function does not return FALSE, the action will be called, otherwise * an error will be generated and processing will be aborted. If this function * already #rendered or #redirected, further processing of the action is * withheld. * * @param string Name of the action to perform. * @param array An array of arguments to the action. * * @return bool */ function before_filter(&$action, &$args) { } /** * Callback function being called after an action is executed. * * @param string Name of the action to perform. * @param array An array of arguments to the action. * * @return void */ function after_filter($action, $args) { } /** * * * @param type * @param type * * @return void */ function does_not_understand($action, $args) { throw new Trails_UnknownAction("No action responded to '$action'."); } /** * * * @param string * * @return void */ function redirect($to) { if ($this->performed) { throw new Trails_DoubleRenderError(); } $this->performed = TRUE; # get uri; keep absolute URIs $url = preg_match('#^(/|\w+://)#', $to) ? $to : $this->url_for($to); $this->response->add_header('Location', $url)->set_status(302); } /** * Renders the given text as the body of the response. * * @param string the text to be rendered * * @return void */ function render_text($text = ' ') { if ($this->performed) { throw new Trails_DoubleRenderError(); } $this->performed = TRUE; $this->response->set_body($text); } /** * Renders the empty string as the response's body. * * @return void */ function render_nothing() { $this->render_text(''); } /** * Renders the template of the given action as the response's body. * * @param string the action * * @return void */ function render_action($action) { $this->render_template($this->get_default_template($action), $this->layout); } function get_default_template($action) { $class = get_class($this); $controller_name = Trails_Inflector::underscore(substr($class, 0, -10)); return $controller_name.'/'.$action; } /** * Renders a template using an optional layout template. * * @param mixed a flexi template * @param mixes a flexi template which is used as layout * * @return void */ function render_template($template_name, $layout = NULL) { # open template $factory = $this->get_template_factory(); $template = $factory->open($template_name); # template requires setup ? switch (get_class($template)) { case 'Flexi_JsTemplate': $this->set_content_type('text/javascript'); break; } $template->set_attributes($this->get_assigned_variables()); if (isset($layout)) { $template->set_layout($layout); } $this->render_text($template->render()); } /** * Create and return a template factory for this controller. * * @return a Flexi_TemplateFactory */ function get_template_factory() { return new Flexi_TemplateFactory($this->dispatcher->trails_root . '/views/'); } /** * This method returns all the set instance variables to be used as attributes * for a template. This controller is returned too as value for * key 'controller'. * * @return array an associative array of variables for the template */ function get_assigned_variables() { $assigns = array(); $protected = get_class_vars(get_class($this)); foreach (get_object_vars($this) as $var => $value) { if (!array_key_exists($var, $protected)) { $assigns[$var] =& $this->$var; } } $assigns['controller'] = $this; return $assigns; } /** * Sets the layout to be used by this controller per default. * * @param mixed a flexi template to be used as layout * * @return void */ function set_layout($layout) { $this->layout = $layout; } /** * Returns a URL to a specified route to your Trails application. * * Example: * Your Trails application is located at 'http://example.com/dispatch.php'. * So your dispatcher's trails_uri is set to 'http://example.com/dispatch.php' * If you want the URL to your 'wiki' controller with action 'show' and * parameter 'page' you should send: * * $url = $controller->url_for('wiki/show', 'page'); * * $url should then contain 'http://example.com/dispatch.php/wiki/show/page'. * * The first parameter is a string containing the controller and optionally an * action: * * - "{controller}/{action}" * - "path/to/controller/action" * - "controller" * * This "controller/action" string is not url encoded. You may provide * additional parameter which will be urlencoded and concatenated with * slashes: * * $controller->url_for('wiki/show', 'page'); * -> 'wiki/show/page' * * $controller->url_for('wiki/show', 'page', 'one and a half'); * -> 'wiki/show/page/one+and+a+half' * * @param string a string containing a controller and optionally an action * @param strings optional arguments * * @return string a URL to this route */ function url_for($to/*, ...*/) { # urlencode all but the first argument $args = func_get_args(); $args = array_map('urlencode', $args); $args[0] = $to; return $this->dispatcher->trails_uri . '/' . join('/', $args); } /** * * * @param type * * @return type */ function set_status($status, $reason_phrase = NULL) { $this->response->set_status($status, $reason_phrase); } /** * Sets the content type of the controller's response. * * @param string the content type * * @return void */ function set_content_type($type) { $this->response->add_header('Content-Type', $type); } /** * Exception handler called when the performance of an action raises an * exception. * * @param object the thrown exception * * @return object a response object */ function rescue($exception) { return $this->dispatcher->trails_error($exception); } function respond_to($ext) { return $this->format === $ext; } } /** * The Inflector class is a namespace for inflections methods. * * @package trails * * @author mlunzena * @copyright (c) Authors * @version $Id: trails.php 7001 2008-04-04 11:20:27Z mlunzena $ */ class Trails_Inflector { /** * Returns a camelized string from a lower case and underscored string by * replacing slash with underscore and upper-casing each letter preceded * by an underscore. TODO * * @param string String to camelize. * * @return string Camelized string. */ static function camelize($word) { $parts = explode('/', $word); foreach ($parts as $key => $part) { $parts[$key] = str_replace(' ', '', ucwords(str_replace('_', ' ', $part))); } return join('_', $parts); } /** * * * @param type * * @return type */ static function underscore($word) { $parts = explode('_', $word); foreach ($parts as $key => $part) { $parts[$key] = preg_replace('/(?<=\w)([A-Z])/', '_\\1', $part); } return strtolower(join('/', $parts)); } } /** * The flash provides a way to pass temporary objects between actions. * Anything you place in the flash will be exposed to the very next action and * then cleared out. This is a great way of doing notices and alerts, such as * a create action that sets * $flash->set('notice', "Successfully created") * before redirecting to a display action that can then expose the flash to its * template. * * @package trails * * @author mlunzena * @copyright (c) Authors * @version $Id: trails.php 7001 2008-04-04 11:20:27Z mlunzena $ */ class Trails_Flash implements ArrayAccess { /** * @ignore */ public $flash = array(), $used = array(); /** * * * @return type */ static function instance() { if (!isset($_SESSION)) { throw new Trails_SessionRequiredException(); } if (!isset($_SESSION['trails_flash'])) { $_SESSION['trails_flash'] = new Trails_Flash(); } return $_SESSION['trails_flash']; } function offsetExists($offset) { return isset($this->flash[$offset]); } function offsetGet($offset) { return $this->get($offset); } function offsetSet($offset, $value) { $this->set($offset, $value); } function offsetUnset($offset) { unset($this->flash[$offset], $this->used[$offset]); } /** * Used internally by the keep and discard methods * use() # marks the entire flash as used * use('msg') # marks the "msg" entry as used * use(null, false) # marks the entire flash as unused * # (keeps it around for one more action) * use('msg', false) # marks the "msg" entry as unused * # (keeps it around for one more action) * * @param mixed a key. * @param bool used flag. * * @return void */ function _use($k = NULL, $v = TRUE) { if ($k) { $this->used[$k] = $v; } else { foreach ($this->used as $k => $value) { $this->_use($k, $v); } } } /** * Marks the entire flash or a single flash entry to be discarded by the end * of the current action. * * $flash->discard() # discards entire flash * # (it'll still be available for the * # current action) * $flash->discard('warning') # discard the "warning" entry * # (it'll still be available for the * # current action) * * @param mixed a key. * * @return void */ function discard($k = NULL) { $this->_use($k); } /** * Returns the value to the specified key. * * @param mixed a key. * * @return mixed the key's value. */ function &get($k) { $return = NULL; if (isset($this->flash[$k])) { $return =& $this->flash[$k]; } return $return; } /** * Keeps either the entire current flash or a specific flash entry available * for the next action: * * $flash->keep() # keeps the entire flash * $flash->keep('notice') # keeps only the "notice" entry, the rest of * # the flash is discarded * * @param mixed a key. * * @return void */ function keep($k = NULL) { $this->_use($k, FALSE); } /** * Sets a key's value. * * @param mixed a key. * @param mixed its value. * * @return void */ function set($k, $v) { $this->keep($k); $this->flash[$k] = $v; } /** * Sets a key's value by reference. * * @param mixed a key. * @param mixed its value. * * @return void */ function set_ref($k, &$v) { $this->keep($k); $this->flash[$k] =& $v; } /** * * * @return type */ function sweep() { # remove used values foreach (array_keys($this->flash) as $k) { if ($this->used[$k]) { unset($this->flash[$k], $this->used[$k]); } else { $this->_use($k); } } # cleanup if someone meddled with flash or used $fkeys = array_keys($this->flash); $ukeys = array_keys($this->used); foreach (array_diff($fkeys, $ukeys) as $k => $v) { unset($this->used[$k]); } } /** * * * @return type */ function __toString() { $values = array(); foreach ($this->flash as $k => $v) { $values[] = sprintf("'%s': [%s, '%s']", $k, var_export($v, TRUE), $this->used[$k] ? "used" : "unused"); } return "{" . join(", ", $values) . "}\n"; } /** * * * @param type * * @return type */ function __sleep() { $this->sweep(); return array('flash', 'used'); } /** * * * @param type * * @return type */ function __wakeUp() { $this->discard(); } } /** * TODO * * @package trails * * @author mlunzena * @copyright (c) Authors * @version $Id: trails.php 7001 2008-04-04 11:20:27Z mlunzena $ */ class Trails_Exception extends Exception { /** * * * @access private * @var */ public $headers; /** * @param int the status code to be set in the response * @param string a human readable presentation of the status code * @param array a hash of additional headers to be set in the response * * @return void */ function __construct($status = 500, $reason = NULL, $headers = array()) { if ($reason === NULL) { $reason = Trails_Response::get_reason($status); } parent::__construct($reason, $status); $this->headers = $headers; } /** * * * @param type * * @return type */ function __toString() { return "{$this->code} {$this->message}"; } } class Trails_DoubleRenderError extends Trails_Exception { function __construct() { $message = "Render and/or redirect were called multiple times in this action. ". "Please note that you may only call render OR redirect, and at most ". "once per action."; parent::__construct(500, $message); } } class Trails_MissingFile extends Trails_Exception { function __construct($message) { parent::__construct(500, $message); } } class Trails_RoutingError extends Trails_Exception { function __construct($message) { parent::__construct(400, $message); } } class Trails_UnknownAction extends Trails_Exception { function __construct($message) { parent::__construct(404, $message); } } class Trails_UnknownController extends Trails_Exception { function __construct($message) { parent::__construct(404, $message); } } class Trails_SessionRequiredException extends Trails_Exception { function __construct() { $message = "Tried to access a non existing session."; parent::__construct(500, $message); } }