A modest collection of PHP libraries used at SparkFun.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

613 lines
19 KiB

  1. <?php
  2. namespace SparkLib;
  3. use \SparkLib\Application\Environment;
  4. use \SparkLib\Application\Controller;
  5. use \SparkLib\Application\Link;
  6. use \SparkLib\Application\Redirect;
  7. use \SparkLib\Application\Request;
  8. use \SparkLib\Application\Route;
  9. use \SparkLib\Application\RouteMap;
  10. use \SparkLib\Exception\SparkException;
  11. use \SparkLib\Exception\AuthenticationException;
  12. use \SparkLib\Blode\Event;
  13. use \SparkLib\Fail;
  14. use \SparkLib\User;
  15. use \Exception;
  16. /**
  17. * SparkLib\Application - A (relatively) simple web application framework
  18. *
  19. * @author Brennen Bearnes <brennen@sparkfun.com>
  20. * @author Ben LeMasurier <ben@sparkfun.com>
  21. * @author Casey Dentinger <caseyd@sparkfun.com>
  22. * @author David Stillman <dave@sparkfun.com>
  23. * @author Rob Carpenter <robert@sparkfun.com>
  24. * @author Todd Treece <ttreece@sparkfun.com>
  25. */
  26. abstract class Application {
  27. protected $_env; // Environment of request.
  28. protected $_req; // Request which models current HTTP request, more or less
  29. protected $_referer; // SparkReferer which models HTTP_REFERER for current request, if possible
  30. protected $_route; // Route which models elements of current route, more or less
  31. protected $_sparkrev; // stat() mktime of the last deploy
  32. protected $_defaultController = 'index'; // Default controller for this app
  33. protected $_controller; // Name of current controller
  34. protected $_defaultAction = 'index'; // Name of default action
  35. protected $_action; // Name of current action
  36. protected $_fallbackController = 'fallback'; // Name of fallback controller - go here if we don't find anything else
  37. protected $_dispatcher = 'index.php'; // Filename of script what does dispatching
  38. protected $_controllerClass = null;
  39. protected $_namespace = false; // Always use namespaces for controllers?
  40. protected $_routeMap; // Route map object
  41. protected $_ctl;
  42. protected static $_hostname = 'localhost'; // Hostname where this app lives
  43. protected static $_root = '/'; // Root directory for the application - override in child classes
  44. protected static $_instance = null; // Current instance of Application - see app() below
  45. public static $mimeToExtension = [
  46. 'text/html' => 'html',
  47. 'application/xhtml+xml' => 'html',
  48. 'application/xml' => 'html', // ARGH - TODO: WTF is this about again?
  49. 'application/json' => 'json',
  50. 'application/javascript' => 'js',
  51. 'text/csv' => 'csv',
  52. 'text/plain' => 'txt',
  53. 'text/xml' => 'xml',
  54. 'application/rss+xml' => 'rss',
  55. 'application/atom+xml' => 'xml',
  56. 'application/vnd.google-earth.kml+xml' => 'kml',
  57. 'font/ttf' => 'ttf',
  58. 'image/png' => 'png',
  59. 'application/pdf' => 'pdf',
  60. ];
  61. public static $extensionToMime = [
  62. 'html' => 'text/html',
  63. 'xml' => 'text/xml',
  64. 'json' => 'application/json',
  65. 'js' => 'application/javascript',
  66. 'csv' => 'text/csv',
  67. 'txt' => 'text/plain',
  68. 'rss' => 'application/rss+xml',
  69. 'kml' => 'application/vnd.google-earth.kml+xml',
  70. 'ttf' => 'font/ttf',
  71. 'png' => 'image/png',
  72. 'pdf' => 'application/pdf',
  73. ];
  74. /**
  75. * Instantiate an application, handling all of the top-level stuff we're
  76. * going to deal with - GET and POST variables, sessions, you name it,
  77. * and then route it to the correct controller and action.
  78. *
  79. * There are some intentional constraints in place here. You aren't
  80. * allowed to combine a POST with GET variables. Hopefully this makes
  81. * it a bit easier to think about whether an operation retrieves a
  82. * resource or makes some change to one.
  83. *
  84. * You are allowed to instantiate no more than one child of Application,
  85. * at least for now. Once this method ends, the application is done
  86. * running.
  87. */
  88. public function __construct (Environment $environment)
  89. {
  90. if (isset(static::$_instance))
  91. throw new Exception("tried to instantiate more than one Application");
  92. // store the instance so it can be accessed statically with app()
  93. // (in retrospect this might not have been such a hot idea):
  94. static::$_instance = $this;
  95. $this->_env = $environment;
  96. $this->_req = $this->_env->req();
  97. $this->_controller = $this->_defaultController;
  98. $this->_action = $this->_defaultAction;
  99. $map_class = get_class($this) . 'RouteMap';
  100. $this->_routeMap = new $map_class;
  101. $this->_env->startSession(); // start a session and populate it
  102. $this->userInit();
  103. /* From here, we dispatch a request to the correct controller. This is
  104. the guts of everything. */
  105. // figure it out:
  106. $this->discernRoute();
  107. // to be overloaded in a child class (e.g.: Sparkle) if needed:
  108. $this->init();
  109. // no more mucking with the request object:
  110. $this->_req->finalize();
  111. // TODO: this is also called in setController() - seems redundant here,
  112. // but breaks things if it goes away. Should be sorted.
  113. $this->_controllerClass = $this->makeControllerName($this->_controller);
  114. // verify controller and action:
  115. $this->validate();
  116. // stash the controller instance for later use
  117. $this->_ctl = new $this->_controllerClass($this);
  118. $action_method = $this->_action;
  119. try {
  120. // We want _only_ public methods to ever be called as actions. By
  121. // checking is_callable() in this context, and passing the result
  122. // of the action method off to the controller, we make sure that anything
  123. // private/protected is uncallable:
  124. if (is_callable(array($this->_ctl, $action_method))) {
  125. $this->_ctl->handleResult( $this->_ctl->$action_method() );
  126. } else {
  127. $this->fallback('action not callable');
  128. }
  129. } catch (SparkException $e) {
  130. // Finally, we might have gotten a user-level exception:
  131. $this->handleException($e);
  132. }
  133. }
  134. /**
  135. * Set the controller. Expects a string.
  136. */
  137. protected function setController ($controller)
  138. {
  139. $class = $this->makeControllerName($controller);
  140. // Let's make sure $class is actually a Controller. It's not
  141. // paranoia when they're really out to get you.
  142. if ( (! class_exists($class)) || (! is_subclass_of($class, 'SparkLib\Application\Controller')) ) {
  143. if ($class === 'fallback')
  144. return false; // avoid infinite loopage
  145. $this->fallback($class . ' is not a Controller.');
  146. return false;
  147. }
  148. $this->_controller = $controller;
  149. $this->_controllerClass = $class;
  150. return true;
  151. }
  152. /**
  153. * Figure out where we are supposed to go, based on path info.
  154. *
  155. * TODO: Most of this stuff should not live in Application.
  156. */
  157. protected function discernRoute ()
  158. {
  159. // Did we get a path at all?
  160. if (! strlen($path = trim($this->_env->path())))
  161. return;
  162. $route_found = false;
  163. $matches = array();
  164. // Have we defined any direct routing?
  165. if (isset($this->_routeMap->directRoutes)) {
  166. foreach ($this->_routeMap->directRoutes as $direct_pattern => $direct_route) {
  167. if (preg_match($direct_pattern, $path, $direct_route_matches)) {
  168. $this->setController($direct_route[0]);
  169. if (isset($direct_route[1]))
  170. $action = $direct_route[1];
  171. elseif (isset($direct_route_matches['action']))
  172. $action = $direct_route_matches['action'];
  173. else
  174. $action = $this->_defaultAction;
  175. $route_found = true;
  176. $matches = $direct_route_matches;
  177. foreach ($matches as $key => $value) {
  178. if (! is_numeric($key))
  179. $this->_req->inject($key, $value);
  180. }
  181. }
  182. }
  183. }
  184. // If we haven't got a route by now (from the directRoutes) we'll try and
  185. // figure out a controller...
  186. $initial_pattern = '{^/(' . $this->_routeMap->patterns['controller'] . ').*$}';
  187. if ((! $route_found) && preg_match($initial_pattern, $path, $initial_route)) {
  188. if ($this->setController($initial_route[1])) {
  189. foreach ($this->_routeMap->buildRoutes($this->_env->method()) as $route => $action) {
  190. $pattern = $this->_routeMap->makePattern($route);
  191. if ($route_found = preg_match($pattern, $path, $matches)) {
  192. break;
  193. }
  194. }
  195. }
  196. }
  197. if ($route_found) {
  198. // add these to the request, if we got 'em:
  199. if (isset($matches['id'])) { $this->_req->inject('id', $matches['id']); }
  200. if (isset($matches['bson'])) { $this->_req->inject('bson', $matches['bson']); }
  201. $this->_action = is_null($action)
  202. ? $matches['action']
  203. : $action;
  204. } else {
  205. $this->fallback('No route found for request, path given: ' . $this->_env->path());
  206. }
  207. // tell the request about any file type extension we got
  208. if (isset($matches['type'])) {
  209. if (! $this->_req->setTypeFromExtension($matches['type'])) {
  210. $this->fallback('Unable to map an appropriate type for ' . $matches['type']);
  211. }
  212. }
  213. // This will be empty if we haven't found anything by now.
  214. $this->_route = new Route($matches);
  215. }
  216. /**
  217. * Do the controller and its action exist? Will only pass with a
  218. * public method named after the action, or a __call() method which
  219. * can dynamically handle actions.
  220. */
  221. protected function validate ()
  222. {
  223. $is_real_method = method_exists($this->_controllerClass, $this->_action);
  224. $has_call = method_exists($this->_controllerClass, '__call');
  225. if (! $is_real_method && ! $has_call) {
  226. $this->fallback(
  227. 'No such action (and no __call() fallback) when looking for ' . $this->_controllerClass . '&' . $this->_action
  228. );
  229. }
  230. }
  231. /**
  232. * Handle user-level exceptions
  233. */
  234. protected function handleException (SparkException $e)
  235. {
  236. // Inform the user
  237. $this->passMessage($e->getMessage());
  238. // Override the current controller and validate the new target
  239. $this->_controller = $e->controller();
  240. $this->_controllerClass = $this->makeControllerName($this->_controller, $e->appName());
  241. $this->_ctl = new $this->_controllerClass($this);
  242. $this->_action = $e->action();
  243. $this->validate();
  244. if ($e->appName()) {
  245. $app_name = $e->appName();
  246. } else {
  247. $app_name = $this->appName();
  248. }
  249. $redirect = $app_name::externalLink($this->_controller)->action($this->_action)->params($e->params())->redirect(301);
  250. $redirect->fire();
  251. }
  252. /**
  253. * To be overloaded in child classes for doing constructor-like things;
  254. * called in the constructor.
  255. */
  256. protected function init() { }
  257. /**
  258. * To be overloaded in child classes for doing user init-like things
  259. * called in startSession()
  260. */
  261. protected function userInit() { }
  262. /**
  263. * Log an error and let the fallback controller decide what to do.
  264. * Assumes that a fallback controller will be present. (Not found errors
  265. * as well as any necessary magic on URLs not modeled by controllers can
  266. * happen here.)
  267. */
  268. protected function fallback ($logmsg)
  269. {
  270. $this->setController('fallback');
  271. $this->_action = 'index';
  272. }
  273. /**
  274. * Tell Blode to record this run of the application.
  275. * Child classes of Application can call this as-appropriate.
  276. */
  277. protected function blodeRun ()
  278. {
  279. $run = [
  280. 'event' => 'app.run',
  281. 'app' => get_class($this),
  282. ];
  283. Event::debug($run);
  284. }
  285. /**
  286. * Go down in flames if someone tries to make a copy of the application.
  287. */
  288. public function __clone ()
  289. {
  290. throw new Exception("why you try and make clone of Application? >:|");
  291. }
  292. /**
  293. * Return a Link modelling a URL/path for the given controller.
  294. * See the docs in that class for more detail. Should enable something like:
  295. *
  296. * <code>
  297. * echo $app->link('products')->id(666)->a('Arduino Duemilanove');
  298. * </code>
  299. *
  300. * @param string controller name
  301. * @return Link for given controller
  302. */
  303. public function link ($controller = null)
  304. {
  305. $base = 'https://' . static::$_hostname . $this->url();
  306. return new Link($base, $controller);
  307. }
  308. /**
  309. * Return a Link for another Application. This
  310. * should be better.
  311. *
  312. * @param string controller name
  313. * @return Link for given app/controller
  314. */
  315. public static function externalLink ($controller = null)
  316. {
  317. $base = 'https://' . static::$_hostname . static::$_root;
  318. return new Link($base, $controller);
  319. }
  320. /**
  321. * Return a Template for the given partial.
  322. *
  323. * Look in [template dir]/[application]/partials/ for corresponding
  324. * template files. If a controller name is specified as the second
  325. * parameter, use [template dir]/[application]/[controller]/ instead.
  326. *
  327. * @param string name of partial
  328. * @return Template instance for corresponding partial
  329. */
  330. public function partial ($name, $controller = null)
  331. {
  332. $partial = new \SparkLib\Template(null, array(
  333. 'app' => $this,
  334. 'ctl' => $this->_ctl
  335. ));
  336. if (null === $controller) {
  337. $partial->setTemplateDirRel($this->_templateDir . '/partials');
  338. } elseif (is_string($controller)) {
  339. $partial->setTemplateDirRel($this->_templateDir . '/' . $controller);
  340. } else {
  341. throw new Exception('If supplied, controller name must be a string.');
  342. }
  343. $partial->setTemplate($name . '.tpl.php');
  344. if (! $partial->templateFileExists()) {
  345. // we haaaaaaaaates it:
  346. $partial->setTemplateDir(\LIBDIR . 'templates/partials');
  347. }
  348. return $partial;
  349. }
  350. public function ctl () { return $this->_ctl; }
  351. /**
  352. * @return \SparkLib\Application current instance of Application
  353. */
  354. public static function app () { return static::$_instance; }
  355. /**
  356. * timestamp of the last deploy. Useful for bumping version numbers of static files.
  357. *
  358. * @return int timestamp
  359. */
  360. public function sparkrev ()
  361. {
  362. if ($this->_sparkrev)
  363. return $this->_sparkrev;
  364. $s = stat(BASEDIR . '/sparkrev');
  365. $this->_sparkrev = $s['mtime'];
  366. return $this->_sparkrev;
  367. }
  368. /**
  369. * Various other getters. These are getters, in case you're wondering, so
  370. * that they will be hard to mess with. If you're trying to change one
  371. * of the underlying values for some reason and there's not already a
  372. * method for it, talk to Brennen.
  373. */
  374. /**
  375. * Base path of the application.
  376. *
  377. * @return string url
  378. */
  379. public function baseUrl () { return static::$_root; }
  380. /**
  381. * How should external code point to this application.
  382. *
  383. * @return string url
  384. */
  385. public static function linkPath () { return static::$_root; }
  386. /**
  387. * URL of the dispatcher itself. Will likely be the same as baseUrl() if
  388. * URL rewriting is in place on the web server.
  389. *
  390. * @return string url
  391. */
  392. public function url () { return static::$_root . $this->_dispatcher; }
  393. /**
  394. * Full path of the request string without the request params
  395. *
  396. * @return string url
  397. */
  398. public function requestUrl ()
  399. {
  400. if (strpos($_SERVER['REQUEST_URI'], '?'))
  401. // is this faster than explode()? _dave
  402. return substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '?'));
  403. return $_SERVER['REQUEST_URI'];
  404. }
  405. /**
  406. * An object modeling the components of the referer, or false
  407. * if we can't parse same.
  408. *
  409. * Arguably maybe this should be chained off the request, but
  410. * for now I'm putting it here in the interest of less typing.
  411. */
  412. public function referer ()
  413. {
  414. if (! isset($this->_referer)) {
  415. $ref = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
  416. $this->_referer = new \SparkLib\URLParser($ref);
  417. }
  418. return $this->_referer;
  419. }
  420. /**
  421. * @return Environment for the current SAPI
  422. */
  423. public function env () { return $this->_env; }
  424. /**
  425. * @return Request which models the current request
  426. */
  427. public function req () { return $this->_req; }
  428. /**
  429. * @return Route which models the current route
  430. */
  431. public function route () { return $this->_route; }
  432. /**
  433. * @return string name of the current action
  434. */
  435. public function action () { return $this->_action; }
  436. /**
  437. * Set the current action.
  438. *
  439. * @param string action name
  440. */
  441. public function setAction ($action)
  442. {
  443. return $this->_action = $action;
  444. }
  445. /**
  446. * @return string name of the current controller
  447. */
  448. public function controller() { return $this->_controller; }
  449. /**
  450. * Camelcase a name_with_underscores and come up with a class name
  451. * like ApplicationWhateverName.
  452. *
  453. * @param string name of a controller
  454. * @return string name of a controller class
  455. */
  456. public function makeControllerName ($controller, $app_name_str = '')
  457. {
  458. $parts = explode('_', $controller);
  459. foreach ($parts as &$part) {
  460. $part = ucfirst($part);
  461. }
  462. if (! $app_name_str) {
  463. $app_name_str = get_class($this);
  464. }
  465. // this lives in Application\Controller
  466. $namespaced = $app_name_str . '\\' . implode('', $parts);
  467. // this tells us everything will be namespaced, so we can
  468. // shortcut right to using that one
  469. if ($this->_namespace)
  470. return $namespaced;
  471. // this lives in Application\ApplicationController
  472. // it is the old way and we hates it
  473. $un_namespaced = $app_name_str . implode('', $parts);
  474. if (class_exists($un_namespaced)) {
  475. return $un_namespaced;
  476. }
  477. return $namespaced;
  478. }
  479. /**
  480. * Return application name
  481. *
  482. * @author tylerc <tyler.cipriani@sparkfun.com>
  483. */
  484. public function appName() { return get_class($this); }
  485. /**
  486. * End the current session.
  487. */
  488. public function endSession ()
  489. {
  490. $this->_env->endSession();
  491. }
  492. /**
  493. * Stash a message in the session to be passed on to the user.
  494. */
  495. public function passMessage ($message, $type = 'error', $delay = 0, $fixed = false)
  496. {
  497. // replaces with full class?
  498. $messageO = new \stdClass();
  499. $messageO->type = $type;
  500. $messageO->delay = $delay;
  501. $messageO->fixed = $fixed;
  502. $messageO->text = $message;
  503. $_SESSION['messages'][] = $messageO;
  504. }
  505. public function getMessages ($type = null)
  506. {
  507. $messages = [];
  508. foreach ($_SESSION['messages'] as $message){
  509. if ($type == null || $message->type == $type){
  510. $messages[] = $message;
  511. }
  512. }
  513. // Reset user messages, we've seen these now.
  514. $_SESSION['messages'] = array();
  515. return $messages;
  516. }
  517. public function hasMessages ()
  518. {
  519. return count($_SESSION['messages']) > 0;
  520. }
  521. public function require_authentication ($redirect = null)
  522. {
  523. if (! $_SESSION['user']->isAuthenticated())
  524. throw new AuthenticationException($redirect);
  525. }
  526. }