PageRenderTime 47ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/base.php

https://gitlab.com/f3/f3-skeleton
PHP | 2360 lines | 1605 code | 83 blank | 672 comment | 235 complexity | 42f0a5aa4f309174e75d51f6267049c3 MD5 | raw file
Possible License(s): GPL-3.0
  1. <?php
  2. /**
  3. PHP Fat-Free Framework
  4. The contents of this file are subject to the terms of the GNU General
  5. Public License Version 3.0. You may not use this file except in
  6. compliance with the license. Any of the license terms and conditions
  7. can be waived if you get permission from the copyright holder.
  8. Copyright (c) 2009-2011 F3::Factory
  9. Bong Cosca <bong.cosca@yahoo.com>
  10. @package Base
  11. @version 2.0.5
  12. **/
  13. //! Base structure
  14. class Base {
  15. //@{ Framework details
  16. const
  17. TEXT_AppName='Fat-Free Framework',
  18. TEXT_Version='2.0.5',
  19. TEXT_AppURL='http://fatfree.sourceforge.net';
  20. //@}
  21. //@{ Locale-specific error/exception messages
  22. const
  23. TEXT_Illegal='%s is not a valid framework variable name',
  24. TEXT_Config='The configuration file %s was not found',
  25. TEXT_Section='%s is not a valid section',
  26. TEXT_MSet='Invalid multi-variable assignment',
  27. TEXT_NotArray='%s is not an array',
  28. TEXT_PHPExt='PHP extension %s is not enabled',
  29. TEXT_Apache='Apache mod_rewrite module is not enabled',
  30. TEXT_Object='%s cannot be used in object context',
  31. TEXT_Class='Undefined class %s',
  32. TEXT_Method='Undefined method %s',
  33. TEXT_NotFound='The URL %s was not found',
  34. TEXT_NotAllowed='%s request is not allowed for the URL %s',
  35. TEXT_Callback='The callback function %s is invalid',
  36. TEXT_Import='Import file %s not found',
  37. TEXT_NoRoutes='No routes specified',
  38. TEXT_HTTP='HTTP status code %s is invalid',
  39. TEXT_Render='Unable to render %s - file does not exist',
  40. TEXT_Form='The input handler for %s is invalid',
  41. TEXT_Static='%s must be a static method',
  42. TEXT_Fatal='Fatal error: %s',
  43. TEXT_Write='%s must have write permission on %s',
  44. TEXT_Tags='PHP short tags are not supported by this server';
  45. //@}
  46. //@{ HTTP status codes (RFC 2616)
  47. const
  48. HTTP_100='Continue',
  49. HTTP_101='Switching Protocols',
  50. HTTP_200='OK',
  51. HTTP_201='Created',
  52. HTTP_202='Accepted',
  53. HTTP_203='Non-Authorative Information',
  54. HTTP_204='No Content',
  55. HTTP_205='Reset Content',
  56. HTTP_206='Partial Content',
  57. HTTP_300='Multiple Choices',
  58. HTTP_301='Moved Permanently',
  59. HTTP_302='Found',
  60. HTTP_303='See Other',
  61. HTTP_304='Not Modified',
  62. HTTP_305='Use Proxy',
  63. HTTP_307='Temporary Redirect',
  64. HTTP_400='Bad Request',
  65. HTTP_401='Unauthorized',
  66. HTTP_402='Payment Required',
  67. HTTP_403='Forbidden',
  68. HTTP_404='Not Found',
  69. HTTP_405='Method Not Allowed',
  70. HTTP_406='Not Acceptable',
  71. HTTP_407='Proxy Authentication Required',
  72. HTTP_408='Request Timeout',
  73. HTTP_409='Conflict',
  74. HTTP_410='Gone',
  75. HTTP_411='Length Required',
  76. HTTP_412='Precondition Failed',
  77. HTTP_413='Request Entity Too Large',
  78. HTTP_414='Request-URI Too Long',
  79. HTTP_415='Unsupported Media Type',
  80. HTTP_416='Requested Range Not Satisfiable',
  81. HTTP_417='Expectation Failed',
  82. HTTP_500='Internal Server Error',
  83. HTTP_501='Not Implemented',
  84. HTTP_502='Bad Gateway',
  85. HTTP_503='Service Unavailable',
  86. HTTP_504='Gateway Timeout',
  87. HTTP_505='HTTP Version Not Supported';
  88. //@}
  89. //@{ HTTP headers (RFC 2616)
  90. const
  91. HTTP_AcceptEnc='Accept-Encoding',
  92. HTTP_Agent='User-Agent',
  93. HTTP_Allow='Allow',
  94. HTTP_Cache='Cache-Control',
  95. HTTP_Connect='Connection',
  96. HTTP_Content='Content-Type',
  97. HTTP_Disposition='Content-Disposition',
  98. HTTP_Encoding='Content-Encoding',
  99. HTTP_Expires='Expires',
  100. HTTP_Host='Host',
  101. HTTP_IfMod='If-Modified-Since',
  102. HTTP_Keep='Keep-Alive',
  103. HTTP_LastMod='Last-Modified',
  104. HTTP_Length='Content-Length',
  105. HTTP_Location='Location',
  106. HTTP_Partial='Accept-Ranges',
  107. HTTP_Powered='X-Powered-By',
  108. HTTP_Pragma='Pragma',
  109. HTTP_Referer='Referer',
  110. HTTP_Transfer='Content-Transfer-Encoding',
  111. HTTP_WebAuth='WWW-Authenticate';
  112. //@}
  113. const
  114. //! Framework-mapped PHP globals
  115. PHP_Globals='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV',
  116. //! HTTP methods for RESTful interface
  117. HTTP_Methods='GET|HEAD|POST|PUT|DELETE|OPTIONS';
  118. //@{ Global variables and references to constants
  119. protected static
  120. $vars,
  121. $null=NULL,
  122. $true=TRUE,
  123. $false=FALSE;
  124. //@}
  125. private static
  126. //! Read-only framework variables
  127. $readonly='BASE|PROTOCOL|ROUTES|STATS|VERSION';
  128. /**
  129. Convert Windows double-backslashes to slashes; Also for
  130. referencing namespaced classes in subdirectories
  131. @return string
  132. @param $str string
  133. @public
  134. **/
  135. static function fixslashes($str) {
  136. return $str?strtr($str,'\\','/'):$str;
  137. }
  138. /**
  139. Convert PHP expression/value to string
  140. @return string
  141. @param $val mixed
  142. @public
  143. **/
  144. static function stringify($val) {
  145. return preg_replace('/\s+=>\s+/','=>',
  146. is_object($val) && !method_exists($val,'__set_state')?
  147. (method_exists($val,'__toString')?
  148. var_export((string)stripslashes($val),TRUE):
  149. ('object:'.get_class($val))):
  150. var_export($val,TRUE));
  151. }
  152. /**
  153. Flatten array values and return as CSV string
  154. @return string
  155. @param $args mixed
  156. @public
  157. **/
  158. static function csv($args) {
  159. if (!is_array($args))
  160. $args=array($args);
  161. $str='';
  162. foreach ($args as $key=>$val) {
  163. $str.=($str?',':'');
  164. if (is_string($key))
  165. $str.=var_export($key,TRUE).'=>';
  166. $str.=is_array($val)?
  167. ('array('.self::csv($val).')'):self::stringify($val);
  168. }
  169. return $str;
  170. }
  171. /**
  172. Split pipe-, semi-colon, comma-separated string
  173. @return array
  174. @param $str string
  175. @public
  176. **/
  177. static function split($str) {
  178. return array_map('trim',
  179. preg_split('/[|;,]/',$str,0,PREG_SPLIT_NO_EMPTY));
  180. }
  181. /**
  182. Generate Base36/CRC32 hash code
  183. @return string
  184. @param $str string
  185. @public
  186. **/
  187. static function hash($str) {
  188. return str_pad(base_convert(
  189. sprintf('%u',crc32($str)),10,36),7,'0',STR_PAD_LEFT);
  190. }
  191. /**
  192. Convert hexadecimal to binary-packed data
  193. @return string
  194. @param $hex string
  195. @public
  196. **/
  197. static function hexbin($hex) {
  198. return pack('H*',$hex);
  199. }
  200. /**
  201. Convert binary-packed data to hexadecimal
  202. @return string
  203. @param $bin string
  204. @public
  205. **/
  206. static function binhex($bin) {
  207. return implode('',unpack('H*',$bin));
  208. }
  209. /**
  210. Returns -1 if the specified number is negative, 0 if zero, or 1 if
  211. the number is positive
  212. @return int
  213. @param $num mixed
  214. @public
  215. **/
  216. static function sign($num) {
  217. return $num?$num/abs($num):0;
  218. }
  219. /**
  220. Convert engineering-notated string to bytes
  221. @return int
  222. @param $str string
  223. @public
  224. **/
  225. static function bytes($str) {
  226. $greek='KMGT';
  227. $exp=strpbrk($str,$greek);
  228. return pow(1024,strpos($greek,$exp)+1)*(int)$str;
  229. }
  230. /**
  231. Convert from JS dot notation to PHP array notation
  232. @return string
  233. @param $key string
  234. @public
  235. **/
  236. static function remix($key) {
  237. $out='';
  238. $obj=FALSE;
  239. foreach (preg_split('/\[\s*[\'"]?|[\'"]?\s*\]|\.|(->)/',
  240. $key,NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE) as $fix) {
  241. if ($out) {
  242. if ($fix=='->') {
  243. $obj=TRUE;
  244. continue;
  245. }
  246. elseif ($obj) {
  247. $obj=FALSE;
  248. $fix='->'.$fix;
  249. }
  250. else
  251. $fix='['.var_export($fix,TRUE).']';
  252. }
  253. $out.=$fix;
  254. }
  255. return $out;
  256. }
  257. /**
  258. Return TRUE if specified string is a valid framework variable name
  259. @return bool
  260. @param $key string
  261. @public
  262. **/
  263. static function valid($key) {
  264. if (preg_match('/^(\w+(?:\[[^\]]+\]|\.\w+|\s*->\s*\w+)*)$/',$key))
  265. return TRUE;
  266. // Invalid variable name
  267. trigger_error(sprintf(self::TEXT_Illegal,var_export($key,TRUE)));
  268. return FALSE;
  269. }
  270. /**
  271. Get framework variable reference/contents
  272. @return mixed
  273. @param $key string
  274. @param $set bool
  275. @public
  276. **/
  277. static function &ref($key,$set=TRUE) {
  278. // Traverse array
  279. $matches=preg_split(
  280. '/\[\s*[\'"]?|[\'"]?\s*\]|\.|(->)/',self::remix($key),
  281. NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
  282. // Referencing a SESSION variable element auto-starts a session
  283. if (count($matches)>1 && $matches[0]=='SESSION' && !session_id()) {
  284. session_start();
  285. // Sync framework and PHP global
  286. self::$vars['SESSION']=&$_SESSION;
  287. }
  288. // Read-only framework variable?
  289. if ($set && !preg_match('/^('.self::$readonly.')\b/',$matches[0]))
  290. $var=&self::$vars;
  291. else
  292. $var=self::$vars;
  293. $obj=FALSE;
  294. foreach ($matches as $match)
  295. if ($match=='->')
  296. $obj=TRUE;
  297. else {
  298. if (preg_match('/@(\w+)/',$match,$token))
  299. // Token found
  300. $match=&self::ref($token[1]);
  301. if ($set) {
  302. // Create property/array element if not found
  303. if ($obj) {
  304. if (!is_object($var))
  305. $var=new stdClass;
  306. if (!isset($var->$match))
  307. $var->$match=NULL;
  308. $var=&$var->$match;
  309. $obj=FALSE;
  310. }
  311. else
  312. $var=&$var[$match];
  313. }
  314. elseif ($obj && isset($var->$match)) {
  315. // Object property found
  316. $var=$var->$match;
  317. $obj=FALSE;
  318. }
  319. elseif (is_array($var) && isset($var[$match]))
  320. // Array element found
  321. $var=$var[$match];
  322. else
  323. // Property/array element doesn't exist
  324. return self::$null;
  325. }
  326. if ($set && count($matches)>1 &&
  327. preg_match('/GET|POST|COOKIE/',$matches[0],$php)) {
  328. // Sync with REQUEST
  329. $req=&self::ref(preg_replace('/^'.$php[0].'\b/','REQUEST',$key));
  330. $req=$var;
  331. }
  332. return $var;
  333. }
  334. /**
  335. Copy contents of framework variable to another
  336. @param $src string
  337. @param $dst string
  338. @public
  339. **/
  340. static function copy($src,$dst) {
  341. $ref=&self::ref($dst);
  342. $ref=self::ref($src);
  343. }
  344. /**
  345. Concatenate string to framework string variable
  346. @param $var string
  347. @param $val string
  348. @public
  349. **/
  350. static function concat($var,$val) {
  351. $ref=&self::ref($var);
  352. $ref.=$val;
  353. }
  354. /**
  355. Format framework string variable
  356. @return string
  357. @public
  358. **/
  359. static function sprintf() {
  360. return call_user_func_array('sprintf',
  361. array_map('self::resolve',func_get_args()));
  362. }
  363. /**
  364. Add keyed element to the end of framework array variable
  365. @param $var string
  366. @param $key string
  367. @param $val mixed
  368. @public
  369. **/
  370. static function append($var,$key,$val) {
  371. $ref=&self::ref($var);
  372. $ref[self::resolve($key)]=$val;
  373. }
  374. /**
  375. Swap keys and values of framework array variable
  376. @param $var string
  377. @public
  378. **/
  379. static function flip($var) {
  380. $ref=&self::ref($var);
  381. $ref=array_combine(array_values($ref),array_keys($ref));
  382. }
  383. /**
  384. Merge one or more framework array variables
  385. @public
  386. **/
  387. static function merge() {
  388. $args=func_get_args();
  389. foreach ($args as &$arg) {
  390. if (is_string($arg))
  391. $arg=self::ref($arg);
  392. if (!is_array($arg))
  393. trigger_error(sprintf(self::TEXT_NotArray,
  394. self::stringify($arg)));
  395. }
  396. $ref=&self::ref($var);
  397. $ref=call_user_func_array('array_merge',$args);
  398. }
  399. /**
  400. Add element to the end of framework array variable
  401. @param $var string
  402. @param $val mixed
  403. @public
  404. **/
  405. static function push($var,$val) {
  406. $ref=&self::ref($var);
  407. if (!is_array($ref))
  408. $ref=array();
  409. array_push($ref,is_array($val)?
  410. array_map('self::resolve',$val):
  411. (is_string($val)?self::resolve($val):$val));
  412. }
  413. /**
  414. Remove last element of framework array variable and
  415. return the element
  416. @return mixed
  417. @param $var string
  418. @public
  419. **/
  420. static function pop($var) {
  421. $ref=&self::ref($var);
  422. if (is_array($ref))
  423. return array_pop($ref);
  424. trigger_error(sprintf(self::TEXT_NotArray,$var));
  425. return FALSE;
  426. }
  427. /**
  428. Add element to the beginning of framework array variable
  429. @param $var string
  430. @param $val mixed
  431. @public
  432. **/
  433. static function unshift($var,$val) {
  434. $ref=&self::ref($var);
  435. if (!is_array($ref))
  436. $ref=array();
  437. array_unshift($ref,is_array($val)?
  438. array_map('self::resolve',$val):
  439. (is_string($val)?self::resolve($val):$val));
  440. }
  441. /**
  442. Remove first element of framework array variable and
  443. return the element
  444. @return mixed
  445. @param $var string
  446. @public
  447. **/
  448. static function shift($var) {
  449. $ref=&self::ref($var);
  450. if (is_array($ref))
  451. return array_shift($ref);
  452. trigger_error(sprintf(self::TEXT_NotArray,$var));
  453. return FALSE;
  454. }
  455. /**
  456. Execute callback as a mutex operation
  457. @return mixed
  458. @public
  459. **/
  460. static function mutex() {
  461. $args=func_get_args();
  462. $func=array_shift($args);
  463. $handles=array();
  464. foreach ($args as $file) {
  465. $lock=$file.'.lock';
  466. while (TRUE) {
  467. usleep(mt_rand(0,100));
  468. if (is_resource($handle=@fopen($lock,'x'))) {
  469. $handles[$lock]=$handle;
  470. break;
  471. }
  472. }
  473. }
  474. $out=$func();
  475. foreach ($handles as $lock=>$handle) {
  476. fclose($handle);
  477. unlink($lock);
  478. }
  479. return $out;
  480. }
  481. /**
  482. Lock-aware file reader
  483. @param $file string
  484. @public
  485. **/
  486. static function getfile($file) {
  487. if (!function_exists('flock'))
  488. $out=self::mutex(
  489. function() use($file) {
  490. return file_get_contents($file);
  491. },
  492. $file
  493. );
  494. elseif ($handle=@fopen($file,'r')) {
  495. flock($handle,LOCK_EX);
  496. $size=filesize($file);
  497. if ($size)
  498. $out=fread($handle,$size);
  499. flock($handle,LOCK_UN);
  500. fclose($handle);
  501. }
  502. return $out;
  503. }
  504. /**
  505. Lock-aware file writer
  506. @param $file string
  507. @param $data string
  508. @public
  509. **/
  510. static function putfile($file,$data) {
  511. if (!function_exists('flock'))
  512. $out=self::mutex(
  513. function() use($file,$data) {
  514. return file_put_contents($file,$data,LOCK_EX);
  515. },
  516. $file
  517. );
  518. else
  519. $out=file_put_contents($file,$data,LOCK_EX);
  520. return $out;
  521. }
  522. /**
  523. Evaluate template expressions in string
  524. @return string
  525. @param $val mixed
  526. @public
  527. **/
  528. static function resolve($val) {
  529. // Analyze string for correct framework expression syntax
  530. $self=__CLASS__;
  531. $str=preg_replace_callback(
  532. // Expression
  533. '/{{(.+?)}}/i',
  534. function($expr) use($self) {
  535. // Evaluate expression
  536. $out=preg_replace_callback(
  537. // Function
  538. '/(?<!@)\b(\w+)\s*\(([^\)]*)\)/',
  539. function($func) use($self) {
  540. return is_callable($ref=$self::ref($func[1],FALSE))?
  541. // Variable holds an anonymous function
  542. call_user_func_array($ref,str_getcsv($func[2])):
  543. // Check if empty array
  544. ($func[1].$func[2]=='array'?'NULL':$func[0]);
  545. },
  546. preg_replace_callback(
  547. // Framework variable
  548. '/(?<!\w)@(\w+(?:\[[^\]]+\]|\.\w+)*'.
  549. '(?:\s*->\s*\w+)?)\s*(\(([^\)]*)\))?(?:\\\(.+))?/',
  550. function($var) use($self) {
  551. // Retrieve variable contents
  552. $val=$self::ref($var[1],FALSE);
  553. if (isset($var[2]) && is_callable($val))
  554. // Anonymous function
  555. $val=call_user_func_array(
  556. $val,str_getcsv($var[3]));
  557. if (isset($var[4]) && class_exists('ICU',FALSE))
  558. // ICU-formatted string
  559. $val=call_user_func_array('ICU::format',
  560. array($val,str_getcsv($var[4])));
  561. return $self::stringify($val);
  562. },
  563. $expr[1]
  564. )
  565. );
  566. return !preg_match('/@|\bnew\s+/i',$out) &&
  567. ($eval=eval('return (string)'.$out.';'))!==FALSE?
  568. $eval:$out;
  569. },
  570. $val
  571. );
  572. return $str;
  573. }
  574. /**
  575. Sniff headers for real IP address
  576. @return string
  577. @public
  578. **/
  579. static function realip() {
  580. if (isset($_SERVER['HTTP_CLIENT_IP']))
  581. // Behind proxy
  582. return $_SERVER['HTTP_CLIENT_IP'];
  583. elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  584. // Use first IP address in list
  585. list($ip)=explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
  586. return $ip;
  587. }
  588. return $_SERVER['REMOTE_ADDR'];
  589. }
  590. /**
  591. Return TRUE if IP address is local or within a private IPv4 range
  592. @return bool
  593. @param $addr string
  594. @public
  595. **/
  596. static function privateip($addr=NULL) {
  597. if (!$addr)
  598. $addr=self::realip();
  599. return preg_match('/^127\.0\.0\.\d{1,3}$/',$addr) ||
  600. !filter_var($addr,FILTER_VALIDATE_IP,
  601. FILTER_FLAG_IPV4|FILTER_FLAG_NO_PRIV_RANGE);
  602. }
  603. /**
  604. Clean and repair HTML
  605. @return string
  606. @param $html string
  607. @public
  608. **/
  609. static function tidy($html) {
  610. if (!extension_loaded('tidy'))
  611. return $html;
  612. $tidy=new Tidy;
  613. $tidy->parseString($html,self::$vars['TIDY'],
  614. str_replace('-','',self::$vars['ENCODING']));
  615. $tidy->cleanRepair();
  616. return (string)$tidy;
  617. }
  618. /**
  619. Create folder; Trigger error and return FALSE if script has no
  620. permission to create folder in the specified path
  621. @param $name string
  622. @param $perm int
  623. @public
  624. **/
  625. static function mkdir($name,$perm=0755) {
  626. if (!is_writable(dirname($name)) &&
  627. function_exists('posix_getpwuid')) {
  628. $uid=posix_getpwuid(posix_geteuid());
  629. trigger_error(sprintf(self::TEXT_Write,
  630. $uid['name'],realpath(dirname($name))));
  631. return FALSE;
  632. }
  633. // Create the folder
  634. umask(0);
  635. mkdir($name,$perm);
  636. }
  637. /**
  638. Intercept calls to undefined methods
  639. @param $func string
  640. @param $args array
  641. @public
  642. **/
  643. function __call($func,array $args) {
  644. trigger_error(sprintf(self::TEXT_Method,get_called_class().'->'.
  645. $func.'('.self::csv($args).')'));
  646. }
  647. /**
  648. Intercept calls to undefined static methods
  649. @param $func string
  650. @param $args array
  651. @public
  652. **/
  653. static function __callStatic($func,array $args) {
  654. trigger_error(sprintf(self::TEXT_Method,get_called_class().'::'.
  655. $func.'('.self::csv($args).')'));
  656. }
  657. /**
  658. Return instance of child class
  659. @public
  660. **/
  661. static function instance() {
  662. return eval('return new '.get_called_class().
  663. '('.self::csv(func_get_args()).');');
  664. }
  665. /**
  666. Class constructor
  667. @public
  668. **/
  669. function __construct() {
  670. // Prohibit use of class as an object
  671. trigger_error(sprintf(self::TEXT_Object,get_called_class()));
  672. }
  673. }
  674. //! Main framework code
  675. class F3 extends Base {
  676. /**
  677. Bind value to framework variable
  678. @param $key string
  679. @param $val mixed
  680. @param $persist bool
  681. @param $resolve bool
  682. @public
  683. **/
  684. static function set($key,$val,$persist=FALSE,$resolve=TRUE) {
  685. if (preg_match('/{{.+}}/',$key))
  686. // Variable variable
  687. $key=self::resolve($key);
  688. if (!self::valid($key))
  689. return;
  690. if (preg_match('/COOKIE\b/',$key) && !headers_sent()) {
  691. // Create/modify cookie
  692. $matches=preg_split(
  693. '/\[\s*[\'"]?|[\'"]?\s*\]|\./',self::remix($key),
  694. NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
  695. array_shift($matches);
  696. if ($matches) {
  697. $var='';
  698. foreach ($matches as $match)
  699. if (!$var)
  700. $var=$match;
  701. else
  702. $var.='[\''.$match.'\']';
  703. $val=array($var,$val);
  704. }
  705. if (is_array($val))
  706. foreach ($val as $var=>$sub) {
  707. $func=self::$vars['COOKIES'];
  708. array_unshift($func,$var,$sub);
  709. call_user_func_array('setcookie',$func);
  710. }
  711. return;
  712. }
  713. $var=&self::ref($key);
  714. if (is_string($val) && $resolve)
  715. $val=self::resolve($val);
  716. elseif (is_array($val)) {
  717. $var=array();
  718. // Recursive token substitution
  719. foreach ($val as $subk=>$subv)
  720. self::set($key.'['.var_export($subk,TRUE).']',
  721. $subv,FALSE);
  722. return;
  723. }
  724. $var=$val;
  725. if (preg_match('/LANGUAGE|LOCALES/',$key) && class_exists('ICU'))
  726. // Load appropriate dictionaries
  727. ICU::load();
  728. // Initialize cache if explicitly defined
  729. elseif ($key=='CACHE' && $val)
  730. Cache::prep();
  731. if ($persist) {
  732. $hash='var.'.self::hash(self::remix($key));
  733. Cache::set($hash,$val);
  734. }
  735. }
  736. /**
  737. Retrieve value of framework variable and apply locale rules
  738. @return mixed
  739. @param $key string
  740. @param $args mixed
  741. @public
  742. **/
  743. static function get($key,$args=NULL) {
  744. if (preg_match('/{{.+}}/',$key))
  745. // Variable variable
  746. $key=self::resolve($key);
  747. if (!self::valid($key))
  748. return self::$null;
  749. $val=self::ref($key,FALSE);
  750. if (is_string($val))
  751. return class_exists('ICU',FALSE) && $args?
  752. ICU::format($val,$args):$val;
  753. elseif (is_null($val)) {
  754. // Attempt to retrieve from cache
  755. $hash='var.'.self::hash(self::remix($key));
  756. if (Cache::cached($hash))
  757. $val=Cache::get($hash);
  758. }
  759. return $val;
  760. }
  761. /**
  762. Unset framework variable
  763. @param $key string
  764. @public
  765. **/
  766. static function clear($key) {
  767. if (preg_match('/{{.+}}/',$key))
  768. // Variable variable
  769. $key=self::resolve($key);
  770. if (!self::valid($key))
  771. return;
  772. if (preg_match('/COOKIE/',$key) && !headers_sent()) {
  773. $val=$_COOKIE;
  774. $matches=preg_split(
  775. '/\[\s*[\'"]?|[\'"]?\s*\]|\./',self::remix($key),
  776. NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
  777. array_shift($matches);
  778. if ($matches) {
  779. // Expire specific cookie
  780. $var='';
  781. foreach ($matches as $match)
  782. if (!$var)
  783. $var=$match;
  784. else
  785. $var.='[\''.$match.'\']';
  786. $val=array($var,FALSE);
  787. }
  788. if (is_array($val))
  789. // Expire all cookies
  790. foreach ($val as $var=>$sub) {
  791. $func=self::$vars['COOKIES'];
  792. $func['expire']=strtotime('-1 year');
  793. array_unshift($func,$var,FALSE);
  794. call_user_func_array('setcookie',$func);
  795. }
  796. return;
  797. }
  798. // Clearing SESSION array ends the current session
  799. if ($key=='SESSION') {
  800. if (!session_id())
  801. session_start();
  802. // End the session
  803. session_unset();
  804. session_destroy();
  805. }
  806. preg_match('/^('.self::PHP_Globals.')(.*)$/',$key,$match);
  807. if (isset($match[1])) {
  808. $name=self::remix($key,FALSE);
  809. eval($match[2]?'unset($_'.$name.');':'$_'.$name.'=NULL;');
  810. }
  811. $name=preg_replace('/^(\w+)/','[\'\1\']',self::remix($key));
  812. // Assign NULL to framework variables; do not unset
  813. eval(ctype_upper(preg_replace('/^\w+/','\0',$key))?
  814. 'self::$vars'.$name.'=NULL;':'unset(self::$vars'.$name.');');
  815. // Remove from cache
  816. $hash='var.'.self::hash(self::remix($key));
  817. if (Cache::cached($hash))
  818. Cache::clear($hash);
  819. }
  820. /**
  821. Return TRUE if framework variable has been assigned a value
  822. @return bool
  823. @param $key string
  824. @public
  825. **/
  826. static function exists($key) {
  827. if (preg_match('/{{.+}}/',$key))
  828. // Variable variable
  829. $key=self::resolve($key);
  830. if (!self::valid($key))
  831. return FALSE;
  832. $var=&self::ref($key,FALSE);
  833. return isset($var);
  834. }
  835. /**
  836. Multi-variable assignment using associative array
  837. @param $arg array
  838. @param $pfx string
  839. @public
  840. **/
  841. static function mset($arg,$pfx='') {
  842. if (!is_array($arg))
  843. // Invalid argument
  844. trigger_error(self::TEXT_MSet);
  845. else
  846. // Bind key-value pairs
  847. foreach ($arg as $key=>$val)
  848. self::set($pfx.$key,$val);
  849. }
  850. /**
  851. Determine if framework variable has been cached
  852. @return mixed
  853. @param $key string
  854. @public
  855. **/
  856. static function cached($key) {
  857. if (preg_match('/{{.+}}/',$key))
  858. // Variable variable
  859. $key=self::resolve($key);
  860. return self::valid($key)?
  861. Cache::cached('var.'.self::hash(self::remix($key))):
  862. FALSE;
  863. }
  864. /**
  865. Configure framework according to INI-style file settings;
  866. Cache auto-generated PHP code to speed up execution
  867. @param $file string
  868. @public
  869. **/
  870. static function config($file) {
  871. // Generate hash code for config file
  872. $hash='php.'.self::hash($file);
  873. $cached=Cache::cached($hash);
  874. if ($cached && filemtime($file)<$cached)
  875. // Retrieve from cache
  876. $save=Cache::get($hash);
  877. else {
  878. if (!is_file($file)) {
  879. // Configuration file not found
  880. trigger_error(sprintf(self::TEXT_Config,$file));
  881. return;
  882. }
  883. // Map sections to framework methods
  884. $map=array('globals'=>'set','routes'=>'route','maps'=>'map');
  885. // Read the .ini file
  886. preg_match_all(
  887. '/\s*(?:\[(.+?)\]|(?:;.+?)?|(?:([^=]+)=(.+?)))(?:\v|$)/s',
  888. self::getfile($file),$matches,PREG_SET_ORDER);
  889. $cfg=array();
  890. $ptr=&$cfg;
  891. foreach ($matches as $match)
  892. if (isset($match[1]) && !empty($match[1])) {
  893. // Section header
  894. if (!isset($map[$match[1]])) {
  895. // Unknown section
  896. trigger_error(sprintf(self::TEXT_Section,$section));
  897. return;
  898. }
  899. $ptr=&$cfg[$match[1]];
  900. }
  901. elseif (isset($match[2]) && !empty($match[2])) {
  902. $csv=array_map(
  903. function($val) {
  904. // Typecast if necessary
  905. return is_numeric($val) ||
  906. preg_match('/^(TRUE|FALSE)\b/i',$val)?
  907. eval('return '.$val.';'):$val;
  908. },
  909. str_getcsv($match[3])
  910. );
  911. // Convert comma-separated values to array
  912. $match[3]=count($csv)>1?$csv:$csv[0];
  913. if (preg_match('/([^\[]+)\[([^\]]*)\]/',$match[2],$sub)) {
  914. if ($sub[2])
  915. // Associative array
  916. $ptr[$sub[1]][$sub[2]]=$match[3];
  917. else
  918. // Numeric-indexed array
  919. $ptr[$sub[1]][]=$match[3];
  920. }
  921. else
  922. // Key-value pair
  923. $ptr[trim($match[2])]=$match[3];
  924. }
  925. ob_start();
  926. foreach ($cfg as $section=>$pairs)
  927. if (isset($map[$section]) && is_array($pairs)) {
  928. $func=$map[$section];
  929. foreach ($pairs as $key=>$val)
  930. // Generate PHP snippet
  931. echo 'self::'.$func.'('.var_export($key,TRUE).','.
  932. ($func=='set' || !is_array($val)?
  933. var_export($val,TRUE):self::csv($val)).
  934. ');'."\n";
  935. }
  936. $save=ob_get_clean();
  937. // Compress and save to cache
  938. Cache::set($hash,$save);
  939. }
  940. // Execute cached PHP code
  941. eval($save);
  942. if (!is_null(self::$vars['ERROR']))
  943. // Remove from cache
  944. Cache::clear($hash);
  945. }
  946. /**
  947. Convert special characters to HTML entities using globally-
  948. defined character set
  949. @return string
  950. @param $str string
  951. @param $all bool
  952. @public
  953. **/
  954. static function htmlencode($str,$all=FALSE) {
  955. return call_user_func(
  956. $all?'htmlentities':'htmlspecialchars',
  957. $str,ENT_COMPAT,self::$vars['ENCODING'],TRUE);
  958. }
  959. /**
  960. Convert HTML entities back to their equivalent characters
  961. @return string
  962. @param $str string
  963. @param $all bool
  964. @public
  965. **/
  966. static function htmldecode($str,$all=FALSE) {
  967. return $all?
  968. html_entity_decode($str,ENT_COMPAT,self::$vars['ENCODING']):
  969. htmlspecialchars_decode($str,ENT_COMPAT);
  970. }
  971. /**
  972. Send HTTP status header; Return text equivalent of status code
  973. @return mixed
  974. @param $code int
  975. @public
  976. **/
  977. static function status($code) {
  978. if (!defined('self::HTTP_'.$code)) {
  979. // Invalid status code
  980. trigger_error(sprintf(self::TEXT_HTTP,$code));
  981. return FALSE;
  982. }
  983. // Get description
  984. $response=constant('self::HTTP_'.$code);
  985. // Send raw HTTP header
  986. if (PHP_SAPI!='cli' && !headers_sent())
  987. header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$response);
  988. return $response;
  989. }
  990. /**
  991. Retrieve HTTP headers
  992. @return array
  993. @public
  994. **/
  995. static function headers() {
  996. if (PHP_SAPI!='cli') {
  997. if (function_exists('getallheaders'))
  998. // Apache server
  999. return getallheaders();
  1000. // Workaround
  1001. $req=array();
  1002. foreach ($_SERVER as $key=>$val)
  1003. if (substr($key,0,5)=='HTTP_')
  1004. $req[preg_replace_callback(
  1005. '/\w+\b/',
  1006. function($word) {
  1007. return ucfirst(strtolower($word[0]));
  1008. },
  1009. strtr(substr($key,5),'_','-')
  1010. )]=$val;
  1011. return $req;
  1012. }
  1013. return array();
  1014. }
  1015. /**
  1016. Send HTTP header with expiration date (seconds from current time)
  1017. @param $secs int
  1018. @public
  1019. **/
  1020. static function expire($secs=0) {
  1021. if (PHP_SAPI!='cli' && !headers_sent()) {
  1022. $time=time();
  1023. $req=self::headers();
  1024. if (isset($req[self::HTTP_IfMod]) &&
  1025. strtotime($req[self::HTTP_IfMod])+$secs>$time) {
  1026. self::status(304);
  1027. die;
  1028. }
  1029. header(self::HTTP_Powered.': '.self::TEXT_AppName.' '.
  1030. '('.self::TEXT_AppURL.')');
  1031. if ($secs) {
  1032. header_remove(self::HTTP_Pragma);
  1033. header(self::HTTP_Expires.': '.gmdate('r',$time+$secs));
  1034. header(self::HTTP_Cache.': max-age='.$secs);
  1035. header(self::HTTP_LastMod.': '.gmdate('r'));
  1036. }
  1037. else {
  1038. header(self::HTTP_Pragma.': no-cache');
  1039. header(self::HTTP_Cache.': no-cache, must-revalidate');
  1040. }
  1041. }
  1042. }
  1043. /**
  1044. Reroute to specified URI
  1045. @param $uri string
  1046. @public
  1047. **/
  1048. static function reroute($uri) {
  1049. $uri=self::resolve($uri);
  1050. if (PHP_SAPI!='cli' && !headers_sent()) {
  1051. // HTTP redirect
  1052. self::status($_SERVER['REQUEST_METHOD']=='GET'?301:303);
  1053. if (session_id())
  1054. session_commit();
  1055. header(self::HTTP_Location.': '.
  1056. (preg_match('/^https?:\/\//',$uri)?
  1057. $uri:(self::$vars['BASE'].$uri)));
  1058. die;
  1059. }
  1060. self::mock('GET '.$uri);
  1061. self::run();
  1062. }
  1063. /**
  1064. Assign handler to route pattern
  1065. @param $pattern string
  1066. @param $funcs mixed
  1067. @param $ttl int
  1068. @param $throttle int
  1069. @param $hotlink bool
  1070. @public
  1071. **/
  1072. static function route($pattern,$funcs,$ttl=0,$throttle=0,$hotlink=TRUE) {
  1073. list($methods,$uri)=
  1074. preg_split('/\s+/',$pattern,2,PREG_SPLIT_NO_EMPTY);
  1075. foreach (self::split($methods) as $method)
  1076. // Use pattern and HTTP methods as route indexes
  1077. self::$vars['ROUTES'][$uri][strtoupper($method)]=
  1078. // Save handler, cache timeout and hotlink permission
  1079. array($funcs,$ttl,$throttle,$hotlink);
  1080. }
  1081. /**
  1082. Provide REST interface by mapping URL to object/class
  1083. @param $url string
  1084. @param $class mixed
  1085. @param $ttl int
  1086. @param $throttle int
  1087. @param $hotlink bool
  1088. @public
  1089. **/
  1090. static function map($url,$class,$ttl=0,$throttle=0,$hotlink=TRUE) {
  1091. foreach (explode('|',self::HTTP_Methods) as $method)
  1092. if (method_exists($class,$method)) {
  1093. $ref=new ReflectionMethod($class,$method);
  1094. self::route($method.' '.$url,$ref->isStatic()?
  1095. array($class,$method):array(new $class,$method),$ttl,
  1096. $throttle,$hotlink);
  1097. unset($ref);
  1098. }
  1099. }
  1100. /**
  1101. Call route handler
  1102. @return mixed
  1103. @param $funcs string
  1104. @param $listen bool
  1105. @public
  1106. **/
  1107. static function call($funcs,$listen=FALSE) {
  1108. $classes=array();
  1109. $funcs=is_string($funcs)?self::split($funcs):array($funcs);
  1110. foreach ($funcs as $func) {
  1111. if (is_string($func)) {
  1112. $func=self::resolve($func);
  1113. if (preg_match('/(.+)\s*(->|::)\s*(.+)/s',$func,$match)) {
  1114. if (!class_exists($match[1]) ||
  1115. !method_exists($match[1],$match[3])) {
  1116. trigger_error(sprintf(self::TEXT_Callback,$func));
  1117. return FALSE;
  1118. }
  1119. $func=array($match[2]=='->'?
  1120. new $match[1]:$match[1],$match[3]);
  1121. }
  1122. elseif (!function_exists($func)) {
  1123. if (preg_match('/\.php$/i',$func)) {
  1124. foreach (self::split(self::$vars['IMPORTS'])
  1125. as $path)
  1126. if (is_file($file=$path.$func)) {
  1127. $instance=new F3instance;
  1128. return $instance->sandbox($file);
  1129. }
  1130. trigger_error(sprintf(self::TEXT_Import,$func));
  1131. }
  1132. else
  1133. trigger_error(sprintf(self::TEXT_Callback,$func));
  1134. return FALSE;
  1135. }
  1136. }
  1137. if (!is_callable($func)) {
  1138. trigger_error(sprintf(self::TEXT_Callback,
  1139. is_array($func) && count($func)>1?
  1140. (get_class($func[0]).(is_object($func[0])?'->':'::').
  1141. $func[1]):$func));
  1142. return FALSE;
  1143. }
  1144. $oop=is_array($func) &&
  1145. (is_object($func[0]) || is_string($func[0]));
  1146. if ($listen && $oop &&
  1147. method_exists($func[0],$before='beforeRoute') &&
  1148. !in_array($func[0],$classes)) {
  1149. // Execute beforeRoute() once per class
  1150. if (call_user_func(array($func[0],$before))===FALSE)
  1151. return FALSE;
  1152. $classes[]=is_object($func[0])?get_class($func[0]):$func[0];
  1153. }
  1154. $out=call_user_func($func);
  1155. if ($listen && $oop &&
  1156. method_exists($func[0],$after='afterRoute') &&
  1157. !in_array($func[0],$classes)) {
  1158. // Execute afterRoute() once per class
  1159. call_user_func(array($func[0],$after));
  1160. $classes[]=is_object($func[0])?get_class($func[0]):$func[0];
  1161. }
  1162. }
  1163. return $out;
  1164. }
  1165. /**
  1166. Process routes based on incoming URI
  1167. @public
  1168. **/
  1169. static function run() {
  1170. // Validate user against spam blacklists
  1171. if (self::$vars['DNSBL'] && !self::privateip($addr=self::realip()) &&
  1172. (!self::$vars['EXEMPT'] ||
  1173. !in_array($addr,self::split(self::$vars['EXEMPT'])))) {
  1174. // Convert to reverse IP dotted quad
  1175. $quad=implode('.',array_reverse(explode('.',$addr)));
  1176. foreach (self::split(self::$vars['DNSBL']) as $list)
  1177. // Check against DNS blacklist
  1178. if (gethostbyname($quad.'.'.$list)!=$quad.'.'.$list) {
  1179. if (self::$vars['SPAM'])
  1180. // Spammer detected; Send to blackhole
  1181. self::reroute(self::$vars['SPAM']);
  1182. else {
  1183. // Forbidden
  1184. self::error(403);
  1185. die;
  1186. }
  1187. }
  1188. }
  1189. // Process routes
  1190. if (!isset(self::$vars['ROUTES']) || !self::$vars['ROUTES']) {
  1191. trigger_error(self::TEXT_NoRoutes);
  1192. return;
  1193. }
  1194. $found=FALSE;
  1195. // Detailed routes get matched first
  1196. krsort(self::$vars['ROUTES']);
  1197. $time=time();
  1198. $req=preg_replace('/^'.preg_quote(self::$vars['BASE'],'/').
  1199. '\b(.+)/','\1',rawurldecode($_SERVER['REQUEST_URI']));
  1200. foreach (self::$vars['ROUTES'] as $uri=>$route) {
  1201. if (!preg_match('/^'.
  1202. preg_replace(
  1203. '/(?:{{)?@(\w+\b)(?:}})?/i',
  1204. // Valid URL characters (RFC 1738)
  1205. '(?P<\1>[\w\-\.!~\*\'"(),\s]+)',
  1206. // Wildcard character in URI
  1207. str_replace('\*','(.*)',preg_quote($uri,'/'))
  1208. ).'\/?(?:\?.*)?$/i',$req,$args))
  1209. continue;
  1210. $found=TRUE;
  1211. // Inspect each defined route
  1212. foreach ($route as $method=>$proc) {
  1213. if (!preg_match('/'.$method.'/',$_SERVER['REQUEST_METHOD']))
  1214. continue;
  1215. list($funcs,$ttl,$throttle,$hotlink)=$proc;
  1216. if (!$hotlink && isset(self::$vars['HOTLINK']) &&
  1217. isset($_SERVER['HTTP_REFERER']) &&
  1218. parse_url($_SERVER['HTTP_REFERER'],PHP_URL_HOST)!=
  1219. $_SERVER['SERVER_NAME'])
  1220. // Hot link detected; Redirect page
  1221. self::reroute(self::$vars['HOTLINK']);
  1222. // Save named uri captures
  1223. foreach ($args as $key=>$arg)
  1224. // Remove non-zero indexed elements
  1225. if (is_numeric($key) && $key)
  1226. unset($args[$key]);
  1227. self::$vars['PARAMS']=$args;
  1228. // Default: Do not cache
  1229. self::expire(0);
  1230. if ($_SERVER['REQUEST_METHOD']=='GET' && $ttl) {
  1231. $_SERVER['REQUEST_TTL']=$ttl;
  1232. // Get HTTP request headers
  1233. $req=self::headers();
  1234. // Content divider
  1235. $div=chr(0);
  1236. // Get hash code for this Web page
  1237. $hash='url.'.self::hash(
  1238. $_SERVER['REQUEST_METHOD'].' '.
  1239. $_SERVER['REQUEST_URI']
  1240. );
  1241. $cached=Cache::cached($hash);
  1242. $uri='/^'.self::HTTP_Content.':.+/';
  1243. if ($cached && $time-$cached<$ttl) {
  1244. if (!isset($req[self::HTTP_IfMod]) ||
  1245. $cached>strtotime($req[self::HTTP_IfMod])) {
  1246. // Activate cache timer
  1247. self::expire($cached+$ttl-$time);
  1248. // Retrieve from cache
  1249. $buffer=Cache::get($hash);
  1250. $type=strstr($buffer,$div,TRUE);
  1251. if (PHP_SAPI!='cli' && !headers_sent() &&
  1252. preg_match($uri,$type,$match))
  1253. // Cached MIME type
  1254. header($match[0]);
  1255. // Save response
  1256. self::$vars['RESPONSE']=substr(
  1257. strstr($buffer,$div),1);
  1258. }
  1259. else {
  1260. // Client-side cache is still fresh
  1261. self::status(304);
  1262. die;
  1263. }
  1264. }
  1265. else {
  1266. // Activate cache timer
  1267. self::expire($ttl);
  1268. $type='';
  1269. foreach (headers_list() as $hdr)
  1270. if (preg_match($uri,$hdr)) {
  1271. // Add Content-Type header to buffer
  1272. $type=$hdr;
  1273. break;
  1274. }
  1275. // Cache this page
  1276. ob_start();
  1277. self::call($funcs,TRUE);
  1278. self::$vars['RESPONSE']=ob_get_clean();
  1279. if (!self::$vars['ERROR'] &&
  1280. self::$vars['RESPONSE'])
  1281. // Compress and save to cache
  1282. Cache::set($hash,
  1283. $type.$div.self::$vars['RESPONSE']);
  1284. }
  1285. }
  1286. else {
  1287. self::expire(0);
  1288. // Capture output
  1289. ob_start();
  1290. self::$vars['REQBODY']=file_get_contents('php://input');
  1291. self::call($funcs,TRUE);
  1292. self::$vars['RESPONSE']=ob_get_clean();
  1293. }
  1294. $elapsed=time()-$time;
  1295. $throttle=$throttle?:self::$vars['THROTTLE'];
  1296. if ($throttle/1e3>$elapsed)
  1297. // Delay output
  1298. usleep(1e6*($throttle/1e3-$elapsed));
  1299. if (self::$vars['RESPONSE'] && !self::$vars['QUIET'])
  1300. // Display response
  1301. echo self::$vars['RESPONSE'];
  1302. // Hail the conquering hero
  1303. return;
  1304. }
  1305. // Method not allowed
  1306. if (PHP_SAPI!='cli' && !headers_sent())
  1307. header(self::HTTP_Allow.': '.
  1308. implode(',',array_keys($route)));
  1309. self::error(405);
  1310. return;
  1311. }
  1312. // No such Web page
  1313. self::error(404);
  1314. }
  1315. /**
  1316. Transmit a file for downloading by HTTP client; If kilobytes per
  1317. second is specified, output is throttled (bandwidth will not be
  1318. controlled by default); Return TRUE if successful, FALSE otherwise;
  1319. Support for partial downloads is indicated by third argument
  1320. @param $file string
  1321. @param $kbps int
  1322. @param $partial
  1323. @public
  1324. **/
  1325. static function send($file,$kbps=0,$partial=TRUE) {
  1326. $file=self::resolve($file);
  1327. if (!is_file($file)) {
  1328. self::error(404);
  1329. return FALSE;
  1330. }
  1331. if (PHP_SAPI!='cli' && !headers_sent()) {
  1332. header(self::HTTP_Content.': application/octet-stream');
  1333. header(self::HTTP_Partial.': '.($partial?'bytes':'none'));
  1334. header(self::HTTP_Length.': '.filesize($file));
  1335. }
  1336. $ctr=1;
  1337. $handle=fopen($file,'r');
  1338. $time=microtime(TRUE);
  1339. while (!feof($handle) && !connection_aborted()) {
  1340. if ($kbps) {
  1341. // Throttle bandwidth
  1342. $ctr++;
  1343. if (($ctr/$kbps)>microtime(TRUE)-$time)
  1344. usleep(1e6*($ctr/$kbps-$elapsed));
  1345. }
  1346. // Send 1KiB and reset timer
  1347. echo fread($handle,1024);
  1348. }
  1349. fclose($handle);
  1350. die;
  1351. }
  1352. /**
  1353. Remove HTML tags (except those enumerated) to protect against
  1354. XSS/code injection attacks
  1355. @return mixed
  1356. @param $input string
  1357. @param $tags string
  1358. @public
  1359. **/
  1360. static function scrub($input,$tags=NULL) {
  1361. if (is_array($input))
  1362. foreach ($input as &$val)
  1363. $val=self::scrub($val,$tags);
  1364. if (is_string($input)) {
  1365. $input=($tags=='*')?
  1366. $input:strip_tags($input,is_string($tags)?
  1367. ('<'.implode('><',self::split($tags)).'>'):$tags);
  1368. }
  1369. return $input;
  1370. }
  1371. /**
  1372. Call form field handler
  1373. @param $fields string
  1374. @param $funcs mixed
  1375. @param $tags string
  1376. @param $filter int
  1377. @param $opt array
  1378. @param $assign bool
  1379. @public
  1380. **/
  1381. static function input($fields,$funcs=NULL,
  1382. $tags=NULL,$filter=FILTER_UNSAFE_RAW,$opt=array(),$assign=TRUE) {
  1383. $funcs=is_string($funcs)?self::split($funcs):array($funcs);
  1384. foreach (self::split($fields) as $field) {
  1385. $found=FALSE;
  1386. // Sanitize relevant globals
  1387. foreach (explode('|','GET|POST|REQUEST') as $var)
  1388. if (self::exists($var.'.'.$field)) {
  1389. $key=&self::ref($var.'.'.$field);
  1390. $key=self::scrub($key,$tags);
  1391. $val=filter_var($key,$filter,$opt);
  1392. foreach ($funcs as $func)
  1393. if ($func) {
  1394. if (is_string($func) &&
  1395. preg_match('/([\w\\\]+)\s*->\s*(\w+)/',
  1396. $func,$match))
  1397. // Convert class->method syntax
  1398. $func=array(new $match[1],$match[2]);
  1399. if (!is_callable($func)) {
  1400. // Invalid handler
  1401. trigger_error(
  1402. sprintf(self::TEXT_Form,$field)
  1403. );
  1404. return;
  1405. }
  1406. if (!$found) {
  1407. $out=call_user_func($func,$val,$field);
  1408. if (!$assign)
  1409. return $out;
  1410. if ($out)
  1411. $key=$out;
  1412. $found=TRUE;
  1413. }
  1414. elseif ($assign && $out)
  1415. $key=$val;
  1416. }
  1417. }
  1418. if (!$found) {
  1419. // Invalid handler
  1420. trigger_error(sprintf(self::TEXT_Form,$field));
  1421. return;
  1422. }
  1423. }
  1424. }
  1425. /**
  1426. Render user interface
  1427. @return string
  1428. @param $file string
  1429. @public
  1430. **/
  1431. static function render($file) {
  1432. $file=self::resolve($file);
  1433. foreach (self::split(self::$vars['GUI']) as $gui)
  1434. if (is_file($view=self::fixslashes($gui.$file))) {
  1435. $instance=new F3instance;
  1436. $out=$instance->grab($view);
  1437. return self::$vars['TIDY']?self::tidy($out):$out;
  1438. }
  1439. trigger_error(sprintf(self::TEXT_Render,$view));
  1440. }
  1441. /**
  1442. Return runtime performance analytics
  1443. @return array
  1444. @public
  1445. **/
  1446. static function profile() {
  1447. $stats=&self::$vars['STATS'];
  1448. // Compute elapsed time
  1449. $stats['TIME']['elapsed']=microtime(TRUE)-$stats['TIME']['start'];
  1450. // Compute memory consumption
  1451. $stats['MEMORY']['current']=memory_get_usage();
  1452. $stats['MEMORY']['peak']=memory_get_peak_usage();
  1453. return $stats;
  1454. }
  1455. /**
  1456. Mock environment for command-line use and/or unit testing
  1457. @param $pattern string
  1458. @param $params array
  1459. @public
  1460. **/
  1461. static function mock($pattern,array $params=NULL) {
  1462. // Override PHP globals
  1463. list($method,$uri)=preg_split('/\s+/',
  1464. $pattern,2,PREG_SPLIT_NO_EMPTY);
  1465. $query=explode('&',parse_url($uri,PHP_URL_QUERY));
  1466. foreach ($query as $pair)
  1467. if (strpos($pair,'=')) {
  1468. list($var,$val)=explode('=',$pair);
  1469. self::$vars[$method][$var]=$val;
  1470. self::$vars['REQUEST'][$var]=$val;
  1471. }
  1472. if (is_array($params))
  1473. foreach ($params as $var=>$val) {
  1474. self::$vars[$method][$var]=$val;
  1475. self::$vars['REQUEST'][$var]=$val;
  1476. }
  1477. $_SERVER['REQUEST_METHOD']=$method;
  1478. $_SERVER['REQUEST_URI']=self::$vars['BASE'].$uri;
  1479. }
  1480. /**
  1481. Perform test and append result to TEST global variable
  1482. @return string
  1483. @param $cond bool
  1484. @param $pass string
  1485. @param $fail string
  1486. @public
  1487. **/
  1488. static function expect($cond,$pass=NULL,$fail=NULL) {
  1489. if (is_string($cond))
  1490. $cond=self::resolve($cond);
  1491. $text=$cond?$pass:$fail;
  1492. self::$vars['TEST'][]=array(
  1493. 'result'=>(int)(boolean)$cond,
  1494. 'text'=>is_string($text)?
  1495. self::resolve($text):var_export($text,TRUE)
  1496. );
  1497. return $text;
  1498. }
  1499. /**
  1500. Display default error page; Use custom page if found
  1501. @param $code int
  1502. @param $str string
  1503. @param $trace array
  1504. @param $fatal bool
  1505. @public
  1506. **/
  1507. static function error($code,$str='',array $trace=NULL,$fatal=FALSE) {
  1508. $prior=self::$vars['ERROR'];
  1509. $out='';
  1510. switch ($code) {
  1511. case 404:
  1512. $str=sprintf(self::TEXT_NotFound,$_SERVER['REQUEST_URI']);
  1513. break;
  1514. case 405:
  1515. $str=sprintf(self::TEXT_NotAllowed,
  1516. $_SERVER['REQUEST_METHOD'],$_SERVER['REQUEST_URI']);
  1517. break;
  1518. default:
  1519. // Generate internal server error if code is zero
  1520. if (!$code)
  1521. $code=500;
  1522. if (!self::$vars['DEBUG'])
  1523. // Disable stack trace
  1524. $trace=NULL;
  1525. elseif ($code==500 && !$trace)
  1526. $trace=debug_backtrace();
  1527. if (is_array($trace)) {
  1528. $line=0;
  1529. $plugins=is_array(
  1530. $plugins=glob(self::$vars['PLUGINS'].'*.php'))?
  1531. array_map('self::fixslashes',$plugins):array();
  1532. // Stringify the stack trace
  1533. ob_start();
  1534. foreach ($trace as $nexus) {
  1535. // Remove stack trace noise
  1536. if (self::$vars['DEBUG']<3 && !$fatal &&
  1537. (!isset($nexus['file']) ||
  1538. self::$vars['DEBUG']<2 &&
  1539. (strrchr(basename($nexus['file']),'.')=='.tmp' ||
  1540. in_array(self::fixslashes(
  1541. $nexus['file']),$plugins)) ||
  1542. isset($nexus['function']) &&
  1543. preg_match('/^(call_user_func(?:_array)?|'.
  1544. 'trigger_error|{.+}|'.__FUNCTION__.'|__)/',
  1545. $nexus['function'])))
  1546. continue;
  1547. echo '#'.$line.' '.
  1548. (isset($nexus['line'])?
  1549. (urldecode(self::fixslashes(
  1550. $nexus['file'])).':'.
  1551. $nexus['line'].' '):'').
  1552. (isset($nexus['function'])?
  1553. ((isset($nexus['class'])?
  1554. ($nexus['class'].$nexus['type']):'').
  1555. $nexus['function'].
  1556. '('.(!preg_match('/{{.+}}/',
  1557. $nexus['function']) &&
  1558. isset($nexus['args'])?
  1559. (self::csv($nexus['args'])):'').')'):'').
  1560. "\n";
  1561. $line++;
  1562. }
  1563. $out=ob_get_clean();
  1564. }
  1565. }
  1566. // Save error details
  1567. self::$vars['ERROR']=array(
  1568. 'code'=>$code,
  1569. 'title'=>self::status($code),
  1570. 'text'=>preg_replace('/\v/','',$str),
  1571. 'trace'=>self::$vars['DEBUG']?$out:''
  1572. );
  1573. $error=&self::$vars['ERROR'];
  1574. if (self::$vars['DEBUG']<2 && self::$vars['QUIET'])
  1575. return;
  1576. // Write to server's error log (with complete stack trace)
  1577. error_log($error['text']);
  1578. foreach (explode("\n",$out) as $str)
  1579. if ($str)
  1580. error_log($str);
  1581. if ($prior || self::$vars['QUIET'])
  1582. return;
  1583. foreach (array('title','text','trace') as $sub)
  1584. // Convert to HTML entities for safety
  1585. $error[$sub]=self::htmlencode(rawurldecode($error[$sub]));
  1586. $error['trace']=nl2br($error['trace']);
  1587. $func=self::$vars['ONERROR'];
  1588. if ($func && !$fatal)
  1589. self::call($func,TRUE);
  1590. else
  1591. echo '<html>'.
  1592. '<head>'.
  1593. '<title>'.$error['code'].' '.$error['title'].'</title>'.
  1594. '</head>'.
  1595. '<body>'.
  1596. '<h1>'.$error['title'].'</h1>'.
  1597. '<p><i>'.$error['text'].'</i></p>'.
  1598. '<p>'.$error['trace'].'</p>'.
  1599. '</body>'.
  1600. '</html>';
  1601. if (self::$vars['STRICT'])
  1602. die;
  1603. }
  1604. /**
  1605. Bootstrap code
  1606. @public
  1607. **/
  1608. static function start() {
  1609. // Prohibit multiple calls
  1610. if (self::$vars)
  1611. return;
  1612. // Handle all exceptions/non-fatal errors
  1613. error_reporting(E_ALL|E_STRICT);
  1614. ini_set('display_errors',0);
  1615. ini_set('register_globals',0);
  1616. // Get PHP settings
  1617. $ini=ini_get_all(NULL,FALSE);
  1618. // Intercept errors and send output to browser
  1619. set_error_handler(
  1620. function($errno,$errstr) {
  1621. if (error_reporting()) {
  1622. // Error suppression (@) is not enabled
  1623. $self=__CLASS__;
  1624. $self::error(500,$errstr);
  1625. }
  1626. }
  1627. );
  1628. // Do the same for PHP exceptions
  1629. set_exception_handler(
  1630. function($ex) {
  1631. if (!count($trace=$ex->getTrace())) {
  1632. // Translate exception trace
  1633. list($trace)=debug_backtrace();
  1634. $arg=$trace['args'][0];
  1635. $trace=array(
  1636. array(
  1637. 'file'=>$arg->getFile(),
  1638. 'line'=>$arg->getLine(),
  1639. 'function'=>'{main}',
  1640. 'args'=>array()
  1641. )
  1642. );
  1643. }
  1644. $self=__CLASS__;
  1645. $self::error(500,$ex->getMessage(),$trace);
  1646. // PHP aborts at this point
  1647. }
  1648. );
  1649. // Apache mod_rewrite enabled?
  1650. if (function_exists('apache_get_modules') &&
  1651. !in_array('mod_rewrite',apache_get_modules())) {
  1652. trigger_error(self::TEXT_Apache);
  1653. return;
  1654. }
  1655. // Fix Apache's VirtualDocumentRoot limitation
  1656. $_SERVER['DOCUMENT_ROOT']=str_replace($_SERVER['SCRIPT_NAME'],'',
  1657. $_SERVER['SCRIPT_FILENAME']);
  1658. // Adjust HTTP request time precision
  1659. $_SERVER['REQUEST_TIME']=microtime(TRUE);
  1660. // Hydrate framework variables
  1661. $root=self::fixslashes(realpath('.')).'/';
  1662. $base=self::fixslashes(
  1663. preg_replace('/\/[^\/]+$/','',$_SERVER['SCRIPT_NAME']));
  1664. $scheme=PHP_SAPI=='cli'?
  1665. NULL:
  1666. isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']!='off' ||
  1667. isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
  1668. $_SERVER['HTTP_X_FORWARDED_PROTO']=='https'?'https':'http';
  1669. self::$vars=array(
  1670. // Autoload folders
  1671. 'AUTOLOAD'=>$root,
  1672. // Web root folder
  1673. 'BASE'=>$base,
  1674. // Cache backend to use (autodetect if true; disable if false)
  1675. 'CACHE'=>FALSE,
  1676. // Default cookie settings
  1677. 'COOKIES'=>array(
  1678. 'expire'=>0,
  1679. 'path'=>$base?:'/',
  1680. 'domain'=>'.'.$_SERVER['SERVER_NAME'],
  1681. 'secure'=>($scheme=='https'),
  1682. 'httponly'=>TRUE
  1683. ),
  1684. // Stack trace verbosity:
  1685. // 0-no stack trace, 1-noise removed, 2-normal, 3-verbose
  1686. 'DEBUG'=>1,
  1687. // DNS black lists
  1688. 'DNSBL'=>NULL,
  1689. // Document encoding
  1690. 'ENCODING'=>'utf-8',
  1691. // Last error
  1692. 'ERROR'=>NULL,
  1693. // Allow/prohibit framework class extension
  1694. 'EXTEND'=>TRUE,
  1695. // IP addresses exempt from spam detection
  1696. 'EXEMPT'=>NULL,
  1697. // User interface folders
  1698. 'GUI'=>$root,
  1699. // URL for hotlink redirection
  1700. 'HOTLINK'=>NULL,
  1701. // Include path for procedural code
  1702. 'IMPORTS'=>$root,
  1703. // Default language (auto-detect if null)
  1704. 'LANGUAGE'=>NULL,
  1705. // Autoloaded classes
  1706. 'LOADED'=>NULL,
  1707. // Dictionary folder
  1708. 'LOCALES'=>$root,
  1709. // Maximum POST size
  1710. 'MAXSIZE'=>self::bytes($ini['post_max_size']),
  1711. // Custom error handler
  1712. 'ONERROR'=>NULL,
  1713. // Plugins folder
  1714. 'PLUGINS'=>self::fixslashes(__DIR__).'/',
  1715. // Scheme/protocol
  1716. 'PROTOCOL'=>$scheme,
  1717. // Allow framework to proxy for plugins
  1718. 'PROXY'=>FALSE,
  1719. // Stream handle for HTTP PUT method
  1720. 'PUT'=>NULL,
  1721. // Output suppression switch
  1722. 'QUIET'=>FALSE,
  1723. // Absolute path to document root folder
  1724. 'ROOT'=>$root,
  1725. // Framework routes
  1726. 'ROUTES'=>NULL,
  1727. // URL for spam redirection
  1728. 'SPAM'=>NULL,
  1729. // Stop script on error?
  1730. 'STRICT'=>TRUE,
  1731. // Profiler statistics
  1732. 'STATS'=>array(
  1733. 'MEMORY'=>array('start'=>memory_get_usage()),
  1734. 'TIME'=>array('start'=>microtime(TRUE))
  1735. ),
  1736. // Temporary folder
  1737. 'TEMP'=>$root.'temp/',
  1738. // Minimum script execution time
  1739. 'THROTTLE'=>0,
  1740. // Tidy options
  1741. 'TIDY'=>array(),
  1742. // Framework version
  1743. 'VERSION'=>self::TEXT_AppName.' '.self::TEXT_Version,
  1744. // Default whois server
  1745. 'WHOIS'=>'whois.internic.net'
  1746. );
  1747. // Alias the GUI variable (2.0+)
  1748. self::$vars['UI']=&self::$vars['GUI'];
  1749. // Create convenience containers for PHP globals
  1750. foreach (explode('|',self::PHP_Globals) as $var) {
  1751. // Sync framework and PHP globals
  1752. self::$vars[$var]=&$GLOBALS['_'.$var];
  1753. if ($ini['magic_quotes_gpc'] && preg_match('/^[GPCR]/',$var))
  1754. // Corrective action on PHP magic quotes
  1755. array_walk_recursive(
  1756. self::$vars[$var],
  1757. function(&$val) {
  1758. $val=stripslashes($val);
  1759. }
  1760. );
  1761. }
  1762. if (PHP_SAPI=='cli') {
  1763. // Command line: Parse GET variables in URL, if any
  1764. if (isset($_SERVER['argc']) && $_SERVER['argc']<2)
  1765. array_push($_SERVER['argv'],'/');
  1766. preg_match_all('/[\?&]([^=]+)=([^&$]*)/',
  1767. $_SERVER['argv'][1],$matches,PREG_SET_ORDER);
  1768. foreach ($matches as $match) {
  1769. $_REQUEST[$match[1]]=$match[2];
  1770. $_GET[$match[1]]=$match[2];
  1771. }
  1772. // Detect host name from environment
  1773. $_SERVER['SERVER_NAME']=gethostname();
  1774. // Convert URI to human-readable string
  1775. self::mock('GET '.$_SERVER['argv'][1]);
  1776. }
  1777. // Initialize autoload stack and shutdown sequence
  1778. spl_autoload_register(__CLASS__.'::autoload');
  1779. register_shutdown_function(__CLASS__.'::stop');
  1780. }
  1781. /**
  1782. Execute shutdown function
  1783. @public
  1784. **/
  1785. static function stop() {
  1786. chdir($_SERVER['DOCUMENT_ROOT']);
  1787. $error=error_get_last();
  1788. if ($error && !self::$vars['QUIET'] && in_array($error['type'],
  1789. array(E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR)))
  1790. // Intercept fatal error
  1791. self::error(500,sprintf(self::TEXT_Fatal,$error['message']),
  1792. array($error),TRUE);
  1793. if (isset(self::$vars['UNLOAD']) &&
  1794. is_callable(self::$vars['UNLOAD']))
  1795. self::call(self::$vars['UNLOAD']);
  1796. }
  1797. /**
  1798. onLoad event handler (static class initializer)
  1799. @public
  1800. **/
  1801. static function loadstatic($class) {
  1802. $loaded=&self::$vars['LOADED'];
  1803. $lower=strtolower($class);
  1804. if (!isset($loaded[$lower])) {
  1805. $loaded[$lower]=
  1806. array_map('strtolower',get_class_methods($class));
  1807. if (in_array('onload',$loaded[$lower])) {
  1808. // Execute onload method
  1809. $method=new ReflectionMethod($class,'onload');
  1810. if ($method->isStatic())
  1811. call_user_func(array($class,'onload'));
  1812. else
  1813. trigger_error(sprintf(self::TEXT_Static,
  1814. $class.'::onload'));
  1815. }
  1816. }
  1817. }
  1818. /**
  1819. Intercept instantiation of objects in undefined classes
  1820. @param $class string
  1821. @public
  1822. **/
  1823. static function autoload($class) {
  1824. $list=array_map('self::fixslashes',get_included_files());
  1825. // Support NS_class.php and NS/class.php namespace-mapping styles
  1826. foreach (array(str_replace('\\','_',$class),$class) as $style)
  1827. // Prioritize plugins before app classes
  1828. foreach (self::split(self::$vars['PLUGINS'].';'.
  1829. self::$vars['AUTOLOAD']) as $auto) {
  1830. $abs=self::fixslashes(realpath($auto));
  1831. if (!$abs)
  1832. continue;
  1833. $raw=self::fixslashes($style);
  1834. if (is_int(strpos($raw,'/'))) {
  1835. $ok=FALSE;
  1836. // Case-insensitive check for folders
  1837. foreach (explode('/',
  1838. self::fixslashes(dirname($raw))) as $dir) {
  1839. $globs=glob($abs.'/*',GLOB_ONLYDIR);
  1840. if ($globs!==FALSE)
  1841. foreach (glob($abs.'/*',GLOB_ONLYDIR) as $found) {
  1842. $found=self::fixslashes($found);
  1843. if (strtolower($abs.'/'.$dir)==
  1844. strtolower($found)) {
  1845. $abs=self::fixslashes($found);
  1846. $ok=TRUE;
  1847. }
  1848. }
  1849. }
  1850. if (!$ok)
  1851. continue;
  1852. $raw=basename($raw);
  1853. }
  1854. $glob=glob($abs.'/*',GLOB_NOSORT);
  1855. if ($glob) {
  1856. // Case-insensitive check for file presence
  1857. $found=preg_grep('/^'.preg_quote($abs.'/'.$raw,'/').
  1858. '(?:\.class)?\.php/i',
  1859. array_map('self::fixslashes',$glob));
  1860. if ($found && ($file=current($found)) &&
  1861. !in_array($file,$list)) {
  1862. $instance=new F3instance;
  1863. $instance->sandbox($file);
  1864. // Verify that the class was loaded
  1865. if (class_exists($class,FALSE)) {
  1866. // Run onLoad event handler if defined
  1867. self::loadstatic($class);
  1868. return;
  1869. }
  1870. elseif (interface_exists($class,FALSE))
  1871. return;
  1872. }
  1873. }
  1874. }
  1875. if (count(spl_autoload_functions())==1)
  1876. // No other registered autoload functions exist
  1877. trigger_error(sprintf(self::TEXT_Class,$class));
  1878. }
  1879. /**
  1880. Intercept calls to undefined static methods and proxy for the
  1881. called class if found in the plugins folder
  1882. @return mixed
  1883. @param $func string
  1884. @param $args array
  1885. @public
  1886. **/
  1887. static function __callStatic($func,array $args) {
  1888. if (self::$vars['PROXY'] &&
  1889. $glob=glob(self::fixslashes(
  1890. self::$vars['PLUGINS'].'/*.php',GLOB_NOSORT)))
  1891. foreach ($glob as $file) {
  1892. $class=strstr(basename($file),'.php',TRUE);
  1893. // Prevent recursive calls
  1894. $found=FALSE;
  1895. foreach (debug_backtrace() as $trace)
  1896. if (isset($trace['class']) &&
  1897. // Support namespaces
  1898. preg_match('/\b'.preg_quote($trace['class']).'\b/i',
  1899. strtolower($class)) &&
  1900. preg_match('/'.$trace['function'].'/i',
  1901. strtolower($func))) {
  1902. $found=TRUE;
  1903. break;
  1904. }
  1905. if ($found)
  1906. continue;
  1907. // Run onLoad event handler if defined
  1908. self::loadstatic($class);
  1909. if (in_array($func,self::$vars['LOADED'][$class]))
  1910. // Proxy for plugin
  1911. return call_user_func_array(array($class,$func),$args);
  1912. }
  1913. if (count(spl_autoload_functions())==1)
  1914. // No other registered autoload functions exist
  1915. trigger_error(sprintf(self::TEXT_Method,$func));
  1916. return FALSE;
  1917. }
  1918. }
  1919. //! Cache engine
  1920. class Cache extends Base {
  1921. //@{ Locale-specific error/exception messages
  1922. const
  1923. TEXT_Backend='Cache back-end is invalid',
  1924. TEXT_Store='Unable to save %s to cache',
  1925. TEXT_Fetch='Unable to retrieve %s from cache',
  1926. TEXT_Clear='Unable to clear %s from cache';
  1927. //@}
  1928. static
  1929. //! Level-1 cached object
  1930. $buffer,
  1931. //! Cache back-end
  1932. $backend;
  1933. /**
  1934. Auto-detect extensions usable as cache back-ends; MemCache must be
  1935. explicitly activated to work properly; Fall back to file system if
  1936. none declared or detected
  1937. @public
  1938. **/
  1939. static function detect() {
  1940. $ref=array_merge(array_intersect(array('apc','xcache'),
  1941. array_map('strtolower',get_loaded_extensions())),array());
  1942. self::$vars['CACHE']=array_shift($ref)?:
  1943. ('folder='.self::$vars['ROOT'].'cache/');
  1944. }
  1945. /**
  1946. Initialize cache backend
  1947. @return bool
  1948. @public
  1949. **/
  1950. static function prep() {
  1951. if (!self::$vars['CACHE'])
  1952. return TRUE;
  1953. if (is_bool(self::$vars['CACHE']))
  1954. // Auto-detect backend
  1955. self::detect();
  1956. if (preg_match(
  1957. '/^(apc)|(memcache)=(.+)|(xcache)|(folder)\=(.+\/)/i',
  1958. self::$vars['CACHE'],$match)) {
  1959. if (isset($match[5]) && $match[5]) {
  1960. if (!is_dir($match[6]))
  1961. self::mkdir($match[6]);
  1962. // File system
  1963. self::$backend=array('type'=>'folder','id'=>$match[6]);
  1964. }
  1965. else {
  1966. $ext=strtolower($match[1]?:($match[2]?:$match[4]));
  1967. if (!extension_loaded($ext)) {
  1968. trigger_error(sprintf(self::TEXT_PHPExt,$ext));
  1969. return FALSE;
  1970. }
  1971. if (isset($match[2]) && $match[2]) {
  1972. // Open persistent MemCache connection(s)
  1973. $pool=self::split($match[3]);
  1974. $mcache=NULL;
  1975. foreach ($pool as $server) {
  1976. // Hostname:port
  1977. list($host,$port)=explode(':',$server);
  1978. if (is_null($port))
  1979. // Use default port
  1980. $port=11211;
  1981. // Connect to each server
  1982. if (is_null($mcache))
  1983. $mcache=memcache_pconnect($host,$port);
  1984. else
  1985. memcache_add_server($mcache,$host,$port);
  1986. }
  1987. // MemCache
  1988. self::$backend=array('type'=>$ext,'id'=>$mcache);
  1989. }
  1990. else
  1991. // APC and XCache
  1992. self::$backend=array('type'=>$ext);
  1993. }
  1994. self::$buffer=NULL;
  1995. return TRUE;
  1996. }
  1997. // Unknown back-end
  1998. trigger_error(self::TEXT_Backend);
  1999. return FALSE;
  2000. }
  2001. /**
  2002. Store data in framework cache; Return TRUE/FALSE on success/failure
  2003. @return bool
  2004. @param $name string
  2005. @param $data mixed
  2006. @public
  2007. **/
  2008. static function set($name,$data) {
  2009. if (!self::$vars['CACHE'])
  2010. return TRUE;
  2011. if (is_null(self::$backend)) {
  2012. // Auto-detect back-end
  2013. self::detect();
  2014. if (!self::prep())
  2015. return FALSE;
  2016. }
  2017. $key=$_SERVER['SERVER_NAME'].'.'.$name;
  2018. // Serialize data for storage
  2019. $time=time();
  2020. // Add timestamp
  2021. $val=gzdeflate(serialize(array($time,$data)));
  2022. // Instruct back-end to store data
  2023. switch (self::$backend['type']) {
  2024. case 'apc':
  2025. $ok=apc_store($key,$val);
  2026. break;
  2027. case 'memcache':
  2028. $ok=memcache_set(self::$backend['id'],$key,$val);
  2029. break;
  2030. case 'xcache':
  2031. $ok=xcache_set($key,$val);
  2032. break;
  2033. case 'folder':
  2034. $ok=self::putfile(self::$backend['id'].$key,$val);
  2035. break;
  2036. }
  2037. if (is_bool($ok) && !$ok) {
  2038. trigger_error(sprintf(self::TEXT_Store,$name));
  2039. return FALSE;
  2040. }
  2041. // Free up space for level-1 cache
  2042. while (count(self::$buffer) && strlen(serialize($data))+
  2043. strlen(serialize(array_slice(self::$buffer,1)))>
  2044. ini_get('memory_limit')-memory_get_peak_usage())
  2045. self::$buffer=array_slice(self::$buffer,1);
  2046. self::$buffer[$name]=array('data'=>$data,'time'=>$time);
  2047. return TRUE;
  2048. }
  2049. /**
  2050. Retrieve value from framework cache
  2051. @return mixed
  2052. @param $name string
  2053. @param $quiet bool
  2054. @public
  2055. **/
  2056. static function get($name,$quiet=FALSE) {
  2057. if (!self::$vars['CACHE'])
  2058. return FALSE;
  2059. if (is_null(self::$backend)) {
  2060. // Auto-detect back-end
  2061. self::detect();
  2062. if (!self::prep())
  2063. return FALSE;
  2064. }
  2065. $stats=&self::$vars['STATS'];
  2066. if (!isset($stats['CACHE']))
  2067. $stats['CACHE']=array(
  2068. 'level-1'=>array('hits'=>0,'misses'=>0),
  2069. 'backend'=>array('hits'=>0,'misses'=>0)
  2070. );
  2071. // Check level-1 cache first
  2072. if (isset(self::$buffer) && isset(self::$buffer[$name])) {
  2073. $stats['CACHE']['level-1']['hits']++;
  2074. return self::$buffer[$name]['data'];
  2075. }
  2076. else
  2077. $stats['CACHE']['level-1']['misses']++;
  2078. $key=$_SERVER['SERVER_NAME'].'.'.$name;
  2079. // Instruct back-end to fetch data
  2080. switch (self::$backend['type']) {
  2081. case 'apc':
  2082. $val=apc_fetch($key);
  2083. break;
  2084. case 'memcache':
  2085. $val=memcache_get(self::$backend['id'],$key);
  2086. break;
  2087. case 'xcache':
  2088. $val=xcache_get($key);
  2089. break;
  2090. case 'folder':
  2091. $val=is_file(self::$backend['id'].$key)?
  2092. self::getfile(self::$backend['id'].$key):FALSE;
  2093. break;
  2094. }
  2095. if (is_bool($val)) {
  2096. $stats['CACHE']['backend']['misses']++;
  2097. // No error display if specified
  2098. if (!$quiet)
  2099. trigger_error(sprintf(self::TEXT_Fetch,$name));
  2100. self::$buffer[$name]=NULL;
  2101. return FALSE;
  2102. }
  2103. // Unserialize timestamp and data
  2104. list($time,$data)=unserialize(gzinflate($val));
  2105. $stats['CACHE']['backend']['hits']++;
  2106. // Free up space for level-1 cache
  2107. while (count(self::$buffer) && strlen(serialize($data))+
  2108. strlen(serialize(array_slice(self::$buffer,1)))>
  2109. ini_get('memory_limit')-memory_get_peak_usage())
  2110. self::$buffer=array_slice(self::$buffer,1);
  2111. self::$buffer[$name]=array('data'=>$data,'time'=>$time);
  2112. return $data;
  2113. }
  2114. /**
  2115. Delete variable from framework cache
  2116. @return bool
  2117. @param $name string
  2118. @param $quiet bool
  2119. @public
  2120. **/
  2121. static function clear($name,$quiet=FALSE) {
  2122. if (!self::$vars['CACHE'])
  2123. return TRUE;
  2124. if (is_null(self::$backend)) {
  2125. // Auto-detect back-end
  2126. self::detect();
  2127. if (!self::prep())
  2128. return FALSE;
  2129. }
  2130. $key=$_SERVER['SERVER_NAME'].'.'.$name;
  2131. // Instruct back-end to clear data
  2132. switch (self::$backend['type']) {
  2133. case 'apc':
  2134. $ok=!apc_exists($key) || apc_delete($key);
  2135. break;
  2136. case 'memcache':
  2137. $ok=memcache_delete(self::$backend['id'],$key);
  2138. break;
  2139. case 'xcache':
  2140. $ok=!xcache_isset($key) || xcache_unset($key);
  2141. break;
  2142. case 'folder':
  2143. $ok=!is_file(self::$backend['id'].$key) ||
  2144. @unlink(self::$backend['id'].$key);
  2145. break;
  2146. }
  2147. if (is_bool($ok) && !$ok) {
  2148. if (!$quiet)
  2149. trigger_error(sprintf(self::TEXT_Clear,$name));
  2150. return FALSE;
  2151. }
  2152. // Check level-1 cache first
  2153. if (isset(self::$buffer) && isset(self::$buffer[$name]))
  2154. unset(self::$buffer[$name]);
  2155. return TRUE;
  2156. }
  2157. /**
  2158. Return FALSE if specified variable is not in cache;
  2159. otherwise, return Un*x timestamp
  2160. @return mixed
  2161. @param $name string
  2162. @public
  2163. **/
  2164. static function cached($name) {
  2165. return self::get($name,TRUE)?self::$buffer[$name]['time']:FALSE;
  2166. }
  2167. }
  2168. //! F3 object mode
  2169. class F3instance {
  2170. const
  2171. TEXT_Conflict='%s conflicts with framework method name';
  2172. /**
  2173. Get framework variable reference; Workaround for PHP's
  2174. call_user_func() reference limitation
  2175. @return mixed
  2176. @param $key string
  2177. @param $set bool
  2178. @public
  2179. **/
  2180. function &ref($key,$set=TRUE) {
  2181. return F3::ref($key,$set);
  2182. }
  2183. /*
  2184. Run PHP code in sandbox
  2185. @param $file string
  2186. @public
  2187. */
  2188. function sandbox($file) {
  2189. return require $file;
  2190. }
  2191. /**
  2192. Grab file contents
  2193. @return mixed
  2194. @param $file string
  2195. @public
  2196. **/
  2197. function grab($file) {
  2198. $file=F3::resolve($file);
  2199. if (!ini_get('short_open_tag')) {
  2200. $text=preg_replace_callback(
  2201. '/<\?(?:\s|\s*(=))(.+?)\?>/s',
  2202. function($tag) {
  2203. return '<?php '.($tag[1]?'echo ':'').trim($tag[2]).' ?>';
  2204. },
  2205. $orig=self::getfile($file)
  2206. );
  2207. if (ini_get('allow_url_fopen') && ini_get('allow_url_include'))
  2208. // Stream wrap
  2209. $file='data:text/plain,'.urlencode($text);
  2210. elseif ($text!=$orig) {
  2211. // Save re-tagged file in temporary folder
  2212. if (!is_dir($ref=F3::ref('TEMP')))
  2213. F3::mkdir($ref);
  2214. $temp=$ref.$_SERVER['SERVER_NAME'].'.tpl.'.F3::hash($file);
  2215. if (!is_file($temp))
  2216. self::mutex(
  2217. function() use($temp,$text) {
  2218. file_put_contents($temp,$text);
  2219. }
  2220. );
  2221. $file=$temp;
  2222. }
  2223. }
  2224. ob_start();
  2225. // Render
  2226. $this->sandbox($file);
  2227. return ob_get_clean();
  2228. }
  2229. /**
  2230. Proxy for framework methods
  2231. @return mixed
  2232. @param $func string
  2233. @param $args array
  2234. @public
  2235. **/
  2236. function __call($func,array $args) {
  2237. return call_user_func_array('F3::'.$func,$args);
  2238. }
  2239. /**
  2240. Class constructor
  2241. @param $boot bool
  2242. @public
  2243. **/
  2244. function __construct($boot=FALSE) {
  2245. if ($boot)
  2246. F3::start();
  2247. // Allow application to override framework methods?
  2248. if (F3::ref('EXTEND'))
  2249. // User assumes risk
  2250. return;
  2251. // Get all framework methods not defined in this class
  2252. $def=array_diff(get_class_methods('F3'),get_class_methods(__CLASS__));
  2253. // Check for conflicts
  2254. $class=new ReflectionClass($this);
  2255. foreach ($class->getMethods() as $func)
  2256. if (in_array($func->name,$def))
  2257. trigger_error(sprintf(self::TEXT_Conflict,
  2258. get_called_class().'->'.$func->name));
  2259. }
  2260. }
  2261. // Bootstrap
  2262. return new F3instance(TRUE);