* @author Ben LeMasurier * @author Casey Dentinger * @author David Stillman * @author Rob Carpenter * @author Todd Treece */ abstract class Application { protected $_env; // Environment of request. protected $_req; // Request which models current HTTP request, more or less protected $_referer; // SparkReferer which models HTTP_REFERER for current request, if possible protected $_route; // Route which models elements of current route, more or less protected $_sparkrev; // stat() mktime of the last deploy protected $_defaultController = 'index'; // Default controller for this app protected $_controller; // Name of current controller protected $_defaultAction = 'index'; // Name of default action protected $_action; // Name of current action protected $_fallbackController = 'fallback'; // Name of fallback controller - go here if we don't find anything else protected $_dispatcher = 'index.php'; // Filename of script what does dispatching protected $_controllerClass = null; protected $_namespace = false; // Always use namespaces for controllers? protected $_routeMap; // Route map object protected $_ctl; protected static $_hostname = 'localhost'; // Hostname where this app lives protected static $_root = '/'; // Root directory for the application - override in child classes protected static $_instance = null; // Current instance of Application - see app() below public static $mimeToExtension = [ 'text/html' => 'html', 'application/xhtml+xml' => 'html', 'application/xml' => 'html', // ARGH - TODO: WTF is this about again? 'application/json' => 'json', 'application/javascript' => 'js', 'text/csv' => 'csv', 'text/plain' => 'txt', 'text/xml' => 'xml', 'application/rss+xml' => 'rss', 'application/atom+xml' => 'xml', 'application/vnd.google-earth.kml+xml' => 'kml', 'font/ttf' => 'ttf', 'image/png' => 'png', 'application/pdf' => 'pdf', ]; public static $extensionToMime = [ 'html' => 'text/html', 'xml' => 'text/xml', 'json' => 'application/json', 'js' => 'application/javascript', 'csv' => 'text/csv', 'txt' => 'text/plain', 'rss' => 'application/rss+xml', 'kml' => 'application/vnd.google-earth.kml+xml', 'ttf' => 'font/ttf', 'png' => 'image/png', 'pdf' => 'application/pdf', ]; /** * Instantiate an application, handling all of the top-level stuff we're * going to deal with - GET and POST variables, sessions, you name it, * and then route it to the correct controller and action. * * There are some intentional constraints in place here. You aren't * allowed to combine a POST with GET variables. Hopefully this makes * it a bit easier to think about whether an operation retrieves a * resource or makes some change to one. * * You are allowed to instantiate no more than one child of Application, * at least for now. Once this method ends, the application is done * running. */ public function __construct (Environment $environment) { if (isset(static::$_instance)) throw new Exception("tried to instantiate more than one Application"); // store the instance so it can be accessed statically with app() // (in retrospect this might not have been such a hot idea): static::$_instance = $this; $this->_env = $environment; $this->_req = $this->_env->req(); $this->_controller = $this->_defaultController; $this->_action = $this->_defaultAction; $map_class = get_class($this) . 'RouteMap'; $this->_routeMap = new $map_class; $this->_env->startSession(); // start a session and populate it $this->userInit(); /* From here, we dispatch a request to the correct controller. This is the guts of everything. */ // figure it out: $this->discernRoute(); // to be overloaded in a child class (e.g.: Sparkle) if needed: $this->init(); // no more mucking with the request object: $this->_req->finalize(); // TODO: this is also called in setController() - seems redundant here, // but breaks things if it goes away. Should be sorted. $this->_controllerClass = $this->makeControllerName($this->_controller); // verify controller and action: $this->validate(); // stash the controller instance for later use $this->_ctl = new $this->_controllerClass($this); $action_method = $this->_action; try { // We want _only_ public methods to ever be called as actions. By // checking is_callable() in this context, and passing the result // of the action method off to the controller, we make sure that anything // private/protected is uncallable: if (is_callable(array($this->_ctl, $action_method))) { $this->_ctl->handleResult( $this->_ctl->$action_method() ); } else { $this->fallback('action not callable'); } } catch (SparkException $e) { // Finally, we might have gotten a user-level exception: $this->handleException($e); } } /** * Set the controller. Expects a string. */ protected function setController ($controller) { $class = $this->makeControllerName($controller); // Let's make sure $class is actually a Controller. It's not // paranoia when they're really out to get you. if ( (! class_exists($class)) || (! is_subclass_of($class, 'SparkLib\Application\Controller')) ) { if ($class === 'fallback') return false; // avoid infinite loopage $this->fallback($class . ' is not a Controller.'); return false; } $this->_controller = $controller; $this->_controllerClass = $class; return true; } /** * Figure out where we are supposed to go, based on path info. * * TODO: Most of this stuff should not live in Application. */ protected function discernRoute () { // Did we get a path at all? if (! strlen($path = trim($this->_env->path()))) return; $route_found = false; $matches = array(); // Have we defined any direct routing? if (isset($this->_routeMap->directRoutes)) { foreach ($this->_routeMap->directRoutes as $direct_pattern => $direct_route) { if (preg_match($direct_pattern, $path, $direct_route_matches)) { $this->setController($direct_route[0]); if (isset($direct_route[1])) $action = $direct_route[1]; elseif (isset($direct_route_matches['action'])) $action = $direct_route_matches['action']; else $action = $this->_defaultAction; $route_found = true; $matches = $direct_route_matches; foreach ($matches as $key => $value) { if (! is_numeric($key)) $this->_req->inject($key, $value); } } } } // If we haven't got a route by now (from the directRoutes) we'll try and // figure out a controller... $initial_pattern = '{^/(' . $this->_routeMap->patterns['controller'] . ').*$}'; if ((! $route_found) && preg_match($initial_pattern, $path, $initial_route)) { if ($this->setController($initial_route[1])) { foreach ($this->_routeMap->buildRoutes($this->_env->method()) as $route => $action) { $pattern = $this->_routeMap->makePattern($route); if ($route_found = preg_match($pattern, $path, $matches)) { break; } } } } if ($route_found) { // add these to the request, if we got 'em: if (isset($matches['id'])) { $this->_req->inject('id', $matches['id']); } if (isset($matches['bson'])) { $this->_req->inject('bson', $matches['bson']); } $this->_action = is_null($action) ? $matches['action'] : $action; } else { $this->fallback('No route found for request, path given: ' . $this->_env->path()); } // tell the request about any file type extension we got if (isset($matches['type'])) { if (! $this->_req->setTypeFromExtension($matches['type'])) { $this->fallback('Unable to map an appropriate type for ' . $matches['type']); } } // This will be empty if we haven't found anything by now. $this->_route = new Route($matches); } /** * Do the controller and its action exist? Will only pass with a * public method named after the action, or a __call() method which * can dynamically handle actions. */ protected function validate () { $is_real_method = method_exists($this->_controllerClass, $this->_action); $has_call = method_exists($this->_controllerClass, '__call'); if (! $is_real_method && ! $has_call) { $this->fallback( 'No such action (and no __call() fallback) when looking for ' . $this->_controllerClass . '&' . $this->_action ); } } /** * Handle user-level exceptions */ protected function handleException (SparkException $e) { // Inform the user $this->passMessage($e->getMessage()); // Override the current controller and validate the new target $this->_controller = $e->controller(); $this->_controllerClass = $this->makeControllerName($this->_controller, $e->appName()); $this->_ctl = new $this->_controllerClass($this); $this->_action = $e->action(); $this->validate(); if ($e->appName()) { $app_name = $e->appName(); } else { $app_name = $this->appName(); } $redirect = $app_name::externalLink($this->_controller)->action($this->_action)->params($e->params())->redirect(301); $redirect->fire(); } /** * To be overloaded in child classes for doing constructor-like things; * called in the constructor. */ protected function init() { } /** * To be overloaded in child classes for doing user init-like things * called in startSession() */ protected function userInit() { } /** * Log an error and let the fallback controller decide what to do. * Assumes that a fallback controller will be present. (Not found errors * as well as any necessary magic on URLs not modeled by controllers can * happen here.) */ protected function fallback ($logmsg) { $this->setController('fallback'); $this->_action = 'index'; } /** * Tell Blode to record this run of the application. * Child classes of Application can call this as-appropriate. */ protected function blodeRun () { $run = [ 'event' => 'app.run', 'app' => get_class($this), ]; Event::debug($run); } /** * Go down in flames if someone tries to make a copy of the application. */ public function __clone () { throw new Exception("why you try and make clone of Application? >:|"); } /** * Return a Link modelling a URL/path for the given controller. * See the docs in that class for more detail. Should enable something like: * * * echo $app->link('products')->id(666)->a('Arduino Duemilanove'); * * * @param string controller name * @return Link for given controller */ public function link ($controller = null) { $base = 'https://' . static::$_hostname . $this->url(); return new Link($base, $controller); } /** * Return a Link for another Application. This * should be better. * * @param string controller name * @return Link for given app/controller */ public static function externalLink ($controller = null) { $base = 'https://' . static::$_hostname . static::$_root; return new Link($base, $controller); } /** * Return a Template for the given partial. * * Look in [template dir]/[application]/partials/ for corresponding * template files. If a controller name is specified as the second * parameter, use [template dir]/[application]/[controller]/ instead. * * @param string name of partial * @return Template instance for corresponding partial */ public function partial ($name, $controller = null) { $partial = new \SparkLib\Template(null, array( 'app' => $this, 'ctl' => $this->_ctl )); if (null === $controller) { $partial->setTemplateDirRel($this->_templateDir . '/partials'); } elseif (is_string($controller)) { $partial->setTemplateDirRel($this->_templateDir . '/' . $controller); } else { throw new Exception('If supplied, controller name must be a string.'); } $partial->setTemplate($name . '.tpl.php'); if (! $partial->templateFileExists()) { // we haaaaaaaaates it: $partial->setTemplateDir(\LIBDIR . 'templates/partials'); } return $partial; } public function ctl () { return $this->_ctl; } /** * @return \SparkLib\Application current instance of Application */ public static function app () { return static::$_instance; } /** * timestamp of the last deploy. Useful for bumping version numbers of static files. * * @return int timestamp */ public function sparkrev () { if ($this->_sparkrev) return $this->_sparkrev; $s = stat(BASEDIR . '/sparkrev'); $this->_sparkrev = $s['mtime']; return $this->_sparkrev; } /** * Various other getters. These are getters, in case you're wondering, so * that they will be hard to mess with. If you're trying to change one * of the underlying values for some reason and there's not already a * method for it, talk to Brennen. */ /** * Base path of the application. * * @return string url */ public function baseUrl () { return static::$_root; } /** * How should external code point to this application. * * @return string url */ public static function linkPath () { return static::$_root; } /** * URL of the dispatcher itself. Will likely be the same as baseUrl() if * URL rewriting is in place on the web server. * * @return string url */ public function url () { return static::$_root . $this->_dispatcher; } /** * Full path of the request string without the request params * * @return string url */ public function requestUrl () { if (strpos($_SERVER['REQUEST_URI'], '?')) // is this faster than explode()? _dave return substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '?')); return $_SERVER['REQUEST_URI']; } /** * An object modeling the components of the referer, or false * if we can't parse same. * * Arguably maybe this should be chained off the request, but * for now I'm putting it here in the interest of less typing. */ public function referer () { if (! isset($this->_referer)) { $ref = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; $this->_referer = new \SparkLib\URLParser($ref); } return $this->_referer; } /** * @return Environment for the current SAPI */ public function env () { return $this->_env; } /** * @return Request which models the current request */ public function req () { return $this->_req; } /** * @return Route which models the current route */ public function route () { return $this->_route; } /** * @return string name of the current action */ public function action () { return $this->_action; } /** * Set the current action. * * @param string action name */ public function setAction ($action) { return $this->_action = $action; } /** * @return string name of the current controller */ public function controller() { return $this->_controller; } /** * Camelcase a name_with_underscores and come up with a class name * like ApplicationWhateverName. * * @param string name of a controller * @return string name of a controller class */ public function makeControllerName ($controller, $app_name_str = '') { $parts = explode('_', $controller); foreach ($parts as &$part) { $part = ucfirst($part); } if (! $app_name_str) { $app_name_str = get_class($this); } // this lives in Application\Controller $namespaced = $app_name_str . '\\' . implode('', $parts); // this tells us everything will be namespaced, so we can // shortcut right to using that one if ($this->_namespace) return $namespaced; // this lives in Application\ApplicationController // it is the old way and we hates it $un_namespaced = $app_name_str . implode('', $parts); if (class_exists($un_namespaced)) { return $un_namespaced; } return $namespaced; } /** * Return application name * * @author tylerc */ public function appName() { return get_class($this); } /** * End the current session. */ public function endSession () { $this->_env->endSession(); } /** * Stash a message in the session to be passed on to the user. */ public function passMessage ($message, $type = 'error', $delay = 0, $fixed = false) { // replaces with full class? $messageO = new \stdClass(); $messageO->type = $type; $messageO->delay = $delay; $messageO->fixed = $fixed; $messageO->text = $message; $_SESSION['messages'][] = $messageO; } public function getMessages ($type = null) { $messages = []; foreach ($_SESSION['messages'] as $message){ if ($type == null || $message->type == $type){ $messages[] = $message; } } // Reset user messages, we've seen these now. $_SESSION['messages'] = array(); return $messages; } public function hasMessages () { return count($_SESSION['messages']) > 0; } public function require_authentication ($redirect = null) { if (! $_SESSION['user']->isAuthenticated()) throw new AuthenticationException($redirect); } }