PageRenderTime 127ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/base.php

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