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

/libs/f3/base.php

https://github.com/floviolleau/selfoss
PHP | 2619 lines | 2032 code | 99 blank | 488 comment | 205 complexity | 27cc1f68dd0bf80a18223f724f56fd49 MD5 | raw file
Possible License(s): GPL-3.0
  1. <?php
  2. /*
  3. Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
  4. This file is part of the Fat-Free Framework (http://fatfree.sf.net).
  5. THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
  6. ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  7. IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
  8. PURPOSE.
  9. Please see the license.txt file for more information.
  10. */
  11. //! Base structure
  12. final class Base {
  13. //@{ Framework details
  14. const
  15. PACKAGE='Fat-Free Framework',
  16. VERSION='3.2.1-Dev';
  17. //@}
  18. //@{ HTTP status codes (RFC 2616)
  19. const
  20. HTTP_100='Continue',
  21. HTTP_101='Switching Protocols',
  22. HTTP_200='OK',
  23. HTTP_201='Created',
  24. HTTP_202='Accepted',
  25. HTTP_203='Non-Authorative Information',
  26. HTTP_204='No Content',
  27. HTTP_205='Reset Content',
  28. HTTP_206='Partial Content',
  29. HTTP_300='Multiple Choices',
  30. HTTP_301='Moved Permanently',
  31. HTTP_302='Found',
  32. HTTP_303='See Other',
  33. HTTP_304='Not Modified',
  34. HTTP_305='Use Proxy',
  35. HTTP_307='Temporary Redirect',
  36. HTTP_400='Bad Request',
  37. HTTP_401='Unauthorized',
  38. HTTP_402='Payment Required',
  39. HTTP_403='Forbidden',
  40. HTTP_404='Not Found',
  41. HTTP_405='Method Not Allowed',
  42. HTTP_406='Not Acceptable',
  43. HTTP_407='Proxy Authentication Required',
  44. HTTP_408='Request Timeout',
  45. HTTP_409='Conflict',
  46. HTTP_410='Gone',
  47. HTTP_411='Length Required',
  48. HTTP_412='Precondition Failed',
  49. HTTP_413='Request Entity Too Large',
  50. HTTP_414='Request-URI Too Long',
  51. HTTP_415='Unsupported Media Type',
  52. HTTP_416='Requested Range Not Satisfiable',
  53. HTTP_417='Expectation Failed',
  54. HTTP_500='Internal Server Error',
  55. HTTP_501='Not Implemented',
  56. HTTP_502='Bad Gateway',
  57. HTTP_503='Service Unavailable',
  58. HTTP_504='Gateway Timeout',
  59. HTTP_505='HTTP Version Not Supported';
  60. //@}
  61. const
  62. //! Mapped PHP globals
  63. GLOBALS='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV',
  64. //! HTTP verbs
  65. VERBS='GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT',
  66. //! Default directory permissions
  67. MODE=0755,
  68. //! Syntax highlighting stylesheet
  69. CSS='code.css';
  70. //@{ HTTP request types
  71. const
  72. REQ_SYNC=1,
  73. REQ_AJAX=2;
  74. //@}
  75. //@{ Error messages
  76. const
  77. E_Pattern='Invalid routing pattern: %s',
  78. E_Named='Named route does not exist: %s',
  79. E_Fatal='Fatal error: %s',
  80. E_Open='Unable to open %s',
  81. E_Routes='No routes specified',
  82. E_Method='Invalid method %s',
  83. E_Hive='Invalid hive key %s';
  84. //@}
  85. private
  86. //! Globals
  87. $hive,
  88. //! Initial settings
  89. $init,
  90. //! Language lookup sequence
  91. $languages,
  92. //! Equivalent Locales
  93. $locales,
  94. //! Default fallback language
  95. $fallback='en',
  96. //! NULL reference
  97. $null=NULL;
  98. /**
  99. * Sync PHP global with corresponding hive key
  100. * @return array
  101. * @param $key string
  102. **/
  103. function sync($key) {
  104. return $this->hive[$key]=&$GLOBALS['_'.$key];
  105. }
  106. /**
  107. * Return the parts of specified hive key
  108. * @return array
  109. * @param $key string
  110. **/
  111. private function cut($key) {
  112. return preg_split('/\[\h*[\'"]?(.+?)[\'"]?\h*\]|(->)|\./',
  113. $key,NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
  114. }
  115. /**
  116. * Replace tokenized URL with current route's token values
  117. * @return string
  118. * @param $url string
  119. **/
  120. function build($url) {
  121. if (preg_match_all('/@(\w+)/',$url,$matches,PREG_SET_ORDER))
  122. foreach ($matches as $match)
  123. if (array_key_exists($match[1],$this->hive['PARAMS']))
  124. $url=str_replace($match[0],
  125. $this->hive['PARAMS'][$match[1]],$url);
  126. return $url;
  127. }
  128. /**
  129. * Parse string containing key-value pairs and use as routing tokens
  130. * @return NULL
  131. * @param $str string
  132. **/
  133. function parse($str) {
  134. preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/',
  135. $str,$pairs,PREG_SET_ORDER);
  136. foreach ($pairs as $pair)
  137. $this->hive['PARAMS'][$pair[1]]=trim($pair[2]);
  138. }
  139. /**
  140. * Get hive key reference/contents; Add non-existent hive keys,
  141. * array elements, and object properties by default
  142. * @return mixed
  143. * @param $key string
  144. * @param $add bool
  145. **/
  146. function &ref($key,$add=TRUE) {
  147. $parts=$this->cut($key);
  148. if ($parts[0]=='SESSION') {
  149. @session_start();
  150. $this->sync('SESSION');
  151. }
  152. elseif (!preg_match('/^\w+$/',$parts[0]))
  153. user_error(sprintf(self::E_Hive,$this->stringify($key)));
  154. if ($add)
  155. $var=&$this->hive;
  156. else
  157. $var=$this->hive;
  158. $obj=FALSE;
  159. foreach ($parts as $part)
  160. if ($part=='->')
  161. $obj=TRUE;
  162. elseif ($obj) {
  163. $obj=FALSE;
  164. if ($add) {
  165. if (!is_object($var))
  166. $var=new stdclass;
  167. $var=&$var->$part;
  168. }
  169. elseif (isset($var->$part))
  170. $var=$var->$part;
  171. else
  172. return $this->null;
  173. }
  174. elseif ($add) {
  175. if (!is_array($var))
  176. $var=array();
  177. $var=&$var[$part];
  178. }
  179. elseif (is_array($var) && isset($var[$part]))
  180. $var=$var[$part];
  181. else
  182. return $this->null;
  183. if ($parts[0]=='ALIASES')
  184. $var=$this->build($var);
  185. return $var;
  186. }
  187. /**
  188. * Return TRUE if hive key is not set
  189. * (or return timestamp and TTL if cached)
  190. * @return bool
  191. * @param $key string
  192. * @param $val mixed
  193. **/
  194. function exists($key,&$val=NULL) {
  195. $val=$this->ref($key,FALSE);
  196. return isset($val)?
  197. TRUE:
  198. (Cache::instance()->exists($this->hash($key).'.var',$val)?
  199. $val:FALSE);
  200. }
  201. /**
  202. * Return TRUE if hive key is empty and not cached
  203. * @return bool
  204. * @param $key string
  205. **/
  206. function devoid($key) {
  207. $val=$this->ref($key,FALSE);
  208. return empty($val) &&
  209. (!Cache::instance()->exists($this->hash($key).'.var',$val) ||
  210. !$val);
  211. }
  212. /**
  213. * Bind value to hive key
  214. * @return mixed
  215. * @param $key string
  216. * @param $val mixed
  217. * @param $ttl int
  218. **/
  219. function set($key,$val,$ttl=0) {
  220. if (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) {
  221. $this->set('REQUEST'.$expr[2],$val);
  222. if ($expr[1]=='COOKIE') {
  223. $parts=$this->cut($key);
  224. $jar=$this->hive['JAR'];
  225. if ($ttl)
  226. $jar['expire']=time()+$ttl;
  227. call_user_func_array('setcookie',
  228. array_merge(array($parts[1],$val),$jar));
  229. }
  230. }
  231. else switch ($key) {
  232. case 'CACHE':
  233. $val=Cache::instance()->load($val,TRUE);
  234. break;
  235. case 'ENCODING':
  236. $val=ini_set('default_charset',$val);
  237. if (extension_loaded('mbstring'))
  238. mb_internal_encoding($val);
  239. break;
  240. case 'FALLBACK':
  241. $this->fallback=$val;
  242. $lang=$this->language($this->hive['LANGUAGE']);
  243. case 'LANGUAGE':
  244. if (isset($lang) || $lang=$this->language($val))
  245. $val=$this->language($val);
  246. $lex=$this->lexicon($this->hive['LOCALES']);
  247. case 'LOCALES':
  248. if (isset($lex) || $lex=$this->lexicon($val))
  249. $this->mset($lex,$this->hive['PREFIX'],$ttl);
  250. break;
  251. case 'TZ':
  252. date_default_timezone_set($val);
  253. break;
  254. }
  255. $ref=&$this->ref($key);
  256. $ref=$val;
  257. if (preg_match('/^JAR\b/',$key))
  258. call_user_func_array(
  259. 'session_set_cookie_params',$this->hive['JAR']);
  260. elseif (preg_match('/^SESSION\b/',$key)) {
  261. session_commit();
  262. session_start();
  263. }
  264. $cache=Cache::instance();
  265. if ($cache->exists($hash=$this->hash($key).'.var') || $ttl)
  266. // Persist the key-value pair
  267. $cache->set($hash,$val,$ttl);
  268. return $ref;
  269. }
  270. /**
  271. * Retrieve contents of hive key
  272. * @return mixed
  273. * @param $key string
  274. * @param $args string|array
  275. **/
  276. function get($key,$args=NULL) {
  277. if (is_string($val=$this->ref($key,FALSE)) && !is_null($args))
  278. return call_user_func_array(
  279. array($this,'format'),
  280. array_merge(array($val),is_array($args)?$args:array($args))
  281. );
  282. if (is_null($val)) {
  283. // Attempt to retrieve from cache
  284. if (Cache::instance()->exists($this->hash($key).'.var',$data))
  285. return $data;
  286. }
  287. return $val;
  288. }
  289. /**
  290. * Unset hive key
  291. * @return NULL
  292. * @param $key string
  293. **/
  294. function clear($key) {
  295. // Normalize array literal
  296. $cache=Cache::instance();
  297. $parts=$this->cut($key);
  298. if ($key=='CACHE')
  299. // Clear cache contents
  300. $cache->reset();
  301. elseif (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) {
  302. $this->clear('REQUEST'.$expr[2]);
  303. if ($expr[1]=='COOKIE') {
  304. $parts=$this->cut($key);
  305. $jar=$this->hive['JAR'];
  306. $jar['expire']=strtotime('-1 year');
  307. call_user_func_array('setcookie',
  308. array_merge(array($parts[1],''),$jar));
  309. unset($_COOKIE[$parts[1]]);
  310. }
  311. }
  312. elseif ($parts[0]=='SESSION') {
  313. @session_start();
  314. if (empty($parts[1])) {
  315. // End session
  316. session_unset();
  317. session_destroy();
  318. unset($_COOKIE[session_name()]);
  319. header_remove('Set-Cookie');
  320. }
  321. $this->sync('SESSION');
  322. }
  323. if (!isset($parts[1]) && array_key_exists($parts[0],$this->init))
  324. // Reset global to default value
  325. $this->hive[$parts[0]]=$this->init[$parts[0]];
  326. else {
  327. $out='';
  328. $obj=FALSE;
  329. foreach ($parts as $part)
  330. if ($part=='->')
  331. $obj=TRUE;
  332. elseif ($obj) {
  333. $obj=FALSE;
  334. $out.='->'.$out;
  335. }
  336. else
  337. $out.='['.$this->stringify($part).']';
  338. // PHP can't unset a referenced variable
  339. eval('unset($this->hive'.$out.');');
  340. if ($parts[0]=='SESSION') {
  341. session_commit();
  342. session_start();
  343. }
  344. if ($cache->exists($hash=$this->hash($key).'.var'))
  345. // Remove from cache
  346. $cache->clear($hash);
  347. }
  348. }
  349. /**
  350. * Multi-variable assignment using associative array
  351. * @return NULL
  352. * @param $vars array
  353. * @param $prefix string
  354. * @param $ttl int
  355. **/
  356. function mset(array $vars,$prefix='',$ttl=0) {
  357. foreach ($vars as $key=>$val)
  358. $this->set($prefix.$key,$val,$ttl);
  359. }
  360. /**
  361. * Publish hive contents
  362. * @return array
  363. **/
  364. function hive() {
  365. return $this->hive;
  366. }
  367. /**
  368. * Copy contents of hive variable to another
  369. * @return mixed
  370. * @param $src string
  371. * @param $dst string
  372. **/
  373. function copy($src,$dst) {
  374. $ref=&$this->ref($dst,TRUE);
  375. return $ref=$this->ref($src);
  376. }
  377. /**
  378. * Concatenate string to hive string variable
  379. * @return string
  380. * @param $key string
  381. * @param $val string
  382. **/
  383. function concat($key,$val) {
  384. $ref=&$this->ref($key,TRUE);
  385. $ref.=$val;
  386. return $ref;
  387. }
  388. /**
  389. * Swap keys and values of hive array variable
  390. * @return array
  391. * @param $key string
  392. * @public
  393. **/
  394. function flip($key) {
  395. $ref=&$this->ref($key,TRUE);
  396. return $ref=array_combine(array_values($ref),array_keys($ref));
  397. }
  398. /**
  399. * Add element to the end of hive array variable
  400. * @return mixed
  401. * @param $key string
  402. * @param $val mixed
  403. **/
  404. function push($key,$val) {
  405. $ref=&$this->ref($key,TRUE);
  406. array_push($ref,$val);
  407. return $val;
  408. }
  409. /**
  410. * Remove last element of hive array variable
  411. * @return mixed
  412. * @param $key string
  413. **/
  414. function pop($key) {
  415. $ref=&$this->ref($key,TRUE);
  416. return array_pop($ref);
  417. }
  418. /**
  419. * Add element to the beginning of hive array variable
  420. * @return mixed
  421. * @param $key string
  422. * @param $val mixed
  423. **/
  424. function unshift($key,$val) {
  425. $ref=&$this->ref($key,TRUE);
  426. array_unshift($ref,$val);
  427. return $val;
  428. }
  429. /**
  430. * Remove first element of hive array variable
  431. * @return mixed
  432. * @param $key string
  433. **/
  434. function shift($key) {
  435. $ref=&$this->ref($key,TRUE);
  436. return array_shift($ref);
  437. }
  438. /**
  439. * Merge array with hive array variable
  440. * @return array
  441. * @param $key string
  442. * @param $src array
  443. **/
  444. function merge($key,$src) {
  445. $ref=&$this->ref($key,TRUE);
  446. return array_merge($ref,$src);
  447. }
  448. /**
  449. * Convert backslashes to slashes
  450. * @return string
  451. * @param $str string
  452. **/
  453. function fixslashes($str) {
  454. return $str?strtr($str,'\\','/'):$str;
  455. }
  456. /**
  457. * Split comma-, semi-colon, or pipe-separated string
  458. * @return array
  459. * @param $str string
  460. **/
  461. function split($str) {
  462. return array_map('trim',
  463. preg_split('/[,;|]/',$str,0,PREG_SPLIT_NO_EMPTY));
  464. }
  465. /**
  466. * Convert PHP expression/value to compressed exportable string
  467. * @return string
  468. * @param $arg mixed
  469. * @param $detail bool
  470. **/
  471. function stringify($arg,$detail=TRUE) {
  472. $fw=$this;
  473. $func=function($arg,$val) use($detail,$fw) {
  474. return $arg===$val && !is_scalar($val)?
  475. '*RECURSION*':$fw->stringify($val,$detail);
  476. };
  477. switch (gettype($arg)) {
  478. case 'object':
  479. $str='';
  480. if (!preg_match('/Base|Closure/',get_class($arg)) && $detail)
  481. foreach ((array)$arg as $key=>$val)
  482. $str.=($str?',':'').$this->stringify(
  483. preg_replace('/[\x00].+?[\x00]/','',$key)).'=>'.
  484. $func($arg,$val);
  485. return method_exists($arg,'__tostring')?
  486. $arg:
  487. (addslashes(get_class($arg)).'::__set_state('.$str.')');
  488. case 'array':
  489. $str='';
  490. $num=isset($arg[0]) &&
  491. ctype_digit(implode('',array_keys($arg)));
  492. foreach ($arg as $key=>$val)
  493. $str.=($str?',':'').
  494. ($num?'':($this->stringify($key).'=>')).
  495. $func($arg,$val);
  496. return 'array('.$str.')';
  497. default:
  498. return var_export($arg,TRUE);
  499. }
  500. }
  501. /**
  502. * Flatten array values and return as CSV string
  503. * @return string
  504. * @param $args array
  505. **/
  506. function csv(array $args) {
  507. return implode(',',array_map('stripcslashes',
  508. array_map(array($this,'stringify'),$args)));
  509. }
  510. /**
  511. * Convert snakecase string to camelcase
  512. * @return string
  513. * @param $str string
  514. **/
  515. function camelcase($str) {
  516. return preg_replace_callback(
  517. '/_(\w)/',
  518. function($match) {
  519. return strtoupper($match[1]);
  520. },
  521. $str
  522. );
  523. }
  524. /**
  525. * Convert camelcase string to snakecase
  526. * @return string
  527. * @param $str string
  528. **/
  529. function snakecase($str) {
  530. return strtolower(preg_replace('/[[:upper:]]/','_\0',$str));
  531. }
  532. /**
  533. * Return -1 if specified number is negative, 0 if zero,
  534. * or 1 if the number is positive
  535. * @return int
  536. * @param $num mixed
  537. **/
  538. function sign($num) {
  539. return $num?($num/abs($num)):0;
  540. }
  541. /**
  542. * Generate 64bit/base36 hash
  543. * @return string
  544. * @param $str
  545. **/
  546. function hash($str) {
  547. return str_pad(base_convert(
  548. hexdec(substr(sha1($str),-16)),10,36),11,'0',STR_PAD_LEFT);
  549. }
  550. /**
  551. * Return Base64-encoded equivalent
  552. * @return string
  553. * @param $data string
  554. * @param $mime string
  555. **/
  556. function base64($data,$mime) {
  557. return 'data:'.$mime.';base64,'.base64_encode($data);
  558. }
  559. /**
  560. * Convert special characters to HTML entities
  561. * @return string
  562. * @param $str string
  563. **/
  564. function encode($str) {
  565. return @htmlentities($str,ENT_COMPAT,$this->hive['ENCODING'],FALSE)?:
  566. $this->scrub($str);
  567. }
  568. /**
  569. * Convert HTML entities back to characters
  570. * @return string
  571. * @param $str string
  572. **/
  573. function decode($str) {
  574. return html_entity_decode($str,ENT_COMPAT,$this->hive['ENCODING']);
  575. }
  576. /**
  577. * Remove HTML tags (except those enumerated) and non-printable
  578. * characters to mitigate XSS/code injection attacks
  579. * @return mixed
  580. * @param $var mixed
  581. * @param $tags string
  582. **/
  583. function clean($var,$tags=NULL) {
  584. if (is_string($var) && strlen($var)) {
  585. if ($tags!='*')
  586. $var=strip_tags($var,
  587. '<'.implode('><',$this->split($tags)).'>');
  588. $var=trim(preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/','',$var));
  589. }
  590. elseif (is_array($var))
  591. foreach ($var as &$val) {
  592. $val=$this->clean($val,$tags);
  593. unset($val);
  594. }
  595. return $var;
  596. }
  597. /**
  598. * Similar to clean(), except that variable is passed by reference
  599. * @return mixed
  600. * @param $var mixed
  601. * @param $tags string
  602. **/
  603. function scrub(&$var,$tags=NULL) {
  604. return $var=$this->clean($var,$tags);
  605. }
  606. /**
  607. * Return locale-aware formatted string
  608. * @return string
  609. **/
  610. function format() {
  611. $args=func_get_args();
  612. $val=array_shift($args);
  613. setlocale(LC_ALL,str_replace('-','_',$this->locales));
  614. // Get formatting rules
  615. $conv=localeconv();
  616. return preg_replace_callback(
  617. '/\{(?P<pos>\d+)\s*(?:,\s*(?P<type>\w+)\s*'.
  618. '(?:,\s*(?P<mod>(?:\w+(?:\s*\{.+?\}\s*,?)?)*)'.
  619. '(?:,\s*(?P<prop>.+?))?)?)?\}/',
  620. function($expr) use($args,$conv) {
  621. extract($expr);
  622. extract($conv);
  623. if (!array_key_exists($pos,$args))
  624. return $expr[0];
  625. if (isset($type))
  626. switch ($type) {
  627. case 'plural':
  628. preg_match_all('/(?<tag>\w+)'.
  629. '(?:\s+\{\s*(?<data>.+?)\s*\})/',
  630. $mod,$matches,PREG_SET_ORDER);
  631. $ord=array('zero','one','two');
  632. foreach ($matches as $match) {
  633. extract($match);
  634. if (isset($ord[$args[$pos]]) &&
  635. $tag==$ord[$args[$pos]] || $tag=='other')
  636. return str_replace('#',$args[$pos],$data);
  637. }
  638. case 'number':
  639. if (isset($mod))
  640. switch ($mod) {
  641. case 'integer':
  642. return number_format(
  643. $args[$pos],0,'',$thousands_sep);
  644. case 'currency':
  645. if (function_exists('money_format'))
  646. return money_format(
  647. '%n',$args[$pos]);
  648. $fmt=array(
  649. 0=>'(nc)',1=>'(n c)',
  650. 2=>'(nc)',10=>'+nc',
  651. 11=>'+n c',12=>'+ nc',
  652. 20=>'nc+',21=>'n c+',
  653. 22=>'nc +',30=>'n+c',
  654. 31=>'n +c',32=>'n+ c',
  655. 40=>'nc+',41=>'n c+',
  656. 42=>'nc +',100=>'(cn)',
  657. 101=>'(c n)',102=>'(cn)',
  658. 110=>'+cn',111=>'+c n',
  659. 112=>'+ cn',120=>'cn+',
  660. 121=>'c n+',122=>'cn +',
  661. 130=>'+cn',131=>'+c n',
  662. 132=>'+ cn',140=>'c+n',
  663. 141=>'c+ n',142=>'c +n'
  664. );
  665. if ($args[$pos]<0) {
  666. $sgn=$negative_sign;
  667. $pre='n';
  668. }
  669. else {
  670. $sgn=$positive_sign;
  671. $pre='p';
  672. }
  673. return str_replace(
  674. array('+','n','c'),
  675. array($sgn,number_format(
  676. abs($args[$pos]),
  677. $frac_digits,
  678. $decimal_point,
  679. $thousands_sep),
  680. $currency_symbol),
  681. $fmt[(int)(
  682. (${$pre.'_cs_precedes'}%2).
  683. (${$pre.'_sign_posn'}%5).
  684. (${$pre.'_sep_by_space'}%3)
  685. )]
  686. );
  687. case 'percent':
  688. return number_format(
  689. $args[$pos]*100,0,$decimal_point,
  690. $thousands_sep).'%';
  691. case 'decimal':
  692. return number_format(
  693. $args[$pos],$prop,$decimal_point,
  694. $thousands_sep);
  695. }
  696. break;
  697. case 'date':
  698. return strftime(empty($mod) ||
  699. $mod=='short'?'%x':'%A, %d %B %Y',
  700. $args[$pos]);
  701. case 'time':
  702. return strftime('%X',$args[$pos]);
  703. default:
  704. return $expr[0];
  705. }
  706. return $args[$pos];
  707. },
  708. $val
  709. );
  710. }
  711. /**
  712. * Assign/auto-detect language
  713. * @return string
  714. * @param $code string
  715. **/
  716. function language($code) {
  717. $code=preg_replace('/;q=.+?(?=,|$)/','',$code);
  718. $code.=($code?',':'').$this->fallback;
  719. $this->languages=array();
  720. foreach (array_reverse(explode(',',$code)) as $lang) {
  721. if (preg_match('/^(\w{2})(?:-(\w{2}))?\b/i',$lang,$parts)) {
  722. // Generic language
  723. array_unshift($this->languages,$parts[1]);
  724. if (isset($parts[2])) {
  725. // Specific language
  726. $parts[0]=$parts[1].'-'.($parts[2]=strtoupper($parts[2]));
  727. array_unshift($this->languages,$parts[0]);
  728. }
  729. }
  730. }
  731. $this->languages=array_unique($this->languages);
  732. $this->locales=array();
  733. $windows=preg_match('/^win/i',PHP_OS);
  734. foreach ($this->languages as $locale) {
  735. if ($windows) {
  736. $parts=explode('-',$locale);
  737. $locale=@constant('ISO::LC_'.$parts[0]);
  738. if (isset($parts[1]) &&
  739. $country=@constant('ISO::CC_'.strtolower($parts[1])))
  740. $locale.='-'.$country;
  741. }
  742. $this->locales[]=$locale;
  743. $this->locales[]=$locale.'.'.ini_get('default_charset');
  744. }
  745. return implode(',',$this->languages);
  746. }
  747. /**
  748. * Transfer lexicon entries to hive
  749. * @return array
  750. * @param $path string
  751. **/
  752. function lexicon($path) {
  753. $lex=array();
  754. foreach ($this->languages?:array($this->fallback) as $lang) {
  755. if ((is_file($file=($base=$path.$lang).'.php') ||
  756. is_file($file=$base.'.php')) &&
  757. is_array($dict=require($file)))
  758. $lex+=$dict;
  759. elseif (is_file($file=$base.'.ini')) {
  760. preg_match_all(
  761. '/(?<=^|\n)(?:'.
  762. '(?:;[^\n]*)|(?:<\?php.+?\?>?)|'.
  763. '(.+?)\h*=\h*'.
  764. '((?:\\\\\h*\r?\n|.+?)*)'.
  765. ')(?=\r?\n|$)/',
  766. $this->read($file),$matches,PREG_SET_ORDER);
  767. if ($matches)
  768. foreach ($matches as $match)
  769. if (isset($match[1]) &&
  770. !array_key_exists($match[1],$lex))
  771. $lex[$match[1]]=trim(preg_replace(
  772. '/(?<!\\\\)"|\\\\\h*\r?\n/','',$match[2]));
  773. }
  774. }
  775. return $lex;
  776. }
  777. /**
  778. * Return string representation of PHP value
  779. * @return string
  780. * @param $arg mixed
  781. **/
  782. function serialize($arg) {
  783. switch (strtolower($this->hive['SERIALIZER'])) {
  784. case 'igbinary':
  785. return igbinary_serialize($arg);
  786. default:
  787. return serialize($arg);
  788. }
  789. }
  790. /**
  791. * Return PHP value derived from string
  792. * @return string
  793. * @param $arg mixed
  794. **/
  795. function unserialize($arg) {
  796. switch (strtolower($this->hive['SERIALIZER'])) {
  797. case 'igbinary':
  798. return igbinary_unserialize($arg);
  799. default:
  800. return unserialize($arg);
  801. }
  802. }
  803. /**
  804. * Send HTTP/1.1 status header; Return text equivalent of status code
  805. * @return string
  806. * @param $code int
  807. **/
  808. function status($code) {
  809. $reason=@constant('self::HTTP_'.$code);
  810. if (PHP_SAPI!='cli')
  811. header('HTTP/1.1 '.$code.' '.$reason);
  812. return $reason;
  813. }
  814. /**
  815. * Send cache metadata to HTTP client
  816. * @return NULL
  817. * @param $secs int
  818. **/
  819. function expire($secs=0) {
  820. if (PHP_SAPI!='cli') {
  821. header('X-Content-Type-Options: nosniff');
  822. header('X-Frame-Options: SAMEORIGIN');
  823. header('X-Powered-By: '.$this->hive['PACKAGE']);
  824. header('X-XSS-Protection: 1; mode=block');
  825. if ($secs) {
  826. $time=microtime(TRUE);
  827. header_remove('Pragma');
  828. header('Expires: '.gmdate('r',$time+$secs));
  829. header('Cache-Control: max-age='.$secs);
  830. header('Last-Modified: '.gmdate('r'));
  831. $headers=$this->hive['HEADERS'];
  832. if (isset($headers['If-Modified-Since']) &&
  833. strtotime($headers['If-Modified-Since'])+$secs>$time) {
  834. $this->status(304);
  835. die;
  836. }
  837. }
  838. else
  839. header('Cache-Control: no-cache, no-store, must-revalidate');
  840. }
  841. }
  842. /**
  843. * Log error; Execute ONERROR handler if defined, else display
  844. * default error page (HTML for synchronous requests, JSON string
  845. * for AJAX requests)
  846. * @return NULL
  847. * @param $code int
  848. * @param $text string
  849. * @param $trace array
  850. **/
  851. function error($code,$text='',array $trace=NULL) {
  852. $prior=$this->hive['ERROR'];
  853. $header=$this->status($code);
  854. $req=$this->hive['VERB'].' '.$this->hive['PATH'];
  855. if (!$text)
  856. $text='HTTP '.$code.' ('.$req.')';
  857. error_log($text);
  858. if (!$trace)
  859. $trace=array_slice(debug_backtrace(FALSE),1);
  860. $debug=$this->hive['DEBUG'];
  861. $trace=array_filter(
  862. $trace,
  863. function($frame) use($debug) {
  864. return $debug && isset($frame['file']) &&
  865. ($frame['file']!=__FILE__ || $debug>1) &&
  866. (empty($frame['function']) ||
  867. !preg_match('/^(?:(?:trigger|user)_error|'.
  868. '__call|call_user_func)/',$frame['function']));
  869. }
  870. );
  871. $highlight=PHP_SAPI!='cli' &&
  872. $this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS);
  873. $out='';
  874. $eol="\n";
  875. // Analyze stack trace
  876. foreach ($trace as $frame) {
  877. $line='';
  878. if ($debug>1) {
  879. if (isset($frame['class']))
  880. $line.=$frame['class'].$frame['type'];
  881. if (isset($frame['function']))
  882. $line.=$frame['function'].'('.(isset($frame['args'])?
  883. $this->csv($frame['args']):'').')';
  884. }
  885. $src=$this->fixslashes(str_replace($_SERVER['DOCUMENT_ROOT'].
  886. '/','',$frame['file'])).':'.$frame['line'].' ';
  887. error_log('- '.$src.$line);
  888. $out.='• '.($highlight?
  889. ($this->highlight($src).' '.$this->highlight($line)):
  890. ($src.$line)).$eol;
  891. }
  892. $this->hive['ERROR']=array(
  893. 'status'=>$header,
  894. 'code'=>$code,
  895. 'text'=>$text,
  896. 'trace'=>$trace
  897. );
  898. if (!$debug && ob_get_level())
  899. ob_end_clean();
  900. $handler=$this->hive['ONERROR'];
  901. $this->hive['ONERROR']=NULL;
  902. if ((!$handler ||
  903. $this->call($handler,$this,'beforeroute,afterroute')===FALSE) &&
  904. !$prior && PHP_SAPI!='cli' && !$this->hive['QUIET'])
  905. echo $this->hive['AJAX']?
  906. json_encode($this->hive['ERROR']):
  907. ('<!DOCTYPE html>'.$eol.
  908. '<html>'.$eol.
  909. '<head>'.
  910. '<title>'.$code.' '.$header.'</title>'.
  911. ($highlight?
  912. ('<style>'.$this->read($css).'</style>'):'').
  913. '</head>'.$eol.
  914. '<body>'.$eol.
  915. '<h1>'.$header.'</h1>'.$eol.
  916. '<p>'.$this->encode($text?:$req).'</p>'.$eol.
  917. ($debug?('<pre>'.$out.'</pre>'.$eol):'').
  918. '</body>'.$eol.
  919. '</html>');
  920. if ($this->hive['HALT'])
  921. die;
  922. }
  923. /**
  924. * Mock HTTP request
  925. * @return NULL
  926. * @param $pattern string
  927. * @param $args array
  928. * @param $headers array
  929. * @param $body string
  930. **/
  931. function mock($pattern,array $args=NULL,array $headers=NULL,$body=NULL) {
  932. $types=array('sync','ajax');
  933. preg_match('/([\|\w]+)\h+(?:@(\w+)(?:(\(.+?)\))*|([^\h]+))'.
  934. '(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts);
  935. $verb=strtoupper($parts[1]);
  936. if ($parts[2]) {
  937. if (empty($this->hive['ALIASES'][$parts[2]])) {
  938. var_dump($parts);
  939. user_error(sprintf(self::E_Named,$parts[2]));
  940. }
  941. $parts[4]=$this->hive['ALIASES'][$parts[2]];
  942. if (isset($parts[3]))
  943. $this->parse($parts[3]);
  944. $parts[4]=$this->build($parts[4]);
  945. }
  946. if (empty($parts[4]))
  947. user_error(sprintf(self::E_Pattern,$pattern));
  948. $url=parse_url($parts[4]);
  949. $query='';
  950. if ($args)
  951. $query.=http_build_query($args);
  952. $query.=isset($url['query'])?(($query?'&':'').$url['query']):'';
  953. if ($query && preg_match('/GET|POST/',$verb)) {
  954. parse_str($query,$GLOBALS['_'.$verb]);
  955. parse_str($query,$GLOBALS['_REQUEST']);
  956. }
  957. foreach ($headers?:array() as $key=>$val)
  958. $_SERVER['HTTP_'.str_replace('-','_',strtoupper($key))]=$val;
  959. $this->hive['VERB']=$verb;
  960. $this->hive['URI']=$this->hive['BASE'].$url['path'];
  961. $this->hive['AJAX']=isset($parts[5]) &&
  962. preg_match('/ajax/i',$parts[5]);
  963. if (preg_match('/GET|HEAD/',$verb) && $query)
  964. $this->hive['URI'].='?'.$query;
  965. else
  966. $this->hive['BODY']=$body?:$query;
  967. $this->run();
  968. }
  969. /**
  970. * Bind handler to route pattern
  971. * @return NULL
  972. * @param $pattern string|array
  973. * @param $handler callback
  974. * @param $ttl int
  975. * @param $kbps int
  976. **/
  977. function route($pattern,$handler,$ttl=0,$kbps=0) {
  978. $types=array('sync','ajax');
  979. if (is_array($pattern)) {
  980. foreach ($pattern as $item)
  981. $this->route($item,$handler,$ttl,$kbps);
  982. return;
  983. }
  984. preg_match('/([\|\w]+)\h+(?:(?:@(\w+)\h*:\h*)?([^\h]+)|@(\w+))'.
  985. '(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts);
  986. if ($parts[2])
  987. $this->hive['ALIASES'][$parts[2]]=$parts[3];
  988. elseif (!empty($parts[4])) {
  989. if (empty($this->hive['ALIASES'][$parts[4]]))
  990. user_error(sprintf(self::E_Named,$parts[4]));
  991. $parts[3]=$this->hive['ALIASES'][$parts[4]];
  992. }
  993. if (empty($parts[3]))
  994. user_error(sprintf(self::E_Pattern,$pattern));
  995. $type=empty($parts[5])?
  996. self::REQ_SYNC|self::REQ_AJAX:
  997. constant('self::REQ_'.strtoupper($parts[5]));
  998. foreach ($this->split($parts[1]) as $verb) {
  999. if (!preg_match('/'.self::VERBS.'/',$verb))
  1000. $this->error(501,$verb.' '.$this->hive['URI']);
  1001. $this->hive['ROUTES'][str_replace('@',"\x00".'@',$parts[3])]
  1002. [$type][strtoupper($verb)]=array($handler,$ttl,$kbps);
  1003. }
  1004. }
  1005. /**
  1006. * Reroute to specified URI
  1007. * @return NULL
  1008. * @param $url string
  1009. * @param $permanent bool
  1010. **/
  1011. function reroute($url,$permanent=FALSE) {
  1012. if (PHP_SAPI!='cli') {
  1013. if (preg_match('/^(?:@(\w+)(?:(\(.+?)\))*|https?:\/\/)/',
  1014. $url,$parts)) {
  1015. if (isset($parts[1])) {
  1016. if (empty($this->hive['ALIASES'][$parts[1]]))
  1017. user_error(sprintf(self::E_Named,$parts[1]));
  1018. $url=$this->hive['BASE'].
  1019. $this->hive['ALIASES'][$parts[1]];
  1020. if (isset($parts[2]))
  1021. $this->parse($parts[2]);
  1022. $url=$this->build($url);
  1023. }
  1024. }
  1025. else
  1026. $url=$this->hive['BASE'].$url;
  1027. header('Location: '.$url);
  1028. $this->status($permanent?
  1029. 301:($_SERVER['SERVER_PROTOCOL']<'HTTP/1.1'?302:303));
  1030. die;
  1031. }
  1032. $this->mock('GET '.$url);
  1033. }
  1034. /**
  1035. * Provide ReST interface by mapping HTTP verb to class method
  1036. * @return NULL
  1037. * @param $url string
  1038. * @param $class string
  1039. * @param $ttl int
  1040. * @param $kbps int
  1041. **/
  1042. function map($url,$class,$ttl=0,$kbps=0) {
  1043. if (is_array($url)) {
  1044. foreach ($url as $item)
  1045. $this->map($item,$class,$ttl,$kbps);
  1046. return;
  1047. }
  1048. $fluid=preg_match('/@\w+/',$class);
  1049. foreach (explode('|',self::VERBS) as $method)
  1050. if ($fluid ||
  1051. method_exists($class,$method) ||
  1052. method_exists($class,'__call'))
  1053. $this->route($method.' '.
  1054. $url,$class.'->'.strtolower($method),$ttl,$kbps);
  1055. }
  1056. /**
  1057. * Return TRUE if IPv4 address exists in DNSBL
  1058. * @return bool
  1059. * @param $ip string
  1060. **/
  1061. function blacklisted($ip) {
  1062. if ($this->hive['DNSBL'] &&
  1063. !in_array($ip,
  1064. is_array($this->hive['EXEMPT'])?
  1065. $this->hive['EXEMPT']:
  1066. $this->split($this->hive['EXEMPT']))) {
  1067. // Reverse IPv4 dotted quad
  1068. $rev=implode('.',array_reverse(explode('.',$ip)));
  1069. foreach (is_array($this->hive['DNSBL'])?
  1070. $this->hive['DNSBL']:
  1071. $this->split($this->hive['DNSBL']) as $server)
  1072. // DNSBL lookup
  1073. if (checkdnsrr($rev.'.'.$server,'A'))
  1074. return TRUE;
  1075. }
  1076. return FALSE;
  1077. }
  1078. /**
  1079. * Match routes against incoming URI
  1080. * @return NULL
  1081. **/
  1082. function run() {
  1083. if ($this->blacklisted($this->hive['IP']))
  1084. // Spammer detected
  1085. $this->error(403);
  1086. if (!$this->hive['ROUTES'])
  1087. // No routes defined
  1088. user_error(self::E_Routes);
  1089. // Match specific routes first
  1090. krsort($this->hive['ROUTES']);
  1091. // Convert to BASE-relative URL
  1092. $req=preg_replace(
  1093. '/^'.preg_quote($this->hive['BASE'],'/').'(\/.*|$)/','\1',
  1094. $this->hive['URI']
  1095. );
  1096. $allowed=array();
  1097. $case=$this->hive['CASELESS']?'i':'';
  1098. foreach ($this->hive['ROUTES'] as $url=>$routes) {
  1099. $url=str_replace("\x00".'@','@',$url);
  1100. if (!preg_match('/^'.
  1101. preg_replace('/@(\w+\b)/','(?P<\1>[^\/\?]+)',
  1102. str_replace('\*','(.*)',preg_quote($url,'/'))).
  1103. '\/?(?:\?.*)?$/'.$case.'um',$req,$args))
  1104. continue;
  1105. $route=NULL;
  1106. if (isset($routes[$this->hive['AJAX']+1]))
  1107. $route=$routes[$this->hive['AJAX']+1];
  1108. elseif (isset($routes[self::REQ_SYNC|self::REQ_AJAX]))
  1109. $route=$routes[self::REQ_SYNC|self::REQ_AJAX];
  1110. if (!$route)
  1111. continue;
  1112. if ($this->hive['VERB']!='OPTIONS' &&
  1113. isset($route[$this->hive['VERB']])) {
  1114. $parts=parse_url($req);
  1115. if ($this->hive['VERB']=='GET' &&
  1116. preg_match('/.+\/$/',$parts['path']))
  1117. $this->reroute(substr($parts['path'],0,-1).
  1118. (isset($parts['query'])?('?'.$parts['query']):''));
  1119. list($handler,$ttl,$kbps)=$route[$this->hive['VERB']];
  1120. if (is_bool(strpos($url,'/*')))
  1121. foreach (array_keys($args) as $key)
  1122. if (is_numeric($key) && $key)
  1123. unset($args[$key]);
  1124. if (is_string($handler))
  1125. // Replace route pattern tokens in handler if any
  1126. $handler=preg_replace_callback('/@(\w+\b)/',
  1127. function($id) use($args) {
  1128. return isset($args[$id[1]])?$args[$id[1]]:$id[0];
  1129. },
  1130. $handler
  1131. );
  1132. // Capture values of route pattern tokens
  1133. $this->hive['PARAMS']=$args=array_map('urldecode',$args);
  1134. // Save matching route
  1135. $this->hive['PATTERN']=$url;
  1136. // Process request
  1137. $body='';
  1138. $now=microtime(TRUE);
  1139. if (preg_match('/GET|HEAD/',$this->hive['VERB']) &&
  1140. isset($ttl)) {
  1141. // Only GET and HEAD requests are cacheable
  1142. $headers=$this->hive['HEADERS'];
  1143. $cache=Cache::instance();
  1144. $cached=$cache->exists(
  1145. $hash=$this->hash($this->hive['VERB'].' '.
  1146. $this->hive['URI']).'.url',$data);
  1147. if ($cached && $cached[0]+$ttl>$now) {
  1148. // Retrieve from cache backend
  1149. list($headers,$body)=$data;
  1150. if (PHP_SAPI!='cli')
  1151. array_walk($headers,'header');
  1152. $this->expire($cached[0]+$ttl-$now);
  1153. }
  1154. else
  1155. // Expire HTTP client-cached page
  1156. $this->expire($ttl);
  1157. }
  1158. else
  1159. $this->expire(0);
  1160. if (!strlen($body)) {
  1161. if (!$this->hive['RAW'])
  1162. $this->hive['BODY']=file_get_contents('php://input');
  1163. ob_start();
  1164. // Call route handler
  1165. $this->call($handler,array($this,$args),
  1166. 'beforeroute,afterroute');
  1167. $body=ob_get_clean();
  1168. if ($ttl && !error_get_last())
  1169. // Save to cache backend
  1170. $cache->set($hash,array(headers_list(),$body),$ttl);
  1171. }
  1172. $this->hive['RESPONSE']=$body;
  1173. if (!$this->hive['QUIET']) {
  1174. if ($kbps) {
  1175. $ctr=0;
  1176. foreach (str_split($body,1024) as $part) {
  1177. // Throttle output
  1178. $ctr++;
  1179. if ($ctr/$kbps>($elapsed=microtime(TRUE)-$now) &&
  1180. !connection_aborted())
  1181. usleep(1e6*($ctr/$kbps-$elapsed));
  1182. echo $part;
  1183. }
  1184. }
  1185. else
  1186. echo $body;
  1187. }
  1188. return;
  1189. }
  1190. $allowed=array_keys($route);
  1191. break;
  1192. }
  1193. if (!$allowed)
  1194. // URL doesn't match any route
  1195. $this->error(404);
  1196. elseif (PHP_SAPI!='cli') {
  1197. // Unhandled HTTP method
  1198. header('Allow: '.implode(',',$allowed));
  1199. if ($this->hive['VERB']!='OPTIONS')
  1200. $this->error(405);
  1201. }
  1202. }
  1203. /**
  1204. * Execute callback/hooks (supports 'class->method' format)
  1205. * @return mixed|FALSE
  1206. * @param $func callback
  1207. * @param $args mixed
  1208. * @param $hooks string
  1209. **/
  1210. function call($func,$args=NULL,$hooks='') {
  1211. if (!is_array($args))
  1212. $args=array($args);
  1213. // Execute function; abort if callback/hook returns FALSE
  1214. if (is_string($func) &&
  1215. preg_match('/(.+)\h*(->|::)\h*(.+)/s',$func,$parts)) {
  1216. // Convert string to executable PHP callback
  1217. if (!class_exists($parts[1]))
  1218. $this->error(404);
  1219. if ($parts[2]=='->')
  1220. $parts[1]=is_subclass_of($parts[1],'Prefab')?
  1221. call_user_func($parts[1].'::instance'):
  1222. new $parts[1];
  1223. $func=array($parts[1],$parts[3]);
  1224. }
  1225. if (!is_callable($func) && $hooks=='beforeroute,afterroute')
  1226. // No route handler
  1227. $this->error(404);
  1228. $obj=FALSE;
  1229. if (is_array($func)) {
  1230. $hooks=$this->split($hooks);
  1231. $obj=TRUE;
  1232. }
  1233. // Execute pre-route hook if any
  1234. if ($obj && $hooks && in_array($hook='beforeroute',$hooks) &&
  1235. method_exists($func[0],$hook) &&
  1236. call_user_func_array(array($func[0],$hook),$args)===FALSE)
  1237. return FALSE;
  1238. // Execute callback
  1239. $out=call_user_func_array($func,$args?:array());
  1240. if ($out===FALSE)
  1241. return FALSE;
  1242. // Execute post-route hook if any
  1243. if ($obj && $hooks && in_array($hook='afterroute',$hooks) &&
  1244. method_exists($func[0],$hook) &&
  1245. call_user_func_array(array($func[0],$hook),$args)===FALSE)
  1246. return FALSE;
  1247. return $out;
  1248. }
  1249. /**
  1250. * Execute specified callbacks in succession; Apply same arguments
  1251. * to all callbacks
  1252. * @return array
  1253. * @param $funcs array|string
  1254. * @param $args mixed
  1255. **/
  1256. function chain($funcs,$args=NULL) {
  1257. $out=array();
  1258. foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func)
  1259. $out[]=$this->call($func,$args);
  1260. return $out;
  1261. }
  1262. /**
  1263. * Execute specified callbacks in succession; Relay result of
  1264. * previous callback as argument to the next callback
  1265. * @return array
  1266. * @param $funcs array|string
  1267. * @param $args mixed
  1268. **/
  1269. function relay($funcs,$args=NULL) {
  1270. foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func)
  1271. $args=array($this->call($func,$args));
  1272. return array_shift($args);
  1273. }
  1274. /**
  1275. * Configure framework according to .ini-style file settings
  1276. * @return NULL
  1277. * @param $file string
  1278. **/
  1279. function config($file) {
  1280. preg_match_all(
  1281. '/(?<=^|\n)(?:'.
  1282. '(?:;[^\n]*)|(?:<\?php.+?\?>?)|'.
  1283. '(?:\[(.+?)\])|'.
  1284. '(.+?)\h*=\h*'.
  1285. '((?:\\\\\h*\r?\n|.+?)*)'.
  1286. ')(?=\r?\n|$)/',
  1287. $this->read($file),$matches,PREG_SET_ORDER);
  1288. if ($matches) {
  1289. $sec='globals';
  1290. foreach ($matches as $match) {
  1291. if (count($match)<2)
  1292. continue;
  1293. if ($match[1])
  1294. $sec=$match[1];
  1295. elseif (in_array($sec,array('routes','maps'))) {
  1296. call_user_func_array(
  1297. array($this,rtrim($sec,'s')),
  1298. array_merge(array($match[2]),str_getcsv($match[3])));
  1299. }
  1300. else {
  1301. $args=array_map(
  1302. function($val) {
  1303. $quote=(isset($val[0]) && $val[0]=="\x00");
  1304. $val=trim($val);
  1305. if (!$quote && is_numeric($val))
  1306. return $val+0;
  1307. if (preg_match('/^\w+$/i',$val) && defined($val))
  1308. return constant($val);
  1309. return preg_replace('/\\\\\h*(\r?\n)/','\1',$val);
  1310. },
  1311. // Mark quoted strings with 0x00 whitespace
  1312. str_getcsv(preg_replace(
  1313. '/(")(.+?)\1/',"\\1\x00\\2\\1",$match[3]))
  1314. );
  1315. call_user_func_array(array($this,'set'),
  1316. array_merge(
  1317. array($match[2]),
  1318. count($args)>1?array($args):$args));
  1319. }
  1320. }
  1321. }
  1322. }
  1323. /**
  1324. * Create mutex, invoke callback then drop ownership when done
  1325. * @return mixed
  1326. * @param $id string
  1327. * @param $func callback
  1328. * @param $args mixed
  1329. **/
  1330. function mutex($id,$func,$args=NULL) {
  1331. if (!is_dir($tmp=$this->hive['TEMP']))
  1332. mkdir($tmp,self::MODE,TRUE);
  1333. // Use filesystem lock
  1334. if (is_file($lock=$tmp.
  1335. $this->hash($this->hive['ROOT'].$this->hive['BASE']).'.'.
  1336. $this->hash($id).'.lock') &&
  1337. filemtime($lock)+ini_get('max_execution_time')<microtime(TRUE))
  1338. // Stale lock
  1339. @unlink($lock);
  1340. while (!($handle=@fopen($lock,'x')) && !connection_aborted())
  1341. usleep(mt_rand(0,100));
  1342. $out=$this->call($func,$args);
  1343. fclose($handle);
  1344. @unlink($lock);
  1345. return $out;
  1346. }
  1347. /**
  1348. * Read file (with option to apply Unix LF as standard line ending)
  1349. * @return string
  1350. * @param $file string
  1351. * @param $lf bool
  1352. **/
  1353. function read($file,$lf=FALSE) {
  1354. $out=file_get_contents($file);
  1355. return $lf?preg_replace('/\r\n|\r/',"\n",$out):$out;
  1356. }
  1357. /**
  1358. * Exclusive file write
  1359. * @return int|FALSE
  1360. * @param $file string
  1361. * @param $data mixed
  1362. * @param $append bool
  1363. **/
  1364. function write($file,$data,$append=FALSE) {
  1365. return file_put_contents($file,$data,LOCK_EX|($append?FILE_APPEND:0));
  1366. }
  1367. /**
  1368. * Apply syntax highlighting
  1369. * @return string
  1370. * @param $text string
  1371. **/
  1372. function highlight($text) {
  1373. $out='';
  1374. $pre=FALSE;
  1375. $text=trim($text);
  1376. if (!preg_match('/^<\?php/',$text)) {
  1377. $text='<?php '.$text;
  1378. $pre=TRUE;
  1379. }
  1380. foreach (token_get_all($text) as $token)
  1381. if ($pre)
  1382. $pre=FALSE;
  1383. else
  1384. $out.='<span'.
  1385. (is_array($token)?
  1386. (' class="'.
  1387. substr(strtolower(token_name($token[0])),2).'">'.
  1388. $this->encode($token[1]).''):
  1389. ('>'.$this->encode($token))).
  1390. '</span>';
  1391. return $out?('<code>'.$out.'</code>'):$text;
  1392. }
  1393. /**
  1394. * Dump expression with syntax highlighting
  1395. * @return NULL
  1396. * @param $expr mixed
  1397. **/
  1398. function dump($expr) {
  1399. echo $this->highlight($this->stringify($expr,$this->hive['DEBUG']>2));
  1400. }
  1401. /**
  1402. * Return path relative to the base directory
  1403. * @return string
  1404. * @param $url string
  1405. **/
  1406. function rel($url) {
  1407. return preg_replace('/(?:https?:\/\/)?'.
  1408. preg_quote($this->hive['BASE'],'/').'/','',rtrim($url,'/'));
  1409. }
  1410. /**
  1411. * Namespace-aware class autoloader
  1412. * @return mixed
  1413. * @param $class string
  1414. **/
  1415. protected function autoload($class) {
  1416. $class=$this->fixslashes(ltrim($class,'\\'));
  1417. foreach ($this->split($this->hive['PLUGINS'].';'.
  1418. $this->hive['AUTOLOAD']) as $auto)
  1419. if (is_file($file=$auto.$class.'.php') ||
  1420. is_file($file=$auto.strtolower($class).'.php') ||
  1421. is_file($file=strtolower($auto.$class).'.php'))
  1422. return require($file);
  1423. }
  1424. /**
  1425. * Execute framework/application shutdown sequence
  1426. * @return NULL
  1427. * @param $cwd string
  1428. **/
  1429. function unload($cwd) {
  1430. chdir($cwd);
  1431. @session_commit();
  1432. $handler=$this->hive['UNLOAD'];
  1433. if ((!$handler || $this->call($handler,$this)===FALSE) &&
  1434. ($error=error_get_last()) && in_array($error['type'],
  1435. array(E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR)))
  1436. // Fatal error detected
  1437. $this->error(500,sprintf(self::E_Fatal,$error['message']),
  1438. array($error));
  1439. }
  1440. /**
  1441. * Return class instance
  1442. * @return object
  1443. **/
  1444. static function instance() {
  1445. if (!Registry::exists($class=__CLASS__))
  1446. Registry::set($class,new $class);
  1447. return Registry::get($class);
  1448. }
  1449. //! Prohibit cloning
  1450. private function __clone() {
  1451. }
  1452. //! Bootstrap
  1453. private function __construct() {
  1454. // Managed directives
  1455. ini_set('default_charset',$charset='UTF-8');
  1456. if (extension_loaded('mbstring'))
  1457. mb_internal_encoding($charset);
  1458. ini_set('display_errors',0);
  1459. // Deprecated directives
  1460. @ini_set('magic_quotes_gpc',0);
  1461. @ini_set('register_globals',0);
  1462. // Abort on startup error
  1463. // Intercept errors/exceptions; PHP5.3-compatible
  1464. error_reporting(E_ALL|E_STRICT);
  1465. $fw=$this;
  1466. set_exception_handler(
  1467. function($obj) use($fw) {
  1468. $fw->error(500,$obj->getmessage(),$obj->gettrace());
  1469. }
  1470. );
  1471. set_error_handler(
  1472. function($code,$text) use($fw) {
  1473. if (error_reporting())
  1474. throw new ErrorException($text,$code);
  1475. }
  1476. );
  1477. if (!isset($_SERVER['SERVER_NAME']))
  1478. $_SERVER['SERVER_NAME']=gethostname();
  1479. if (PHP_SAPI=='cli') {
  1480. // Emulate HTTP request
  1481. if (isset($_SERVER['argc']) && $_SERVER['argc']<2) {
  1482. $_SERVER['argc']++;
  1483. $_SERVER['argv'][1]='/';
  1484. }
  1485. $_SERVER['REQUEST_METHOD']='GET';
  1486. $_SERVER['REQUEST_URI']=$_SERVER['argv'][1];
  1487. }
  1488. $headers=array();
  1489. if (PHP_SAPI!='cli')
  1490. foreach (array_keys($_SERVER) as $key)
  1491. if (substr($key,0,5)=='HTTP_')
  1492. $headers[strtr(ucwords(strtolower(strtr(
  1493. substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key];
  1494. if (isset($headers['X-HTTP-Method-Override']))
  1495. $_SERVER['REQUEST_METHOD']=$headers['X-HTTP-Method-Override'];
  1496. elseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method']))
  1497. $_SERVER['REQUEST_METHOD']=$_POST['_method'];
  1498. $scheme=isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' ||
  1499. isset($headers['X-Forwarded-Proto']) &&
  1500. $headers['X-Forwarded-Proto']=='https'?'https':'http';
  1501. $base='';
  1502. if (PHP_SAPI!='cli')
  1503. $base=rtrim(dirname($_SERVER['SCRIPT_NAME']),'/');
  1504. $path=preg_replace('/^'.preg_quote($base,'/').'/','',
  1505. parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH));
  1506. call_user_func_array('session_set_cookie_params',
  1507. $jar=array(
  1508. 'expire'=>0,
  1509. 'path'=>$base?:'/',
  1510. 'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) &&
  1511. !filter_var($_SERVER['SERVER_NAME'],FILTER_VALIDATE_IP)?
  1512. $_SERVER['SERVER_NAME']:'',
  1513. 'secure'=>($scheme=='https'),
  1514. 'httponly'=>TRUE
  1515. )
  1516. );
  1517. if (function_exists('apache_setenv')) {
  1518. // Work around Apache pre-2.4 VirtualDocumentRoot bug
  1519. $_SERVER['DOCUMENT_ROOT']=str_replace($_SERVER['SCRIPT_NAME'],'',
  1520. $_SERVER['SCRIPT_FILENAME']);
  1521. apache_setenv("DOCUMENT_ROOT",$_SERVER['DOCUMENT_ROOT']);
  1522. }
  1523. $_SERVER['DOCUMENT_ROOT']=realpath($_SERVER['DOCUMENT_ROOT']);
  1524. // Default configuration
  1525. $this->hive=array(
  1526. 'AGENT'=>isset($headers['X-Operamini-Phone-UA'])?
  1527. $headers['X-Operamini-Phone-UA']:
  1528. (isset($headers['X-Skyfire-Phone'])?
  1529. $headers['X-Skyfire-Phone']:
  1530. (isset($headers['User-Agent'])?
  1531. $headers['User-Agent']:'')),
  1532. 'AJAX'=>isset($headers['X-Requested-With']) &&
  1533. $headers['X-Requested-With']=='XMLHttpRequest',
  1534. 'ALIASES'=>array(),
  1535. 'AUTOLOAD'=>'./',
  1536. 'BASE'=>$base,
  1537. 'BODY'=>NULL,
  1538. 'CACHE'=>FALSE,
  1539. 'CASELESS'=>TRUE,
  1540. 'DEBUG'=>0,
  1541. 'DIACRITICS'=>array(),
  1542. 'DNSBL'=>'',
  1543. 'ENCODING'=>$charset,
  1544. 'ERROR'=>NULL,
  1545. 'ESCAPE'=>TRUE,
  1546. 'EXEMPT'=>NULL,
  1547. 'FALLBACK'=>$this->fallback,
  1548. 'HEADERS'=>$headers,
  1549. 'HALT'=>TRUE,
  1550. 'HIGHLIGHT'=>TRUE,
  1551. 'HOST'=>$_SERVER['SERVER_NAME'],
  1552. 'IP'=>isset($headers['Client-IP'])?
  1553. $headers['Client-IP']:
  1554. (isset($headers['X-Forwarded-For'])?
  1555. $headers['X-Forwarded-For']:
  1556. (isset($_SERVER['REMOTE_ADDR'])?
  1557. $_SERVER['REMOTE_ADDR']:'')),
  1558. 'JAR'=>$jar,
  1559. 'LANGUAGE'=>isset($headers['Accept-Language'])?
  1560. $this->language($headers['Accept-Language']):
  1561. $this->fallback,
  1562. 'LOCALES'=>'./',
  1563. 'LOGS'=>'./',
  1564. 'ONERROR'=>NULL,
  1565. 'PACKAGE'=>self::PACKAGE,
  1566. 'PARAMS'=>array(),
  1567. 'PATH'=>$path,
  1568. 'PATTERN'=>NULL,
  1569. 'PLUGINS'=>$this->fixslashes(__DIR__).'/',
  1570. 'PORT'=>isset($_SERVER['SERVER_PORT'])?
  1571. $_SERVER['SERVER_PORT']:NULL,
  1572. 'PREFIX'=>NULL,
  1573. 'QUIET'=>FALSE,
  1574. 'RAW'=>FALSE,
  1575. 'REALM'=>$scheme.'://'.
  1576. $_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'],
  1577. 'RESPONSE'=>'',
  1578. 'ROOT'=>$_SERVER['DOCUMENT_ROOT'],
  1579. 'ROUTES'=>array(),
  1580. 'SCHEME'=>$scheme,
  1581. 'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php',
  1582. 'TEMP'=>'tmp/',
  1583. 'TIME'=>microtime(TRUE),
  1584. 'TZ'=>(@ini_get('date.timezone'))?:'UTC',
  1585. 'UI'=>'./',
  1586. 'UNLOAD'=>NULL,
  1587. 'UPLOADS'=>'./',
  1588. 'URI'=>&$_SERVER['REQUEST_URI'],
  1589. 'VERB'=>&$_SERVER['REQUEST_METHOD'],
  1590. 'VERSION'=>self::VERSION
  1591. );
  1592. if (PHP_SAPI=='cli-server' &&
  1593. preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI']))
  1594. $this->reroute('/');
  1595. if (ini_get('auto_globals_jit'))
  1596. // Override setting
  1597. $GLOBALS+=array('_ENV'=>$_ENV,'_REQUEST'=>$_REQUEST);
  1598. // Sync PHP globals with corresponding hive keys
  1599. $this->init=$this->hive;
  1600. foreach (explode('|',self::GLOBALS) as $global) {
  1601. $sync=$this->sync($global);
  1602. $this->init+=array(
  1603. $global=>preg_match('/SERVER|ENV/',$global)?$sync:array()
  1604. );
  1605. }
  1606. if ($error=error_get_last())
  1607. // Error detected
  1608. $this->error(500,sprintf(self::E_Fatal,$error['message']),
  1609. array($error));
  1610. date_default_timezone_set($this->hive['TZ']);
  1611. // Register framework autoloader
  1612. spl_autoload_register(array($this,'autoload'));
  1613. // Register shutdown handler
  1614. register_shutdown_function(array($this,'unload'),getcwd());
  1615. }
  1616. }
  1617. //! Prefab for classes with constructors and static factory methods
  1618. abstract class Prefab {
  1619. /**
  1620. * Return class instance
  1621. * @return object
  1622. **/
  1623. static function instance() {
  1624. if (!Registry::exists($class=get_called_class())) {
  1625. $ref=new Reflectionclass($class);
  1626. $args=func_get_args();
  1627. Registry::set($class,
  1628. $args?$ref->newinstanceargs($args):new $class);
  1629. }
  1630. return Registry::get($class);
  1631. }
  1632. }
  1633. //! Cache engine
  1634. class Cache extends Prefab {
  1635. protected
  1636. //! Cache DSN
  1637. $dsn,
  1638. //! Prefix for cache entries
  1639. $prefix,
  1640. //! MemCache object
  1641. $ref;
  1642. /**
  1643. * Return timestamp and TTL of cache entry or FALSE if not found
  1644. * @return float|FALSE
  1645. * @param $key string
  1646. * @param $val mixed
  1647. **/
  1648. function exists($key,&$val=NULL) {
  1649. $fw=Base::instance();
  1650. if (!$this->dsn)
  1651. return FALSE;
  1652. $ndx=$this->prefix.'.'.$key;
  1653. $parts=explode('=',$this->dsn,2);
  1654. switch ($parts[0]) {
  1655. case 'apc':
  1656. case 'apcu':
  1657. $raw=apc_fetch($ndx);
  1658. break;
  1659. case 'memcache':
  1660. $raw=memcache_get($this->ref,$ndx);
  1661. break;
  1662. case 'wincache':
  1663. $raw=wincache_ucache_get($ndx);
  1664. break;
  1665. case 'xcache':
  1666. $raw=xcache_get($ndx);
  1667. break;
  1668. case 'folder':
  1669. if (is_file($file=$parts[1].$ndx))
  1670. $raw=$fw->read($file);
  1671. break;
  1672. }
  1673. if (!empty($raw)) {
  1674. list($val,$time,$ttl)=(array)$fw->unserialize($raw);
  1675. if ($ttl===0 || $time+$ttl>microtime(TRUE))
  1676. return array($time,$ttl);
  1677. $this->clear($key);
  1678. }
  1679. return FALSE;
  1680. }
  1681. /**
  1682. * Store value in cache
  1683. * @return mixed|FALSE
  1684. * @param $key string
  1685. * @param $val mixed
  1686. * @param $ttl int
  1687. **/
  1688. function set($key,$val,$ttl=0) {
  1689. $fw=Base::instance();
  1690. if (!$this->dsn)
  1691. return TRUE;
  1692. $ndx=$this->prefix.'.'.$key;
  1693. $time=microtime(TRUE);
  1694. if ($cached=$this->exists($key))
  1695. list($time,$ttl)=$cached;
  1696. $data=$fw->serialize(array($val,$time,$ttl));
  1697. $parts=explode('=',$this->dsn,2);
  1698. switch ($parts[0]) {
  1699. case 'apc':
  1700. case 'apcu':
  1701. return apc_store($ndx,$data,$ttl);
  1702. case 'memcache':
  1703. return memcache_set($this->ref,$ndx,$data,0,$ttl);
  1704. case 'wincache':
  1705. return wincache_ucache_set($ndx,$data,$ttl);
  1706. case 'xcache':
  1707. return xcache_set($ndx,$data,$ttl);
  1708. case 'folder':
  1709. return $fw->write($parts[1].$ndx,$data);
  1710. }
  1711. return FALSE;
  1712. }
  1713. /**
  1714. * Retrieve value of cache entry
  1715. * @return mixed|FALSE
  1716. * @param $key string
  1717. **/
  1718. function get($key) {
  1719. return $this->dsn && $this->exists($key,$data)?$data:FALSE;
  1720. }
  1721. /**
  1722. * Delete cache entry
  1723. * @return bool
  1724. * @param $key string
  1725. **/
  1726. function clear($key) {
  1727. if (!$this->dsn)
  1728. return;
  1729. $ndx=$this->prefix.'.'.$key;
  1730. $parts=explode('=',$this->dsn,2);
  1731. switch ($parts[0]) {
  1732. case 'apc':
  1733. case 'apcu':
  1734. return apc_delete($ndx);
  1735. case 'memcache':
  1736. return memcache_delete($this->ref,$ndx);
  1737. case 'wincache':
  1738. return wincache_ucache_delete($ndx);
  1739. case 'xcache':
  1740. return xcache_unset($ndx);
  1741. case 'folder':
  1742. return is_file($file=$parts[1].$ndx) && @unlink($file);
  1743. }
  1744. return FALSE;
  1745. }
  1746. /**
  1747. * Clear contents of cache backend
  1748. * @return bool
  1749. * @param $suffix string
  1750. * @param $lifetime int
  1751. **/
  1752. function reset($suffix=NULL,$lifetime=0) {
  1753. if (!$this->dsn)
  1754. return TRUE;
  1755. $regex='/'.preg_quote($this->prefix.'.','/').'.+?'.
  1756. preg_quote($suffix,'/').'/';
  1757. $parts=explode('=',$this->dsn,2);
  1758. switch ($parts[0]) {
  1759. case 'apc':
  1760. $key='info';
  1761. case 'apcu':
  1762. if (empty($key))
  1763. $key='key';
  1764. $info=apc_cache_info('user');
  1765. foreach ($info['cache_list'] as $item)
  1766. if (preg_match($regex,$item[$key]) &&
  1767. $item['mtime']+$lifetime<time())
  1768. apc_delete($item[$key]);
  1769. return TRUE;
  1770. case 'memcache':
  1771. foreach (memcache_get_extended_stats(
  1772. $this->ref,'slabs') as $slabs)
  1773. foreach (array_filter(array_keys($slabs),'is_numeric')
  1774. as $id)
  1775. foreach (memcache_get_extended_stats(
  1776. $this->ref,'cachedump',$id) as $data)
  1777. if (is_array($data))
  1778. foreach ($data as $key=>$val)
  1779. if (preg_match($regex,$key) &&
  1780. $val[1]+$lifetime<time())
  1781. memcache_delete($this->ref,$key);
  1782. return TRUE;
  1783. case 'wincache':
  1784. $info=wincache_ucache_info();
  1785. foreach ($info['ucache_entries'] as $item)
  1786. if (preg_match($regex,$item['key_name']) &&
  1787. $item['use_time']+$lifetime<time())
  1788. wincache_ucache_delete($item['key_name']);
  1789. return TRUE;
  1790. case 'xcache':
  1791. return TRUE; /* Not supported */
  1792. case 'folder':
  1793. if ($glob=@glob($parts[1].'*'))
  1794. foreach ($glob as $file)
  1795. if (preg_match($regex,basename($file)) &&
  1796. filemtime($file)+$lifetime<time())
  1797. @unlink($file);
  1798. return TRUE;
  1799. }
  1800. return FALSE;
  1801. }
  1802. /**
  1803. * Load/auto-detect cache backend
  1804. * @return string
  1805. * @param $dsn bool|string
  1806. **/
  1807. function load($dsn) {
  1808. $fw=Base::instance();
  1809. if ($dsn=trim($dsn)) {
  1810. if (preg_match('/^memcache=(.+)/',$dsn,$parts) &&
  1811. extension_loaded('memcache'))
  1812. foreach ($fw->split($parts[1]) as $server) {
  1813. $port=11211;
  1814. $parts=explode(':',$server,2);
  1815. if (count($parts)>1)
  1816. list($host,$port)=$parts;
  1817. else
  1818. $host=$parts[0];
  1819. if (empty($this->ref))
  1820. $this->ref=@memcache_connect($host,$port)?:NULL;
  1821. else
  1822. memcache_add_server($this->ref,$host,$port);
  1823. }
  1824. if (empty($this->ref) && !preg_match('/^folder\h*=/',$dsn))
  1825. $dsn=($grep=preg_grep('/^(apc|wincache|xcache)/',
  1826. array_map('strtolower',get_loaded_extensions())))?
  1827. // Auto-detect
  1828. current($grep):
  1829. // Use filesystem as fallback
  1830. ('folder='.$fw->get('TEMP').'cache/');
  1831. if (preg_match('/^folder\h*=\h*(.+)/',$dsn,$parts) &&
  1832. !is_dir($parts[1]))
  1833. mkdir($parts[1],Base::MODE,TRUE);
  1834. }
  1835. $this->prefix=$fw->hash($_SERVER['SERVER_NAME'].$fw->get('BASE'));
  1836. return $this->dsn=$dsn;
  1837. }
  1838. /**
  1839. * Class constructor
  1840. * @return object
  1841. * @param $dsn bool|string
  1842. **/
  1843. function __construct($dsn=FALSE) {
  1844. if ($dsn)
  1845. $this->load($dsn);
  1846. }
  1847. }
  1848. //! View handler
  1849. class View extends Prefab {
  1850. protected
  1851. //! Template file
  1852. $view;
  1853. /**
  1854. * Attempt to clone object
  1855. * @return object
  1856. * @return $arg object
  1857. **/
  1858. function dupe($arg) {
  1859. if (method_exists('ReflectionClass','iscloneable')) {
  1860. $ref=new ReflectionClass($arg);
  1861. if ($ref->iscloneable())
  1862. $arg=clone($arg);
  1863. }
  1864. return $arg;
  1865. }
  1866. /**
  1867. * Encode characters to equivalent HTML entities
  1868. * @return string
  1869. * @param $arg mixed
  1870. **/
  1871. function esc($arg) {
  1872. if (is_array($arg)) {
  1873. $tmp=array();
  1874. foreach ($arg as $key=>$val)
  1875. $tmp[$key]=$this->esc($val);
  1876. return $tmp;
  1877. }
  1878. if (is_object($arg)) {
  1879. $obj=$this->dupe($arg);
  1880. foreach (get_object_vars($obj) as $key=>$val)
  1881. $obj->$key=$this->esc($val);
  1882. return $obj;
  1883. }
  1884. $arg=unserialize(serialize($arg));
  1885. if (is_string($arg))
  1886. $arg=\Base::instance()->encode($arg);
  1887. return $arg;
  1888. }
  1889. /**
  1890. * Decode HTML entities to equivalent characters
  1891. * @return string
  1892. * @param $arg mixed
  1893. **/
  1894. function raw($arg) {
  1895. if (is_array($arg)) {
  1896. $tmp=array();
  1897. foreach ($arg as $key=>$val)
  1898. $tmp[$key]=$this->raw($val);
  1899. return $tmp;
  1900. }
  1901. if (is_object($arg)) {
  1902. $obj=$this->dupe($arg);
  1903. foreach (get_object_vars($obj) as $key=>$val)
  1904. $obj->$key=$this->raw($val);
  1905. return $obj;
  1906. }
  1907. $arg=unserialize(serialize($arg));
  1908. if (is_string($arg))
  1909. $arg=\Base::instance()->decode($arg);
  1910. return $arg;
  1911. }
  1912. /**
  1913. * Create sandbox for template execution
  1914. * @return string
  1915. * @param $hive array
  1916. **/
  1917. protected function sandbox(array $hive=NULL) {
  1918. $fw=Base::instance();
  1919. if (!$hive)
  1920. $hive=$fw->hive();
  1921. if ($fw->get('ESCAPE'))
  1922. $hive=$this->esc($hive);
  1923. extract($hive);
  1924. unset($fw);
  1925. unset($hive);
  1926. ob_start();
  1927. require($this->view);
  1928. return ob_get_clean();
  1929. }
  1930. /**
  1931. * Render template
  1932. * @return string
  1933. * @param $file string
  1934. * @param $mime string
  1935. * @param $hive array
  1936. * @param $ttl int
  1937. **/
  1938. function render($file,$mime='text/html',array $hive=NULL,$ttl=0) {
  1939. $fw=Base::instance();
  1940. $cache=Cache::instance();
  1941. $cached=$cache->exists($hash=$fw->hash($file),$data);
  1942. if ($cached && $cached[0]+$ttl>microtime(TRUE))
  1943. return $data;
  1944. foreach ($fw->split($fw->get('UI').';./') as $dir)
  1945. if (is_file($this->view=$fw->fixslashes($dir.$file))) {
  1946. if (isset($_COOKIE[session_name()]))
  1947. @session_start();
  1948. $fw->sync('SESSION');
  1949. if (PHP_SAPI!='cli')
  1950. header('Content-Type: '.$mime.'; '.
  1951. 'charset='.$fw->get('ENCODING'));
  1952. $data=$this->sandbox($hive);
  1953. if ($ttl)
  1954. $cache->set($hash,$data);
  1955. return $data;
  1956. }
  1957. user_error(sprintf(Base::E_Open,$file));
  1958. }
  1959. }
  1960. //! Lightweight template engine
  1961. class Preview extends View {
  1962. protected
  1963. //! MIME type
  1964. $mime;
  1965. /**
  1966. * Convert token to variable
  1967. * @return string
  1968. * @param $str string
  1969. **/
  1970. function token($str) {
  1971. $self=$this;
  1972. $str=preg_replace_callback(
  1973. '/(?<!\w)@(\w(?:[\w\.\[\]]|\->|::)*)/',
  1974. function($var) use($self) {
  1975. // Convert from JS dot notation to PHP array notation
  1976. return '$'.preg_replace_callback(
  1977. '/(\.\w+)|\[((?:[^\[\]]*|(?R))*)\]/',
  1978. function($expr) use($self) {
  1979. $fw=Base::instance();
  1980. return
  1981. '['.
  1982. ($expr[1]?
  1983. $fw->stringify(substr($expr[1],1)):
  1984. (preg_match('/^\w+/',
  1985. $mix=$self->token($expr[2]))?
  1986. $fw->stringify($mix):
  1987. $mix)).
  1988. ']';
  1989. },
  1990. $var[1]
  1991. );
  1992. },
  1993. $str
  1994. );
  1995. return trim(preg_replace('/\{\{(.+?)\}\}/s',trim('\1'),$str));
  1996. }
  1997. /**
  1998. * Assemble markup
  1999. * @return string
  2000. * @param $node string
  2001. **/
  2002. protected function build($node) {
  2003. $self=$this;
  2004. return preg_replace_callback(
  2005. '/\{\{(.+?)\}\}/s',
  2006. function($expr) use($self) {
  2007. $str=trim($self->token($expr[1]));
  2008. if (preg_match('/^(.+?)\h*\|\h*(raw|esc|format)$/',
  2009. $str,$parts))
  2010. $str=(($parts[2]=='format')?
  2011. '\Base::instance()':'$this').'->'.$parts[2].
  2012. '('.$parts[1].')';
  2013. return '<?php echo '.$str.'; ?>';
  2014. },
  2015. preg_replace_callback(
  2016. '/\{\{?~(.+?)~\}?\}/s',
  2017. function($expr) use($self) {
  2018. return '<?php '.$self->token($expr[1]).' ?>';
  2019. },
  2020. $node
  2021. )
  2022. );
  2023. }
  2024. /**
  2025. * Render template string
  2026. * @return string
  2027. * @param $str string
  2028. * @param $hive array
  2029. **/
  2030. function resolve($str,array $hive=NULL) {
  2031. if (!$hive)
  2032. $hive=\Base::instance()->hive();
  2033. extract($hive);
  2034. ob_start();
  2035. eval(' ?>'.$this->build($str).'<?php ');
  2036. return ob_get_clean();
  2037. }
  2038. /**
  2039. * Render template
  2040. * @return string
  2041. * @param $file string
  2042. * @param $mime string
  2043. * @param $hive array
  2044. * @param $ttl int
  2045. **/
  2046. function render($file,$mime='text/html',array $hive=NULL,$ttl=0) {
  2047. $fw=Base::instance();
  2048. $cache=Cache::instance();
  2049. $cached=$cache->exists($hash=$fw->hash($file),$data);
  2050. if ($cached && $cached[0]+$ttl>microtime(TRUE))
  2051. return $data;
  2052. if (!is_dir($tmp=$fw->get('TEMP')))
  2053. mkdir($tmp,Base::MODE,TRUE);
  2054. foreach ($fw->split($fw->get('UI')) as $dir)
  2055. if (is_file($view=$fw->fixslashes($dir.$file))) {
  2056. if (!is_file($this->view=($tmp.
  2057. $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
  2058. $fw->hash($view).'.php')) ||
  2059. filemtime($this->view)<filemtime($view)) {
  2060. // Remove PHP code and comments
  2061. $text=preg_replace(
  2062. '/(?<!["\'])\h*<\?(?:php|\s*=).+?\?>\h*(?!["\'])|'.
  2063. '\{\{\*.+?\*\}\}|\{\*.+?\*\}/is','',
  2064. $fw->read($view));
  2065. if (method_exists($this,'parse'))
  2066. $text=$this->parse($text);
  2067. $fw->write($this->view,$this->build($text));
  2068. }
  2069. if (isset($_COOKIE[session_name()]))
  2070. @session_start();
  2071. $fw->sync('SESSION');
  2072. if (PHP_SAPI!='cli')
  2073. header('Content-Type: '.($this->mime=$mime).'; '.
  2074. 'charset='.$fw->get('ENCODING'));
  2075. $data=$this->sandbox($hive);
  2076. if ($ttl)
  2077. $cache->set($hash,$data);
  2078. return $data;
  2079. }
  2080. user_error(sprintf(Base::E_Open,$file));
  2081. }
  2082. }
  2083. //! ISO language/country codes
  2084. class ISO extends Prefab {
  2085. //@{ ISO 3166-1 country codes
  2086. const
  2087. CC_af='Afghanistan',
  2088. CC_ax='Åland Islands',
  2089. CC_al='Albania',
  2090. CC_dz='Algeria',
  2091. CC_as='American Samoa',
  2092. CC_ad='Andorra',
  2093. CC_ao='Angola',
  2094. CC_ai='Anguilla',
  2095. CC_aq='Antarctica',
  2096. CC_ag='Antigua and Barbuda',
  2097. CC_ar='Argentina',
  2098. CC_am='Armenia',
  2099. CC_aw='Aruba',
  2100. CC_au='Australia',
  2101. CC_at='Austria',
  2102. CC_az='Azerbaijan',
  2103. CC_bs='Bahamas',
  2104. CC_bh='Bahrain',
  2105. CC_bd='Bangladesh',
  2106. CC_bb='Barbados',
  2107. CC_by='Belarus',
  2108. CC_be='Belgium',
  2109. CC_bz='Belize',
  2110. CC_bj='Benin',
  2111. CC_bm='Bermuda',
  2112. CC_bt='Bhutan',
  2113. CC_bo='Bolivia',
  2114. CC_bq='Bonaire, Sint Eustatius and Saba',
  2115. CC_ba='Bosnia and Herzegovina',
  2116. CC_bw='Botswana',
  2117. CC_bv='Bouvet Island',
  2118. CC_br='Brazil',
  2119. CC_io='British Indian Ocean Territory',
  2120. CC_bn='Brunei Darussalam',
  2121. CC_bg='Bulgaria',
  2122. CC_bf='Burkina Faso',
  2123. CC_bi='Burundi',
  2124. CC_kh='Cambodia',
  2125. CC_cm='Cameroon',
  2126. CC_ca='Canada',
  2127. CC_cv='Cape Verde',
  2128. CC_ky='Cayman Islands',
  2129. CC_cf='Central African Republic',
  2130. CC_td='Chad',
  2131. CC_cl='Chile',
  2132. CC_cn='China',
  2133. CC_cx='Christmas Island',
  2134. CC_cc='Cocos (Keeling) Islands',
  2135. CC_co='Colombia',
  2136. CC_km='Comoros',
  2137. CC_cg='Congo',
  2138. CC_cd='Congo, The Democratic Republic of',
  2139. CC_ck='Cook Islands',
  2140. CC_cr='Costa Rica',
  2141. CC_ci='Côte d\'ivoire',
  2142. CC_hr='Croatia',
  2143. CC_cu='Cuba',
  2144. CC_cw='Curaçao',
  2145. CC_cy='Cyprus',
  2146. CC_cz='Czech Republic',
  2147. CC_dk='Denmark',
  2148. CC_dj='Djibouti',
  2149. CC_dm='Dominica',
  2150. CC_do='Dominican Republic',
  2151. CC_ec='Ecuador',
  2152. CC_eg='Egypt',
  2153. CC_sv='El Salvador',
  2154. CC_gq='Equatorial Guinea',
  2155. CC_er='Eritrea',
  2156. CC_ee='Estonia',
  2157. CC_et='Ethiopia',
  2158. CC_fk='Falkland Islands (Malvinas)',
  2159. CC_fo='Faroe Islands',
  2160. CC_fj='Fiji',
  2161. CC_fi='Finland',
  2162. CC_fr='France',
  2163. CC_gf='French Guiana',
  2164. CC_pf='French Polynesia',
  2165. CC_tf='French Southern Territories',
  2166. CC_ga='Gabon',
  2167. CC_gm='Gambia',
  2168. CC_ge='Georgia',
  2169. CC_de='Germany',
  2170. CC_gh='Ghana',
  2171. CC_gi='Gibraltar',
  2172. CC_gr='Greece',
  2173. CC_gl='Greenland',
  2174. CC_gd='Grenada',
  2175. CC_gp='Guadeloupe',
  2176. CC_gu='Guam',
  2177. CC_gt='Guatemala',
  2178. CC_gg='Guernsey',
  2179. CC_gn='Guinea',
  2180. CC_gw='Guinea-Bissau',
  2181. CC_gy='Guyana',
  2182. CC_ht='Haiti',
  2183. CC_hm='Heard Island and McDonald Islands',
  2184. CC_va='Holy See (Vatican City State)',
  2185. CC_hn='Honduras',
  2186. CC_hk='Hong Kong',
  2187. CC_hu='Hungary',
  2188. CC_is='Iceland',
  2189. CC_in='India',
  2190. CC_id='Indonesia',
  2191. CC_ir='Iran, Islamic Republic of',
  2192. CC_iq='Iraq',
  2193. CC_ie='Ireland',
  2194. CC_im='Isle of Man',
  2195. CC_il='Israel',
  2196. CC_it='Italy',
  2197. CC_jm='Jamaica',
  2198. CC_jp='Japan',
  2199. CC_je='Jersey',
  2200. CC_jo='Jordan',
  2201. CC_kz='Kazakhstan',
  2202. CC_ke='Kenya',
  2203. CC_ki='Kiribati',
  2204. CC_kp='Korea, Democratic People\'s Republic of',
  2205. CC_kr='Korea, Republic of',
  2206. CC_kw='Kuwait',
  2207. CC_kg='Kyrgyzstan',
  2208. CC_la='Lao People\'s Democratic Republic',
  2209. CC_lv='Latvia',
  2210. CC_lb='Lebanon',
  2211. CC_ls='Lesotho',
  2212. CC_lr='Liberia',
  2213. CC_ly='Libya',
  2214. CC_li='Liechtenstein',
  2215. CC_lt='Lithuania',
  2216. CC_lu='Luxembourg',
  2217. CC_mo='Macao',
  2218. CC_mk='Macedonia, The Former Yugoslav Republic of',
  2219. CC_mg='Madagascar',
  2220. CC_mw='Malawi',
  2221. CC_my='Malaysia',
  2222. CC_mv='Maldives',
  2223. CC_ml='Mali',
  2224. CC_mt='Malta',
  2225. CC_mh='Marshall Islands',
  2226. CC_mq='Martinique',
  2227. CC_mr='Mauritania',
  2228. CC_mu='Mauritius',
  2229. CC_yt='Mayotte',
  2230. CC_mx='Mexico',
  2231. CC_fm='Micronesia, Federated States of',
  2232. CC_md='Moldova, Republic of',
  2233. CC_mc='Monaco',
  2234. CC_mn='Mongolia',
  2235. CC_me='Montenegro',
  2236. CC_ms='Montserrat',
  2237. CC_ma='Morocco',
  2238. CC_mz='Mozambique',
  2239. CC_mm='Myanmar',
  2240. CC_na='Namibia',
  2241. CC_nr='Nauru',
  2242. CC_np='Nepal',
  2243. CC_nl='Netherlands',
  2244. CC_nc='New Caledonia',
  2245. CC_nz='New Zealand',
  2246. CC_ni='Nicaragua',
  2247. CC_ne='Niger',
  2248. CC_ng='Nigeria',
  2249. CC_nu='Niue',
  2250. CC_nf='Norfolk Island',
  2251. CC_mp='Northern Mariana Islands',
  2252. CC_no='Norway',
  2253. CC_om='Oman',
  2254. CC_pk='Pakistan',
  2255. CC_pw='Palau',
  2256. CC_ps='Palestinian Territory, Occupied',
  2257. CC_pa='Panama',
  2258. CC_pg='Papua New Guinea',
  2259. CC_py='Paraguay',
  2260. CC_pe='Peru',
  2261. CC_ph='Philippines',
  2262. CC_pn='Pitcairn',
  2263. CC_pl='Poland',
  2264. CC_pt='Portugal',
  2265. CC_pr='Puerto Rico',
  2266. CC_qa='Qatar',
  2267. CC_re='Réunion',
  2268. CC_ro='Romania',
  2269. CC_ru='Russian Federation',
  2270. CC_rw='Rwanda',
  2271. CC_bl='Saint Barthélemy',
  2272. CC_sh='Saint Helena, Ascension and Tristan da Cunha',
  2273. CC_kn='Saint Kitts and Nevis',
  2274. CC_lc='Saint Lucia',
  2275. CC_mf='Saint Martin (French Part)',
  2276. CC_pm='Saint Pierre and Miquelon',
  2277. CC_vc='Saint Vincent and The Grenadines',
  2278. CC_ws='Samoa',
  2279. CC_sm='San Marino',
  2280. CC_st='Sao Tome and Principe',
  2281. CC_sa='Saudi Arabia',
  2282. CC_sn='Senegal',
  2283. CC_rs='Serbia',
  2284. CC_sc='Seychelles',
  2285. CC_sl='Sierra Leone',
  2286. CC_sg='Singapore',
  2287. CC_sk='Slovakia',
  2288. CC_sx='Sint Maarten (Dutch Part)',
  2289. CC_si='Slovenia',
  2290. CC_sb='Solomon Islands',
  2291. CC_so='Somalia',
  2292. CC_za='South Africa',
  2293. CC_gs='South Georgia and The South Sandwich Islands',
  2294. CC_ss='South Sudan',
  2295. CC_es='Spain',
  2296. CC_lk='Sri Lanka',
  2297. CC_sd='Sudan',
  2298. CC_sr='Suriname',
  2299. CC_sj='Svalbard and Jan Mayen',
  2300. CC_sz='Swaziland',
  2301. CC_se='Sweden',
  2302. CC_ch='Switzerland',
  2303. CC_sy='Syrian Arab Republic',
  2304. CC_tw='Taiwan, Province of China',
  2305. CC_tj='Tajikistan',
  2306. CC_tz='Tanzania, United Republic of',
  2307. CC_th='Thailand',
  2308. CC_tl='Timor-Leste',
  2309. CC_tg='Togo',
  2310. CC_tk='Tokelau',
  2311. CC_to='Tonga',
  2312. CC_tt='Trinidad and Tobago',
  2313. CC_tn='Tunisia',
  2314. CC_tr='Turkey',
  2315. CC_tm='Turkmenistan',
  2316. CC_tc='Turks and Caicos Islands',
  2317. CC_tv='Tuvalu',
  2318. CC_ug='Uganda',
  2319. CC_ua='Ukraine',
  2320. CC_ae='United Arab Emirates',
  2321. CC_gb='United Kingdom',
  2322. CC_us='United States',
  2323. CC_um='United States Minor Outlying Islands',
  2324. CC_uy='Uruguay',
  2325. CC_uz='Uzbekistan',
  2326. CC_vu='Vanuatu',
  2327. CC_ve='Venezuela',
  2328. CC_vn='Viet Nam',
  2329. CC_vg='Virgin Islands, British',
  2330. CC_vi='Virgin Islands, U.S.',
  2331. CC_wf='Wallis and Futuna',
  2332. CC_eh='Western Sahara',
  2333. CC_ye='Yemen',
  2334. CC_zm='Zambia',
  2335. CC_zw='Zimbabwe';
  2336. //@}
  2337. //@{ ISO 639-1 language codes (Windows-compatibility subset)
  2338. const
  2339. LC_af='Afrikaans',
  2340. LC_am='Amharic',
  2341. LC_ar='Arabic',
  2342. LC_as='Assamese',
  2343. LC_ba='Bashkir',
  2344. LC_be='Belarusian',
  2345. LC_bg='Bulgarian',
  2346. LC_bn='Bengali',
  2347. LC_bo='Tibetan',
  2348. LC_br='Breton',
  2349. LC_ca='Catalan',
  2350. LC_co='Corsican',
  2351. LC_cs='Czech',
  2352. LC_cy='Welsh',
  2353. LC_da='Danish',
  2354. LC_de='German',
  2355. LC_dv='Divehi',
  2356. LC_el='Greek',
  2357. LC_en='English',
  2358. LC_es='Spanish',
  2359. LC_et='Estonian',
  2360. LC_eu='Basque',
  2361. LC_fa='Persian',
  2362. LC_fi='Finnish',
  2363. LC_fo='Faroese',
  2364. LC_fr='French',
  2365. LC_gd='Scottish Gaelic',
  2366. LC_gl='Galician',
  2367. LC_gu='Gujarati',
  2368. LC_he='Hebrew',
  2369. LC_hi='Hindi',
  2370. LC_hr='Croatian',
  2371. LC_hu='Hungarian',
  2372. LC_hy='Armenian',
  2373. LC_id='Indonesian',
  2374. LC_ig='Igbo',
  2375. LC_is='Icelandic',
  2376. LC_it='Italian',
  2377. LC_ja='Japanese',
  2378. LC_ka='Georgian',
  2379. LC_kk='Kazakh',
  2380. LC_km='Khmer',
  2381. LC_kn='Kannada',
  2382. LC_ko='Korean',
  2383. LC_lb='Luxembourgish',
  2384. LC_lo='Lao',
  2385. LC_lt='Lithuanian',
  2386. LC_lv='Latvian',
  2387. LC_mi='Maori',
  2388. LC_ml='Malayalam',
  2389. LC_mr='Marathi',
  2390. LC_ms='Malay',
  2391. LC_mt='Maltese',
  2392. LC_ne='Nepali',
  2393. LC_nl='Dutch',
  2394. LC_no='Norwegian',
  2395. LC_oc='Occitan',
  2396. LC_or='Oriya',
  2397. LC_pl='Polish',
  2398. LC_ps='Pashto',
  2399. LC_pt='Portuguese',
  2400. LC_qu='Quechua',
  2401. LC_ro='Romanian',
  2402. LC_ru='Russian',
  2403. LC_rw='Kinyarwanda',
  2404. LC_sa='Sanskrit',
  2405. LC_si='Sinhala',
  2406. LC_sk='Slovak',
  2407. LC_sl='Slovenian',
  2408. LC_sq='Albanian',
  2409. LC_sv='Swedish',
  2410. LC_ta='Tamil',
  2411. LC_te='Telugu',
  2412. LC_th='Thai',
  2413. LC_tk='Turkmen',
  2414. LC_tr='Turkish',
  2415. LC_tt='Tatar',
  2416. LC_uk='Ukrainian',
  2417. LC_ur='Urdu',
  2418. LC_vi='Vietnamese',
  2419. LC_wo='Wolof',
  2420. LC_yo='Yoruba',
  2421. LC_zh='Chinese';
  2422. //@}
  2423. /**
  2424. * Convert class constants to array
  2425. * @return array
  2426. * @param $prefix string
  2427. **/
  2428. protected function constants($prefix) {
  2429. $ref=new ReflectionClass($this);
  2430. $out=array();
  2431. foreach (preg_grep('/^'.$prefix.'/',array_keys($ref->getconstants()))
  2432. as $val) {
  2433. $out[$key=substr($val,strlen($prefix))]=
  2434. constant('self::'.$prefix.$key);
  2435. }
  2436. unset($ref);
  2437. return $out;
  2438. }
  2439. /**
  2440. * Return list of languages indexed by ISO 639-1 language code
  2441. * @return array
  2442. **/
  2443. function languages() {
  2444. return $this->constants('LC_');
  2445. }
  2446. /**
  2447. * Return list of countries indexed by ISO 3166-1 country code
  2448. * @return array
  2449. **/
  2450. function countries() {
  2451. return $this->constants('CC_');
  2452. }
  2453. }
  2454. //! Container for singular object instances
  2455. final class Registry {
  2456. private static
  2457. //! Object catalog
  2458. $table;
  2459. /**
  2460. * Return TRUE if object exists in catalog
  2461. * @return bool
  2462. * @param $key string
  2463. **/
  2464. static function exists($key) {
  2465. return isset(self::$table[$key]);
  2466. }
  2467. /**
  2468. * Add object to catalog
  2469. * @return object
  2470. * @param $key string
  2471. * @param $obj object
  2472. **/
  2473. static function set($key,$obj) {
  2474. return self::$table[$key]=$obj;
  2475. }
  2476. /**
  2477. * Retrieve object from catalog
  2478. * @return object
  2479. * @param $key string
  2480. **/
  2481. static function get($key) {
  2482. return self::$table[$key];
  2483. }
  2484. /**
  2485. * Delete object from catalog
  2486. * @return NULL
  2487. * @param $key string
  2488. **/
  2489. static function clear($key) {
  2490. self::$table[$key]=NULL;
  2491. unset(self::$table[$key]);
  2492. }
  2493. //! Prohibit cloning
  2494. private function __clone() {
  2495. }
  2496. //! Prohibit instantiation
  2497. private function __construct() {
  2498. }
  2499. }
  2500. return Base::instance();