PageRenderTime 28ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/webphp/web.php

http://webpy-php-port.googlecode.com/
PHP | 734 lines | 396 code | 98 blank | 240 comment | 69 complexity | 54fcd4fc7ad6a1d1bd8f86c35973b641 MD5 | raw file
  1. <?php
  2. include_once 'exceptions/RequestErrorException.php';
  3. /**
  4. * web loader
  5. */
  6. class Web
  7. {
  8. protected $_baseURLPath = '/';
  9. protected $_urls = null;
  10. protected $_requestUrl = null;
  11. public $params = null;
  12. protected $_debug = false;
  13. protected $_debugMessages = array();
  14. protected $_layoutFile = null;
  15. protected $_layoutDir = null;
  16. protected $_useLayout = false;
  17. protected $_renderedText = null;
  18. protected $_tpl = null;
  19. static protected $_plugins = array();
  20. static protected $_instance = NULL;
  21. public function __construct($baseURLPath = null)
  22. {
  23. if (!is_null($baseURLPath)) {
  24. $this->_baseURLPath = $baseURLPath;
  25. }
  26. }
  27. /**
  28. * call a registered plugin
  29. * it uses call_user_func_array which can be slow
  30. * over many iterations
  31. *
  32. * @param string $method
  33. * @param string $args
  34. * @return void
  35. * @author Kenrick Buchanan
  36. */
  37. private function __call($method, $args)
  38. {
  39. if (in_array($method, self::$_plugins)) {
  40. array_unshift($args, $this);
  41. return call_user_func_array($method, $args);
  42. }
  43. }
  44. /**
  45. * register a plugin to call via __call
  46. * first argument passed to function is an
  47. * instance of web.
  48. *
  49. * @param string $func
  50. * @return void
  51. * @author Kenrick Buchanan
  52. */
  53. public static function registerPlugin($func)
  54. {
  55. self::$_plugins[] = $func;
  56. }
  57. /**
  58. * return _baseURLPath
  59. * @param string baseURLPath
  60. * @return string _baseURLPath
  61. * @author Kenrick Buchanan
  62. */
  63. function baseURL($baseURLPath=null)
  64. {
  65. if (!is_null($baseURLPath)) {
  66. $this->_baseURLPath = $baseURLPath;
  67. }
  68. return $this->_baseURLPath;
  69. }
  70. /**
  71. * turn on/off debugging. output is printed to screen
  72. *
  73. * @param string $onoff
  74. * @return void
  75. * @author Kenrick Buchanan
  76. */
  77. public function debug($onoff = null)
  78. {
  79. if (!is_null($onoff)) {
  80. $this->_debug = $onoff;
  81. } else {
  82. return $this->_debug;
  83. }
  84. }
  85. /**
  86. * save debug messages to var for displaying later.
  87. *
  88. * @param string $msg
  89. * @return void
  90. * @author Kenrick Buchanan
  91. */
  92. public function debugMsg($msg)
  93. {
  94. $this->_debugMessages[] = $msg;
  95. }
  96. /**
  97. * display debug messages
  98. *
  99. * @return void
  100. * @author Kenrick Buchanan
  101. */
  102. public function debugDisplay()
  103. {
  104. if (!$this->_debugMessages
  105. || !is_array($this->_debugMessages)) {
  106. return;
  107. }
  108. echo "<h2>Debug Messages</h2>\n<ol>";
  109. foreach ($this->_debugMessages as $msg) {
  110. printf("<li>%s</li>", $msg);
  111. }
  112. echo "</ol>";
  113. }
  114. /**
  115. * create instance of Web object. Only use this method
  116. * to get an instance of Web.
  117. *
  118. * @author Kenrick Buchanan
  119. */
  120. public static function &instance()
  121. {
  122. if (self::$_instance == NULL) {
  123. self::$_instance = new Web();
  124. }
  125. return self::$_instance;
  126. }
  127. /**
  128. * don't allow cloning of the web instance
  129. *
  130. * @return void
  131. * @author Kenrick Buchanan
  132. */
  133. public final function __clone()
  134. {
  135. trigger_error("You can not clone an instance of the web class", E_USER_ERROR);
  136. }
  137. /**
  138. * requestUri
  139. *
  140. * inspects $_SERVER['REQUEST_URI'] and returns a sanitized
  141. * path without a leading/trailing slashes
  142. * @return void
  143. * @author Kenrick Buchanan
  144. */
  145. private function requestUri()
  146. {
  147. // have seen apache set either or.
  148. $uri = isset($_SERVER['REDIRECT_URL']) ? $_SERVER['REDIRECT_URL']
  149. : $_SERVER['REQUEST_URI'];
  150. // sanitize it
  151. $uri = filter_var($uri, FILTER_SANITIZE_URL);
  152. // display URL
  153. $this->debugMsg('URI is: '.htmlspecialchars($uri));
  154. // kill query string off REQUEST_URI
  155. if ( strpos($uri,'?') !== false ) {
  156. $uri = substr($uri,0,strpos($uri,'?'));
  157. }
  158. // ok knock off the _baseURLPath
  159. if (strlen($this->_baseURLPath) > 1) {
  160. $this->debugMsg("baseURLPath is: {$this->_baseURLPath}");
  161. $uri = str_replace($this->_baseURLPath, '', $uri);
  162. }
  163. // strip off first slash
  164. if(substr($uri, 0, 1) == "/"){
  165. $uri = substr($uri, 1);
  166. }
  167. // knock off last slash
  168. if (substr($uri, -1) == "/") {
  169. $uri = substr($uri, 0, -1);
  170. }
  171. $this->request_uri = $uri;
  172. }
  173. /**
  174. * send 303 by default so browser won't cache the 200 or 301 headers
  175. *
  176. * @param string $location
  177. * @param string $status
  178. * @return void
  179. * @author Kenrick Buchanan
  180. */
  181. public static function redirect($location, $status=303)
  182. {
  183. self::httpHeader($status);
  184. header("Location: $location");
  185. }
  186. /**
  187. * send header code to browser
  188. *
  189. * @param string $code
  190. * @return void
  191. * @author Kenrick Buchanan
  192. */
  193. public static function httpHeader($code)
  194. {
  195. $http = array (
  196. 100 => "HTTP/1.1 100 Continue",
  197. 101 => "HTTP/1.1 101 Switching Protocols",
  198. 200 => "HTTP/1.1 200 OK",
  199. 201 => "HTTP/1.1 201 Created",
  200. 202 => "HTTP/1.1 202 Accepted",
  201. 203 => "HTTP/1.1 203 Non-Authoritative Information",
  202. 204 => "HTTP/1.1 204 No Content",
  203. 205 => "HTTP/1.1 205 Reset Content",
  204. 206 => "HTTP/1.1 206 Partial Content",
  205. 300 => "HTTP/1.1 300 Multiple Choices",
  206. 301 => "HTTP/1.1 301 Moved Permanently",
  207. 302 => "HTTP/1.1 302 Found",
  208. 303 => "HTTP/1.1 303 See Other",
  209. 304 => "HTTP/1.1 304 Not Modified",
  210. 305 => "HTTP/1.1 305 Use Proxy",
  211. 307 => "HTTP/1.1 307 Temporary Redirect",
  212. 400 => "HTTP/1.1 400 Bad Request",
  213. 401 => "HTTP/1.1 401 Unauthorized",
  214. 402 => "HTTP/1.1 402 Payment Required",
  215. 403 => "HTTP/1.1 403 Forbidden",
  216. 404 => "HTTP/1.1 404 Not Found",
  217. 405 => "HTTP/1.1 405 Method Not Allowed",
  218. 406 => "HTTP/1.1 406 Not Acceptable",
  219. 407 => "HTTP/1.1 407 Proxy Authentication Required",
  220. 408 => "HTTP/1.1 408 Request Time-out",
  221. 409 => "HTTP/1.1 409 Conflict",
  222. 410 => "HTTP/1.1 410 Gone",
  223. 411 => "HTTP/1.1 411 Length Required",
  224. 412 => "HTTP/1.1 412 Precondition Failed",
  225. 413 => "HTTP/1.1 413 Request Entity Too Large",
  226. 414 => "HTTP/1.1 414 Request-URI Too Large",
  227. 415 => "HTTP/1.1 415 Unsupported Media Type",
  228. 416 => "HTTP/1.1 416 Requested range not satisfiable",
  229. 417 => "HTTP/1.1 417 Expectation Failed",
  230. 500 => "HTTP/1.1 500 Internal Server Error",
  231. 501 => "HTTP/1.1 501 Not Implemented",
  232. 502 => "HTTP/1.1 502 Bad Gateway",
  233. 503 => "HTTP/1.1 503 Service Unavailable",
  234. 504 => "HTTP/1.1 504 Gateway Time-out"
  235. );
  236. header($http[$code]);
  237. }
  238. /**
  239. * inspect urls, find matched class and then run requested method
  240. *
  241. * @param string $array
  242. * @param string $_baseURLPath
  243. * @return void
  244. * @author Kenrick Buchanan
  245. */
  246. public static function run(array $urls, $baseURLPath = null)
  247. {
  248. if (empty($urls)) {
  249. throw new Exception("You must pass an array of valid urls to web::run()");
  250. return;
  251. }
  252. // get instance of Web
  253. $instance = self::instance();
  254. $instance->baseUrl($baseURLPath);
  255. // process the request uri
  256. $instance->requestUri();
  257. // debug
  258. $instance->debugMsg('START URL matching');
  259. foreach ($urls as $url_path => $options) {
  260. $instance->debugMsg(htmlspecialchars($url_path) .
  261. ' : '.
  262. htmlspecialchars($instance->request_uri));
  263. if (preg_match($url_path, $instance->request_uri, $matches)) {
  264. // assuming pattern => class in URLS array
  265. if (is_string($options)) {
  266. $saved = $options;
  267. $options = array();
  268. $options['module'] = $saved;
  269. unset($saved);
  270. }
  271. if ($options) {
  272. $route = array_merge($matches, $options);
  273. } else {
  274. $route = $matches;
  275. }
  276. unset($matches);
  277. $instance->params = $route;
  278. $instance->debugMsg('Matched URL: '.htmlspecialchars($url_path));
  279. break;
  280. }
  281. }
  282. // if there is no uri match - module not at least set, throw a 404 error.
  283. if (!array_key_exists('module', $route)) {
  284. throw new RequestErrorException("Page not found.", 404);
  285. return;
  286. }
  287. // lets check matches for named patterns
  288. // be aware this is somewhat tied into __autoload function
  289. $class_to_load = $route['module'];
  290. // now check that module exists:
  291. // finds it based on __autoload function
  292. if (!class_exists($class_to_load)) {
  293. throw new RequestErrorException("Page not found.", 404);
  294. return;
  295. }
  296. // ok now check if there was an 'action', if action is named index
  297. // it just loads the forementioned module
  298. if (array_key_exists('class', $route)
  299. && $route['class'] != ''
  300. && $route['class'] != $route['module']) {
  301. // do an index check.
  302. if ($route['class'] != 'index') {
  303. $class_to_load = $route['class'];
  304. }
  305. // now check that the requested action class exists,
  306. // this assumes that it has been included because of the module
  307. // check from above.
  308. if (!class_exists($class_to_load)) {
  309. throw new RequestErrorException("Page not found.", 404);
  310. return;
  311. }
  312. }
  313. // instantiate class
  314. $instance->debugMsg("Loading Class: <b>$class_to_load</b>");
  315. $loaded_class = new $class_to_load();
  316. // see if class has any pre-run hooks
  317. $instance->debugMsg('Checking for preRun method');
  318. if (method_exists($loaded_class, 'preRun')) {
  319. $instance->debugMsg('Calling for preRun method');
  320. $retval = $loaded_class->preRun();
  321. // if pre-run hook returns false, stop processing.
  322. if($retval === false) {
  323. return;
  324. }
  325. }
  326. // check for class method based on REQUEST_METHOD
  327. $method = $_SERVER['REQUEST_METHOD'];
  328. $instance->debugMsg("About to run method: $method");
  329. // ajax hook
  330. if ($instance->isAjaxRequest()) {
  331. $method = "AJAX";
  332. }
  333. // whitelist of allowed method
  334. if (!in_array($method, array('GET', 'POST', 'PUT', 'DELETE', 'AJAX'))) {
  335. throw new RequestErrorException("HTTP Method not supported.", 405);
  336. return;
  337. }
  338. // see if currently loaded class even supports it
  339. if (!method_exists($loaded_class, $_SERVER['REQUEST_METHOD'])) {
  340. throw new RequestErrorException("HTTP Method not supported by class.", 405);
  341. return;
  342. }
  343. // run request
  344. $loaded_class->$method();
  345. $instance->debugMsg('Checking for postRun method');
  346. // see if class has any post-run hooks
  347. if (method_exists($loaded_class, 'postRun')) {
  348. $instance->debugMsg('Calling postRun method');
  349. $retval = $loaded_class->postRun();
  350. // if post-run hook returns false, stop processing.
  351. if($retval === false) {
  352. return;
  353. }
  354. }
  355. }
  356. /**
  357. * inspect headers to see if request is of ajax variety
  358. *
  359. * @return void
  360. * @author Kenrick Buchanan
  361. */
  362. private function isAjaxRequest()
  363. {
  364. return ( isset($_SERVER['HTTP_X_REQUESTED_WITH'])
  365. && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest');
  366. }
  367. /**
  368. * getTemplate
  369. * creates instance of template object
  370. *
  371. * @return object tpl instance
  372. * @author Kenrick Buchanan
  373. */
  374. public function &getTemplate()
  375. {
  376. if (!$this->_tpl) {
  377. require_once 'lib/Savant3/Savant3.php';
  378. $this->_tpl = new Savant3();
  379. }
  380. return $this->_tpl;
  381. }
  382. /**
  383. * use savant3 to render template from a file
  384. * echo Web::render('template.html', $tplvars);
  385. *
  386. * @param string $file
  387. * @param array $tpl_vars
  388. * @return void
  389. * @author Kenrick Buchanan
  390. */
  391. public static function render($file, array $tpl_vars=null)
  392. {
  393. $instance = self::instance();
  394. $_tpl = $instance->getTemplate();
  395. // the only real magic, its really for convenice.
  396. // if you included the class via autoload, it will try and look
  397. // in an expected TEMPLATES_DIR for the file to render.
  398. if (defined('TEMPLATES_DIR')) {
  399. $_tpl->addPath('template', TEMPLATES_DIR);
  400. $instance->debugMsg("Added template path: ". TEMPLATES_DIR);
  401. }
  402. // assign template vars by copy?
  403. // if you need to assign something by reference to the tpl, just do this:
  404. // Web::instance()->getTemplate()->assignRef('name','value);
  405. if($tpl_vars) {
  406. $_tpl->assign($tpl_vars);
  407. $instance->debugMsg("Assigned these variables to template: <b>".
  408. implode(", ", array_keys($tpl_vars))."</b>");
  409. }
  410. $instance->debugMsg("Loading template: $file");
  411. $output = $_tpl->fetch($file);
  412. // check for template errors
  413. if ($_tpl->isError($output)) {
  414. $instance->debugMsg("<pre>".htmlspecialchars(print_r($output, 1))."</pre>");
  415. return "Failed to load template: ".basename($file);
  416. }
  417. // wrap content in layout. if its being used
  418. if ($instance->_useLayout && $instance->_layoutFile) {
  419. $instance->debugMsg("Loading layout template: {$instance->_layoutFile}");
  420. $_tpl->addPath('template', $instance->_layoutDir);
  421. $_tpl->assign('content_for_layout', $output);
  422. $output = $_tpl->fetch($instance->_layoutFile);
  423. // check for template errors
  424. if ($_tpl->isError($output)) {
  425. $instance->debugMsg("<pre>".htmlspecialchars(print_r($output, 1))."</pre>");
  426. return "Failed to load layout template: ".basename($instance->_layoutFile);
  427. }
  428. }
  429. return $output;
  430. }
  431. /**
  432. * useLayout - sets the layout file to use when rendering
  433. * if set to false, then layout rendering is bypassed
  434. * echo Web::layout('file.php')->render('template.html', $tplvars);
  435. *
  436. * @param mixed $file
  437. * @return void
  438. * @author Kenrick Buchanan
  439. */
  440. public static function layout($file=null,$dir=null)
  441. {
  442. $instance = self::instance();
  443. // quick switch, turn off layout if $file is FALSE
  444. if ($file === false) {
  445. $instance->_useLayout = false;
  446. return;
  447. }
  448. $_tpl = $instance->getTemplate();
  449. // will check this dir first
  450. if (defined('LAYOUT_DIR')) {
  451. $instance->setLayoutDirectory(LAYOUT_DIR);
  452. }
  453. // if dir is set, it will overide
  454. if ($dir !== null) {
  455. $instance->setLayoutDirectory($dir);
  456. }
  457. if ($file !== null) {
  458. $instance->useLayout($file);
  459. }
  460. $instance->_useLayout = true;
  461. return $instance;
  462. }
  463. /**
  464. * setLayoutDirectory
  465. * sets the directory path of layouts
  466. *
  467. * @param string $dir
  468. * @return void
  469. * @author Kenrick Buchanan
  470. */
  471. public function setLayoutDirectory($dir)
  472. {
  473. $this->_layoutDir = $dir;
  474. return $this;
  475. }
  476. /**
  477. * useLayout
  478. *
  479. * @param string $filename
  480. * @return $this
  481. * @author Kenrick Buchanan
  482. */
  483. public function useLayout($filename)
  484. {
  485. if($filename === false) {
  486. $this->_layoutFile = null;
  487. } else {
  488. $this->_layoutFile = $filename;
  489. return $this;
  490. }
  491. }
  492. /**
  493. * send a request to a url and get back the response
  494. * useful for background requests
  495. * dependent upon allow_url_fopen being turned on
  496. * http://us3.php.net/manual/en/filesystem.configuration.php#ini.allow-url-fopen
  497. * if allow_url_fopen is off, it will try curl
  498. *
  499. * @param string $url
  500. * @param array $data
  501. * @param string $method http method
  502. * @param array $optional_headers
  503. * @return string $response
  504. * @throws RequestErrorException 400 on bad request
  505. * @author Kenrick Buchanan
  506. */
  507. public function request($url, array $data=null, $method='POST', array $optional_headers=null)
  508. {
  509. self::instance()->debugMsg('Sending a request via fopen to: '.$url);
  510. if (!$on = ini_get('allow_url_fopen')) {
  511. return self::curlRequest($url, $data, $method, $optional_headers);
  512. }
  513. $params = array('http' => array(
  514. 'method' => $method,
  515. 'content' => http_build_query($data)
  516. ));
  517. if ($optional_headers !== null) {
  518. $params['http']['header'] = $optional_headers;
  519. }
  520. $ctx = stream_context_create($params);
  521. $fp = fopen($url, 'rb', false, $ctx);
  522. if (!$fp) {
  523. throw new RequestErrorException("Problem with $url, $php_errormsg", 400);
  524. }
  525. $response = stream_get_contents($fp);
  526. if ($response === false) {
  527. throw new RequestErrorException("Problem reading data from $url, $php_errormsg", 400);
  528. }
  529. return $response;
  530. }
  531. /**
  532. * request a url via curl instead of fopen if allow_url_fopen is off
  533. * takes the same parameters as request, the is called by self::request()
  534. * if allow_url_fopen is off.
  535. *
  536. * @param string $url
  537. * @param array $data
  538. * @param string $method
  539. * @param string $optional_headers
  540. * @return string $response
  541. * @throws RequestErrorException
  542. * @author Kenrick Buchanan
  543. */
  544. function curlRequest($url, array $data=null, $method='POST', array $optional_headers=null)
  545. {
  546. self::instance()->debugMsg('Sending a request via CURL to: '.$url);
  547. $ch = curl_init();
  548. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  549. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
  550. if (!is_null($optional_headers)) {
  551. curl_setopt($ch, CURLOPT_HTTPHEADER, $optional_headers);
  552. }
  553. // check method
  554. if ($method == 'POST') {
  555. curl_setopt($ch, CURLOPT_POST, true);
  556. curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  557. curl_setopt($ch, CURLOPT_URL, $url);
  558. } else {
  559. if (!empty($data)) {
  560. $url .= '?'.http_build_query($data);
  561. }
  562. curl_setopt($ch, CURLOPT_URL, $url);
  563. }
  564. $response = curl_exec($ch);
  565. curl_close($ch);
  566. if ($response === false) {
  567. throw new RequestErrorException("$url not reachable", 400);
  568. }
  569. return $response;
  570. }
  571. /**
  572. * cache a string to the file system
  573. * its not overly robust, and if you need something super
  574. * awesome, use pear's Cache_Lite
  575. *
  576. * @param string $cache_id
  577. * @param string $content
  578. * @param string $cache_time
  579. * @param string $cache_dir
  580. * @return string $content
  581. * @author Kenrick Buchanan
  582. */
  583. public function cache($cache_id, $content=null, $cache_time=3600, $cache_dir=null)
  584. {
  585. if (is_null($cache_dir)) {
  586. $cache_dir = sys_get_temp_dir();
  587. }
  588. if (!is_writable($cache_dir)) {
  589. return false;
  590. }
  591. $cache_id = md5($cache_id);
  592. if ($cached = self::isCached($cache_id, $cache_time, $cache_dir)) {
  593. return $cached;
  594. } else {
  595. // write to cache
  596. $fname = $cache_dir.'/'.$cache_id.'.cache';
  597. file_put_contents($fname, $content, LOCK_EX);
  598. return $content;
  599. }
  600. }
  601. /**
  602. * isCached
  603. * checks to see if given id is cached, and if so
  604. * returns that content
  605. *
  606. * @param string $cache_id
  607. * @param string $cache_time
  608. * @param string $cache_dir
  609. * @return void
  610. * @author Kenrick Buchanan
  611. */
  612. public function isCached($cache_id, $cache_time, $cache_dir)
  613. {
  614. clearstatcache();
  615. $fname = $cache_dir.'/'.$cache_id.'.cache';
  616. if (!file_exists($fname)) {
  617. return false;
  618. }
  619. if ( (filemtime($fname) + $cache_time) < time() ) {
  620. unlink($fname);
  621. return false;
  622. } else {
  623. return file_get_contents($fname);
  624. }
  625. }
  626. /**
  627. * params
  628. *
  629. * @return stored web params from request_uri
  630. * @author Kenrick Buchanan
  631. */
  632. public static function params()
  633. {
  634. return self::instance()->params;
  635. }
  636. }