PageRenderTime 60ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/framework/F3/base.php

https://bitbucket.org/lxa478/qcrt
PHP | 2350 lines | 1660 code | 82 blank | 608 comment | 237 complexity | 112c384619dfbb88e7f3c9c46033cbce MD5 | raw file
Possible License(s): GPL-3.0

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

  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-2012 F3::Factory
  9. Bong Cosca <bong.cosca@yahoo.com>
  10. @package Base
  11. @version 2.1.0
  12. **/
  13. //! Base structure
  14. class Base {
  15. //@{ Framework details
  16. const
  17. TEXT_AppName='Fat-Free Framework',
  18. TEXT_Version='2.1.0',
  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_NoRoutes='No routes specified',
  36. TEXT_HTTP='HTTP status code %s is invalid',
  37. TEXT_Render='Unable to render %s - file does not exist',
  38. TEXT_Form='The input handler for %s is invalid',
  39. TEXT_Static='%s must be a static method',
  40. TEXT_Fatal='Fatal error: %s',
  41. TEXT_Write='%s must have write permission on %s',
  42. TEXT_Tags='PHP short tags are not supported by this server';
  43. //@}
  44. //@{ HTTP status codes (RFC 2616)
  45. const
  46. HTTP_100='Continue',
  47. HTTP_101='Switching Protocols',
  48. HTTP_200='OK',
  49. HTTP_201='Created',
  50. HTTP_202='Accepted',
  51. HTTP_203='Non-Authorative Information',
  52. HTTP_204='No Content',
  53. HTTP_205='Reset Content',
  54. HTTP_206='Partial Content',
  55. HTTP_300='Multiple Choices',
  56. HTTP_301='Moved Permanently',
  57. HTTP_302='Found',
  58. HTTP_303='See Other',
  59. HTTP_304='Not Modified',
  60. HTTP_305='Use Proxy',
  61. HTTP_307='Temporary Redirect',
  62. HTTP_400='Bad Request',
  63. HTTP_401='Unauthorized',
  64. HTTP_402='Payment Required',
  65. HTTP_403='Forbidden',
  66. HTTP_404='Not Found',
  67. HTTP_405='Method Not Allowed',
  68. HTTP_406='Not Acceptable',
  69. HTTP_407='Proxy Authentication Required',
  70. HTTP_408='Request Timeout',
  71. HTTP_409='Conflict',
  72. HTTP_410='Gone',
  73. HTTP_411='Length Required',
  74. HTTP_412='Precondition Failed',
  75. HTTP_413='Request Entity Too Large',
  76. HTTP_414='Request-URI Too Long',
  77. HTTP_415='Unsupported Media Type',
  78. HTTP_416='Requested Range Not Satisfiable',
  79. HTTP_417='Expectation Failed',
  80. HTTP_500='Internal Server Error',
  81. HTTP_501='Not Implemented',
  82. HTTP_502='Bad Gateway',
  83. HTTP_503='Service Unavailable',
  84. HTTP_504='Gateway Timeout',
  85. HTTP_505='HTTP Version Not Supported';
  86. //@}
  87. //@{ HTTP headers (RFC 2616)
  88. const
  89. HTTP_AcceptEnc='Accept-Encoding',
  90. HTTP_Agent='User-Agent',
  91. HTTP_Allow='Allow',
  92. HTTP_Cache='Cache-Control',
  93. HTTP_Connect='Connection',
  94. HTTP_Content='Content-Type',
  95. HTTP_Disposition='Content-Disposition',
  96. HTTP_Encoding='Content-Encoding',
  97. HTTP_Expires='Expires',
  98. HTTP_Host='Host',
  99. HTTP_IfMod='If-Modified-Since',
  100. HTTP_Keep='Keep-Alive',
  101. HTTP_LastMod='Last-Modified',
  102. HTTP_Length='Content-Length',
  103. HTTP_Location='Location',
  104. HTTP_Partial='Accept-Ranges',
  105. HTTP_Powered='X-Powered-By',
  106. HTTP_Pragma='Pragma',
  107. HTTP_Referer='Referer',
  108. HTTP_Transfer='Content-Transfer-Encoding',
  109. HTTP_WebAuth='WWW-Authenticate';
  110. //@}
  111. const
  112. //! Framework-mapped PHP globals
  113. PHP_Globals='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV',
  114. //! HTTP methods for RESTful interface
  115. HTTP_Methods='GET|HEAD|POST|PUT|DELETE|OPTIONS|TRACE|CONNECT';
  116. //@{ Global variables and references to constants
  117. protected static
  118. $vars,
  119. $classes,
  120. $null=NULL,
  121. $true=TRUE,
  122. $false=FALSE;
  123. //@}
  124. private static
  125. //! Read-only framework variables
  126. $readonly='BASE|PROTOCOL|ROUTES|STATS|VERSION';
  127. /**
  128. Convert Windows double-backslashes to slashes; Also for
  129. referencing namespaced classes in subdirectories
  130. @return string
  131. @param $str string
  132. @public
  133. **/
  134. static function fixslashes($str) {
  135. return $str?strtr($str,'\\','/'):$str;
  136. }
  137. /**
  138. Convert PHP expression/value to compressed exportable string
  139. @return string
  140. @param $arg mixed
  141. @public
  142. **/
  143. static function stringify($arg) {
  144. switch (gettype($arg)) {
  145. case 'object':
  146. return method_exists($arg,'__tostring')?
  147. stripslashes($arg):
  148. get_class($arg).'::__set_state()';
  149. case 'array':
  150. $str='';
  151. foreach ($arg as $key=>$val)
  152. $str.=($str?',':'').
  153. self::stringify($key).'=>'.self::stringify($val);
  154. return 'array('.$str.')';
  155. default:
  156. return var_export($arg,TRUE);
  157. }
  158. }
  159. /**
  160. Flatten array values and return as CSV string
  161. @return string
  162. @param $args mixed
  163. @public
  164. **/
  165. static function csv($args) {
  166. return implode(',',array_map('stripcslashes',
  167. array_map('self::stringify',$args)));
  168. }
  169. /**
  170. Split pipe-, semi-colon, comma-separated string
  171. @return array
  172. @param $str string
  173. @public
  174. **/
  175. static function split($str) {
  176. return array_map('trim',
  177. preg_split('/[|;,]/',$str,0,PREG_SPLIT_NO_EMPTY));
  178. }
  179. /**
  180. Generate Base36/CRC32 hash code
  181. @return string
  182. @param $str string
  183. @public
  184. **/
  185. static function hash($str) {
  186. return str_pad(base_convert(
  187. sprintf('%u',crc32($str)),10,36),7,'0',STR_PAD_LEFT);
  188. }
  189. /**
  190. Convert hexadecimal to binary-packed data
  191. @return string
  192. @param $hex string
  193. @public
  194. **/
  195. static function hexbin($hex) {
  196. return pack('H*',$hex);
  197. }
  198. /**
  199. Convert binary-packed data to hexadecimal
  200. @return string
  201. @param $bin string
  202. @public
  203. **/
  204. static function binhex($bin) {
  205. return implode('',unpack('H*',$bin));
  206. }
  207. /**
  208. Returns -1 if the specified number is negative, 0 if zero, or 1 if
  209. the number is positive
  210. @return int
  211. @param $num mixed
  212. @public
  213. **/
  214. static function sign($num) {
  215. return $num?$num/abs($num):0;
  216. }
  217. /**
  218. Convert engineering-notated string to bytes
  219. @return int
  220. @param $str string
  221. @public
  222. **/
  223. static function bytes($str) {
  224. $greek='KMGT';
  225. $exp=strpbrk($str,$greek);
  226. return $exp?pow(1024,strpos($greek,$exp)+1)*(int)$str:$str;
  227. }
  228. /**
  229. Convert from JS dot notation to PHP array notation
  230. @return string
  231. @param $key string
  232. @public
  233. **/
  234. static function remix($key) {
  235. $out='';
  236. $obj=FALSE;
  237. foreach (preg_split('/\[\s*[\'"]?|[\'"]?\s*\]|\.|(->)/',
  238. $key,NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE) as $fix)
  239. if (!$out)
  240. $out=$fix;
  241. elseif ($fix=='->')
  242. $obj=TRUE;
  243. elseif ($obj) {
  244. $obj=FALSE;
  245. $out.='->'.$fix;
  246. }
  247. else
  248. $out.='['.self::stringify($fix).']';
  249. return $out;
  250. }
  251. /**
  252. Return TRUE if specified string is a valid framework variable name
  253. @return bool
  254. @param $key string
  255. @public
  256. **/
  257. static function valid($key) {
  258. if (preg_match('/^(\w+(?:\[[^\]]+\]|\.\w+|\s*->\s*\w+)*)$/',$key))
  259. return TRUE;
  260. // Invalid variable name
  261. trigger_error(sprintf(self::TEXT_Illegal,var_export($key,TRUE)));
  262. return FALSE;
  263. }
  264. /**
  265. Get framework variable reference/contents
  266. @return mixed
  267. @param $key string
  268. @param $set bool
  269. @public
  270. **/
  271. static function &ref($key,$set=TRUE) {
  272. // Traverse array
  273. $matches=preg_split(
  274. '/\[\s*[\'"]?|[\'"]?\s*\]|\.|(->)/',$key,
  275. NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
  276. // Referencing SESSION element auto-starts a session
  277. if ($matches[0]=='SESSION' && !session_id()) {
  278. // Use cookie jar setup
  279. call_user_func_array('session_set_cookie_params',
  280. self::$vars['JAR']);
  281. session_start();
  282. // Sync framework and PHP global
  283. self::$vars['SESSION']=&$_SESSION;
  284. }
  285. // Read-only framework variable?
  286. if ($set && !preg_match('/^('.self::$readonly.')\b/',$matches[0]))
  287. $var=&self::$vars;
  288. else
  289. $var=self::$vars;
  290. $obj=FALSE;
  291. $i=0;
  292. foreach ($matches as $match) {
  293. if ($match=='->')
  294. $obj=TRUE;
  295. else {
  296. if (preg_match('/@(\w+)/',$match,$token))
  297. // Token found
  298. $match=self::resolve('{{'.$match.'}}');
  299. if ($set) {
  300. // Create property/array element if not found
  301. if ($obj) {
  302. if (!is_object($var))
  303. $var=new stdClass;
  304. if (!isset($var->$match))
  305. $var->$match=NULL;
  306. $var=&$var->$match;
  307. $obj=FALSE;
  308. }
  309. else
  310. $var=&$var[$match];
  311. }
  312. elseif ($obj &&
  313. (isset($var->$match) || method_exists($var,'__get'))) {
  314. // Object property found
  315. $var=$var->$match;
  316. $obj=FALSE;
  317. }
  318. elseif (is_array($var)) {
  319. // Array element found
  320. if (isset($var[$match]))
  321. // Normal key
  322. $var=$var[$match];
  323. elseif (isset(
  324. $var[$slice=implode(".",array_slice($matches,$i))])) {
  325. // Key contains a dot (.)
  326. $var=$var[$slice];
  327. break;
  328. }
  329. else
  330. // Property/array element doesn't exist
  331. return self::$null;
  332. }
  333. else
  334. // Property/array element doesn't exist
  335. return self::$null;
  336. }
  337. $i++;
  338. }
  339. return $var;
  340. }
  341. /**
  342. Copy contents of framework variable to another
  343. @param $src string
  344. @param $dst string
  345. @public
  346. **/
  347. static function copy($src,$dst) {
  348. $ref=&self::ref($dst);
  349. $ref=self::ref($src);
  350. }
  351. /**
  352. Concatenate string to framework string variable
  353. @param $var string
  354. @param $val string
  355. @public
  356. **/
  357. static function concat($var,$val) {
  358. $ref=&self::ref($var);
  359. $ref.=$val;
  360. }
  361. /**
  362. Format framework string variable
  363. @return string
  364. @public
  365. **/
  366. static function sprintf() {
  367. return call_user_func_array('sprintf',
  368. array_map('self::resolve',func_get_args()));
  369. }
  370. /**
  371. Add keyed element to the end of framework array variable
  372. @param $var string
  373. @param $key string
  374. @param $val mixed
  375. @public
  376. **/
  377. static function append($var,$key,$val) {
  378. $ref=&self::ref($var);
  379. $ref[self::resolve($key)]=$val;
  380. }
  381. /**
  382. Swap keys and values of framework array variable
  383. @param $var string
  384. @public
  385. **/
  386. static function flip($var) {
  387. $ref=&self::ref($var);
  388. $ref=array_combine(array_values($ref),array_keys($ref));
  389. }
  390. /**
  391. Merge one or more framework array variables
  392. @return array
  393. @public
  394. **/
  395. static function merge() {
  396. $args=func_get_args();
  397. foreach ($args as &$arg) {
  398. if (is_string($arg))
  399. $arg=self::ref($arg);
  400. if (!is_array($arg))
  401. trigger_error(sprintf(self::TEXT_NotArray,
  402. self::stringify($arg)));
  403. }
  404. return call_user_func_array('array_merge',$args);
  405. }
  406. /**
  407. Add element to the end of framework array variable
  408. @param $var string
  409. @param $val mixed
  410. @public
  411. **/
  412. static function push($var,$val) {
  413. $ref=&self::ref($var);
  414. if (!is_array($ref))
  415. $ref=array();
  416. array_push($ref,is_array($val)?
  417. array_map('self::resolve',$val):
  418. (is_string($val)?self::resolve($val):$val));
  419. }
  420. /**
  421. Remove last element of framework array variable and
  422. return the element
  423. @return mixed
  424. @param $var string
  425. @public
  426. **/
  427. static function pop($var) {
  428. $ref=&self::ref($var);
  429. if (is_array($ref))
  430. return array_pop($ref);
  431. trigger_error(sprintf(self::TEXT_NotArray,$var));
  432. return FALSE;
  433. }
  434. /**
  435. Add element to the beginning of framework array variable
  436. @param $var string
  437. @param $val mixed
  438. @public
  439. **/
  440. static function unshift($var,$val) {
  441. $ref=&self::ref($var);
  442. if (!is_array($ref))
  443. $ref=array();
  444. array_unshift($ref,is_array($val)?
  445. array_map('self::resolve',$val):
  446. (is_string($val)?self::resolve($val):$val));
  447. }
  448. /**
  449. Remove first element of framework array variable and
  450. return the element
  451. @return mixed
  452. @param $var string
  453. @public
  454. **/
  455. static function shift($var) {
  456. $ref=&self::ref($var);
  457. if (is_array($ref))
  458. return array_shift($ref);
  459. trigger_error(sprintf(self::TEXT_NotArray,$var));
  460. return FALSE;
  461. }
  462. /**
  463. Execute callback as a mutex operation
  464. @return mixed
  465. @public
  466. **/
  467. static function mutex() {
  468. $args=func_get_args();
  469. $func=array_shift($args);
  470. $handles=array();
  471. foreach ($args as $file) {
  472. $lock=$file.'.lock';
  473. while (TRUE) {
  474. usleep(mt_rand(0,100));
  475. if (is_resource($handle=@fopen($lock,'x'))) {
  476. $handles[$lock]=$handle;
  477. break;
  478. }
  479. if (is_file($lock) &&
  480. filemtime($lock)+self::$vars['MUTEX']<time())
  481. unlink($lock);
  482. }
  483. }
  484. $out=$func();
  485. foreach ($handles as $lock=>$handle) {
  486. fclose($handle);
  487. unlink($lock);
  488. }
  489. return $out;
  490. }
  491. /**
  492. Lock-aware file reader
  493. @param $file string
  494. @public
  495. **/
  496. static function getfile($file) {
  497. $out=FALSE;
  498. if (!function_exists('flock'))
  499. $out=self::mutex(
  500. function() use($file) {
  501. return file_get_contents($file);
  502. },
  503. $file
  504. );
  505. elseif ($handle=@fopen($file,'r')) {
  506. flock($handle,LOCK_EX);
  507. $size=filesize($file);
  508. $out=$size?fread($handle,$size):$out;
  509. flock($handle,LOCK_UN);
  510. fclose($handle);
  511. }
  512. return $out;
  513. }
  514. /**
  515. Lock-aware file writer
  516. @param $file string
  517. @param $data string
  518. @param $append bool
  519. @public
  520. **/
  521. static function putfile($file,$data,$append=FALSE) {
  522. if (!function_exists('flock'))
  523. $out=self::mutex(
  524. function() use($file,$data) {
  525. $flag=LOCK_EX;
  526. if ($append)
  527. $flag=$flag|FILE_APPEND;
  528. return file_put_contents($file,$data,$arg);
  529. },
  530. $file
  531. );
  532. else
  533. $out=file_put_contents($file,$data,LOCK_EX);
  534. return $out;
  535. }
  536. /**
  537. Evaluate template expressions in string
  538. @return string
  539. @param $val mixed
  540. @public
  541. **/
  542. static function resolve($val) {
  543. // Analyze string for correct framework expression syntax
  544. $self=__CLASS__;
  545. $str=preg_replace_callback(
  546. // Expression
  547. '/{{(.+?)}}/i',
  548. function($expr) use($self) {
  549. // Evaluate expression
  550. $out=preg_replace_callback(
  551. // Function
  552. '/(?<!@)\b(\w+)\s*\(([^\)]*)\)/',
  553. function($func) use($self) {
  554. return is_callable($ref=$self::ref($func[1],FALSE))?
  555. // Variable holds an anonymous function
  556. call_user_func_array($ref,str_getcsv($func[2])):
  557. // Check if empty array
  558. ($func[1].$func[2]=='array'?'NULL':
  559. ($func[1]=='array'?'\'Array\'':$func[0]));
  560. },
  561. preg_replace_callback(
  562. // Framework variable
  563. '/(?<!\w)@(\w+(?:\[[^\]]+\]|\.\w+)*'.
  564. '(?:\s*->\s*\w+)?)\s*(\(([^\)]*)\))?(?:\\\(.+))?/',
  565. function($var) use($self) {
  566. // Retrieve variable contents
  567. $val=$self::ref($var[1],FALSE);
  568. if (isset($var[2]) && is_callable($val))
  569. // Anonymous function
  570. $val=call_user_func_array(
  571. $val,str_getcsv($var[3]));
  572. if (isset($var[4]) && class_exists('ICU',FALSE))
  573. // ICU-formatted string
  574. $val=call_user_func_array('ICU::format',
  575. array($val,str_getcsv($var[4])));
  576. return $self::stringify($val);
  577. },
  578. $expr[1]
  579. )
  580. );
  581. return preg_match('/(?=\w)@/i',$out) ||
  582. ($eval=eval('return (string)'.$out.';'))===FALSE?
  583. $out:$eval;
  584. },
  585. $val
  586. );
  587. return $str;
  588. }
  589. /**
  590. Sniff headers for real IP address
  591. @return string
  592. @public
  593. **/
  594. static function realip() {
  595. if (isset($_SERVER['HTTP_CLIENT_IP']))
  596. // Behind proxy
  597. return $_SERVER['HTTP_CLIENT_IP'];
  598. elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  599. // Use first IP address in list
  600. list($ip)=explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
  601. return $ip;
  602. }
  603. return $_SERVER['REMOTE_ADDR'];
  604. }
  605. /**
  606. Return TRUE if IP address is local or within a private IPv4 range
  607. @return bool
  608. @param $addr string
  609. @public
  610. **/
  611. static function privateip($addr=NULL) {
  612. if (!$addr)
  613. $addr=self::realip();
  614. return preg_match('/^127\.0\.0\.\d{1,3}$/',$addr) ||
  615. !filter_var($addr,FILTER_VALIDATE_IP,
  616. FILTER_FLAG_IPV4|FILTER_FLAG_NO_PRIV_RANGE);
  617. }
  618. /**
  619. Clean and repair HTML
  620. @return string
  621. @param $html string
  622. @public
  623. **/
  624. static function tidy($html) {
  625. if (!extension_loaded('tidy'))
  626. return $html;
  627. $tidy=new Tidy;
  628. $tidy->parseString($html,self::$vars['TIDY'],
  629. str_replace('-','',self::$vars['ENCODING']));
  630. $tidy->cleanRepair();
  631. return (string)$tidy;
  632. }
  633. /**
  634. Create folder; Trigger error and return FALSE if script has no
  635. permission to create folder in the specified path
  636. @param $name string
  637. @param $perm int
  638. @param $recursive bool
  639. @public
  640. **/
  641. static function mkdir($name,$perm=0775,$recursive=TRUE) {
  642. $parent=dirname($name);
  643. if (!@is_writable($parent) && !chmod($parent,$perm)) {
  644. $uid=posix_getpwuid(posix_geteuid());
  645. trigger_error(sprintf(self::TEXT_Write,
  646. $uid['name'],realpath(dirname($name))));
  647. return FALSE;
  648. }
  649. // Create the folder
  650. mkdir($name,$perm,$recursive);
  651. }
  652. /**
  653. Intercept calls to undefined methods
  654. @param $func string
  655. @param $args array
  656. @public
  657. **/
  658. function __call($func,array $args) {
  659. trigger_error(sprintf(self::TEXT_Method,get_called_class().'->'.
  660. $func.'('.self::csv($args).')'));
  661. }
  662. /**
  663. Intercept calls to undefined static methods
  664. @param $func string
  665. @param $args array
  666. @public
  667. **/
  668. static function __callStatic($func,array $args) {
  669. trigger_error(sprintf(self::TEXT_Method,get_called_class().'::'.
  670. $func.'('.self::csv($args).')'));
  671. }
  672. /**
  673. Return instance of child class
  674. @public
  675. **/
  676. static function instance() {
  677. $ref=new ReflectionClass(get_called_class());
  678. return $ref->newInstanceArgs(func_get_args());
  679. }
  680. /**
  681. Class constructor
  682. @public
  683. **/
  684. function __construct() {
  685. // Prohibit use of class as an object
  686. trigger_error(sprintf(self::TEXT_Object,get_called_class()));
  687. }
  688. }
  689. //! Main framework code
  690. class F3 extends Base {
  691. /**
  692. Bind value to framework variable
  693. @param $key string
  694. @param $val mixed
  695. @param $persist bool
  696. @param $resolve bool
  697. @public
  698. **/
  699. static function set($key,$val,$persist=FALSE,$resolve=NULL) {
  700. $all=($resolve===TRUE);
  701. if (is_null($resolve))
  702. $resolve=TRUE;
  703. if (preg_match('/{{.+}}/',$key))
  704. // Variable variable
  705. $key=self::resolve($key);
  706. if (!self::valid($key))
  707. return;
  708. if (preg_match('/COOKIE\b/',$key) && !headers_sent()) {
  709. // Create/modify cookie
  710. $matches=preg_split(
  711. '/\[\s*[\'"]?|[\'"]?\s*\]|\./',self::remix($key),
  712. NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
  713. array_shift($matches);
  714. if ($matches) {
  715. $var='';
  716. foreach ($matches as $match)
  717. if (!$var)
  718. $var=$match;
  719. else
  720. $var.='[\''.$match.'\']';
  721. $val=array($var=>$val);
  722. }
  723. if (is_array($val))
  724. foreach ($val as $var=>$sub) {
  725. $func=self::$vars['JAR'];
  726. array_unshift($func,$var,$sub);
  727. call_user_func_array('setcookie',$func);
  728. }
  729. return;
  730. }
  731. $var=&self::ref($key);
  732. if ($resolve) {
  733. if (is_string($val))
  734. $val=self::resolve($val);
  735. elseif (is_array($val) && $all) {
  736. $var=array();
  737. // Recursive token substitution
  738. foreach ($val as $subk=>$subv) {
  739. $subp=$key.'['.var_export($subk,TRUE).']';
  740. self::set($subp,$subv,FALSE,$all);
  741. $val[$subk]=self::ref($subp);
  742. }
  743. }
  744. }
  745. $var=$val;
  746. if (preg_match('/^(?:GET|POST|COOKIE)/',$key,$php)) {
  747. // Sync with REQUEST
  748. $var=&self::ref(preg_replace('/^'.$php[0].'\b/','REQUEST',$key));
  749. $var=$val;
  750. }
  751. if (preg_match('/LANGUAGE|LOCALES/',$key) && class_exists('ICU'))
  752. // Load appropriate dictionaries
  753. ICU::load();
  754. elseif ($key=='ENCODING')
  755. ini_set('default_charset',$val);
  756. elseif ($key=='TZ')
  757. date_default_timezone_set($val);
  758. // Initialize cache if explicitly defined
  759. elseif ($key=='CACHE' && $val)
  760. self::$vars['CACHE']=Cache::load($val);
  761. if ($persist) {
  762. $hash='var.'.self::hash(self::remix($key));
  763. Cache::set($hash,$val);
  764. }
  765. }
  766. /**
  767. Retrieve value of framework variable and apply locale rules
  768. @return mixed
  769. @param $key string
  770. @param $args mixed
  771. @public
  772. **/
  773. static function get($key,$args=NULL) {
  774. if (preg_match('/{{.+}}/',$key))
  775. // Variable variable
  776. $key=self::resolve($key);
  777. if (!self::valid($key))
  778. return self::$null;
  779. $val=self::ref($key,FALSE);
  780. if (is_string($val))
  781. return class_exists('ICU',FALSE) && $args?
  782. ICU::format($val,$args):$val;
  783. elseif (is_null($val)) {
  784. // Attempt to retrieve from cache
  785. $hash='var.'.self::hash(self::remix($key));
  786. if (Cache::cached($hash))
  787. $val=Cache::get($hash);
  788. }
  789. return $val;
  790. }
  791. /**
  792. Unset framework variable
  793. @param $key string
  794. @public
  795. **/
  796. static function clear($key) {
  797. if (preg_match('/{{.+}}/',$key))
  798. // Variable variable
  799. $key=self::resolve($key);
  800. if (!self::valid($key))
  801. return;
  802. if (preg_match('/COOKIE/',$key) && !headers_sent()) {
  803. $val=$_COOKIE;
  804. $matches=preg_split(
  805. '/\[\s*[\'"]?|[\'"]?\s*\]|\./',self::remix($key),
  806. NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
  807. array_shift($matches);
  808. if ($matches) {
  809. // Expire specific cookie
  810. $var='';
  811. foreach ($matches as $match)
  812. if (!$var)
  813. $var=$match;
  814. else
  815. $var.='[\''.$match.'\']';
  816. $val=array($var,FALSE);
  817. }
  818. if (is_array($val))
  819. // Expire all cookies
  820. foreach (array_keys($val) as $var) {
  821. $func=self::$vars['JAR'];
  822. $func['expire']=strtotime('-1 year');
  823. array_unshift($func,$var,FALSE);
  824. call_user_func_array('setcookie',$func);
  825. }
  826. return;
  827. }
  828. // Clearing SESSION array ends the current session
  829. if ($key=='SESSION') {
  830. if (!session_id()) {
  831. call_user_func_array('session_set_cookie_params',
  832. self::$vars['JAR']);
  833. session_start();
  834. }
  835. // End the session
  836. session_unset();
  837. session_destroy();
  838. }
  839. preg_match('/^('.self::PHP_Globals.')(.*)$/',$key,$match);
  840. if (isset($match[1])) {
  841. $name=self::remix($key,FALSE);
  842. eval($match[2]?'unset($_'.$name.');':'$_'.$name.'=NULL;');
  843. }
  844. $name=preg_replace('/^(\w+)/','[\'\1\']',self::remix($key));
  845. // Assign NULL to framework variables; do not unset
  846. eval(ctype_upper(preg_replace('/^\w+/','\0',$key))?
  847. 'self::$vars'.$name.'=NULL;':'unset(self::$vars'.$name.');');
  848. // Remove from cache
  849. $hash='var.'.self::hash(self::remix($key));
  850. if (Cache::cached($hash))
  851. Cache::clear($hash);
  852. }
  853. /**
  854. Return TRUE if framework variable has been assigned a value
  855. @return bool
  856. @param $key string
  857. @public
  858. **/
  859. static function exists($key) {
  860. if (preg_match('/{{.+}}/',$key))
  861. // Variable variable
  862. $key=self::resolve($key);
  863. if (!self::valid($key))
  864. return FALSE;
  865. $var=&self::ref($key,FALSE);
  866. return isset($var);
  867. }
  868. /**
  869. Multi-variable assignment using associative array
  870. @param $arg array
  871. @param $pfx string
  872. @param $resolve bool
  873. @public
  874. **/
  875. static function mset($arg,$pfx='',$resolve=TRUE) {
  876. if (!is_array($arg))
  877. // Invalid argument
  878. trigger_error(self::TEXT_MSet);
  879. else
  880. // Bind key-value pairs
  881. foreach ($arg as $key=>$val)
  882. self::set($pfx.$key,$val,FALSE,$resolve);
  883. }
  884. /**
  885. Determine if framework variable has been cached
  886. @return mixed
  887. @param $key string
  888. @public
  889. **/
  890. static function cached($key) {
  891. if (preg_match('/{{.+}}/',$key))
  892. // Variable variable
  893. $key=self::resolve($key);
  894. return self::valid($key)?
  895. Cache::cached('var.'.self::hash(self::remix($key))):
  896. FALSE;
  897. }
  898. /**
  899. Configure framework according to INI-style file settings;
  900. Cache auto-generated PHP code to speed up execution
  901. @param $file string
  902. @public
  903. **/
  904. static function config($file) {
  905. // Generate hash code for config file
  906. $hash='php.'.self::hash($file);
  907. $cached=Cache::cached($hash);
  908. if ($cached && filemtime($file)<$cached)
  909. // Retrieve from cache
  910. $save=Cache::get($hash);
  911. else {
  912. if (!is_file($file)) {
  913. // Configuration file not found
  914. trigger_error(sprintf(self::TEXT_Config,$file));
  915. return;
  916. }
  917. // Load the .ini file
  918. $cfg=array();
  919. $sec='';
  920. if ($ini=file($file))
  921. for ($i=0,$c=count($ini);$i<$c;$i++) {
  922. // cut off CR / LF
  923. $line = rtrim($ini[$i],"\r\n");
  924. preg_match('/^\s*(?:(;)|\[(.+)\]|(.+?)\s*=\s*(.+))/',
  925. $line,$parts);
  926. if (isset($parts[1]) && $parts[1])
  927. // Comment
  928. continue;
  929. elseif (isset($parts[2]) && $parts[2])
  930. // Section
  931. $sec=strtolower($parts[2]);
  932. elseif (isset($parts[3]) && $parts[3]) {
  933. if (substr($line, -1) == "\\") {
  934. // multiline string
  935. $lines = rtrim($parts[4],"\\");
  936. while (++$i < $c) {
  937. $line = rtrim($ini[$i],"\r\n");
  938. $lines.= PHP_EOL .rtrim($line,"\\");
  939. if (substr($line,-1) !== "\\")
  940. break;
  941. }
  942. $parts[4] = $lines;
  943. }
  944. $parts[4]=preg_replace('/(?<=")(.+?)(?=")/',
  945. "\x00\\1",$parts[4]);
  946. // Key-value pair
  947. $csv=array_map(
  948. function($val) {
  949. $q='';
  950. if ($val[0]=="\x00")
  951. $q='"';
  952. $val=trim($val);
  953. return is_numeric($val) ||
  954. preg_match('/^\w+$/i',$val) &&
  955. defined($val)?
  956. eval('return '.$q.$val.$q.';'):$val;
  957. },
  958. str_getcsv($parts[4])
  959. );
  960. $cfg[$sec=$sec?:'globals'][$parts[3]]=
  961. count($csv)>1?$csv:$csv[0];
  962. }
  963. }
  964. $plan=array('globals'=>'set','maps'=>'map','routes'=>'route');
  965. ob_start();
  966. foreach ($cfg as $sec=>$pairs)
  967. if (isset($plan[$sec]))
  968. foreach ($pairs as $key=>$val)
  969. echo 'self::'.$plan[$sec].'('.
  970. self::stringify($key).','.
  971. (is_array($val) && $sec!='globals'?
  972. self::csv($val):self::stringify($val)).');'.
  973. "\n";
  974. $save=ob_get_clean();
  975. // Compress and save to cache
  976. Cache::set($hash,$save);
  977. }
  978. // Execute cached PHP code
  979. eval($save);
  980. if (!is_null(self::$vars['ERROR']))
  981. // Remove from cache
  982. Cache::clear($hash);
  983. }
  984. /**
  985. Convert special characters to HTML entities using globally-
  986. defined character set
  987. @return string
  988. @param $str string
  989. @param $all bool
  990. @public
  991. **/
  992. static function htmlencode($str,$all=FALSE) {
  993. return is_string($str)?
  994. call_user_func($all?'htmlentities':'htmlspecialchars',
  995. $str,ENT_COMPAT,self::$vars['ENCODING'],FALSE):
  996. $str;
  997. }
  998. /**
  999. Convert HTML entities back to their equivalent characters
  1000. @return string
  1001. @param $str string
  1002. @param $all bool
  1003. @public
  1004. **/
  1005. static function htmldecode($str,$all=FALSE) {
  1006. return is_string($str)?
  1007. ($all?
  1008. html_entity_decode($str,ENT_COMPAT,self::$vars['ENCODING']):
  1009. htmlspecialchars_decode($str,ENT_COMPAT)):
  1010. $str;
  1011. }
  1012. /**
  1013. Send HTTP status header; Return text equivalent of status code
  1014. @return mixed
  1015. @param $code int
  1016. @public
  1017. **/
  1018. static function status($code) {
  1019. if (!defined('self::HTTP_'.$code)) {
  1020. // Invalid status code
  1021. trigger_error(sprintf(self::TEXT_HTTP,$code));
  1022. return FALSE;
  1023. }
  1024. // Get description
  1025. $response=constant('self::HTTP_'.$code);
  1026. // Send raw HTTP header
  1027. if (PHP_SAPI!='cli' && !headers_sent())
  1028. header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$response);
  1029. return $response;
  1030. }
  1031. /**
  1032. Retrieve HTTP headers
  1033. @return array
  1034. @public
  1035. **/
  1036. static function headers() {
  1037. if (PHP_SAPI!='cli') {
  1038. if (function_exists('getallheaders'))
  1039. // Apache server
  1040. return getallheaders();
  1041. // Workaround
  1042. $req=array();
  1043. foreach ($_SERVER as $key=>$val)
  1044. if (substr($key,0,5)=='HTTP_')
  1045. $req[strtr(ucwords(strtolower(
  1046. strtr(substr($key,5),'_',' '))),' ','-')]=$val;
  1047. return $req;
  1048. }
  1049. return array();
  1050. }
  1051. /**
  1052. Send HTTP header with expiration date (seconds from current time)
  1053. @param $secs int
  1054. @public
  1055. **/
  1056. static function expire($secs=0) {
  1057. if (PHP_SAPI!='cli' && !headers_sent()) {
  1058. $time=time();
  1059. $req=self::headers();
  1060. if (isset($req[self::HTTP_IfMod]) &&
  1061. strtotime($req[self::HTTP_IfMod])+$secs>$time) {
  1062. self::status(304);
  1063. die;
  1064. }
  1065. header(self::HTTP_Powered.': '.self::TEXT_AppName.' '.
  1066. '('.self::TEXT_AppURL.')');
  1067. if ($secs) {
  1068. header_remove(self::HTTP_Pragma);
  1069. header(self::HTTP_Expires.': '.gmdate('r',$time+$secs));
  1070. header(self::HTTP_Cache.': max-age='.$secs);
  1071. header(self::HTTP_LastMod.': '.gmdate('r'));
  1072. }
  1073. else {
  1074. header(self::HTTP_Pragma.': no-cache');
  1075. header(self::HTTP_Cache.': no-cache, must-revalidate');
  1076. }
  1077. }
  1078. }
  1079. /**
  1080. Reroute to specified URI
  1081. @param $uri string
  1082. @public
  1083. **/
  1084. static function reroute($uri) {
  1085. $uri=self::resolve($uri);
  1086. if (PHP_SAPI!='cli' && !headers_sent()) {
  1087. // HTTP redirect
  1088. self::status($_SERVER['REQUEST_METHOD']=='GET'?301:303);
  1089. if (session_id())
  1090. session_commit();
  1091. header(self::HTTP_Location.': '.
  1092. (preg_match('/^https?:\/\//',$uri)?
  1093. $uri:(self::$vars['BASE'].$uri)));
  1094. die;
  1095. }
  1096. self::mock('GET '.$uri);
  1097. self::run();
  1098. }
  1099. /**
  1100. Assign handler to route pattern
  1101. @param $pattern string
  1102. @param $funcs mixed
  1103. @param $ttl int
  1104. @param $throttle int
  1105. @param $hotlink bool
  1106. @public
  1107. **/
  1108. static function route($pattern,$funcs,$ttl=0,$throttle=0,$hotlink=TRUE) {
  1109. list($methods,$uri)=
  1110. preg_split('/\s+/',$pattern,2,PREG_SPLIT_NO_EMPTY);
  1111. foreach (self::split($methods) as $method)
  1112. // Use pattern and HTTP methods as route indexes
  1113. self::$vars['ROUTES'][$uri][strtoupper($method)]=
  1114. // Save handler, cache timeout and hotlink permission
  1115. array($funcs,$ttl,$throttle,$hotlink);
  1116. }
  1117. /**
  1118. Provide REST interface by mapping URL to object/class
  1119. @param $url string
  1120. @param $class mixed
  1121. @param $ttl int
  1122. @param $throttle int
  1123. @param $hotlink bool
  1124. @param $prefix string
  1125. @public
  1126. **/
  1127. static function map(
  1128. $url,$class,$ttl=0,$throttle=0,$hotlink=TRUE,$prefix='') {
  1129. foreach (explode('|',self::HTTP_Methods) as $method) {
  1130. $func=$prefix.$method;
  1131. if (method_exists($class,$func)) {
  1132. $ref=new ReflectionMethod($class,$func);
  1133. self::route(
  1134. $method.' '.$url,
  1135. $class.($ref->isStatic()?'::':'->').$func,
  1136. $ttl,$throttle,$hotlink
  1137. );
  1138. unset($ref);
  1139. }
  1140. }
  1141. }
  1142. /**
  1143. Call route handler
  1144. @return mixed
  1145. @param $funcs string
  1146. @param $listen bool
  1147. @public
  1148. **/
  1149. static function call($funcs,$listen=FALSE) {
  1150. $funcs=is_string($funcs)?self::split($funcs):array($funcs);
  1151. $out=NULL;
  1152. foreach ($funcs as $func) {
  1153. if (is_string($func)) {
  1154. $func=self::resolve($func);
  1155. if (preg_match('/(.+)\s*(->|::)\s*(.+)/s',$func,$match)) {
  1156. if (!class_exists($match[1]) ||
  1157. !method_exists($match[1],'__call') &&
  1158. !method_exists($match[1],$match[3])) {
  1159. self::error(404);
  1160. return FALSE;
  1161. }
  1162. $func=array($match[2]=='->'?
  1163. new $match[1]:$match[1],$match[3]);
  1164. }
  1165. elseif (!function_exists($func)) {
  1166. $found=FALSE;
  1167. if (preg_match('/\.php$/i',$func)) {
  1168. $found=FALSE;
  1169. foreach (self::split(self::$vars['IMPORTS'])
  1170. as $path)
  1171. if (is_file($file=$path.$func)) {
  1172. $instance=new F3instance;
  1173. if ($instance->sandbox($file)===FALSE)
  1174. return FALSE;
  1175. $found=TRUE;
  1176. }
  1177. if ($found)
  1178. continue;
  1179. }
  1180. self::error(404);
  1181. return FALSE;
  1182. }
  1183. }
  1184. if (!is_callable($func)) {
  1185. self::error(404);
  1186. return FALSE;
  1187. }
  1188. $oop=is_array($func) &&
  1189. (is_object($func[0]) || is_string($func[0]));
  1190. if ($listen && $oop &&
  1191. method_exists($func[0],$before='beforeRoute')) {
  1192. // Execute beforeRoute() once per class
  1193. $key=is_object($func[0])?get_class($func[0]):$func[0];
  1194. if (!isset(self::$classes[$key]) ||
  1195. !isset(self::$classes[$key][0]) ||
  1196. !self::$classes[$key][0]) {
  1197. self::$classes[$key][0]=TRUE;
  1198. if (call_user_func(array($func[0],$before))===FALSE)
  1199. return FALSE;
  1200. }
  1201. }
  1202. $out=call_user_func($func);
  1203. if ($listen && $oop &&
  1204. method_exists($func[0],$after='afterRoute')) {
  1205. // Execute afterRoute() once per class
  1206. $key=is_object($func[0])?get_class($func[0]):$func[0];
  1207. if (!isset(self::$classes[$key]) ||
  1208. !isset(self::$classes[$key][1]) ||
  1209. !self::$classes[$key][1]) {
  1210. self::$classes[$key][1]=TRUE;
  1211. if (call_user_func(array($func[0],$after))===FALSE)
  1212. return FALSE;
  1213. }
  1214. }
  1215. }
  1216. return $out;
  1217. }
  1218. /**
  1219. Process routes based on incoming URI
  1220. @public
  1221. **/
  1222. static function run() {
  1223. // Validate user against spam blacklists
  1224. if (self::$vars['DNSBL'] && !self::privateip($addr=self::realip()) &&
  1225. (!self::$vars['EXEMPT'] ||
  1226. !in_array($addr,self::split(self::$vars['EXEMPT'])))) {
  1227. // Convert to reverse IP dotted quad
  1228. $quad=implode('.',array_reverse(explode('.',$addr)));
  1229. foreach (self::split(self::$vars['DNSBL']) as $list)
  1230. // Check against DNS blacklist
  1231. if (gethostbyname($quad.'.'.$list)!=$quad.'.'.$list) {
  1232. if (self::$vars['SPAM'])
  1233. // Spammer detected; Send to blackhole
  1234. self::reroute(self::$vars['SPAM']);
  1235. else {
  1236. // Forbidden
  1237. self::error(403);
  1238. die;
  1239. }
  1240. }
  1241. }
  1242. // Process routes
  1243. if (!isset(self::$vars['ROUTES']) || !self::$vars['ROUTES']) {
  1244. trigger_error(self::TEXT_NoRoutes);
  1245. return;
  1246. }
  1247. $allowed=array();
  1248. // Detailed routes get matched first
  1249. krsort(self::$vars['ROUTES']);
  1250. $time=time();
  1251. $case=self::$vars['CASELESS']?'i':'';
  1252. $req=preg_replace('/^'.preg_quote(self::$vars['BASE'],'/').
  1253. '\b(.+)/'.$case,'\1',
  1254. rawurldecode($_SERVER['REQUEST_URI']));
  1255. foreach (self::$vars['ROUTES'] as $uri=>$route) {
  1256. if (!preg_match('/^'.
  1257. preg_replace(
  1258. '/(?:\\\{\\\{)?@(\w+\b)(?:\\\}\\\})?/',
  1259. // Delimiter-based matching
  1260. '(?P<\1>[^\/&\?]+)',
  1261. // Wildcard character in URI
  1262. str_replace('\*','(.*)',preg_quote($uri,'/'))
  1263. ).'\/?(?:\?.*)?$/'.$case.'um',
  1264. $req,$args))
  1265. continue;
  1266. $wild=is_int(strpos($uri,'/*'));
  1267. // Inspect each defined route
  1268. foreach ($route as $method=>$proc) {
  1269. if (!preg_match('/HEAD|'.$method.'/',
  1270. $_SERVER['REQUEST_METHOD']))
  1271. continue;
  1272. if ($method=='GET' &&
  1273. strlen($path=parse_url($req,PHP_URL_PATH))>1 &&
  1274. substr($path,-1)=='/') {
  1275. $query=parse_url($req,PHP_URL_QUERY);
  1276. self::reroute(substr($path,0,-1).
  1277. ($query?('?'.$query):''));
  1278. }
  1279. self::$vars['PATTERN']=$uri;
  1280. list($funcs,$ttl,$throttle,$hotlink)=$proc;
  1281. if (!$hotlink && isset(self::$vars['HOTLINK']) &&
  1282. isset($_SERVER['HTTP_REFERER']) &&
  1283. parse_url($_SERVER['HTTP_REFERER'],PHP_URL_HOST)!=
  1284. $_SERVER['SERVER_NAME'])
  1285. // Hot link detected; Redirect page
  1286. self::reroute(self::$vars['HOTLINK']);
  1287. if (!$wild)
  1288. // Save named uri captures
  1289. foreach (array_keys($args) as $key)
  1290. // Remove non-zero indexed elements
  1291. if (is_numeric($key) && $key)
  1292. unset($args[$key]);
  1293. self::$vars['PARAMS']=$args;
  1294. // Default: Do not cache
  1295. self::expire(0);
  1296. if ($_SERVER['REQUEST_METHOD']=='GET' && $ttl) {
  1297. $_SERVER['REQUEST_TTL']=$ttl;
  1298. // Get HTTP request headers
  1299. $req=self::headers();
  1300. // Content divider
  1301. $div=chr(0);
  1302. // Get hash code for this Web page
  1303. $hash='url.'.self::hash(
  1304. $_SERVER['REQUEST_METHOD'].' '.
  1305. $_SERVER['REQUEST_URI']
  1306. );
  1307. $cached=Cache::cached($hash);
  1308. $uri='/^'.self::HTTP_Content.':.+/';
  1309. if ($cached && $time-$cached<$ttl) {
  1310. if (!isset($req[self::HTTP_IfMod]) ||
  1311. $cached>strtotime($req[self::HTTP_IfMod])) {
  1312. // Activate cache timer
  1313. self::expire($cached+$ttl-$time);
  1314. // Retrieve from cache
  1315. $buffer=Cache::get($hash);
  1316. $type=strstr($buffer,$div,TRUE);
  1317. if (PHP_SAPI!='cli' && !headers_sent() &&
  1318. preg_match($uri,$type,$match))
  1319. // Cached MIME type
  1320. header($match[0]);
  1321. // Save response
  1322. self::$vars['RESPONSE']=substr(
  1323. strstr($buffer,$div),1);
  1324. }
  1325. else {
  1326. // Client-side cache is still fresh
  1327. self::status(304);
  1328. die;
  1329. }
  1330. }
  1331. else {
  1332. // Activate cache timer
  1333. self::expire($ttl);
  1334. $type='';
  1335. foreach (headers_list() as $hdr)
  1336. if (preg_match($uri,$hdr)) {
  1337. // Add Content-Type header to buffer
  1338. $type=$hdr;
  1339. break;
  1340. }
  1341. // Cache this page
  1342. ob_start();
  1343. self::call($funcs,TRUE);
  1344. self::$vars['RESPONSE']=ob_get_clean();
  1345. if (!self::$vars['ERROR'] &&
  1346. self::$vars['RESPONSE'])
  1347. // Compress and save to cache
  1348. Cache::set($hash,
  1349. $type.$div.self::$vars['RESPONSE']);
  1350. }
  1351. }
  1352. else {
  1353. // Capture output
  1354. ob_start();
  1355. self::$vars['REQBODY']=file_get_contents('php://input');
  1356. self::call($funcs,TRUE);
  1357. self::$vars['RESPONSE']=ob_get_clean();
  1358. }
  1359. $elapsed=time()-$time;
  1360. $throttle=$throttle?:self::$vars['THROTTLE'];
  1361. if ($throttle/1e3>$elapsed)
  1362. // Delay output
  1363. usleep(1e6*($throttle/1e3-$elapsed));
  1364. if (strlen(self::$vars['RESPONSE']) && !self::$vars['QUIET'])
  1365. // Display response
  1366. echo self::$vars['RESPONSE'];
  1367. // Hail the conquering hero
  1368. return;
  1369. }
  1370. $allowed=array_keys($route);
  1371. }
  1372. if (!$allowed) {
  1373. // No such Web page
  1374. self::error(404);
  1375. return;
  1376. }
  1377. // Method not allowed
  1378. if (PHP_SAPI!='cli' && !headers_sent())
  1379. header(self::HTTP_Allow.': '.implode(',',$allowed));
  1380. self::error(405);
  1381. }
  1382. /**
  1383. Transmit a file for downloading by HTTP client; If kilobytes per
  1384. second is specified, output is throttled (bandwidth will not be
  1385. controlled by default); Return TRUE if successful, FALSE otherwise;
  1386. Support for partial downloads is indicated by third argument;
  1387. Download as attachment if fourth argument is TRUE
  1388. @param $file string
  1389. @param $kbps int
  1390. @param $partial bool
  1391. @param $attach bool
  1392. @public
  1393. **/
  1394. static function send($file,$kbps=0,$partial=TRUE,$attach=FALSE) {
  1395. $file=self::resolve($file);
  1396. if (!is_file($file)) {
  1397. self::error(404);
  1398. return FALSE;
  1399. }
  1400. if (PHP_SAPI!='cli' && !headers_sent()) {
  1401. header(self::HTTP_Content.': application/octet-stream');
  1402. header(self::HTTP_Partial.': '.($partial?'bytes':'none'));
  1403. header(self::HTTP_Length.': '.filesize($file));
  1404. if ($attach)
  1405. header(self::HTTP_Disposition.': attachment; '.
  1406. 'filename="'.basename($file).'"');
  1407. flush();
  1408. }
  1409. $ctr=1;
  1410. $handle=fopen($file,'r');
  1411. $time=microtime(TRUE);
  1412. while (!feof($handle) && !connection_aborted()) {
  1413. if ($kbps) {
  1414. // Throttle bandwidth
  1415. $ctr++;
  1416. if (($ctr/$kbps)>$elapsed=microtime(TRUE)-$time)
  1417. usleep(1e6*($ctr/$kbps-$elapsed));
  1418. }
  1419. // Send 1KiB and reset timer
  1420. echo fread($handle,1024);
  1421. flush();
  1422. }
  1423. fclose($handle);
  1424. die;
  1425. }
  1426. /**
  1427. Remove HTML tags (except those enumerated) to protect against
  1428. XSS/code injection attacks
  1429. @return mixed
  1430. @param $input string
  1431. @param $tags string
  1432. @public
  1433. **/
  1434. static function scrub($input,$tags=NULL) {
  1435. if (is_array($input))
  1436. foreach ($input as &$val)
  1437. $val=self::scrub($val,$tags);
  1438. if (is_string($input)) {
  1439. $input=($tags=='*')?
  1440. $input:strip_tags($input,is_string($tags)?
  1441. ('<'.implode('><',self::split($tags)).'>'):$tags);
  1442. }
  1443. return $input;
  1444. }
  1445. /**
  1446. Call form field handler
  1447. @param $fields string
  1448. @param $funcs mixed
  1449. @param $tags string
  1450. @param $filter int
  1451. @param $opt array
  1452. @param $assign bool
  1453. @public
  1454. **/
  1455. static function input($fields,$funcs=NULL,
  1456. $tags=NULL,$filter=FILTER_UNSAFE_RAW,$opt=array(),$assign=TRUE) {
  1457. $funcs=is_string($funcs)?self::split($funcs):array($funcs);
  1458. $found=FALSE;
  1459. foreach (self::split($fields) as $field)
  1460. // Sanitize relevant globals
  1461. foreach (explode('|','GET|POST|REQUEST') as $var)
  1462. if (self::exists($var.'.'.$field)) {
  1463. $key=&self::ref($var.'.'.$field);
  1464. if (is_array($key))
  1465. foreach ($key as $sub)
  1466. self::input(
  1467. $sub,$funcs,$tags,$filter,$opt,$assign
  1468. );
  1469. else {
  1470. $key=self::scrub($key,$tags);
  1471. $val=filter_var($key,$filter,$opt);
  1472. $out=NULL;
  1473. foreach ($funcs as $func) {
  1474. if (is_string($func)) {
  1475. $func=self::resolve($func);
  1476. if (preg_match('/(.+)\s*(->|::)\s*(.+)/s',
  1477. $func,$match)) {
  1478. if (!class_exists($match[1]) ||
  1479. !method_exists($match[1],'__call') &&
  1480. !method_exists($match[1],$match[3])) {
  1481. // Invalid handler
  1482. trigger_error(
  1483. sprintf(self::TEXT_Form,$field)
  1484. );
  1485. return;
  1486. }
  1487. $func=array($match[2]=='->'?
  1488. new $match[1]:$match[1],$match[3]);
  1489. }
  1490. elseif (!function_exists($func)) {
  1491. // Invalid handler
  1492. trigger_error(
  1493. sprintf(self::TEXT_Form,$field)
  1494. );
  1495. return;
  1496. }
  1497. }
  1498. if (!is_callable($func)) {
  1499. // Invalid handler
  1500. trigger_error(
  1501. sprintf(self::TEXT_Form,$field)
  1502. );
  1503. return;
  1504. }
  1505. $found=TRUE;
  1506. $out=call_user_func($func,$val,$field);
  1507. if ($assign && $out)
  1508. $key=$out;
  1509. }
  1510. }
  1511. }
  1512. if (!$found)
  1513. // Invalid handler
  1514. trigger_error(sprintf(self::TEXT_Form,$field));
  1515. return;
  1516. }
  1517. /**
  1518. Render user interface
  1519. @return string
  1520. @param $file string
  1521. @public
  1522. **/
  1523. static function render($file) {
  1524. $file=self::resolve($file);
  1525. foreach (self::split(self::$vars['GUI']) as $gui)
  1526. if (is_file($view=self::fixslashes($gui.$file))) {
  1527. $instance=new F3instance;
  1528. $out=$instance->grab($view);
  1529. return self::$vars['TIDY']?self::tidy($out):$out;
  1530. }
  1531. trigger_error(sprintf(self::TEXT_Render,$view));
  1532. }
  1533. /**
  1534. Return runtime performance analytics
  1535. @return array
  1536. @public
  1537. **/
  1538. static function profile() {
  1539. $stats=&self::$vars['STATS'];
  1540. // Compute elapsed time
  1541. $stats['TIME']['elapsed']=microtime(TRUE)-$stats['TIME']['start'];
  1542. // Compute memory consumption
  1543. $stats['MEMORY']['current']=memory_get_usage();
  1544. $stats['MEMORY']['peak']=memory_get_peak_usage();
  1545. return $stats;
  1546. }
  1547. /**
  1548. Mock environment for command-line use and/or unit testing
  1549. @param $pattern string
  1550. @param $args array
  1551. @public
  1552. **/
  1553. static function mock($pattern,array $args=NULL) {
  1554. list($method,$uri)=explode(' ',$pattern,2);
  1555. $method=strtoupper($method);
  1556. $url=parse_url($uri);
  1557. $query='';
  1558. if ($args)
  1559. $query.=http_build_query($args);
  1560. $query.=isset($url['query'])?(($query?'&':'').$url['query']):'';
  1561. if ($query) {
  1562. parse_str($query,$GLOBALS['_'.$method]);
  1563. parse_str($query,$GLOBALS['_REQUEST']);
  1564. }
  1565. $_SERVER['REQUEST_METHOD']=$method;
  1566. $_SERVER['REQUEST_URI']=self::$vars['BASE'].$url['path'].
  1567. ($query?('?'.$query):'');
  1568. }
  1569. /**
  1570. Perform test and append result to TEST global variable
  1571. @return string
  1572. @param $cond bool
  1573. @param $pass string
  1574. @param $fail string
  1575. @public
  1576. **/
  1577. static function expect($cond,$pass=NULL,$fail=NULL) {
  1578. if (is_string($cond))
  1579. $cond=self::resolve($cond);
  1580. $text=$cond?$pass:$fail;
  1581. self::$vars['TEST'][]=array(
  1582. 'result'=>(int)(boolean)$cond,
  1583. 'text'=>is_string($text)?
  1584. self::resolve($text):var_export($text,TRUE)
  1585. );
  1586. return $text;
  1587. }
  1588. /**
  1589. Display default error page; Use custom page if found
  1590. @param $code int
  1591. @param $str string
  1592. @param $trace array
  1593. @param $fatal bool
  1594. @public
  1595. **/
  1596. static function error($code,$str='',array $trace=NULL,$fatal=FALSE) {
  1597. $prior=self::$vars['ERROR'];
  1598. $out='';
  1599. switch ($code) {
  1600. case 404:
  1601. $str=sprintf(self::TEXT_NotFound,$_SERVER['REQUEST_URI']);
  1602. break;
  1603. case 405:
  1604. $str=sprintf(self::TEXT_NotAllowed,
  1605. $_SERVER['REQUEST_METHOD'],$_SERVER['REQUEST_URI']);
  1606. break;
  1607. default:
  1608. // Generate internal server error if code is zero
  1609. if (!$code)
  1610. $code=500;
  1611. if (!self::$vars['DEBUG'])
  1612. // Disable stack trace
  1613. $trace=NULL;
  1614. elseif ($code==500 && !$trace)
  1615. $trace=debug_backtrace();
  1616. if (is_array($trace)) {
  1617. $line=0;
  1618. $plugins=is_array(
  1619. $plugins=glob(self::$vars['PLUGINS'].'*.php'))?
  1620. array_map('self::fixslashes',$plugins):array();
  1621. // Stringify the stack trace
  1622. ob_start();
  1623. foreach ($trace as $nexus) {
  1624. // Remove stack trace noise
  1625. if (self::$vars['DEBUG']<3 && !$fatal &&
  1626. (!isset($nexus['file']) ||
  1627. self::$vars['DEBUG']<2 &&
  1628. (strrchr(basename($nexus['file']),'.')=='.tmp' ||
  1629. in_array(self::fixslashes(
  1630. $nexus['file']),$plugins)) ||
  1631. isset($nexus['function']) &&
  1632. preg_match('/^(call_user_func(?:_array)?|'.
  1633. 'trigger_error|{.+}|'.__FUNCTION__.'|__)/',
  1634. $nexus['function'])))
  1635. continue;
  1636. echo '#'.$line.' '.
  1637. (isset($nexus['line'])?
  1638. (urldecode(self::fixslashes(
  1639. $nexus['file'])).':'.
  1640. $nexus['line'].' '):'').
  1641. (isset($nexus['function'])?
  1642. ((isset($nexus['class'])?
  1643. ($nexus['class'].$nexus['type']):'').
  1644. $nexus['function'].
  1645. '('.(!preg_match('/{{.+}}/',
  1646. $nexus['function']) &&
  1647. isset($nexus['args'])?
  1648. (self::csv($nexus['args'])):'').')'):'').
  1649. "\n";
  1650. $line++;
  1651. }
  1652. $out=ob_get_clean();
  1653. }
  1654. }
  1655. // Save error details
  1656. self::$vars['ERROR']=array(
  1657. 'code'=>$code,
  1658. 'title'=>self::status($code),
  1659. 'text'=>preg_replace('/\v/','',$str),
  1660. 'trace'=>self::$vars['DEBUG']?$out:''
  1661. );
  1662. $error=&self::$vars['ERROR'];
  1663. if (self::$vars['DEBUG']<2 && self::$vars['QUIET'])
  1664. return;
  1665. // Write to server's error log (with complete stack trace)
  1666. error_log($error['text']);
  1667. foreach (explode("\n",$out) as $str)
  1668. if ($str)
  1669. error_log(strip_tags($str));
  1670. if ($prior || self::$vars['QUIET'])
  1671. return;
  1672. foreach (array('title','text','trace') as $sub)
  1673. // Convert to HTML entities for safety
  1674. $error[$sub]=self::htmlencode(rawurldecode($error[$sub]));
  1675. $error['trace']=nl2br($error['trace']);
  1676. $func=self::$vars['ONERROR'];
  1677. if ($func && !$fatal)
  1678. self::call($func,TRUE);
  1679. else
  1680. echo '<html>'.
  1681. '<head>'.
  1682. '<title>'.$error['code'].' '.$error['title'].'</title>'.
  1683. '</head>'.
  1684. '<body>'.
  1685. '<h1>'.$error['title'].'</h1>'.
  1686. '<p><i>'.$error['text'].'</i></p>'.
  1687. '<p>'.$error['trace'].'</p>'.
  1688. '</body>'.
  1689. '</html>';
  1690. if (self::$vars['STRICT'])
  1691. die;
  1692. }
  1693. /**
  1694. Bootstrap code
  1695. @public
  1696. **/
  1697. static function start() {
  1698. // Prohibit multiple calls
  1699. if (self::$vars)
  1700. return;
  1701. // Handle all exceptions/non-fatal errors
  1702. error_reporting(E_ALL|E_STRICT);
  1703. $charset='utf-8';
  1704. ini_set('default_charset',$charset);
  1705. ini_set('display_errors',0);
  1706. ini_set('register_globals',0);
  1707. // Get PHP settings
  1708. $ini=ini_get_all(NULL,FALSE);
  1709. $self=__CLASS__;
  1710. // Intercept errors and send output to browser
  1711. set_error_handler(
  1712. function($errno,$errstr) use($self) {
  1713. if (error_reporting())
  1714. // Error suppression (@) is not enabled
  1715. $self::error(500,$errstr);
  1716. }
  1717. );
  1718. // Do the same for PHP exceptions
  1719. set_exception_handler(
  1720. function($ex) use($self) {
  1721. if (!count($trace=$ex->getTrace())) {
  1722. // Translate exception trace
  1723. list($trace)=debug_backtrace();
  1724. $arg=$trace['args'][0];
  1725. $trace=array(
  1726. array(
  1727. 'file'=>$arg->getFile(),
  1728. 'line'=>$arg->getLine(),
  1729. 'function'=>'{main}',
  1730. 'args'=>array()
  1731. )
  1732. );
  1733. }
  1734. $self::error(500,$ex->getMessage(),$trace);
  1735. // PHP aborts at this point
  1736. }
  1737. );
  1738. // Apache mod_rewrite enabled?
  1739. if (function_exists('apache_get_modules') &&
  1740. !in_array('mod_rewrite',apache_get_modules())) {
  1741. trigger_error(self::TEXT_Apache);
  1742. return;
  1743. }
  1744. // Fix Apache's VirtualDocumentRoot limitation
  1745. $_SERVER['DOCUMENT_ROOT']=
  1746. self::fixslashes(dirname($_SERVER['SCRIPT_FILENAME']));
  1747. // Adjust HTTP request time precision
  1748. $_SERVER['REQUEST_TIME']=microtime(TRUE);
  1749. if (PHP_SAPI=='cli') {
  1750. // Command line: Parse GET variables in URL, if any
  1751. if (isset($_SERVER['argc']) && $_SERVER['argc']<2)
  1752. array_push($_SERVER['argv'],'/');
  1753. // Detect host name from environment
  1754. $_SERVER['SERVER_NAME']=gethostname();
  1755. // Convert URI to human-readable string
  1756. self::mock('GET '.$_SERVER['argv'][1]);
  1757. }
  1758. // Hydrate framework variables
  1759. $base=implode('/',array_map('urlencode',
  1760. explode('/',self::fixslashes(
  1761. preg_replace('/\/[^\/]+$/','',$_SERVER['SCRIPT_NAME'])))));
  1762. $scheme=PHP_SAPI=='cli'?
  1763. NULL:
  1764. isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']!='off' ||
  1765. isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
  1766. $_SERVER['HTTP_X_FORWARDED_PROTO']=='https'?'https':'http';
  1767. self::$vars=array(
  1768. // Autoload folders
  1769. 'AUTOLOAD'=>'./',
  1770. // Web root folder
  1771. 'BASE'=>$base,
  1772. // Cache backend to use (autodetect if true; disable if false)
  1773. 'CACHE'=>FALSE,
  1774. // Case-sensitivity of route patterns
  1775. 'CASELESS'=>TRUE,
  1776. // Stack trace verbosity:
  1777. // 0-no stack trace, 1-noise removed, 2-normal, 3-verbose
  1778. 'DEBUG'=>1,
  1779. // DNS black lists
  1780. 'DNSBL'=>NULL,
  1781. // Document encoding
  1782. 'ENCODING'=>$charset,
  1783. // Last error
  1784. 'ERROR'=>NULL,
  1785. // Auto-escape feature
  1786. 'ESCAPE'=>FALSE,
  1787. // Allow/prohibit framework class extension
  1788. 'EXTEND'=>TRUE,
  1789. // IP addresses exempt from spam detection
  1790. 'EXEMPT'=>NULL,
  1791. // User interface folders
  1792. 'GUI'=>'./',
  1793. // URL for hotlink redirection
  1794. 'HOTLINK'=>NULL,
  1795. // Include path for procedural code
  1796. 'IMPORTS'=>'./',
  1797. // Default cookie settings
  1798. 'JAR'=>array(
  1799. 'expire'=>0,
  1800. 'path'=>$base?:'/',
  1801. 'domain'=>isset($_SERVER['SERVER_NAME']) &&
  1802. is_int(strpos($_SERVER['SERVER_NAME'],'.')) &&
  1803. !filter_var($_SERVER['SERVER_NAME'],FILTER_VALIDATE_IP)?
  1804. $_SERVER['SERVER_NAME']:'',
  1805. 'secure'=>($scheme=='https'),
  1806. 'httponly'=>TRUE
  1807. ),
  1808. // Default language (auto-detect if null)
  1809. 'LANGUAGE'=>NULL,
  1810. // Autoloaded classes
  1811. 'LOADED'=>NULL,
  1812. // Dictionary folder
  1813. 'LOCALES'=>'./',
  1814. // Maximum POST size
  1815. 'MAXSIZE'=>self::bytes($ini['post_max_size']),
  1816. // Max mutex lock duration
  1817. 'MUTEX'=>60,
  1818. // Custom error handler
  1819. 'ONERROR'=>NULL,
  1820. // Plugins folder
  1821. 'PLUGINS'=>self::fixslashes(__DIR__).'/',
  1822. // Scheme/protocol
  1823. 'PROTOCOL'=>$scheme,
  1824. // Allow framework to proxy for plugins
  1825. 'PROXY'=>FALSE,
  1826. // Stream handle for HTTP PUT method
  1827. 'PUT'=>NULL,
  1828. // Output suppression switch
  1829. 'QUIET'=>FALSE,
  1830. // Absolute path to document root folder
  1831. 'ROOT'=>$_SERVER['DOCUMENT_ROOT'].'/',
  1832. // Framework routes
  1833. 'ROUTES'=>NULL,
  1834. // URL for spam redirection
  1835. 'SPAM'=>NULL,
  1836. // Stop script on error?
  1837. 'STRICT'=>TRUE,
  1838. // Profiler statistics
  1839. 'STATS'=>array(
  1840. 'MEMORY'=>array('start'=>memory_get_usage()),
  1841. 'TIME'=>array('start'=>microtime(TRUE)),
  1842. 'DB'=>array(),
  1843. 'FILES'=>array(),
  1844. 'CACHEā€¦

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