|
|
- <?php
- namespace SparkLib;
-
- use \SparkLib\Application\Environment;
- use \SparkLib\Application\Controller;
- use \SparkLib\Application\Link;
- use \SparkLib\Application\Redirect;
- use \SparkLib\Application\Request;
- use \SparkLib\Application\Route;
- use \SparkLib\Application\RouteMap;
- use \SparkLib\Exception\SparkException;
- use \SparkLib\Exception\AuthenticationException;
- use \SparkLib\Blode\Event;
- use \SparkLib\Fail;
- use \SparkLib\User;
- use \Exception;
-
- /**
- * SparkLib\Application - A (relatively) simple web application framework
- *
- * @author Brennen Bearnes <brennen@sparkfun.com>
- * @author Ben LeMasurier <ben@sparkfun.com>
- * @author Casey Dentinger <caseyd@sparkfun.com>
- * @author David Stillman <dave@sparkfun.com>
- * @author Rob Carpenter <robert@sparkfun.com>
- * @author Todd Treece <ttreece@sparkfun.com>
- */
- 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:
- *
- * <code>
- * echo $app->link('products')->id(666)->a('Arduino Duemilanove');
- * </code>
- *
- * @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 <tyler.cipriani@sparkfun.com>
- */
- 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);
- }
-
- }
|