PageRenderTime 46ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 1ms

/phocoa/framework/WFRequestController.php

https://github.com/SwissalpS/phocoa
PHP | 497 lines | 302 code | 41 blank | 154 comment | 72 complexity | acf7e35eb88788893a0616a1b2c594a7 MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4: */
  3. /**
  4. * @package WebApplication
  5. * @copyright Copyright (c) 2005 Alan Pinstein. All Rights Reserved.
  6. * @version $Id: kvcoding.php,v 1.3 2004/12/12 02:44:09 alanpinstein Exp $
  7. * @author Alan Pinstein <apinstein@mac.com>
  8. */
  9. /**
  10. * The RequestController object is a singleton controller for the entire request-act-respond cycle.
  11. *
  12. * Basically, the WFRequestController bootstraps the request into a WFModuleInvocation, executes it, and displays the output.
  13. *
  14. * It also has a top-level exception catcher for all uncaught exceptions and displays a friendly error message (or an informative one for development machines).
  15. */
  16. class WFRequestController extends WFObject
  17. {
  18. /**
  19. * @var object The root WFModuleInvocation object used for the request.
  20. */
  21. protected $rootModuleInvocation;
  22. function __construct()
  23. {
  24. }
  25. /* These are the errors that phocoa tries to roll-up and catch and report on. All other errors will be left to their default handling.
  26. E_ERROR
  27. // | E_WARNING
  28. | E_PARSE
  29. // | E_NOTICE
  30. | E_CORE_ERROR
  31. | E_CORE_WARNING
  32. | E_COMPILE_ERROR
  33. | E_COMPILE_WARNING
  34. | E_USER_ERROR
  35. // | E_USER_WARNING
  36. // | E_USER_NOTICE
  37. // | E_STRICT
  38. | E_RECOVERABLE_ERROR
  39. // | E_DEPRECATED
  40. // | E_USER_DEPRECATED
  41. */
  42. private $handleErrors = 4597;
  43. private function phpErrorAsString($e)
  44. {
  45. $el = array(
  46. 1 => 'E_ERROR',
  47. 2 => 'E_WARNING',
  48. 4 => 'E_PARSE',
  49. 8 => 'E_NOTICE',
  50. 16 => 'E_CORE_ERROR',
  51. 32 => 'E_CORE_WARNING',
  52. 64 => 'E_COMPILE_ERROR',
  53. 128 => 'E_COMPILE_WARNING',
  54. 256 => 'E_USER_ERROR',
  55. 512 => 'E_USER_WARNING',
  56. 1024 => 'E_USER_NOTICE',
  57. 2048 => 'E_STRICT',
  58. 4096 => 'E_RECOVERABLE_ERROR',
  59. 8192 => 'E_DEPRECATED',
  60. 16384 => 'E_USER_DEPRECATED',
  61. );
  62. return $el[$e];
  63. }
  64. /**
  65. * Error handler callback for PHP catchable errors; helps synthesize PHP errors and exceptions into the same handling workflow.
  66. */
  67. function handleError($errNum, $errString, $file, $line, $contextArray)
  68. {
  69. $errNum = $this->phpErrorAsString($errNum);
  70. $this->handleException( new ErrorException("{$errNum}: {$errString}\n\nAt {$file}:{$line}") );
  71. }
  72. /**
  73. * Error handler callback for PHP un-catchable errors; helps synthesize PHP errors and exceptions into the same handling workflow.
  74. */
  75. function checkShutdownForFatalErrors()
  76. {
  77. $last_error = error_get_last();
  78. if ($last_error['type'] & $this->handleErrors)
  79. {
  80. $last_error['type'] = $this->phpErrorAsString($last_error['type']);
  81. $this->handleException( new ErrorException("{$last_error['type']}: {$last_error['message']}\n\nAt {$last_error['file']}:{$last_error['line']}") );
  82. }
  83. }
  84. /**
  85. * PHOCOA's default error handling is to try to catch *all* fatal errors and run them through the framework's error handling system.
  86. *
  87. * The error handling system unifies excptions and errors into the same processing stream and additionally gives your application
  88. * an opportunity to handle the error as well. For instance your app may prefer to email out all errors,
  89. * or send them to an exception service like Hoptoad/Exceptional/Loggly
  90. */
  91. private function registerErrorHandlers()
  92. {
  93. // convert these errors into exceptions
  94. set_error_handler(array($this, 'handleError'), $this->handleErrors);
  95. // catch non-catchable errors
  96. register_shutdown_function(array($this, 'checkShutdownForFatalErrors'));
  97. }
  98. /**
  99. * Exception handler for the WFRequestController.
  100. *
  101. * This is basically the uncaught exception handler for the request cycle.
  102. * We want to have this in the request object because we want the result to be displayed within our skin system.
  103. * This function will display the appropriate error page based on the deployment mode for this machine, then exit.
  104. *
  105. * @param Exception The exception object to handle.
  106. */
  107. function handleException(Exception $e)
  108. {
  109. // give ourselves a little more memory so we can process the exception
  110. ini_set('memory_limit', memory_get_usage() + 25000000 /* 25MB */);
  111. // let the current module try to handle the exception
  112. if ($this->rootModuleInvocation)
  113. {
  114. $this->rootModuleInvocation->handleUncaughtException($e);
  115. }
  116. $webAppDelegate = WFWebApplication::sharedWebApplication()->delegate();
  117. if (is_object($webAppDelegate) && method_exists($webAppDelegate, 'handleUncaughtException'))
  118. {
  119. $handled = $webAppDelegate->handleUncaughtException($e);
  120. if ($handled) return;
  121. }
  122. WFExceptionReporting::log($e);
  123. // build stack of errors (php 5.3+)
  124. if (method_exists($e, 'getPrevious'))
  125. {
  126. $tmpE = $e;
  127. $allExceptions = array();
  128. do {
  129. $allExceptions[] = $tmpE;
  130. } while ($tmpE = $tmpE->getPrevious());
  131. }
  132. else
  133. {
  134. $allExceptions = array($e);
  135. }
  136. $exceptionPage = new WFSmarty();
  137. $exceptionPage->assign('exceptions', $allExceptions);
  138. $exceptionPage->assign('exceptionClass', get_class($allExceptions[0]));
  139. $exceptionPage->assign('home_url', WWW_ROOT . '/');
  140. // modern format
  141. $standardErrorData = WFExceptionReporting::generatedStandardizedErrorDataFromException($e);
  142. $exceptionPage->assign('location', "http://{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}");
  143. $exceptionPage->assign('headline', "{$standardErrorData[0]['title']}: {$standardErrorData[0]['message']}");
  144. $exceptionPage->assign('standardErrorData', $standardErrorData);
  145. $exceptionPage->assign('standardErrorDataJSON', WFJSON::encode(array(
  146. 'error' => $standardErrorData,
  147. '$_SERVER' => $_SERVER,
  148. '$_REQUEST' => $_REQUEST,
  149. '$_SESSION' => $_SESSION,
  150. )));
  151. // @todo refactor these templates to use WFExceptionReporting::generatedStandardizedErrorDataFromException($e)
  152. if (IS_PRODUCTION)
  153. {
  154. $exceptionPage->setTemplate(WFWebApplication::appDirPath(WFWebApplication::DIR_SMARTY) . '/app_error_user.tpl');
  155. }
  156. else
  157. {
  158. $exceptionPage->setTemplate(WFWebApplication::appDirPath(WFWebApplication::DIR_SMARTY) . '/app_error_developer.tpl');
  159. }
  160. // display the error and exit
  161. $body_html = $exceptionPage->render(false);
  162. // output error info
  163. header("HTTP/1.0 500 Uncaught Exception");
  164. if (self::isAjax())
  165. {
  166. print strip_tags($body_html);
  167. }
  168. else
  169. {
  170. $skin = new WFSkin();
  171. $skin->setDelegateName(WFWebApplication::sharedWebApplication()->defaultSkinDelegate());
  172. $skin->setBody($body_html);
  173. $skin->setTitle("An error has occurred.");
  174. $skin->render();
  175. }
  176. exit;
  177. }
  178. /**
  179. * Run the web application for the current request.
  180. *
  181. * NOTE: Both a module and page must be specified in the URL. If they are not BOTH specified, the server will REDIRECT the request to the full URL.
  182. * Therefore, you should be sure that when posting form data to a module/page, you use a full path. {@link WFRequestController::WFURL}
  183. *
  184. * Will pass control onto the current module for processing.
  185. *
  186. * Create a WFModuleInvocation based on the current HTTP Request, get the results, and output the completed web page.
  187. *
  188. * @todo Handle 404 situation better -- need to be able to detect this nicely from WFModuleInvocation.. maybe an Exception subclass?
  189. * @todo PATH_INFO with multiple params, where one is blank, isn't working correctly. IE, /url/a//c gets turned into /url/a/c for PATH_INFO thus we skip a "null" param.
  190. * NOTE: Partial solution; use /WFNull/ to indicate NULL param instead of // until we figure something out.
  191. * NOTE: Recent change to REQUEST_URI instead of PATH_INFO to solve decoding problem seems to have also solved the // => / conversion problem... test more!
  192. * WORRY: That the new PATH_INFO calculation will fail when using aliases other than WWW_ROOT. IE: /products/myProduct might break it...
  193. * @todo The set_error_handler doesn't seem to work very well. PHP issue? Or am I doing it wrong? For instance, it doesn't catch $obj->nonExistantMethod().
  194. */
  195. function handleHTTPRequest()
  196. {
  197. // point all error handling to phocoa's internal mechanisms since anything that happens after this line (try) will be routed through the framework's handler
  198. $this->registerErrorHandlers();
  199. try {
  200. $relativeURI = parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH); // need to run this to convert absolute URI's to relative ones (sent by SOME http clients)
  201. if ($relativeURI === false) throw new WFRequestController_NotFoundException("Malformed URI: {$_SERVER['REQUEST_URI']}");
  202. $modInvocationPath = ltrim(substr($relativeURI, strlen(WWW_ROOT)), '/');
  203. $paramsPos = strpos($modInvocationPath, '?');
  204. if ($paramsPos !== false)
  205. {
  206. $modInvocationPath = substr($modInvocationPath, 0, $paramsPos);
  207. }
  208. if ($modInvocationPath == '')
  209. {
  210. $modInvocationPath = WFWebApplication::sharedWebApplication()->defaultInvocationPath();
  211. }
  212. // allow routing delegate to munge modInvocationPath
  213. $webAppDelegate = WFWebApplication::sharedWebApplication()->delegate();
  214. if (is_object($webAppDelegate) && method_exists($webAppDelegate, 'rerouteInvocationPath'))
  215. {
  216. $newInvocationPath = $webAppDelegate->rerouteInvocationPath($modInvocationPath);
  217. if ($newInvocationPath)
  218. {
  219. $modInvocationPath = $newInvocationPath;
  220. }
  221. }
  222. // create the root invocation; only skin if we're not in an XHR
  223. $this->rootModuleInvocation = new WFModuleInvocation($modInvocationPath, NULL, (self::isAjax() ? NULL : WFWebApplication::sharedWebApplication()->defaultSkinDelegate()) );
  224. // get HTML result of the module, and output it
  225. $html = $this->rootModuleInvocation->execute();
  226. // respond to WFRPC::PARAM_ENABLE_AJAX_IFRAME_RESPONSE_MODE for iframe-targeted XHR. Some XHR requests (ie uploads) must be done by creating an iframe and targeting the form
  227. // post to the iframe rather than using XHR (since XHR doesn't support uploads methinks). This WFRPC flag makes these such "ajax" requests need to be wrapped slightly differently
  228. // to prevent the HTML returned in the IFRAME from executing in the IFRAME which would cause errors.
  229. if (isset($_REQUEST[WFRPC::PARAM_ENABLE_AJAX_IFRAME_RESPONSE_MODE]) && $_REQUEST[WFRPC::PARAM_ENABLE_AJAX_IFRAME_RESPONSE_MODE] == 1)
  230. {
  231. header('Content-Type: text/xml');
  232. $html = "<"."?xml version=\"1.0\"?"."><raw><![CDATA[\n{$html}\n]]></raw>";
  233. }
  234. print $html;
  235. } catch (WFPaginatorException $e) {
  236. // paginator fails by default should "route" to bad request. This keeps bots from going crazy.
  237. header("HTTP/1.0 400 Bad Request");
  238. print "Bad Request: " . $e->getMessage();
  239. exit;
  240. } catch (WFRequestController_RedirectException $e) {
  241. header("HTTP/1.1 {$e->getCode()}");
  242. header("Location: {$e->getRedirectURL()}");
  243. exit;
  244. } catch (WFRequestController_HTTPException $e) {
  245. header("HTTP/1.0 {$e->getCode()}");
  246. print $e->getMessage();
  247. exit;
  248. } catch (WFRequestController_BadRequestException $e) {
  249. header("HTTP/1.0 400 Bad Request");
  250. print "Bad Request: " . $e->getMessage();
  251. exit;
  252. } catch (WFRequestController_NotFoundException $e) {
  253. header("HTTP/1.0 404 Not Found");
  254. print $e->getMessage();
  255. exit;
  256. } catch (WFRequestController_InternalRedirectException $e) {
  257. // internal redirect are handled without going back to the browser... a little bit of hacking here to process a new invocationPath as a "request"
  258. // @todo - not sure what consequences this has on $_REQUEST; seems that they'd probably stay intact which could foul things up?
  259. $_SERVER['REQUEST_URI'] = $e->getRedirectURL();
  260. WFLog::log("Internal redirect to: {$_SERVER['REQUEST_URI']}");
  261. self::handleHTTPRequest();
  262. exit;
  263. } catch (WFRedirectRequestException $e) {
  264. header("Location: " . $e->getRedirectURL());
  265. exit;
  266. } catch (Exception $e) {
  267. $this->handleException($e);
  268. }
  269. }
  270. /**
  271. * Is the current request an XHR (XmlHTTPRequest)?
  272. *
  273. * @return boolean
  274. */
  275. static function isAjax()
  276. {
  277. if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) and strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') return true;
  278. if (isset($_REQUEST['HTTP_X_REQUESTED_WITH']) and strtolower($_REQUEST['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') return true; // for debugging
  279. return false;
  280. }
  281. /**
  282. * Determine whether the current request is from a mobile browers
  283. *
  284. *
  285. * @return boolean
  286. */
  287. private $isMobileBrowser = NULL;
  288. function isMobileBrowser()
  289. {
  290. if ($this->isMobileBrowser !== NULL) return $this->isMobileBrowser;
  291. $op = (isset($_SERVER['HTTP_X_OPERAMINI_PHONE']) ? strtolower($_SERVER['HTTP_X_OPERAMINI_PHONE']) : '');
  292. $ua = (isset($_SERVER['HTTP_USER_AGENT']) ? strtolower($_SERVER['HTTP_USER_AGENT']) : '');
  293. $ac = (isset($_SERVER['HTTP_ACCEPT']) ? strtolower($_SERVER['HTTP_ACCEPT']) : '');
  294. $this->isMobileBrowser = strpos($ac, 'application/vnd.wap.xhtml+xml') !== false
  295. || $op != ''
  296. || strpos($ua, 'sony') !== false
  297. || strpos($ua, 'symbian') !== false
  298. || strpos($ua, 'nokia') !== false
  299. || strpos($ua, 'samsung') !== false
  300. || strpos($ua, 'mobile') !== false
  301. || strpos($ua, 'windows ce') !== false
  302. || strpos($ua, 'epoc') !== false
  303. || strpos($ua, 'opera mini') !== false
  304. || strpos($ua, 'nitro') !== false
  305. || strpos($ua, 'j2me') !== false
  306. || strpos($ua, 'midp-') !== false
  307. || strpos($ua, 'cldc-') !== false
  308. || strpos($ua, 'netfront') !== false
  309. || strpos($ua, 'mot') !== false
  310. || strpos($ua, 'up.browser') !== false
  311. || strpos($ua, 'up.link') !== false
  312. || strpos($ua, 'audiovox') !== false
  313. || strpos($ua, 'blackberry') !== false
  314. || strpos($ua, 'ericsson,') !== false
  315. || strpos($ua, 'panasonic') !== false
  316. || strpos($ua, 'philips') !== false
  317. || strpos($ua, 'sanyo') !== false
  318. || strpos($ua, 'sharp') !== false
  319. || strpos($ua, 'sie-') !== false
  320. || strpos($ua, 'portalmmm') !== false
  321. || strpos($ua, 'blazer') !== false
  322. || strpos($ua, 'avantgo') !== false
  323. || strpos($ua, 'danger') !== false
  324. || strpos($ua, 'palm') !== false
  325. || strpos($ua, 'series60') !== false
  326. || strpos($ua, 'palmsource') !== false
  327. || strpos($ua, 'pocketpc') !== false
  328. || strpos($ua, 'smartphone') !== false
  329. || strpos($ua, 'rover') !== false
  330. || strpos($ua, 'ipaq') !== false
  331. || strpos($ua, 'au-mic,') !== false
  332. || strpos($ua, 'alcatel') !== false
  333. || strpos($ua, 'ericy') !== false
  334. || strpos($ua, 'up.link') !== false
  335. || strpos($ua, 'vodafone/') !== false
  336. || strpos($ua, 'wap1.') !== false
  337. || strpos($ua, 'wap2.') !== false;
  338. return $this->isMobileBrowser;
  339. }
  340. /**
  341. * Get the root {@link WFModuleInvocation} used by the request controller.
  342. *
  343. * @return object The root WFModuleInvocation for the page.
  344. */
  345. function rootModuleInvocation()
  346. {
  347. return $this->rootModuleInvocation;
  348. }
  349. /**
  350. * Get a reference to this WFRequestController's skin object.
  351. *
  352. * @static
  353. * @return object A reference to the {@link WFSkin} object.
  354. * @deprecated Use $module->rootSkin()
  355. */
  356. public static function sharedSkin()
  357. {
  358. $rc = WFRequestController::sharedRequestController();
  359. $rootInv = $rc->rootModuleInvocation();
  360. if (!$rootInv) throw( new Exception("No root invocation, thus no shared skin..") );
  361. return $rootInv->skin();
  362. }
  363. /**
  364. * Generate a "full" URL to the given module / page.
  365. *
  366. * It is recommended to use this function to generate all URL's to pages in the application.
  367. * Of course you may append some PATH_INFO or params afterwards.
  368. *
  369. * Guaranteed to *never* end in a trailing slash. Always add your own if you are addition additional stuff to the URL.
  370. *
  371. * @static
  372. * @param string The module name (required).
  373. * @param string The page name (or NULL to use the default page).
  374. * @return string a RELATIVE URL to the requested module/page.
  375. */
  376. public static function WFURL($moduleName, $pageName = NULL)
  377. {
  378. $moduleName = ltrim($moduleName, '/'); // just in case a '/path' path is passed, we normalize it for our needs.
  379. if (empty($moduleName)) throw( new Exception("Module is required to generate a WFURL.") );
  380. $url = WWW_ROOT . '/' . $moduleName;
  381. if ($pageName !== NULL)
  382. {
  383. $url .= '/' . $pageName;
  384. }
  385. return $url;
  386. }
  387. /**
  388. * Get a reference to the shared WFRequestController object.
  389. * @static
  390. * @return object The WFRequestController object.
  391. */
  392. public static function sharedRequestController()
  393. {
  394. static $singleton = NULL;
  395. if (!$singleton) {
  396. $singleton = new WFRequestController();
  397. }
  398. return $singleton;
  399. }
  400. }
  401. /**
  402. * Helper class to allow modules to easily redirect the client to a given URL.
  403. *
  404. * Modules can throw a WFRedirectRequestException anytime to force the client to redirect.
  405. *
  406. * @deprecated
  407. * @see WFRequestController_RedirectException
  408. */
  409. class WFRedirectRequestException extends WFException
  410. {
  411. protected $redirectUrl;
  412. function __construct($message = NULL, $code = 0)
  413. {
  414. parent::__construct($message, $code);
  415. $this->redirectUrl = $message;
  416. }
  417. function getRedirectURL()
  418. {
  419. return $this->redirectUrl;
  420. }
  421. }
  422. class WFRequestController_InternalRedirectException extends WFRedirectRequestException {}
  423. class WFRequestController_NotFoundException extends WFException {}
  424. class WFRequestController_BadRequestException extends WFException {}
  425. class WFRequestController_HTTPException extends WFException
  426. {
  427. public function __construct($message = NULL, $code = 500) { parent::__construct($message, $code); }
  428. }
  429. class WFRequestController_RedirectException extends WFRequestController_HTTPException
  430. {
  431. protected $redirectUrl;
  432. /**
  433. * By default use http code 302, but allow user to override it
  434. * to use e.g. a 301.
  435. */
  436. public function __construct($url, $code = 302)
  437. {
  438. $this->redirectUrl = $url;
  439. return parent::__construct($url, $code);
  440. }
  441. public function getRedirectURL()
  442. {
  443. return $this->redirectUrl;
  444. }
  445. }
  446. /**
  447. * There are certain classes that are needed to successfully handle errors. We need to make sure that they are loaded up front so that
  448. * they don't need to be autoloaded during error handling, which can result in errors during error handling such as:
  449. *
  450. * - Fatal error: Class declarations may not be nested in /Users/alanpinstein/dev/sandbox/showcaseng/showcaseng/externals/phocoa/phocoa/framework/WFExceptionReporting.php on line 14
  451. *
  452. * Simply running a class_exists on each class will force an autoload when WFRequestController is parsed, preventing the problem.
  453. * We don't do a hard require('file.php') here since that would break our automated opcode-cache-friendly require('/full/path/to/file.php') system in phocoa's autoloader.
  454. */
  455. class_exists('WFExceptionReporting');
  456. ?>