PageRenderTime 75ms CodeModel.GetById 34ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/F3.php

https://github.com/seyyah/f3kulmysql
PHP | 2036 lines | 1374 code | 77 blank | 585 comment | 197 complexity | 5f6da925bd893e2733b19917d64ce618 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. PHP Fat-Free Framework - Less Hype, More Meat.
  4. Fat-Free is a powerful yet lightweight PHP 5.3+ Web development
  5. framework designed to help build dynamic Web sites - fast. The
  6. latest version of the software can be downloaded at:-
  7. http://sourceforge.net/projects/fatfree
  8. See the accompanying HISTORY.TXT file for information on the changes
  9. in this release.
  10. If you use the software for business or commercial gain, permissive
  11. and closed-source licensing terms are available. For personal use, the
  12. PHP Fat-Free Framework and other files included in the distribution
  13. are subject to the terms of the GNU GPL v3. You may not use the
  14. software, documentation, and samples except in compliance with the
  15. license.
  16. Copyright (c) 2009-2010 F3 Factory
  17. Bong Cosca <bong.cosca@yahoo.com>
  18. @package Core
  19. @version 1.3.22
  20. **/
  21. //! Core Pack
  22. final class F3 {
  23. //@{
  24. //! Framework details
  25. const
  26. TEXT_AppName='PHP Fat-Free Framework',
  27. TEXT_Version='1.3.22';
  28. //@}
  29. //@{
  30. //! Locale-specific error/exception messages
  31. const
  32. TEXT_NotFound='The requested URL {@CONTEXT} was not found',
  33. TEXT_Route='The route {@CONTEXT} cannot be resolved',
  34. TEXT_Handler='The route handler {@CONTEXT} is invalid',
  35. TEXT_Directive='Custom directive {@CONTEXT} is not implemented',
  36. TEXT_Form='The form field hander {@CONTEXT} is invalid',
  37. TEXT_Object='{@CONTEXT} cannot be used in object context',
  38. TEXT_Instance='The framework cannot be started more than once',
  39. TEXT_Write='{@CONTEXT.0} must have write permission on {@CONTEXT.1}',
  40. TEXT_HTTP='HTTP status code {@CONTEXT} is invalid',
  41. TEXT_Class='Undefined class {@CONTEXT}',
  42. TEXT_Method='Undefined method {@CONTEXT}',
  43. TEXT_Variable='Framework variable must be specified',
  44. TEXT_Illegal='Illegal framework variable name',
  45. TEXT_Attrib='Attribute {@CONTEXT} cannot be resolved',
  46. TEXT_PCRELimit='PCRE backtrack/recurson limits set too low',
  47. TEXT_MSet='Invalid multi-variable assignment',
  48. TEXT_PHPExt='PHP extension {@CONTEXT} is not enabled',
  49. TEXT_Config='The configuration file {@CONTEXT} was not found',
  50. TEXT_Section='{@CONTEXT} is not a valid section',
  51. TEXT_Trace='Stack trace';
  52. //@}
  53. //@{
  54. //! HTTP/1.1 status (RFC 2616)
  55. const
  56. HTTP_100='Continue',
  57. HTTP_101='Switching Protocols',
  58. HTTP_200='OK',
  59. HTTP_201='Created',
  60. HTTP_202='Accepted',
  61. HTTP_203='Non-Authorative Information',
  62. HTTP_204='No Content',
  63. HTTP_205='Reset Content',
  64. HTTP_206='Partial Content',
  65. HTTP_300='Multiple Choices',
  66. HTTP_301='Moved Permanently',
  67. HTTP_302='Found',
  68. HTTP_303='See Other',
  69. HTTP_304='Not Modified',
  70. HTTP_305='Use Proxy',
  71. HTTP_306='Temporary Redirect',
  72. HTTP_400='Bad Request',
  73. HTTP_401='Unauthorized',
  74. HTTP_402='Payment Required',
  75. HTTP_403='Forbidden',
  76. HTTP_404='Not Found',
  77. HTTP_405='Method Not Allowed',
  78. HTTP_406='Not Acceptable',
  79. HTTP_407='Proxy Authentication Required',
  80. HTTP_408='Request Timeout',
  81. HTTP_409='Conflict',
  82. HTTP_410='Gone',
  83. HTTP_411='Length Required',
  84. HTTP_412='Precondition Failed',
  85. HTTP_413='Request Entity Too Large',
  86. HTTP_414='Request-URI Too Long',
  87. HTTP_415='Unsupported Media Type',
  88. HTTP_416='Requested Range Not Satisfiable',
  89. HTTP_417='Expectation Failed',
  90. HTTP_500='Internal Server Error',
  91. HTTP_501='Not Implemented',
  92. HTTP_502='Bad Gateway',
  93. HTTP_503='Service Unavailable',
  94. HTTP_504='Gateway Timeout',
  95. HTTP_505='HTTP Version Not Supported';
  96. //@}
  97. //@{
  98. //! HTTP headers
  99. const
  100. HTTP_Host='Host',
  101. HTTP_Agent='User-Agent',
  102. HTTP_Content='Content-Type',
  103. HTTP_Length='Content-Length',
  104. HTTP_Disposition='Content-Disposition',
  105. HTTP_Transfer='Content-Transfer-Encoding',
  106. HTTP_Expires='Expires',
  107. HTTP_Pragma='Pragma',
  108. HTTP_Cache='Cache-Control',
  109. HTTP_LastMod='Last-Modified',
  110. HTTP_IfMod='If-Modified-Since',
  111. HTTP_Powered='X-Powered-By',
  112. HTTP_AcceptEnc='Accept-Encoding',
  113. HTTP_Encoding='Content-Encoding',
  114. HTTP_Connect='Connection',
  115. HTTP_Location='Location',
  116. HTTP_WebAuth='WWW-Authenticate';
  117. //@}
  118. const
  119. //! Framework-mapped PHP globals
  120. PHP_Globals='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV',
  121. //! HTTP methods for RESTful interface
  122. HTTP_Methods='GET|HEAD|POST|PUT|DELETE',
  123. //! Default extensions allowed in templates
  124. FUNCS_Default='standard|date|pcre',
  125. //! GZip compression level; Any higher just hogs CPU
  126. GZIP_Compress=2,
  127. //! Default cache timeout for Axon sync method
  128. SYNC_Default=60;
  129. //! Container for Fat-Free global variables
  130. public static $global;
  131. //! XML translation table
  132. private static $xmltab=array();
  133. /**
  134. Send HTTP status header; Return text equivalent of status code
  135. @return mixed
  136. @param $_code integer
  137. @public
  138. **/
  139. public static function httpStatus($_code) {
  140. if (!defined('self::HTTP_'.$_code)) {
  141. // Invalid status code
  142. self::$global['CONTEXT']=$_code;
  143. trigger_error(self::TEXT_HTTP);
  144. return FALSE;
  145. }
  146. // Get description
  147. $_response=constant('self::HTTP_'.$_code);
  148. // Send raw HTTP header
  149. if (PHP_SAPI!='cli' && !self::$global['QUIET'] && !headers_sent())
  150. header('HTTP/1.1 '.$_code.' '.$_response);
  151. return $_response;
  152. }
  153. /**
  154. Trigger an HTTP 404 error
  155. @public
  156. **/
  157. public static function http404() {
  158. self::$global['CONTEXT']=$_SERVER['REQUEST_URI'];
  159. self::error(
  160. self::resolve(self::TEXT_NotFound),404,debug_backtrace(FALSE)
  161. );
  162. }
  163. /**
  164. Send HTTP header with expiration date (seconds from current time)
  165. @param $_secs integer
  166. @public
  167. **/
  168. public static function httpCache($_secs=0) {
  169. if (PHP_SAPI!='cli' && !self::$global['QUIET'] && !headers_sent()) {
  170. if ($_secs) {
  171. header_remove(self::HTTP_Pragma);
  172. header(self::HTTP_Cache.': max-age='.$_secs);
  173. header(self::HTTP_Expires.': '.
  174. date('r',time()+$_secs));
  175. }
  176. else {
  177. header(self::HTTP_Pragma.': no-cache');
  178. header(self::HTTP_Cache.': no-cache, must-revalidate');
  179. }
  180. header(self::HTTP_Powered.': '.self::TEXT_AppName);
  181. }
  182. }
  183. /**
  184. Flatten array values and return as a comma-separated string
  185. @return string
  186. @param $_args array
  187. @private
  188. **/
  189. private static function listArgs($_args) {
  190. if (!is_array($_args))
  191. $_args=array($_args);
  192. $_str='';
  193. foreach ($_args as $_key=>$_val)
  194. if ($_key!=='GLOBALS')
  195. $_str.=($_str?',':'').
  196. (is_array($_val) && is_int(key($_val))?
  197. // Numeric-indexed array
  198. ('array('.self::listArgs($_val).')'):
  199. (is_object($_val)?
  200. // Convert closure/object to string
  201. (get_class($_val).'()'):
  202. // Remove whitespaces
  203. preg_replace(
  204. array(
  205. '/,\s+(.+?=>)/','/\s=>\s/',
  206. '/\s*\(\s+/','/,*\s+\)/','/\s+/'
  207. ),
  208. array(',$1','=>','(',')',' '),
  209. stripslashes(var_export($_val,TRUE))
  210. )
  211. )
  212. );
  213. return self::resolve($_str);
  214. }
  215. /**
  216. Convert Windows double-backslashes to slashes
  217. @return string
  218. @param $_str string
  219. @public
  220. **/
  221. public static function fixSlashes($_str) {
  222. return $_str?str_replace('\\','/',$_str):$_str;
  223. }
  224. /**
  225. Convert double quotes to equivalent XML entities (&#34;)
  226. @return string
  227. @param $_val string
  228. @public
  229. **/
  230. public static function fixQuotes($_val) {
  231. if (is_array($_val))
  232. return array_map('self::fixQuotes',$_val);
  233. return is_string($_val)?
  234. str_replace('"','&#34;',self::resolve($_val)):$_val;
  235. }
  236. /**
  237. Display default error page; Use custom page if found
  238. @param $_str string
  239. @param $_code integer
  240. @param $_stack array
  241. @public
  242. **/
  243. public static function error($_str,$_code,$_stack) {
  244. $_prior=self::$global['ERROR'];
  245. // Remove framework methods and extraneous data
  246. $_stack=array_filter(
  247. $_stack,
  248. function($_nexus) {
  249. return isset($_nexus['line']) &&
  250. ((F3::$global['DEBUG'] || $_nexus['file']!=__FILE__) &&
  251. !preg_match(
  252. '/^(call_user_func|include|'.
  253. 'trigger_error|{.+?})/',$_nexus['function']
  254. ) &&
  255. (!isset($_nexus['class']) ||
  256. $_nexus['class']!='Runtime')
  257. );
  258. }
  259. );
  260. rsort($_stack);
  261. // Generate internal server error if code is zero
  262. if (!$_code)
  263. $_code=500;
  264. // Save error details
  265. $_error=&self::$global['ERROR'];
  266. $_error['code']=$_code;
  267. $_error['title']=self::httpStatus($_code);
  268. $_error['text']=self::resolve($_str);
  269. // Stack trace
  270. $_trace='';
  271. foreach ($_stack as $_level=>$_nexus)
  272. $_trace.='#'.$_level.' '.
  273. ($_nexus['line']?
  274. (self::fixSlashes($_nexus['file']).':'.
  275. $_nexus['line'].' '):'').
  276. ($_nexus['function']?
  277. ($_nexus['class'].$_nexus['type'].$_nexus['function'].
  278. (!preg_match('/\{.+\}/',$_nexus['function']) &&
  279. isset($_nexus['args'])?
  280. ('('.self::listArgs($_nexus['args']).')'):'')):'').
  281. "\n";
  282. if (PHP_SAPI!='cli' && !F3::$global['QUIET']) {
  283. // Write to server's error log (with complete stack trace)
  284. error_log($_error['text']);
  285. foreach (explode("\n",$_trace) as $_str)
  286. if ($_str)
  287. error_log($_str);
  288. }
  289. if ($_prior || self::$global['QUIET'])
  290. return;
  291. $_error['trace']='';
  292. foreach (explode('|','title|text|trace') as $_sub)
  293. $_error[$_sub]=htmlspecialchars(rawurldecode($_error[$_sub]));
  294. if (!self::$global['RELEASE'] && trim($_trace))
  295. $_error['trace']=nl2br($_trace);
  296. // Find template referenced by the global variable E<code>
  297. if (isset(self::$global['E'.$_error['code']])) {
  298. $_file=self::fixSlashes(self::$global['E'.$_error['code']]);
  299. if (!is_null($_file) &&
  300. file_exists(self::$global['GUI'].$_file)) {
  301. // Render custom template stored in E<code>
  302. echo self::serve($_file);
  303. return;
  304. }
  305. }
  306. unset(self::$global['CONTEXT']);
  307. // Use default HTML response page
  308. echo self::resolve(
  309. '<html>'.
  310. '<head>'.
  311. '<title>{@ERROR.code} {@ERROR.title}</title>'.
  312. '<style>#trace {padding:10px; background:#eee;}</style>'.
  313. '</head>'.
  314. '<body>'.
  315. '<h1>{@ERROR.title}</h1>'.
  316. '<p>{@ERROR.text}</p>'.
  317. '<p id="trace">{@ERROR.trace}</p>'.
  318. '</body>'.
  319. '</html>'
  320. );
  321. }
  322. /**
  323. Normalize array subscripts
  324. @return string
  325. @param $_str string
  326. @param $_f3var boolean
  327. @private
  328. **/
  329. private static function remix($_str,$_f3var=TRUE) {
  330. $_out='';
  331. return array_reduce(
  332. preg_split(
  333. '/(?:\[[\'"](?![\'"])|\[|(?<![\'"])[\'"]\]|\]|\.)/',$_str
  334. ),
  335. function($_out,$_fix) use($_f3var) {
  336. if (isset($_fix[0])) {
  337. if ($_f3var || $_out)
  338. $_fix='[\''.$_fix.'\']';
  339. }
  340. return $_out.$_fix;
  341. }
  342. );
  343. }
  344. /**
  345. Generate Base36/CRC32 hash code
  346. @return string
  347. @param $_str string
  348. @public
  349. **/
  350. public static function hashCode($_str) {
  351. return str_pad(
  352. base_convert(sprintf('%u',crc32($_str)),10,36),7,'0',
  353. STR_PAD_LEFT
  354. );
  355. }
  356. /**
  357. Remove HTML tags (except those enumerated) to protect against
  358. XSS/code injection attacks
  359. @return mixed
  360. @param $_input string
  361. @param $_tags string
  362. @public
  363. **/
  364. public static function scrub($_input,$_tags=NULL) {
  365. if (is_array($_input))
  366. foreach ($_input as $_key=>$_val)
  367. $_input[$_key]=self::scrub($_val,$_tags);
  368. if (is_string($_tags))
  369. $_tags='<'.implode('><',explode('|',$_tags)).'>';
  370. return is_string($_input)?
  371. htmlspecialchars(
  372. self::fixQuotes(strip_tags($_input,$_tags)),
  373. ENT_COMPAT,self::$global['ENCODING'],FALSE
  374. ):$_input;
  375. }
  376. /**
  377. Get framework variable reference
  378. @return mixed
  379. @param $_name string
  380. @param $_set boolean
  381. @private
  382. **/
  383. private static function &ref($_name,$_set=FALSE) {
  384. $_name=self::remix($_name);
  385. if (empty($_name))
  386. return NULL;
  387. // Traverse array
  388. preg_match_all(
  389. '/\[[\'"]*(.*?)[\'"]*\]/',$_name,$_matches,PREG_SET_ORDER
  390. );
  391. if ($_set)
  392. $_var=&self::$global;
  393. else
  394. $_var=self::$global;
  395. foreach ($_matches as $_match) {
  396. if (!$_set && (!is_array($_var) || !isset($_var[$_match[1]])))
  397. // No such element
  398. return NULL;
  399. if ($_set)
  400. $_var=&$_var[$_match[1]];
  401. else
  402. $_var=$_var[$_match[1]];
  403. }
  404. return $_var;
  405. }
  406. /**
  407. Return TRUE if framework variable has been assigned a value
  408. @return boolean
  409. @param $_name string
  410. @public
  411. **/
  412. public static function exists($_name) {
  413. if (!$_name) {
  414. trigger_error(self::TEXT_Variable);
  415. return array(FALSE,NULL);
  416. }
  417. $_var=&self::ref(self::resolve($_name));
  418. return isset($_var);
  419. }
  420. /**
  421. Return value of framework variable
  422. @return mixed
  423. @param $_name string
  424. @public
  425. **/
  426. public static function get($_name) {
  427. if (!$_name) {
  428. trigger_error(self::TEXT_Variable);
  429. return array(FALSE,NULL);
  430. }
  431. $_name=self::resolve($_name);
  432. $_hash='var.'.self::hashCode(self::remix($_name));
  433. $_cached=Cache::cached($_hash);
  434. if ($_cached)
  435. return unserialize(gzinflate(Cache::fetch($_hash)));
  436. if (preg_match('/^('.self::PHP_Globals.')\b/',$_name,$_match)) {
  437. // Synchronize PHP and framework globals
  438. if (substr($_name,0,7)=='SESSION' && !strlen(session_id()))
  439. session_start();
  440. self::$global[$_match[1]]=&$GLOBALS['_'.$_match[1]];
  441. }
  442. return self::ref($_name);
  443. }
  444. /**
  445. Bind value to framework variable
  446. @param $_name string
  447. @param $_val mixed
  448. @param $_persist boolean
  449. @public
  450. **/
  451. public static function set($_name,$_val,$_persist=FALSE) {
  452. if (!$_name) {
  453. trigger_error(self::TEXT_Variable);
  454. return;
  455. }
  456. $_name=self::resolve($_name);
  457. if (!preg_match('/^\w+\b(?:\[[^\]]+\]|\.\w+\b)*$/',$_name)) {
  458. trigger_error(self::TEXT_Illegal);
  459. return;
  460. }
  461. $_val=self::fixQuotes($_val);
  462. preg_match('/(?<=\[)[^\]]*(?=\])/',self::remix($_name),$_match);
  463. if (preg_match('/^\'('.self::PHP_Globals.')\'/',$_match[0])) {
  464. if (substr($_name,0,7)=='SESSION' && !strlen(session_id()))
  465. session_start();
  466. // Use eval; PHP doesn't allow global variable variables
  467. eval('$_'.self::remix($_name,FALSE).'='.
  468. // Convert to string if __set_state is not implemented
  469. (is_object($_val) && !method_exists($_val,'__set_state')?
  470. ('\''.$_val.'\''):var_export($_val,TRUE)).';'
  471. );
  472. }
  473. // Assign value by reference
  474. $_var=&self::ref($_name,TRUE);
  475. $_var=$_val;
  476. // Cache if specified
  477. $_hash='var.'.self::hashCode(self::remix($_name));
  478. $_cached=Cache::cached($_hash);
  479. if ($_cached || $_persist)
  480. Cache::store($_hash,gzdeflate(serialize($_val)));
  481. }
  482. /**
  483. Multi-variable assignment using associative array
  484. @param $_arg string
  485. @public
  486. **/
  487. public static function mset($_arg) {
  488. if (!is_array($_arg)) {
  489. // Invalid argument
  490. trigger_error(self::TEXT_MSet);
  491. return;
  492. }
  493. // Bind key-value pairs
  494. array_map('self::set',array_keys($_arg),$_arg);
  495. }
  496. /**
  497. Unset framework variable
  498. @param $_name string
  499. @public
  500. **/
  501. public static function clear($_name) {
  502. $_name=self::resolve($_name);
  503. $_hash='var.'.self::hashCode(self::remix($_name));
  504. $_cached=Cache::cached($_hash);
  505. if ($_cached)
  506. Cache::remove($_hash);
  507. preg_match('/(?<=\[)[^\]]*(?=\])/',self::remix($_name),$_match);
  508. if (preg_match('/^\'('.self::PHP_Globals.')\'/',$_match[0])) {
  509. if (substr($_name,0,7)=='SESSION' && !strlen(session_id()))
  510. session_destroy();
  511. // Use eval; PHP doesn't allow global variable variables
  512. eval('unset($_'.self::remix($_name,FALSE).');');
  513. }
  514. eval('unset(self::$global'.self::remix($_name).');');
  515. }
  516. /**
  517. Determine if framework variable has been cached
  518. @param $_name string
  519. @public
  520. **/
  521. public static function cached($_name) {
  522. $_name=self::resolve($_name);
  523. $_hash='var.'.self::hashCode(self::remix($_name));
  524. return Cache::cached($_hash);
  525. }
  526. /**
  527. Reroute to specified URI
  528. @param $_uri string
  529. @public
  530. **/
  531. public static function reroute($_uri=NULL) {
  532. session_commit();
  533. if (PHP_SAPI!='cli' && !self::$global['QUIET'] && !headers_sent()) {
  534. // HTTP redirect
  535. self::httpStatus($_SERVER['REQUEST_METHOD']!='GET'?303:301);
  536. header(self::HTTP_Location.': '.self::resolve($_uri));
  537. }
  538. else {
  539. self::mock('GET '.self::resolve($_uri));
  540. self::run();
  541. }
  542. exit(0);
  543. }
  544. /**
  545. Validate route pattern and break it down into an array consisting
  546. of the request method and request URI
  547. @return mixed
  548. @param $_regex string
  549. @public
  550. **/
  551. public static function checkRoute($_regex) {
  552. if (preg_match(
  553. '/('.self::HTTP_Methods.')\s+(.*)/i',$_regex,$_route))
  554. return array_slice($_route,1);
  555. // Invalid route
  556. self::$global['CONTEXT']=$_regex;
  557. trigger_error(self::TEXT_Route);
  558. return FALSE;
  559. }
  560. /**
  561. Assign handler to route pattern
  562. @param $_pattern string
  563. @param $_funcs mixed
  564. @param $_ttl integer
  565. @public
  566. **/
  567. public static function route($_pattern,$_funcs,$_ttl=0) {
  568. // Check if valid route pattern
  569. $_route=self::checkRoute($_pattern);
  570. // Valid URI pattern
  571. if (is_string($_funcs)) {
  572. // String passed
  573. foreach (explode('|',$_funcs) as $_func) {
  574. // Not a lambda function
  575. if ($_func[0]==':') {
  576. // PHP include file specified
  577. $_file=self::fixSlashes(substr($_func,1)).'.php';
  578. if (!file_exists(self::$global['IMPORTS'].$_file)) {
  579. // Invalid route handler
  580. self::$global['CONTEXT']=$_file;
  581. trigger_error(self::TEXT_Handler);
  582. return;
  583. }
  584. }
  585. elseif (!is_callable($_func)) {
  586. // Invalid route handler
  587. self::$global['CONTEXT']=$_func;
  588. trigger_error(self::TEXT_Handler);
  589. return;
  590. }
  591. }
  592. }
  593. elseif (!is_callable($_funcs)) {
  594. // Invalid route handler
  595. self::$global['CONTEXT']=$_funcs;
  596. trigger_error(self::TEXT_Handler);
  597. return;
  598. }
  599. // Assign name to URI variable
  600. $_regex=preg_replace(
  601. '/\{?@(\w+\b)\}?/i','(?P<$1>[\w-\.!~]+\b)',
  602. // Wildcard character in URI
  603. str_replace('\*','(.*)',preg_quote($_route[1],'/'))
  604. );
  605. // Use pattern and HTTP method as array indices
  606. // Save handlers and cache timeout
  607. self::$global['ROUTES']
  608. ['/^'.(ctype_alnum(substr($_regex,-1))?
  609. ($_regex.'(?:\b\/*)'):$_regex).'\/*(?:\?.*)*$/i']
  610. [$_route[0]]=array($_funcs,$_ttl);
  611. }
  612. /**
  613. Provide REST interface by mapping URL to object/PHP class
  614. @param $_url string
  615. @param $_obj mixed
  616. @public
  617. **/
  618. public static function map($_url,$_obj) {
  619. foreach (explode('|',self::HTTP_Methods) as $_method) {
  620. if (method_exists($_obj,$_method))
  621. self::route(
  622. strtoupper($_method).' '.$_url,array($_obj,$_method)
  623. );
  624. }
  625. }
  626. /**
  627. Workaround for retrieving headers from non-Apache servers
  628. @return array
  629. @private
  630. **/
  631. private static function getHeaders() {
  632. $_hdr=array();
  633. foreach ($_SERVER as $_key=>$_val)
  634. if (substr($_key,0,5)=='HTTP_') {
  635. $_hdr[preg_replace_callback(
  636. '/\w+\b/',
  637. function($_word) {
  638. return ucfirst(strtolower($_word[0]));
  639. },
  640. str_replace('_','-',substr($_key,5))
  641. )]=$_val;
  642. }
  643. return $_hdr;
  644. }
  645. /**
  646. Retrieve from cache; or save all output generated by route
  647. if not previously rendered
  648. @return string
  649. @param $_proc array
  650. @private
  651. **/
  652. private static function urlCache(array $_proc) {
  653. // Get HTTP request headers
  654. $_req=array();
  655. if (PHP_SAPI!='cli' && !self::$global['QUIET']) {
  656. $_req=function_exists('getallheaders')?
  657. getallheaders():self::getHeaders();
  658. }
  659. // Content divider
  660. $_div=chr(0);
  661. // Get hash code for this Web page
  662. $_hash='url.'.self::hashCode(
  663. $_SERVER['REQUEST_METHOD'].' '.$_SERVER['REQUEST_URI']
  664. );
  665. $_cached=Cache::cached($_hash);
  666. // Regex pattern for Content-Type
  667. $_regex='/'.self::HTTP_Content.'.+/';
  668. $_time=time();
  669. if ($_cached && ($_time-$_cached['time'])<$_proc[1]) {
  670. // Activate cache timer
  671. self::httpCache($_cached['time']+$_proc[1]-$_time);
  672. if (!isset($_req[self::HTTP_IfMod]) ||
  673. $_cached['time']>strtotime($_req[self::HTTP_IfMod])) {
  674. // Retrieve from cache and decompress
  675. $_buffer=gzinflate(Cache::fetch($_hash));
  676. $_type=strstr($_buffer,$_div,TRUE);
  677. if (preg_match($_regex,$_type,$_match) &&
  678. PHP_SAPI!='cli' && !self::$global['QUIET'] &&
  679. !headers_sent())
  680. header($_match[0]);
  681. // Save response
  682. self::$global['RESPONSE']=substr(strstr($_buffer,$_div),1);
  683. }
  684. else
  685. // No need to serve page; client-side cache is fresh
  686. self::httpStatus(304);
  687. }
  688. else {
  689. // Cache this page
  690. ob_start();
  691. self::call($_proc[0]);
  692. self::$global['RESPONSE']=ob_get_contents();
  693. ob_end_clean();
  694. if (!self::$global['ERROR'] && self::$global['RESPONSE']) {
  695. $_type='';
  696. foreach (headers_list() as $_hdr)
  697. if (preg_match($_regex,$_hdr,$_match)) {
  698. // Add Content-Type header to buffer
  699. $_type=$_match[0];
  700. break;
  701. }
  702. // Compress and save to cache
  703. Cache::store($_hash,
  704. gzdeflate($_type.$_div.self::$global['RESPONSE'])
  705. );
  706. // Activate cache timer
  707. self::httpCache($_proc[1]);
  708. if (PHP_SAPI!='cli' && !self::$global['QUIET'] &&
  709. !headers_sent())
  710. header(self::HTTP_LastMod.': '.date('r',$_time));
  711. }
  712. }
  713. }
  714. /**
  715. Sniff headers for real IP address
  716. @return string
  717. @public
  718. **/
  719. public static function realIP() {
  720. if (isset($_SERVER['HTTP_CLIENT_IP']))
  721. // Behind proxy
  722. return $_SERVER['HTTP_CLIENT_IP'];
  723. elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  724. // Use first IP address in list
  725. $_ip=explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
  726. return $_ip[0];
  727. }
  728. return $_SERVER['REMOTE_ADDR'];
  729. }
  730. /**
  731. Return TRUE if remote address is listed in spam database
  732. @return boolean
  733. @param $_addr string
  734. @public
  735. **/
  736. public static function spam($_addr) {
  737. if ($_addr!='127.0.0.1' &&
  738. // Not a private IPv4 range
  739. filter_var($_addr,
  740. FILTER_VALIDATE_IP,FILTER_FLAG_NO_PRIV_RANGE) &&
  741. (!isset(self::$global['EXEMPT']) ||
  742. !in_array($_addr,explode('|',self::$global['EXEMPT'])))) {
  743. // Convert to reverse IP dotted quad
  744. $_addr=implode('.',array_reverse(explode('.',$_addr)));
  745. foreach (explode('|',self::$global['DNSBL']) as $_list)
  746. // Check against DNS blacklist
  747. if (gethostbyname($_addr.'.'.$_list)!=$_addr.'.'.$_list)
  748. return TRUE;
  749. }
  750. return FALSE;
  751. }
  752. /**
  753. Process routes based on incoming URI
  754. @public
  755. **/
  756. public static function run() {
  757. // Validate user against spam blacklists
  758. if (PHP_SAPI!='cli' &&
  759. isset(self::$global['DNSBL']) && self::spam(self::realIP())) {
  760. if (isset($_global['SPAM']))
  761. // Spammer detected; Send to blackhole
  762. self::reroute($_global['SPAM']);
  763. else
  764. // HTTP 404 message
  765. self::http404();
  766. }
  767. // Process routes
  768. if (isset(self::$global['ROUTES'])) {
  769. $_found=FALSE;
  770. krsort(self::$global['ROUTES']);
  771. foreach (self::$global['ROUTES'] as $_regex=>$_route) {
  772. if (!preg_match($_regex,$_SERVER['REQUEST_URI'],$_args))
  773. continue;
  774. $_found=TRUE;
  775. // Inspect each defined route
  776. foreach ($_route as $_method=>$_proc) {
  777. if ($_SERVER['REQUEST_METHOD']!=$_method)
  778. continue;
  779. // Save named regex captures
  780. foreach ($_args as $_key=>$_arg)
  781. if (is_numeric($_key) && $_key)
  782. unset($_args[$_key]);
  783. self::$global['PARAMS']=$_args;
  784. // Default: Do not cache
  785. self::httpCache(0);
  786. // Save the current time
  787. $_time=time();
  788. if ($_method=='GET' && $_proc[1]) {
  789. $_SERVER['REQUEST_TTL']=$_proc[1];
  790. // Save to/retrieve from cache
  791. self::urlCache($_proc);
  792. }
  793. else {
  794. // Capture output
  795. ob_start();
  796. self::call($_proc[0]);
  797. self::$global['RESPONSE']=ob_get_contents();
  798. ob_end_clean();
  799. }
  800. $_elapsed=microtime(TRUE)-$_time;
  801. if ((self::$global['THROTTLE']/1e3)>$_elapsed)
  802. usleep(
  803. 1e6*(self::$global['THROTTLE']/
  804. 1e3-$_elapsed)
  805. );
  806. if (self::$global['RESPONSE'] &&
  807. !self::$global['QUIET'])
  808. // Display response
  809. echo self::$global['RESPONSE'];
  810. // Hail the conquering hero
  811. return;
  812. }
  813. }
  814. }
  815. // No such Web page
  816. self::http404();
  817. }
  818. /**
  819. Return XML translation table
  820. @return array
  821. @param $_latin boolean
  822. @public
  823. **/
  824. public static function xmlTable($_latin=FALSE) {
  825. if (!isset(self::$xmltab[$_latin])) {
  826. $_xl8=get_html_translation_table(HTML_ENTITIES,ENT_COMPAT);
  827. if ($_latin)
  828. // Latin index
  829. foreach ($_xl8 as $_key=>$_val)
  830. $_tab[$_val]='&#'.ord($_key).';';
  831. else
  832. // ANSI index
  833. foreach ($_xl8 as $_key=>$_val)
  834. $_tab[$_key]='&#'.ord($_key).';';
  835. self::$xmltab[$_latin]=$_tab;
  836. }
  837. return self::$xmltab[$_latin];
  838. }
  839. /**
  840. Convert plain text to XML entities
  841. @return string
  842. @param $_str string
  843. @param $_latin boolean
  844. @public
  845. **/
  846. public static function xmlEncode($_str,$_latin=FALSE) {
  847. return strtr($_str,self::xmlTable($_latin));
  848. }
  849. /**
  850. Convert XML entities to plain text
  851. @return string
  852. @param $_str string
  853. @param $_latin boolean
  854. @public
  855. **/
  856. public static function xmlDecode($_str,$_latin=FALSE) {
  857. return strtr($_str,array_flip(self::xmlTable($_latin)));
  858. }
  859. /**
  860. Evaluate template expressions in string
  861. @return mixed
  862. @param $_str string
  863. @public
  864. **/
  865. public static function resolve($_str) {
  866. // Analyze string for correct framework expression syntax
  867. $_str=preg_replace_callback(
  868. // Expression
  869. '/\{('.
  870. // Capture group
  871. '(?:'.
  872. // Variable token
  873. '@\w+\b(?:\[[^\]]+\]|\.\w+\b)*|'.
  874. // Function/method/parenthesized expression
  875. '\w*\h*[\(\,\)]|'.
  876. // Whitespaces and operators
  877. '[\h\?\.\+\-\*\/%!=<>&\|:]|'.
  878. // String and numeric constants
  879. '\'[^\']*\'|"[^"]*"|\d*\.?\d+(?:e\d+)*|'.
  880. // Null and boolean constants
  881. 'NULL|TRUE|FALSE'.
  882. // End of captured string
  883. ')+'.
  884. // End of expression
  885. ')\}/i',
  886. // Evaluate expression; This will cause a syntax error
  887. // if framework is running on an old version of PHP!
  888. function($_expr) {
  889. // Find and replace variables
  890. return eval('return (string)'.
  891. preg_replace_callback(
  892. // Framework variable
  893. '/@(\w+\b(?:\[[^\]]+\]|\.\w+\b)*)/',
  894. function($_var) {
  895. $_val=F3::get($_var[1]);
  896. // Retrieve variable contents
  897. return !is_object($_val) ||
  898. method_exists($_val,'__set_state')?
  899. var_export($_val,TRUE):(string)$_val;
  900. },
  901. preg_replace_callback(
  902. // Function
  903. '/(\w+)\h*\(([^\)]*)\)/',
  904. function($_val) {
  905. return ($_val[1].trim($_val[2]))=='array'?
  906. // Null out empty array
  907. '\'\'':
  908. // check if prohibited function
  909. (F3::allowed($_val[1])?
  910. $_val[0]:('\''.$_val[0].'\''));
  911. },
  912. $_expr[1]
  913. )
  914. ).';'
  915. );
  916. },
  917. // Coerce input
  918. (string)$_str
  919. );
  920. if (preg_last_error()!=PREG_NO_ERROR) {
  921. trigger_error(self::TEXT_PCRELimit);
  922. return FALSE;
  923. }
  924. return $_str;
  925. }
  926. /**
  927. Process <F3:include> directives
  928. @return string
  929. @param $_file string
  930. @param $_path string
  931. @public
  932. **/
  933. public static function embed($_file,$_path) {
  934. if (!$_file || !file_exists($_path.$_file))
  935. return '';
  936. $_hash='tpl.'.self::hashCode($_file);
  937. $_cached=Cache::cached($_hash);
  938. if ($_cached && filemtime($_path.$_file)<$_cached['time']) {
  939. $_text=gzinflate(Cache::fetch($_hash));
  940. // Gather template file info for profiler
  941. F3::$global['PROFILE']['TEMPLATES']['cache']
  942. [$_file]=$_cached['size'];
  943. }
  944. else {
  945. $_text=file_get_contents($_path.$_file);
  946. Cache::store($_hash,gzdeflate($_text));
  947. // Gather template file info for profiler
  948. F3::$global['PROFILE']['TEMPLATES']['loaded']
  949. [$_file]=filesize($_path.$_file);
  950. }
  951. $_regex='/<(?:F3:)*include\h*href\h*=\h*"([^"]+)"\h*\/>/i';
  952. // Search/replace <F3:include> regex pattern
  953. if (!preg_match($_regex,$_text))
  954. return $_text;
  955. // Call recursively if included file also has <F3:include>
  956. return preg_replace_callback(
  957. $_regex,
  958. function($_attr) use($_path) {
  959. // Load file
  960. return F3::embed(F3::resolve($_attr[1]),$_path);
  961. },
  962. $_text
  963. );
  964. }
  965. /**
  966. Parse all directives and render HTML/XML template
  967. @return mixed
  968. @param $_file string
  969. @param $_ishtml boolean
  970. @param $_path string
  971. @public
  972. **/
  973. public static function serve($_file,$_ishtml=TRUE,$_path=NULL) {
  974. if (is_null($_path))
  975. $_path=self::fixSlashes(self::$global['GUI']);
  976. // Remove <F3::exclude> blocks
  977. $_text=preg_replace(
  978. '/<(?:F3:)?exclude>.*?<\/(?:F3:)?exclude>/is','',
  979. // Link <F3:include> files
  980. self::embed($_file,$_path)
  981. );
  982. if (preg_match('/<.+>/s',$_text)) {
  983. // Initialize XML tree
  984. $_tree=new XMLTree('1.0',self::$global['ENCODING']);
  985. // Suppress errors caused by invalid HTML structures
  986. libxml_use_internal_errors($_ishtml);
  987. // Populate XML tree
  988. if ($_ishtml) {
  989. // HTML template; Remember defined tags
  990. $_deftags=array(
  991. '/<!DOCTYPE\s+html.*?>\h*\v*/is'=>FALSE,
  992. '/<[\/]?html.*?>\h*\v*/is'=>FALSE,
  993. '/<[\/]?head.*?>\h*\v*/is'=>FALSE,
  994. '/<[\/]?body.*?>\h*\v*/is'=>FALSE
  995. );
  996. foreach ($_deftags as $_regex=>&$_tag)
  997. $_tag=preg_match($_regex,$_text);
  998. // Destroy reference
  999. unset($_tag);
  1000. $_tree->loadHTML($_text);
  1001. }
  1002. else
  1003. // XML template
  1004. $_tree->loadXML($_text,LIBXML_COMPACT|LIBXML_NOERROR);
  1005. // Prepare for XML tree traversal
  1006. $_tree->fragment=$_tree->createDocumentFragment();
  1007. $_2ndp=FALSE;
  1008. $_tree->traverse(
  1009. function() use($_tree,&$_2ndp) {
  1010. $_node=&$_tree->nodeptr;
  1011. $_tag=$_node->tagName;
  1012. $_next=$_node;
  1013. $_parent=$_node->parentNode;
  1014. // Node removal flag
  1015. $_remove=FALSE;
  1016. if ($_tag=='repeat') {
  1017. // Process <F3:repeat> directive
  1018. $_inner=$_tree->innerHTML($_node);
  1019. if ($_inner) {
  1020. foreach ($_node->attributes as $_attr) {
  1021. preg_match(
  1022. '/\{*@(\w+\b(\[[^\]]+\]|\.\w+\b)*)\}*/',
  1023. $_attr->value,$_cap);
  1024. if (!$_cap[1] ||
  1025. isset($_cap[2]) &&
  1026. $_attr->name!='group') {
  1027. // Invalid attribute
  1028. F3::$global['CONTEXT']=$_attr->value;
  1029. trigger_error(F3::TEXT_Attrib);
  1030. return;
  1031. }
  1032. if ($_attr->name=='key')
  1033. $_kvar='/@'.$_cap[1].'\b/';
  1034. elseif ($_attr->name=='index')
  1035. $_ivar='/@'.$_cap[1].'\b/';
  1036. elseif ($_attr->name=='group') {
  1037. $_gcap='@'.$_cap[1];
  1038. $_gvar=F3::get($_cap[1]);
  1039. }
  1040. }
  1041. if (is_array($_gvar) && count($_gvar)) {
  1042. $_block='';
  1043. // Iterate thru group elements
  1044. foreach (array_keys($_gvar) as $_key) {
  1045. $_block.=preg_replace($_ivar,
  1046. // Replace index token
  1047. $_gcap.'[\''.$_key.'\']',
  1048. isset($_kvar)?
  1049. // Replace key token
  1050. preg_replace($_kvar,
  1051. var_export($_key,TRUE),
  1052. $_inner):
  1053. $_inner
  1054. );
  1055. }
  1056. if (isset($_block[0])) {
  1057. $_tree->fragment->appendXML($_block);
  1058. // Insert fragment before current node
  1059. $_next=$_parent->
  1060. insertBefore(
  1061. $_tree->fragment,$_node
  1062. );
  1063. }
  1064. }
  1065. }
  1066. $_remove=TRUE;
  1067. }
  1068. elseif ($_tag=='check' && !$_2ndp)
  1069. // Found <F3:check> directive
  1070. $_2ndp=TRUE;
  1071. elseif (strpos($_tag,'-')) {
  1072. // Process custom template directive
  1073. list($_class,$_method)=explode('-',$_tag);
  1074. $_found=FALSE;
  1075. if (!class_exists($_class,FALSE))
  1076. foreach (explode('|',F3::$global['AUTOLOAD'])
  1077. as $_auto) {
  1078. $_file=$_auto.$_class.'.php';
  1079. // Case-insensitive check for file presence
  1080. $_glob=glob(dirname($_file).'/*.php');
  1081. $_fkey=array_search(
  1082. strtolower($_file),
  1083. array_map('strtolower',$_glob)
  1084. );
  1085. if (is_int($_fkey)) {
  1086. include $_glob[$_fkey];
  1087. if (method_exists($_class,'onLoad'))
  1088. call_user_func(
  1089. array($_class,'onLoad')
  1090. );
  1091. $_found=TRUE;
  1092. break;
  1093. }
  1094. }
  1095. else
  1096. $_found=TRUE;
  1097. if ($_found) {
  1098. // Invoke template directive handler
  1099. call_user_func(array($_class,$_method),$_tree);
  1100. $_remove=TRUE;
  1101. }
  1102. }
  1103. if ($_remove) {
  1104. // Find next node
  1105. if ($_node->isSameNode($_next))
  1106. $_next=$_node->nextSibling?
  1107. $_node->nextSibling:$_parent;
  1108. // Remove current node
  1109. $_parent->removeChild($_node);
  1110. // Replace with next node
  1111. $_node=$_next;
  1112. }
  1113. }
  1114. );
  1115. if ($_2ndp) {
  1116. // Second pass; Template contains <F3:check> directive
  1117. $_tree->traverse(
  1118. function() use($_tree) {
  1119. $_node=&$_tree->nodeptr;
  1120. $_parent=$_node->parentNode;
  1121. $_tag=$_node->tagName;
  1122. // Process <F3:check> directive
  1123. if ($_tag=='check') {
  1124. $_cond=var_export(
  1125. (boolean) F3::resolve(
  1126. rawurldecode(
  1127. $_node->getAttribute('if')
  1128. )
  1129. ),TRUE
  1130. );
  1131. $_block='';
  1132. foreach ($_node->childNodes as $_child)
  1133. if ($_child->nodeType!=XML_TEXT_NODE &&
  1134. $_child->tagName==$_cond) {
  1135. $_inner=$_tree->innerHTML($_child);
  1136. if ($_inner)
  1137. // Replacement
  1138. $_block.=$_inner;
  1139. }
  1140. if (isset($_block[0])) {
  1141. $_tree->fragment->appendXML($_block);
  1142. $_parent->
  1143. insertBefore($_tree->fragment,$_node);
  1144. }
  1145. // Remove current node
  1146. $_parent->removeChild($_node);
  1147. // Re-process parent node
  1148. $_node=$_parent;
  1149. }
  1150. }
  1151. );
  1152. }
  1153. $_text=self::resolve(
  1154. rawurldecode(
  1155. $_ishtml?$_tree->saveHTML():$_tree->saveXML()
  1156. )
  1157. );
  1158. if ($_ishtml) {
  1159. // Fix empty HTML tags
  1160. $_text=preg_replace(
  1161. '/<((?:area|base|br|col|frame|hr|img|input|'.
  1162. 'isindex|link|meta|param).*?)\/?>/is','<$1/>',
  1163. $_text
  1164. );
  1165. // Remove tags inserted by libxml
  1166. foreach ($_deftags as $_regex=>$_tag)
  1167. if (!$_tag)
  1168. $_text=preg_replace($_regex,'',$_text);
  1169. }
  1170. else
  1171. $_text=self::xmlEncode($_text,TRUE);
  1172. unset($_tree);
  1173. }
  1174. else
  1175. // Plain text
  1176. $_text=self::resolve($_text);
  1177. // Remove control characters except whitespaces
  1178. return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/','',$_text);
  1179. }
  1180. /**
  1181. Allow PHP and user-defined functions to be used in templates
  1182. @param $_str string
  1183. @public
  1184. **/
  1185. public static function allow($_str='') {
  1186. // Create lookup table of functions allowed in templates
  1187. $_legal=array();
  1188. // Get list of all defined functions
  1189. $_dfuncs=get_defined_functions();
  1190. foreach (explode('|',$_str) as $_ext) {
  1191. $_funcs=array();
  1192. if (extension_loaded($_ext))
  1193. $_funcs=get_extension_funcs($_ext);
  1194. elseif ($_ext=='user')
  1195. $_funcs=$_dfuncs['user'];
  1196. $_legal=array_merge($_legal,$_funcs);
  1197. }
  1198. // Remove prohibited functions
  1199. $_illegal='/^('.
  1200. 'apache_|call|chdir|env|escape|exec|extract|fclose|fflush|'.
  1201. 'fget|file_put|flock|fopen|fprint|fput|fread|fseek|fscanf|'.
  1202. 'fseek|fsockopen|fstat|ftell|ftp_|ftrunc|get|header|http_|'.
  1203. 'import|ini_|ldap_|link|log_|magic|mail|mcrypt_|mkdir|ob_|'.
  1204. 'php|popen|posix_|proc|rename|rmdir|rpc|set_|sleep|stream|'.
  1205. 'sys|thru|unreg'.
  1206. ')/i';
  1207. $_legal=array_merge(
  1208. array_filter(
  1209. $_legal,
  1210. function($_func) use($_illegal) {
  1211. return !preg_match($_illegal,$_func);
  1212. }
  1213. ),
  1214. // PHP language constructs that may be used in expressions
  1215. array('array','isset')
  1216. );
  1217. self::$global['FUNCS']=array_map('strtolower',$_legal);
  1218. }
  1219. /**
  1220. Return TRUE if function can be used in templates
  1221. @return boolean
  1222. @param $_func string
  1223. @public
  1224. **/
  1225. public static function allowed($_func) {
  1226. if (!isset(self::$global['FUNCS']))
  1227. F3::allow(self::FUNCS_Default);
  1228. return in_array($_func,self::$global['FUNCS']);
  1229. }
  1230. /**
  1231. Call form field handler
  1232. @param $_fields string
  1233. @param $_funcs mixed
  1234. @param $_tags string
  1235. @param $_filter integer
  1236. @param $_options mixed
  1237. @public
  1238. **/
  1239. public static function input(
  1240. $_fields,
  1241. $_funcs,
  1242. $_tags=NULL,
  1243. $_filter=FILTER_UNSAFE_RAW,
  1244. $_options=array()) {
  1245. $_global=&self::$global;
  1246. foreach (explode('|',$_fields) as $_field) {
  1247. // Sanitize relevant globals
  1248. $_php=$_SERVER['REQUEST_METHOD'].'|REQUEST|FILES';
  1249. foreach (explode('|',$_php) as $_var)
  1250. if (isset($_global[$_var][$_field]))
  1251. $_global[$_var][$_field]=filter_var(
  1252. self::scrub($_global[$_var][$_field],$_tags),
  1253. $_filter,$_options
  1254. );
  1255. $_input=&$_global
  1256. [isset($_global['FILES'][$_field])?'FILES':'REQUEST']
  1257. [$_field];
  1258. if (is_string($_funcs)) {
  1259. // String passed
  1260. foreach (explode('|',$_funcs) as $_func) {
  1261. if (!is_callable($_func)) {
  1262. // Invalid handler
  1263. $_global['CONTEXT']=$_include;
  1264. trigger_error(self::TEXT_Form);
  1265. }
  1266. else
  1267. // Call lambda function
  1268. call_user_func($_func,$_input,$_field);
  1269. }
  1270. }
  1271. else {
  1272. // Closure
  1273. if (!is_callable($_funcs)) {
  1274. // Invalid handler
  1275. $_global['CONTEXT']=$_funcs;
  1276. trigger_error(self::TEXT_Form);
  1277. }
  1278. else
  1279. // Call lambda function
  1280. call_user_func($_funcs,$_input,$_field);
  1281. }
  1282. }
  1283. }
  1284. /**
  1285. Mock environment for command-line use and/or unit testing
  1286. @param $_regex string
  1287. @param $_params array
  1288. @public
  1289. **/
  1290. public static function mock($_regex,array $_params=NULL) {
  1291. // Override PHP globals
  1292. list($_method,$_uri)=self::checkRoute($_regex);
  1293. $_query=explode('&',parse_url($_uri,PHP_URL_QUERY));
  1294. foreach ($_query as $_pair)
  1295. if (strpos($_pair,'=')) {
  1296. list($_var,$_val)=explode('=',$_pair);
  1297. self::set($_method.'.'.$_var,$_val);
  1298. self::set('REQUEST.'.$_var,$_val);
  1299. }
  1300. if (is_array($_params))
  1301. foreach ($_params as $_var=>$_val) {
  1302. self::set($_method.'.'.$_var,$_val);
  1303. self::set('REQUEST.'.$_var,$_val);
  1304. }
  1305. self::set('SERVER.REQUEST_METHOD',$_method);
  1306. self::set('SERVER.REQUEST_URI',$_uri);
  1307. }
  1308. /**
  1309. Perform test and append result to TEST global variable
  1310. @return string
  1311. @param $_cond boolean
  1312. @param $_pass string
  1313. @param $_fail string
  1314. @public
  1315. **/
  1316. public static function expect($_cond,$_pass=NULL,$_fail=NULL) {
  1317. if (is_string($_cond))
  1318. $_cond=self::resolve($_cond);
  1319. $_text=$_cond?$_pass:$_fail;
  1320. self::$global['TEST'][]=array(
  1321. 'result'=>(int)(boolean)$_cond,
  1322. 'text'=>is_string($_text)?
  1323. self::resolve($_text):var_export($_text,TRUE)
  1324. );
  1325. return $_text;
  1326. }
  1327. /**
  1328. Convenience method for sandboxing function/script
  1329. @param $_funcs mixed
  1330. @public
  1331. **/
  1332. public static function call($_funcs) {
  1333. Runtime::call($_funcs);
  1334. }
  1335. /**
  1336. Return array of runtime performance analysis data
  1337. @return array
  1338. @public
  1339. **/
  1340. public static function &profile() {
  1341. $_profile=self::$global['PROFILE'];
  1342. // Compute elapsed time
  1343. $_profile['TIME']['start']=&self::$global['TIME'];
  1344. $_profile['TIME']['elapsed']=microtime(TRUE)-self::$global['TIME'];
  1345. // Reset PHP's stat cache
  1346. foreach (get_included_files() as $_file)
  1347. // Gather includes
  1348. $_profile['FILES']['includes']
  1349. [basename($_file)]=filesize($_file);
  1350. // Compute memory consumption
  1351. $_profile['MEMORY']['current']=memory_get_usage();
  1352. $_profile['MEMORY']['peak']=memory_get_peak_usage();
  1353. return $_profile;
  1354. }
  1355. /**
  1356. Configure framework according to .ini file settings and cache
  1357. auto-generated PHP code to speed up execution
  1358. @param $_file string
  1359. @public
  1360. **/
  1361. public static function config($_file) {
  1362. // Generate hash code for config file
  1363. $_hash='php.'.self::hashCode($_file);
  1364. $_cached=Cache::cached($_hash);
  1365. if ($_cached && filemtime($_file)<$_cached['time'])
  1366. // Retrieve from cache
  1367. $_save=gzinflate(Cache::fetch($_hash));
  1368. else {
  1369. if (!file_exists($_file)) {
  1370. // .ini file not found
  1371. self::$global['CONTEXT']=$_file;
  1372. trigger_error(self::TEXT_Config);
  1373. return;
  1374. }
  1375. // Map sections to framework methods
  1376. $_map=array('global'=>'set','routes'=>'route','maps'=>'map');
  1377. // Read the .ini file
  1378. preg_match_all(
  1379. '/\s*(?:\[(.+?)\]|(?:;.+?)*|(?:([^=]+)=(.+?)))(?:\v|$)/s',
  1380. file_get_contents($_file),$_matches,PREG_SET_ORDER
  1381. );
  1382. $_cfg=array();
  1383. $_ptr=&$_cfg;
  1384. foreach ($_matches as $_match) {
  1385. if ($_match[1]) {
  1386. // Section header
  1387. if (!isset($_map[$_match[1]])) {
  1388. // Unknown section
  1389. self::$global['CONTEXT']=$_section;
  1390. trigger_error(self::TEXT_Section);
  1391. return;
  1392. }
  1393. $_ptr=&$_cfg[$_match[1]];
  1394. }
  1395. elseif ($_match[2]) {
  1396. $_csv=array_map(
  1397. function($_val) {
  1398. // Typecast if necessary
  1399. return is_numeric($_val) ||
  1400. preg_match('/^(TRUE|FALSE)\b/i',$_val)?
  1401. eval('return '.$_val.';'):$_val;
  1402. },
  1403. str_getcsv($_match[3])
  1404. );
  1405. // Convert comma-separated values to array
  1406. $_match[3]=count($_csv)>1?$_csv:$_csv[0];
  1407. if (preg_match('/(.+?)\[(.*?)\]/',$_match[2],$_sub)) {
  1408. if ($_sub[2])
  1409. // Associative array
  1410. $_ptr[$_sub[1]][$_sub[2]]=$_match[3];
  1411. else
  1412. // Numeric-indexed array
  1413. $_ptr[$_sub[1]][]=$_match[3];
  1414. }
  1415. else
  1416. // Key-value pair
  1417. $_ptr[$_match[2]]=$_match[3];
  1418. }
  1419. }
  1420. ob_start();
  1421. foreach ($_cfg as $_section=>$_pair) {
  1422. $_func=$_map[$_section];
  1423. foreach ($_pair as $_key=>$_val)
  1424. // Generate PHP snippet
  1425. echo 'F3::'.$_func.'('.
  1426. var_export($_key,TRUE).','.
  1427. ($_func=='set' || !is_array($_val)?
  1428. var_export($_val,TRUE):self::listArgs($_val)).
  1429. ');'."\n";
  1430. }
  1431. $_save=ob_get_contents();
  1432. ob_end_clean();
  1433. // Compress and save to cache
  1434. Cache::store($_hash,gzdeflate($_save));
  1435. }
  1436. // Execute cached PHP code
  1437. eval($_save);
  1438. if (self::$global['ERROR'])
  1439. // Remove from cache
  1440. Cache::remove($_hash);
  1441. }
  1442. /**
  1443. Convert engineering-notated string to bytes
  1444. @return integer
  1445. @param $_str string
  1446. @public
  1447. **/
  1448. public static function bytes($_str) {
  1449. $_greek='KMGT';
  1450. $_exp=strpbrk($_str,$_greek);
  1451. return pow(1024,strpos($_greek,$_exp)+1)*(int)$_str;
  1452. }
  1453. /**
  1454. Kickstart the framework
  1455. @public
  1456. **/
  1457. public static function start() {
  1458. // Get PHP settings
  1459. $_ini=ini_get_all(NULL,FALSE);
  1460. $_level=E_ALL^E_NOTICE;
  1461. ini_set('error_reporting',$_level);
  1462. // Intercept errors and send output to browser
  1463. set_error_handler(
  1464. function($_errno,$_errstr) {
  1465. // Bypass if error suppression (@) is enabled
  1466. if (error_reporting())
  1467. F3::error($_errstr,500,debug_backtrace(FALSE));
  1468. },
  1469. $_level
  1470. );
  1471. // Do the same for PHP exceptions
  1472. set_exception_handler(
  1473. function($_xcpt) {
  1474. if (!count($_xcpt->getTrace())) {
  1475. // Translate exception trace
  1476. $_trace=debug_backtrace(FALSE);
  1477. $_arg=$_trace[0]['args'][0];
  1478. $_trace=array(
  1479. array(
  1480. 'file'=>$_arg->getFile(),
  1481. 'line'=>$_arg->getLine(),
  1482. 'function'=>'{main}',
  1483. 'args'=>array()
  1484. )
  1485. );
  1486. }
  1487. else
  1488. $_trace=$_xcpt->getTrace();
  1489. F3::error($_xcpt->getMessage(),$_xcpt->getCode(),$_trace);
  1490. return;
  1491. // PHP aborts at this point
  1492. }
  1493. );
  1494. if (isset(self::$global)) {
  1495. // Multiple framework instances not allowed
  1496. trigger_error(self::TEXT_Instance);
  1497. return;
  1498. }
  1499. // Hydrate framework variables
  1500. $_base=self::fixSlashes(realpath('.')).'/';
  1501. self::$global=array(
  1502. 'AUTOLOAD'=>$_base.'autoload/',
  1503. 'BASE'=>$_base,
  1504. 'DEBUG'=>FALSE,
  1505. 'ENCODING'=>'UTF-8',
  1506. 'FONTS'=>$_base,
  1507. 'GUI'=>$_base,
  1508. 'IMPORTS'=>$_base,
  1509. 'LOGS'=>$_base.'logs/',
  1510. 'MAXSIZE'=>self::bytes($_ini['post_max_size']),
  1511. 'QUIET'=>FALSE,
  1512. 'RELEASE'=>FALSE,
  1513. 'SYNC'=>self::SYNC_Default,
  1514. 'TEST'=>array(),
  1515. 'TIME'=>time(),
  1516. 'THROTTLE'=>0,
  1517. 'VERSION'=>self::TEXT_Version
  1518. );
  1519. if (!in_array('zlib',get_loaded_extensions())) {
  1520. // ZLib required
  1521. self::$global['CONTEXT']='zlib';
  1522. trigger_error(self::TEXT_PHPExt);
  1523. return;
  1524. }
  1525. // Use plain old output buffering as default
  1526. $_handler=NULL;
  1527. if (PHP_SAPI=='cli') {
  1528. // Command line: Parse GET variables in URL, if any
  1529. preg_match_all(
  1530. '/[\?&]([^=]+)=([^&$]*)/',$_SERVER['REQUEST_URI'],
  1531. $_matches,PREG_SET_ORDER
  1532. );
  1533. foreach ($_matches as $_match) {
  1534. $_REQUEST[$_match[1]]=$_match[2];
  1535. $_GET[$_match[1]]=$_match[2];
  1536. }
  1537. // Custom server name
  1538. $_SERVER['SERVER_NAME']=strtolower($_SERVER['COMPUTERNAME']);
  1539. // Convert URI to human-readable string
  1540. $_SERVER['REQUEST_URI']=rawurldecode($_SERVER['REQUEST_URI']);
  1541. self::mock('GET '.$_SERVER['argv'][1]);
  1542. }
  1543. // Use GZip compression if (1) browser supports GZip-encoded
  1544. // data, (2) ZLib output compression is not set in PHP.INI, and
  1545. // (3) Apache mod_deflate is not active
  1546. elseif (isset($_SERVER['HTTP_ACCEPT_ENCODING']) &&
  1547. preg_match('/gzip|deflate/',$_SERVER['HTTP_ACCEPT_ENCODING']) &&
  1548. !$_ini['zlib.output_compression'] &&
  1549. function_exists('apache_get_modules') &&
  1550. in_array('mod_deflate',apache_get_modules())) {
  1551. // Use a conservative compression level
  1552. ini_set('zlib.output_compression_level',self::GZIP_Compress);
  1553. $_handler='ob_gzhandler';
  1554. }
  1555. ob_start($_handler);
  1556. // Initialize profiler
  1557. self::$global['PROFILE']['MEMORY']['start']=memory_get_usage();
  1558. // Create convenience containers for PHP globals
  1559. foreach (explode('|',self::PHP_Globals) as $_var) {
  1560. self::$global[$_var]=&$GLOBALS['_'.$_var];
  1561. if ($_ini['magic_quotes_gpc'] && preg_match('/^[GPCR]/',$_var))
  1562. // Corrective action on PHP magic quotes
  1563. array_walk_recursive(
  1564. self::$global[$_var],
  1565. function(&$_val) {
  1566. $_val=stripslashes($_val);
  1567. }
  1568. );
  1569. }
  1570. // Initialize autoload stack
  1571. spl_autoload_register('self::onLoad');
  1572. }
  1573. /**
  1574. Intercept instantiation of objects in undefined classes
  1575. @param $_class string
  1576. @private
  1577. **/
  1578. private static function onLoad($_class) {
  1579. foreach (explode('|',self::$global['AUTOLOAD']) as $_auto) {
  1580. // Allow namespaced classes
  1581. $_file=$_auto.self::fixSlashes($_class).'.php';
  1582. // Case-insensitive check for file presence
  1583. $_glob=glob(dirname($_file).'/*.php');
  1584. $_fkey=array_search(
  1585. strtolower($_file),array_map('strtolower',$_glob)
  1586. );
  1587. if (is_int($_fkey)) {
  1588. include $_glob[$_fkey];
  1589. if (method_exists($_class,'onLoad'))
  1590. call_user_func(array($_class,'onLoad'));
  1591. return;
  1592. }
  1593. }
  1594. self::$global['CONTEXT']=$_class;
  1595. trigger_error(self::TEXT_Class);
  1596. }
  1597. /**
  1598. Intercept calls to undefined static methods
  1599. @return mixed
  1600. @param $_func string
  1601. @param $_args array
  1602. @public
  1603. **/
  1604. public static function __callStatic($_func,array $_args) {
  1605. foreach (explode('|',self::$global['AUTOLOAD']) as $_auto) {
  1606. // Proxy for method in autoload class if found
  1607. foreach (glob($_auto.'*.php') as $_file) {
  1608. $_class=strstr(basename($_file),'.php',TRUE);
  1609. include_once $_file;
  1610. if (method_exists($_class,'onLoad'))
  1611. call_user_func(array($_class,'onLoad'));
  1612. if (method_exists($_class,$_func))
  1613. return call_user_func_array(
  1614. array($_class,$_func),$_args
  1615. );
  1616. }
  1617. }
  1618. self::$global['CONTEXT']=__CLASS__.'::'.$_func;
  1619. trigger_error(self::TEXT_Method);
  1620. return FALSE;
  1621. }
  1622. /**
  1623. Class constructor
  1624. @public
  1625. **/
  1626. public function __construct() {
  1627. // Prohibit use of framework as an object
  1628. self::$global['CONTEXT']=__CLASS__;
  1629. trigger_error(self::TEXT_Object);
  1630. }
  1631. }
  1632. //! Framework cache engine
  1633. final class Cache {
  1634. //@{
  1635. //! Locale-specific error/exception messages
  1636. const
  1637. TEXT_Backend='Cache back-end is invalid',
  1638. TEXT_Store='Unable to save {@CONTEXT} to cache',
  1639. TEXT_Fetch='Unable to retrieve {@CONTEXT} from cache',
  1640. TEXT_Clear='Unable to clear {@CONTEXT} from cache';
  1641. //@}
  1642. //! Level-1 cached object
  1643. private static $l1cache;
  1644. /**
  1645. Initialize framework level-2 cache
  1646. @return boolean
  1647. @private
  1648. **/
  1649. private static function prep() {
  1650. if (!isset(F3::$global['CACHE'])) {
  1651. // Extensions usable as cache back-ends
  1652. $_exts=array_intersect(
  1653. explode('|','apc|xcache'),get_loaded_extensions()
  1654. );
  1655. foreach (array_keys($_exts,'') as $_null)
  1656. unset($_exts[$_null]);
  1657. $_exts=array_merge($_exts,array());
  1658. F3::$global['CACHE']=$_exts[0]?:
  1659. ('folder='.F3::$global['BASE'].'cache/');
  1660. }
  1661. if (preg_match(
  1662. '/^(?:(folder)\=(.+\/)|(apc)|(memcache)=(.+))|(xcache)/i',
  1663. F3::$global['CACHE'],$_match)) {
  1664. if ($_match[1]) {
  1665. if (!file_exists($_match[2])) {
  1666. if (!is_writable(dirname($_match[2])) &&
  1667. function_exists('posix_getpwuid')) {
  1668. $_uid=posix_getpwuid(posix_geteuid());
  1669. F3::$global['CONTEXT']=array(
  1670. $_uid['name'],realpath(dirname($_match[2]))
  1671. );
  1672. trigger_error(F3::TEXT_Write);
  1673. return;
  1674. }
  1675. // Create the framework's cache folder
  1676. mkdir($_match[2],0755);
  1677. }
  1678. // File system
  1679. self::$l1cache=array('type'=>'folder','id'=>$_match[2]);
  1680. }
  1681. else {
  1682. $_ext=strtolower($_match[3]?:($_match[4]?:$_match[6]));
  1683. if (!extension_loaded($_ext)) {
  1684. F3::$global['CONTEXT']=$_ext;
  1685. trigger_error(F3::TEXT_PHPExt);
  1686. return;
  1687. }
  1688. if ($_match[4]) {
  1689. // Open persistent MemCache connection(s)
  1690. // Multiple servers separated by semi-colon
  1691. $_pool=explode(';',$_match[5]);
  1692. $_mcache=NULL;
  1693. foreach ($_pool as $_server) {
  1694. // Hostname:port
  1695. list($_host,$_port)=explode(':',$_server);
  1696. if (is_null($_port))
  1697. // Use default port
  1698. $_port=11211;
  1699. // Connect to each server
  1700. if (is_null($_mcache))
  1701. $_mcache=memcache_pconnect($_host,$_port);
  1702. else
  1703. memcache_add_server($_mcache,$_host,$_port);
  1704. }
  1705. // MemCache
  1706. self::$l1cache=array('type'=>$_ext,'id'=>$_mcache);
  1707. }
  1708. else
  1709. // APC and XCache
  1710. self::$l1cache=array('type'=>$_ext);
  1711. }
  1712. self::$l1cache['current']=FALSE;
  1713. return TRUE;
  1714. }
  1715. // Unknown back-end
  1716. trigger_error(self::TEXT_Backend);
  1717. return FALSE;
  1718. }
  1719. /**
  1720. Store data in framework cache
  1721. @return boolean
  1722. @param $_name string
  1723. @param $_data mixed
  1724. @public
  1725. **/
  1726. public static function store($_name,$_data) {
  1727. if (is_null(self::$l1cache) && !self::prep())
  1728. return FALSE;
  1729. $_key=$_SERVER['SERVER_NAME'].'.'.$_name;
  1730. // Serialize data for storage
  1731. $_time=time();
  1732. // Add timestamp
  1733. $_val=serialize(array($_time,$_data));
  1734. // Instruct back-end to store data
  1735. switch (self::$l1cache['type']) {
  1736. case 'folder':
  1737. $_ok=file_put_contents(
  1738. self::$l1cache['id'].$_key,$_val,LOCK_EX
  1739. );
  1740. break;
  1741. case 'apc':
  1742. $_ok=apc_store($_key,$_val);
  1743. break;
  1744. case 'memcache':
  1745. $_ok=memcache_set(self::$l1cache['id'],$_key,$_val);
  1746. break;
  1747. case 'xcache':
  1748. $_ok=xcache_set($_key,$_val);
  1749. break;
  1750. }
  1751. if (is_bool($_ok) && !$_ok) {
  1752. F3::$global['CONTEXT']=$_name;
  1753. trigger_error(self::TEXT_Store);
  1754. return FALSE;
  1755. }
  1756. self::$l1cache['current']=array(
  1757. 'name'=>$_name,'data'

Large files files are truncated, but you can click here to view the full file