PageRenderTime 115ms CodeModel.GetById 3ms app.highlight 67ms RepoModel.GetById 26ms 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

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

   1<?php
   2
   3/*
   4	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
   5
   6	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
   7
   8	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
   9	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  10	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
  11	PURPOSE.
  12
  13	Please see the license.txt file for more information.
  14*/
  15
  16//! Factory class for single-instance objects
  17abstract class Prefab {
  18
  19	/**
  20	*	Return class instance
  21	*	@return static
  22	**/
  23	static function instance() {
  24		if (!Registry::exists($class=get_called_class())) {
  25			$ref=new Reflectionclass($class);
  26			$args=func_get_args();
  27			Registry::set($class,
  28				$args?$ref->newinstanceargs($args):new $class);
  29		}
  30		return Registry::get($class);
  31	}
  32
  33}
  34
  35//! Base structure
  36class Base extends Prefab {
  37
  38	//@{ Framework details
  39	const
  40		PACKAGE='Fat-Free Framework',
  41		VERSION='3.2.2-Release';
  42	//@}
  43
  44	//@{ HTTP status codes (RFC 2616)
  45	const
  46		HTTP_100='Continue',
  47		HTTP_101='Switching Protocols',
  48		HTTP_200='OK',
  49		HTTP_201='Created',
  50		HTTP_202='Accepted',
  51		HTTP_203='Non-Authorative Information',
  52		HTTP_204='No Content',
  53		HTTP_205='Reset Content',
  54		HTTP_206='Partial Content',
  55		HTTP_300='Multiple Choices',
  56		HTTP_301='Moved Permanently',
  57		HTTP_302='Found',
  58		HTTP_303='See Other',
  59		HTTP_304='Not Modified',
  60		HTTP_305='Use Proxy',
  61		HTTP_307='Temporary Redirect',
  62		HTTP_400='Bad Request',
  63		HTTP_401='Unauthorized',
  64		HTTP_402='Payment Required',
  65		HTTP_403='Forbidden',
  66		HTTP_404='Not Found',
  67		HTTP_405='Method Not Allowed',
  68		HTTP_406='Not Acceptable',
  69		HTTP_407='Proxy Authentication Required',
  70		HTTP_408='Request Timeout',
  71		HTTP_409='Conflict',
  72		HTTP_410='Gone',
  73		HTTP_411='Length Required',
  74		HTTP_412='Precondition Failed',
  75		HTTP_413='Request Entity Too Large',
  76		HTTP_414='Request-URI Too Long',
  77		HTTP_415='Unsupported Media Type',
  78		HTTP_416='Requested Range Not Satisfiable',
  79		HTTP_417='Expectation Failed',
  80		HTTP_500='Internal Server Error',
  81		HTTP_501='Not Implemented',
  82		HTTP_502='Bad Gateway',
  83		HTTP_503='Service Unavailable',
  84		HTTP_504='Gateway Timeout',
  85		HTTP_505='HTTP Version Not Supported';
  86	//@}
  87
  88	const
  89		//! Mapped PHP globals
  90		GLOBALS='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV',
  91		//! HTTP verbs
  92		VERBS='GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT',
  93		//! Default directory permissions
  94		MODE=0755,
  95		//! Syntax highlighting stylesheet
  96		CSS='code.css';
  97
  98	//@{ HTTP request types
  99	const
 100		REQ_SYNC=1,
 101		REQ_AJAX=2;
 102	//@}
 103
 104	//@{ Error messages
 105	const
 106		E_Pattern='Invalid routing pattern: %s',
 107		E_Named='Named route does not exist: %s',
 108		E_Fatal='Fatal error: %s',
 109		E_Open='Unable to open %s',
 110		E_Routes='No routes specified',
 111		E_Class='Invalid class %s',
 112		E_Method='Invalid method %s',
 113		E_Hive='Invalid hive key %s';
 114	//@}
 115
 116	private
 117		//! Globals
 118		$hive,
 119		//! Initial settings
 120		$init,
 121		//! Language lookup sequence
 122		$languages,
 123		//! Default fallback language
 124		$fallback='en',
 125		//! NULL reference
 126		$null=NULL;
 127
 128	/**
 129	*	Sync PHP global with corresponding hive key
 130	*	@return array
 131	*	@param $key string
 132	**/
 133	function sync($key) {
 134		return $this->hive[$key]=&$GLOBALS['_'.$key];
 135	}
 136
 137	/**
 138	*	Return the parts of specified hive key
 139	*	@return array
 140	*	@param $key string
 141	**/
 142	private function cut($key) {
 143		return preg_split('/\[\h*[\'"]?(.+?)[\'"]?\h*\]|(->)|\./',
 144			$key,NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
 145	}
 146
 147	/**
 148	*	Replace tokenized URL with current route's token values
 149	*	@return string
 150	*	@param $url array|string
 151	**/
 152	function build($url) {
 153		if (is_array($url))
 154			foreach ($url as &$var) {
 155				$var=$this->build($var);
 156				unset($var);
 157			}
 158		elseif (preg_match_all('/@(\w+)/',$url,$matches,PREG_SET_ORDER))
 159			foreach ($matches as $match)
 160				if (array_key_exists($match[1],$this->hive['PARAMS']))
 161					$url=str_replace($match[0],
 162						$this->hive['PARAMS'][$match[1]],$url);
 163		return $url;
 164	}
 165
 166	/**
 167	*	Parse string containing key-value pairs and use as routing tokens
 168	*	@return NULL
 169	*	@param $str string
 170	**/
 171	function parse($str) {
 172		preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/',
 173			$str,$pairs,PREG_SET_ORDER);
 174		foreach ($pairs as $pair)
 175			$this->hive['PARAMS'][$pair[1]]=trim($pair[2]);
 176	}
 177
 178	/**
 179	*	Convert JS-style token to PHP expression
 180	*	@return string
 181	*	@param $str string
 182	**/
 183	function compile($str) {
 184		$fw=$this;
 185		return preg_replace_callback(
 186			'/(?<!\w)@(\w(?:[\w\.\[\]]|\->|::)*)/',
 187			function($var) use($fw) {
 188				return '$'.preg_replace_callback(
 189					'/\.(\w+)|\[((?:[^\[\]]*|(?R))*)\]/',
 190					function($expr) use($fw) {
 191						return '['.var_export(
 192							isset($expr[2])?
 193								$fw->compile($expr[2]):
 194								(ctype_digit($expr[1])?
 195									(int)$expr[1]:
 196									$expr[1]),TRUE).']';
 197					},
 198					$var[1]
 199				);
 200			},
 201			$str
 202		);
 203	}
 204
 205	/**
 206	*	Get hive key reference/contents; Add non-existent hive keys,
 207	*	array elements, and object properties by default
 208	*	@return mixed
 209	*	@param $key string
 210	*	@param $add bool
 211	**/
 212	function &ref($key,$add=TRUE) {
 213		$parts=$this->cut($key);
 214		if ($parts[0]=='SESSION') {
 215			@session_start();
 216			$this->sync('SESSION');
 217		}
 218		elseif (!preg_match('/^\w+$/',$parts[0]))
 219			user_error(sprintf(self::E_Hive,$this->stringify($key)));
 220		if ($add)
 221			$var=&$this->hive;
 222		else
 223			$var=$this->hive;
 224		$obj=FALSE;
 225		foreach ($parts as $part)
 226			if ($part=='->')
 227				$obj=TRUE;
 228			elseif ($obj) {
 229				$obj=FALSE;
 230				if (!is_object($var))
 231					$var=new stdclass;
 232				$var=&$var->$part;
 233			}
 234			else {
 235				if (!is_array($var))
 236					$var=array();
 237				$var=&$var[$part];
 238			}
 239		if ($parts[0]=='ALIASES')
 240			$var=$this->build($var);
 241		return $var;
 242	}
 243
 244	/**
 245	*	Return TRUE if hive key is not set
 246	*	(or return timestamp and TTL if cached)
 247	*	@return bool
 248	*	@param $key string
 249	*	@param $val mixed
 250	**/
 251	function exists($key,&$val=NULL) {
 252		$val=$this->ref($key,FALSE);
 253		return isset($val)?
 254			TRUE:
 255			(Cache::instance()->exists($this->hash($key).'.var',$val)?
 256				$val:FALSE);
 257	}
 258
 259	/**
 260	*	Return TRUE if hive key is empty and not cached
 261	*	@return bool
 262	*	@param $key string
 263	**/
 264	function devoid($key) {
 265		$val=$this->ref($key,FALSE);
 266		return empty($val) &&
 267			(!Cache::instance()->exists($this->hash($key).'.var',$val) ||
 268				!$val);
 269	}
 270
 271	/**
 272	*	Bind value to hive key
 273	*	@return mixed
 274	*	@param $key string
 275	*	@param $val mixed
 276	*	@param $ttl int
 277	**/
 278	function set($key,$val,$ttl=0) {
 279		if (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) {
 280			$this->set('REQUEST'.$expr[2],$val);
 281			if ($expr[1]=='COOKIE') {
 282				$parts=$this->cut($key);
 283				$jar=$this->hive['JAR'];
 284				if ($ttl)
 285					$jar['expire']=time()+$ttl;
 286				call_user_func_array('setcookie',array($parts[1],$val)+$jar);
 287			}
 288		}
 289		else switch ($key) {
 290			case 'CACHE':
 291				$val=Cache::instance()->load($val,TRUE);
 292				break;
 293			case 'ENCODING':
 294				$val=ini_set('default_charset',$val);
 295				if (extension_loaded('mbstring'))
 296					mb_internal_encoding($val);
 297				break;
 298			case 'FALLBACK':
 299				$this->fallback=$val;
 300				$lang=$this->language($this->hive['LANGUAGE']);
 301			case 'LANGUAGE':
 302				if (isset($lang) || $lang=$this->language($val))
 303					$val=$this->language($val);
 304				$lex=$this->lexicon($this->hive['LOCALES']);
 305			case 'LOCALES':
 306				if (isset($lex) || $lex=$this->lexicon($val))
 307					$this->mset($lex,$this->hive['PREFIX'],$ttl);
 308				break;
 309			case 'TZ':
 310				date_default_timezone_set($val);
 311				break;
 312		}
 313		$ref=&$this->ref($key);
 314		$ref=$val;
 315		if (preg_match('/^JAR\b/',$key))
 316			call_user_func_array(
 317				'session_set_cookie_params',$this->hive['JAR']);
 318		$cache=Cache::instance();
 319		if ($cache->exists($hash=$this->hash($key).'.var') || $ttl)
 320			// Persist the key-value pair
 321			$cache->set($hash,$val,$ttl);
 322		return $ref;
 323	}
 324
 325	/**
 326	*	Retrieve contents of hive key
 327	*	@return mixed
 328	*	@param $key string
 329	*	@param $args string|array
 330	**/
 331	function get($key,$args=NULL) {
 332		if (is_string($val=$this->ref($key,FALSE)) && !is_null($args))
 333			return call_user_func_array(
 334				array($this,'format'),
 335				array_merge(array($val),is_array($args)?$args:array($args))
 336			);
 337		if (is_null($val)) {
 338			// Attempt to retrieve from cache
 339			if (Cache::instance()->exists($this->hash($key).'.var',$data))
 340				return $data;
 341		}
 342		return $val;
 343	}
 344
 345	/**
 346	*	Unset hive key
 347	*	@return NULL
 348	*	@param $key string
 349	**/
 350	function clear($key) {
 351		// Normalize array literal
 352		$cache=Cache::instance();
 353		$parts=$this->cut($key);
 354		if ($key=='CACHE')
 355			// Clear cache contents
 356			$cache->reset();
 357		elseif (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) {
 358			$this->clear('REQUEST'.$expr[2]);
 359			if ($expr[1]=='COOKIE') {
 360				$parts=$this->cut($key);
 361				$jar=$this->hive['JAR'];
 362				$jar['expire']=strtotime('-1 year');
 363				call_user_func_array('setcookie',
 364					array_merge(array($parts[1],''),$jar));
 365				unset($_COOKIE[$parts[1]]);
 366			}
 367		}
 368		elseif ($parts[0]=='SESSION') {
 369			@session_start();
 370			if (empty($parts[1])) {
 371				// End session
 372				session_unset();
 373				session_destroy();
 374				unset($_COOKIE[session_name()]);
 375				header_remove('Set-Cookie');
 376			}
 377			$this->sync('SESSION');
 378		}
 379		if (!isset($parts[1]) && array_key_exists($parts[0],$this->init))
 380			// Reset global to default value
 381			$this->hive[$parts[0]]=$this->init[$parts[0]];
 382		else {
 383			eval('unset('.$this->compile('@this->hive.'.$key).');');
 384			if ($parts[0]=='SESSION') {
 385				session_commit();
 386				session_start();
 387			}
 388			if ($cache->exists($hash=$this->hash($key).'.var'))
 389				// Remove from cache
 390				$cache->clear($hash);
 391		}
 392	}
 393
 394	/**
 395	*	Multi-variable assignment using associative array
 396	*	@return NULL
 397	*	@param $vars array
 398	*	@param $prefix string
 399	*	@param $ttl int
 400	**/
 401	function mset(array $vars,$prefix='',$ttl=0) {
 402		foreach ($vars as $key=>$val)
 403			$this->set($prefix.$key,$val,$ttl);
 404	}
 405
 406	/**
 407	*	Publish hive contents
 408	*	@return array
 409	**/
 410	function hive() {
 411		return $this->hive;
 412	}
 413
 414	/**
 415	*	Copy contents of hive variable to another
 416	*	@return mixed
 417	*	@param $src string
 418	*	@param $dst string
 419	**/
 420	function copy($src,$dst) {
 421		$ref=&$this->ref($dst);
 422		return $ref=$this->ref($src,FALSE);
 423	}
 424
 425	/**
 426	*	Concatenate string to hive string variable
 427	*	@return string
 428	*	@param $key string
 429	*	@param $val string
 430	**/
 431	function concat($key,$val) {
 432		$ref=&$this->ref($key);
 433		$ref.=$val;
 434		return $ref;
 435	}
 436
 437	/**
 438	*	Swap keys and values of hive array variable
 439	*	@return array
 440	*	@param $key string
 441	*	@public
 442	**/
 443	function flip($key) {
 444		$ref=&$this->ref($key);
 445		return $ref=array_combine(array_values($ref),array_keys($ref));
 446	}
 447
 448	/**
 449	*	Add element to the end of hive array variable
 450	*	@return mixed
 451	*	@param $key string
 452	*	@param $val mixed
 453	**/
 454	function push($key,$val) {
 455		$ref=&$this->ref($key);
 456		array_push($ref,$val);
 457		return $val;
 458	}
 459
 460	/**
 461	*	Remove last element of hive array variable
 462	*	@return mixed
 463	*	@param $key string
 464	**/
 465	function pop($key) {
 466		$ref=&$this->ref($key);
 467		return array_pop($ref);
 468	}
 469
 470	/**
 471	*	Add element to the beginning of hive array variable
 472	*	@return mixed
 473	*	@param $key string
 474	*	@param $val mixed
 475	**/
 476	function unshift($key,$val) {
 477		$ref=&$this->ref($key);
 478		array_unshift($ref,$val);
 479		return $val;
 480	}
 481
 482	/**
 483	*	Remove first element of hive array variable
 484	*	@return mixed
 485	*	@param $key string
 486	**/
 487	function shift($key) {
 488		$ref=&$this->ref($key);
 489		return array_shift($ref);
 490	}
 491
 492	/**
 493	*	Merge array with hive array variable
 494	*	@return array
 495	*	@param $key string
 496	*	@param $src string|array
 497	**/
 498	function merge($key,$src) {
 499		$ref=&$this->ref($key);
 500		return array_merge($ref,is_string($src)?$this->hive[$src]:$src);
 501	}
 502
 503	/**
 504	*	Convert backslashes to slashes
 505	*	@return string
 506	*	@param $str string
 507	**/
 508	function fixslashes($str) {
 509		return $str?strtr($str,'\\','/'):$str;
 510	}
 511
 512	/**
 513	*	Split comma-, semi-colon, or pipe-separated string
 514	*	@return array
 515	*	@param $str string
 516	**/
 517	function split($str) {
 518		return array_map('trim',
 519			preg_split('/[,;|]/',$str,0,PREG_SPLIT_NO_EMPTY));
 520	}
 521
 522	/**
 523	*	Convert PHP expression/value to compressed exportable string
 524	*	@return string
 525	*	@param $arg mixed
 526	*	@param $stack array
 527	**/
 528	function stringify($arg,array $stack=NULL) {
 529		if ($stack) {
 530			foreach ($stack as $node)
 531				if ($arg===$node)
 532					return '*RECURSION*';
 533		}
 534		else
 535			$stack=array();
 536		switch (gettype($arg)) {
 537			case 'object':
 538				$str='';
 539				foreach (get_object_vars($arg) as $key=>$val)
 540					$str.=($str?',':'').
 541						var_export($key,TRUE).'=>'.
 542						$this->stringify($val,
 543							array_merge($stack,array($arg)));
 544				return get_class($arg).'::__set_state(array('.$str.'))';
 545			case 'array':
 546				$str='';
 547				$num=isset($arg[0]) &&
 548					ctype_digit(implode('',array_keys($arg)));
 549				foreach ($arg as $key=>$val)
 550					$str.=($str?',':'').
 551						($num?'':(var_export($key,TRUE).'=>')).
 552						$this->stringify($val,
 553							array_merge($stack,array($arg)));
 554				return 'array('.$str.')';
 555			default:
 556				return var_export($arg,TRUE);
 557		}
 558	}
 559
 560	/**
 561	*	Flatten array values and return as CSV string
 562	*	@return string
 563	*	@param $args array
 564	**/
 565	function csv(array $args) {
 566		return implode(',',array_map('stripcslashes',
 567			array_map(array($this,'stringify'),$args)));
 568	}
 569
 570	/**
 571	*	Convert snakecase string to camelcase
 572	*	@return string
 573	*	@param $str string
 574	**/
 575	function camelcase($str) {
 576		return preg_replace_callback(
 577			'/_(\w)/',
 578			function($match) {
 579				return strtoupper($match[1]);
 580			},
 581			$str
 582		);
 583	}
 584
 585	/**
 586	*	Convert camelcase string to snakecase
 587	*	@return string
 588	*	@param $str string
 589	**/
 590	function snakecase($str) {
 591		return strtolower(preg_replace('/[[:upper:]]/','_\0',$str));
 592	}
 593
 594	/**
 595	*	Return -1 if specified number is negative, 0 if zero,
 596	*	or 1 if the number is positive
 597	*	@return int
 598	*	@param $num mixed
 599	**/
 600	function sign($num) {
 601		return $num?($num/abs($num)):0;
 602	}
 603
 604	/**
 605	*	Generate 64bit/base36 hash
 606	*	@return string
 607	*	@param $str
 608	**/
 609	function hash($str) {
 610		return str_pad(base_convert(
 611			hexdec(substr(sha1($str),-16)),10,36),11,'0',STR_PAD_LEFT);
 612	}
 613
 614	/**
 615	*	Return Base64-encoded equivalent
 616	*	@return string
 617	*	@param $data string
 618	*	@param $mime string
 619	**/
 620	function base64($data,$mime) {
 621		return 'data:'.$mime.';base64,'.base64_encode($data);
 622	}
 623
 624	/**
 625	*	Convert special characters to HTML entities
 626	*	@return string
 627	*	@param $str string
 628	**/
 629	function encode($str) {
 630		return @htmlentities($str,$this->hive['BITMASK'],
 631			$this->hive['ENCODING'],FALSE)?:$this->scrub($str);
 632	}
 633
 634	/**
 635	*	Convert HTML entities back to characters
 636	*	@return string
 637	*	@param $str string
 638	**/
 639	function decode($str) {
 640		return html_entity_decode($str,$this->hive['BITMASK'],
 641			$this->hive['ENCODING']);
 642	}
 643
 644	/**
 645	*	Attempt to clone object
 646	*	@return object
 647	*	@return $arg object
 648	**/
 649	function dupe($arg) {
 650		if (method_exists('ReflectionClass','iscloneable')) {
 651			$ref=new ReflectionClass($arg);
 652			if ($ref->iscloneable())
 653				$arg=clone($arg);
 654		}
 655		return $arg;
 656	}
 657
 658	/**
 659	*	Invoke callback recursively for all data types
 660	*	@return mixed
 661	*	@param $arg mixed
 662	*	@param $func callback
 663	*	@param $stack array
 664	**/
 665	function recursive($arg,$func,$stack=NULL) {
 666		if ($stack) {
 667			foreach ($stack as $node)
 668				if ($arg===$node)
 669					return $arg;
 670		}
 671		else
 672			$stack=array();
 673		switch (gettype($arg)) {
 674			case 'object':
 675				$arg=$this->dupe($arg);
 676				foreach (get_object_vars($arg) as $key=>$val)
 677					$arg->$key=$this->recursive($val,$func,
 678						array_merge($stack,array($arg)));
 679				return $arg;
 680			case 'array':
 681				$tmp=array();
 682				foreach ($arg as $key=>$val)
 683					$tmp[$key]=$this->recursive($val,$func,
 684						array_merge($stack,array($arg)));
 685				return $tmp;
 686		}
 687		return $func($arg);
 688	}
 689
 690	/**
 691	*	Remove HTML tags (except those enumerated) and non-printable
 692	*	characters to mitigate XSS/code injection attacks
 693	*	@return mixed
 694	*	@param $arg mixed
 695	*	@param $tags string
 696	**/
 697	function clean($arg,$tags=NULL) {
 698		$fw=$this;
 699		return $this->recursive($arg,
 700			function($val) use($fw,$tags) {
 701				if ($tags!='*')
 702					$val=trim(strip_tags($val,
 703						'<'.implode('><',$fw->split($tags)).'>'));
 704				return trim(preg_replace(
 705					'/[\x00-\x08\x0B\x0C\x0E-\x1F]/','',$val));
 706			}
 707		);
 708	}
 709
 710	/**
 711	*	Similar to clean(), except that variable is passed by reference
 712	*	@return mixed
 713	*	@param $var mixed
 714	*	@param $tags string
 715	**/
 716	function scrub(&$var,$tags=NULL) {
 717		return $var=$this->clean($var,$tags);
 718	}
 719
 720	/**
 721	*	Return locale-aware formatted string
 722	*	@return string
 723	**/
 724	function format() {
 725		$args=func_get_args();
 726		$val=array_shift($args);
 727		// Get formatting rules
 728		$conv=localeconv();
 729		return preg_replace_callback(
 730			'/\{(?P<pos>\d+)\s*(?:,\s*(?P<type>\w+)\s*'.
 731			'(?:,\s*(?P<mod>(?:\w+(?:\s*\{.+?\}\s*,?)?)*)'.
 732			'(?:,\s*(?P<prop>.+?))?)?)?\}/',
 733			function($expr) use($args,$conv) {
 734				extract($expr);
 735				extract($conv);
 736				if (!array_key_exists($pos,$args))
 737					return $expr[0];
 738				if (isset($type))
 739					switch ($type) {
 740						case 'plural':
 741							preg_match_all('/(?<tag>\w+)'.
 742								'(?:\s+\{\s*(?<data>.+?)\s*\})/',
 743								$mod,$matches,PREG_SET_ORDER);
 744							$ord=array('zero','one','two');
 745							foreach ($matches as $match) {
 746								extract($match);
 747								if (isset($ord[$args[$pos]]) &&
 748									$tag==$ord[$args[$pos]] || $tag=='other')
 749									return str_replace('#',$args[$pos],$data);
 750							}
 751						case 'number':
 752							if (isset($mod))
 753								switch ($mod) {
 754									case 'integer':
 755										return number_format(
 756											$args[$pos],0,'',$thousands_sep);
 757									case 'currency':
 758										if (function_exists('money_format'))
 759											return money_format(
 760												'%n',$args[$pos]);
 761										$fmt=array(
 762											0=>'(nc)',1=>'(n c)',
 763											2=>'(nc)',10=>'+nc',
 764											11=>'+n c',12=>'+ nc',
 765											20=>'nc+',21=>'n c+',
 766											22=>'nc +',30=>'n+c',
 767											31=>'n +c',32=>'n+ c',
 768											40=>'nc+',41=>'n c+',
 769											42=>'nc +',100=>'(cn)',
 770											101=>'(c n)',102=>'(cn)',
 771											110=>'+cn',111=>'+c n',
 772											112=>'+ cn',120=>'cn+',
 773											121=>'c n+',122=>'cn +',
 774											130=>'+cn',131=>'+c n',
 775											132=>'+ cn',140=>'c+n',
 776											141=>'c+ n',142=>'c +n'
 777										);
 778										if ($args[$pos]<0) {
 779											$sgn=$negative_sign;
 780											$pre='n';
 781										}
 782										else {
 783											$sgn=$positive_sign;
 784											$pre='p';
 785										}
 786										return str_replace(
 787											array('+','n','c'),
 788											array($sgn,number_format(
 789												abs($args[$pos]),
 790												$frac_digits,
 791												$decimal_point,
 792												$thousands_sep),
 793												$currency_symbol),
 794											$fmt[(int)(
 795												(${$pre.'_cs_precedes'}%2).
 796												(${$pre.'_sign_posn'}%5).
 797												(${$pre.'_sep_by_space'}%3)
 798											)]
 799										);
 800									case 'percent':
 801										return number_format(
 802											$args[$pos]*100,0,$decimal_point,
 803											$thousands_sep).'%';
 804									case 'decimal':
 805										return number_format(
 806											$args[$pos],$prop,$decimal_point,
 807												$thousands_sep);
 808								}
 809							break;
 810						case 'date':
 811							if (empty($mod) || $mod=='short')
 812								$prop='%x';
 813							elseif ($mod=='long')
 814								$prop='%A, %d %B %Y';
 815							return strftime($prop,$args[$pos]);
 816						case 'time':
 817							if (empty($mod) || $mod=='short')
 818								$prop='%X';
 819							return strftime($prop,$args[$pos]);
 820						default:
 821							return $expr[0];
 822					}
 823				return $args[$pos];
 824			},
 825			$val
 826		);
 827	}
 828
 829	/**
 830	*	Assign/auto-detect language
 831	*	@return string
 832	*	@param $code string
 833	**/
 834	function language($code) {
 835		$code=preg_replace('/;q=.+?(?=,|$)/','',$code);
 836		$code.=($code?',':'').$this->fallback;
 837		$this->languages=array();
 838		foreach (array_reverse(explode(',',$code)) as $lang) {
 839			if (preg_match('/^(\w{2})(?:-(\w{2}))?\b/i',$lang,$parts)) {
 840				// Generic language
 841				array_unshift($this->languages,$parts[1]);
 842				if (isset($parts[2])) {
 843					// Specific language
 844					$parts[0]=$parts[1].'-'.($parts[2]=strtoupper($parts[2]));
 845					array_unshift($this->languages,$parts[0]);
 846				}
 847			}
 848		}
 849		$this->languages=array_unique($this->languages);
 850		$locales=array();
 851		$windows=preg_match('/^win/i',PHP_OS);
 852		foreach ($this->languages as $locale) {
 853			if ($windows) {
 854				$parts=explode('-',$locale);
 855				$locale=@constant('ISO::LC_'.$parts[0]);
 856				if (isset($parts[1]) &&
 857					$country=@constant('ISO::CC_'.strtolower($parts[1])))
 858					$locale.='-'.$country;
 859			}
 860			$locales[]=$locale;
 861			$locales[]=$locale.'.'.ini_get('default_charset');
 862		}
 863		setlocale(LC_ALL,str_replace('-','_',$locales));
 864		return implode(',',$this->languages);
 865	}
 866
 867	/**
 868	*	Transfer lexicon entries to hive
 869	*	@return array
 870	*	@param $path string
 871	**/
 872	function lexicon($path) {
 873		$lex=array();
 874		foreach ($this->languages?:array($this->fallback) as $lang) {
 875			if ((is_file($file=($base=$path.$lang).'.php') ||
 876				is_file($file=$base.'.php')) &&
 877				is_array($dict=require($file)))
 878				$lex+=$dict;
 879			elseif (is_file($file=$base.'.ini')) {
 880				preg_match_all(
 881					'/(?<=^|\n)(?:'.
 882					'(.+?)\h*=\h*'.
 883					'((?:\\\\\h*\r?\n|.+?)*)'.
 884					')(?=\r?\n|$)/',
 885					$this->read($file),$matches,PREG_SET_ORDER);
 886				if ($matches)
 887					foreach ($matches as $match)
 888						if (isset($match[1]) &&
 889							!array_key_exists($match[1],$lex))
 890							$lex[$match[1]]=trim(preg_replace(
 891								'/(?<!\\\\)"|\\\\\h*\r?\n/','',$match[2]));
 892			}
 893		}
 894		return $lex;
 895	}
 896
 897	/**
 898	*	Return string representation of PHP value
 899	*	@return string
 900	*	@param $arg mixed
 901	**/
 902	function serialize($arg) {
 903		switch (strtolower($this->hive['SERIALIZER'])) {
 904			case 'igbinary':
 905				return igbinary_serialize($arg);
 906			default:
 907				return serialize($arg);
 908		}
 909	}
 910
 911	/**
 912	*	Return PHP value derived from string
 913	*	@return string
 914	*	@param $arg mixed
 915	**/
 916	function unserialize($arg) {
 917		switch (strtolower($this->hive['SERIALIZER'])) {
 918			case 'igbinary':
 919				return igbinary_unserialize($arg);
 920			default:
 921				return unserialize($arg);
 922		}
 923	}
 924
 925	/**
 926	*	Send HTTP/1.1 status header; Return text equivalent of status code
 927	*	@return string
 928	*	@param $code int
 929	**/
 930	function status($code) {
 931		$reason=@constant('self::HTTP_'.$code);
 932		if (PHP_SAPI!='cli')
 933			header('HTTP/1.1 '.$code.' '.$reason);
 934		return $reason;
 935	}
 936
 937	/**
 938	*	Send cache metadata to HTTP client
 939	*	@return NULL
 940	*	@param $secs int
 941	**/
 942	function expire($secs=0) {
 943		if (PHP_SAPI!='cli') {
 944			header('X-Content-Type-Options: nosniff');
 945			header('X-Frame-Options: '.$this->hive['XFRAME']);
 946			header('X-Powered-By: '.$this->hive['PACKAGE']);
 947			header('X-XSS-Protection: 1; mode=block');
 948			if ($secs) {
 949				$time=microtime(TRUE);
 950				header_remove('Pragma');
 951				header('Expires: '.gmdate('r',$time+$secs));
 952				header('Cache-Control: max-age='.$secs);
 953				header('Last-Modified: '.gmdate('r'));
 954				$headers=$this->hive['HEADERS'];
 955				if (isset($headers['If-Modified-Since']) &&
 956					strtotime($headers['If-Modified-Since'])+$secs>$time) {
 957					$this->status(304);
 958					die;
 959				}
 960			}
 961			else
 962				header('Cache-Control: no-cache, no-store, must-revalidate');
 963		}
 964	}
 965
 966	/**
 967	*	Log error; Execute ONERROR handler if defined, else display
 968	*	default error page (HTML for synchronous requests, JSON string
 969	*	for AJAX requests)
 970	*	@return NULL
 971	*	@param $code int
 972	*	@param $text string
 973	*	@param $trace array
 974	**/
 975	function error($code,$text='',array $trace=NULL) {
 976		$prior=$this->hive['ERROR'];
 977		$header=$this->status($code);
 978		$req=$this->hive['VERB'].' '.$this->hive['PATH'];
 979		if (!$text)
 980			$text='HTTP '.$code.' ('.$req.')';
 981		error_log($text);
 982		if (!$trace)
 983			$trace=array_slice(debug_backtrace(FALSE),1);
 984		$debug=$this->hive['DEBUG'];
 985		$trace=array_filter(
 986			$trace,
 987			function($frame) use($debug) {
 988				return $debug && isset($frame['file']) &&
 989					($frame['file']!=__FILE__ || $debug>1) &&
 990					(empty($frame['function']) ||
 991					!preg_match('/^(?:(?:trigger|user)_error|'.
 992						'__call|call_user_func)/',$frame['function']));
 993			}
 994		);
 995		$highlight=PHP_SAPI!='cli' &&
 996			$this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS);
 997		$out='';
 998		$eol="\n";
 999		// Analyze stack trace
1000		foreach ($trace as $frame) {
1001			$line='';
1002			if (isset($frame['class']))
1003				$line.=$frame['class'].$frame['type'];
1004			if (isset($frame['function']))
1005				$line.=$frame['function'].'('.
1006					($debug>2 && isset($frame['args'])?
1007						$this->csv($frame['args']):'').')';
1008			$src=$this->fixslashes(str_replace($_SERVER['DOCUMENT_ROOT'].
1009				'/','',$frame['file'])).':'.$frame['line'].' ';
1010			error_log('- '.$src.$line);
1011			$out.='• '.($highlight?
1012				($this->highlight($src).' '.$this->highlight($line)):
1013				($src.$line)).$eol;
1014		}
1015		$this->hive['ERROR']=array(
1016			'status'=>$header,
1017			'code'=>$code,
1018			'text'=>$text,
1019			'trace'=>$trace
1020		);
1021		$handler=$this->hive['ONERROR'];
1022		$this->hive['ONERROR']=NULL;
1023		if ((!$handler ||
1024			$this->call($handler,$this,'beforeroute,afterroute')===FALSE) &&
1025			!$prior && PHP_SAPI!='cli' && !$this->hive['QUIET'])
1026			echo $this->hive['AJAX']?
1027				json_encode($this->hive['ERROR']):
1028				('<!DOCTYPE html>'.$eol.
1029				'<html>'.$eol.
1030				'<head>'.
1031					'<title>'.$code.' '.$header.'</title>'.
1032					($highlight?
1033						('<style>'.$this->read($css).'</style>'):'').
1034				'</head>'.$eol.
1035				'<body>'.$eol.
1036					'<h1>'.$header.'</h1>'.$eol.
1037					'<p>'.$this->encode($text?:$req).'</p>'.$eol.
1038					($debug?('<pre>'.$out.'</pre>'.$eol):'').
1039				'</body>'.$eol.
1040				'</html>');
1041		if ($this->hive['HALT'])
1042			die;
1043	}
1044
1045	/**
1046	*	Mock HTTP request
1047	*	@return NULL
1048	*	@param $pattern string
1049	*	@param $args array
1050	*	@param $headers array
1051	*	@param $body string
1052	**/
1053	function mock($pattern,array $args=NULL,array $headers=NULL,$body=NULL) {
1054		$types=array('sync','ajax');
1055		preg_match('/([\|\w]+)\h+(?:@(\w+)(?:(\(.+?)\))*|([^\h]+))'.
1056			'(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts);
1057		$verb=strtoupper($parts[1]);
1058		if ($parts[2]) {
1059			if (empty($this->hive['ALIASES'][$parts[2]]))
1060				user_error(sprintf(self::E_Named,$parts[2]));
1061			$parts[4]=$this->hive['ALIASES'][$parts[2]];
1062			if (isset($parts[3]))
1063				$this->parse($parts[3]);
1064			$parts[4]=$this->build($parts[4]);
1065		}
1066		if (empty($parts[4]))
1067			user_error(sprintf(self::E_Pattern,$pattern));
1068		$url=parse_url($parts[4]);
1069		$query='';
1070		if ($args)
1071			$query.=http_build_query($args);
1072		$query.=isset($url['query'])?(($query?'&':'').$url['query']):'';
1073		if ($query && preg_match('/GET|POST/',$verb)) {
1074			parse_str($query,$GLOBALS['_'.$verb]);
1075			parse_str($query,$GLOBALS['_REQUEST']);
1076		}
1077		foreach ($headers?:array() as $key=>$val)
1078			$_SERVER['HTTP_'.strtr(strtoupper($key),'-','_')]=$val;
1079		$this->hive['VERB']=$verb;
1080		$this->hive['URI']=$this->hive['BASE'].$url['path'];
1081		$this->hive['AJAX']=isset($parts[5]) &&
1082			preg_match('/ajax/i',$parts[5]);
1083		if (preg_match('/GET|HEAD/',$verb) && $query)
1084			$this->hive['URI'].='?'.$query;
1085		else
1086			$this->hive['BODY']=$body?:$query;
1087		$this->run();
1088	}
1089
1090	/**
1091	*	Bind handler to route pattern
1092	*	@return NULL
1093	*	@param $pattern string|array
1094	*	@param $handler callback
1095	*	@param $ttl int
1096	*	@param $kbps int
1097	**/
1098	function route($pattern,$handler,$ttl=0,$kbps=0) {
1099		$types=array('sync','ajax');
1100		if (is_array($pattern)) {
1101			foreach ($pattern as $item)
1102				$this->route($item,$handler,$ttl,$kbps);
1103			return;
1104		}
1105		preg_match('/([\|\w]+)\h+(?:(?:@(\w+)\h*:\h*)?([^\h]+)|@(\w+))'.
1106			'(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts);
1107		if ($parts[2])
1108			$this->hive['ALIASES'][$parts[2]]=$parts[3];
1109		elseif (!empty($parts[4])) {
1110			if (empty($this->hive['ALIASES'][$parts[4]]))
1111				user_error(sprintf(self::E_Named,$parts[4]));
1112			$parts[3]=$this->hive['ALIASES'][$parts[4]];
1113		}
1114		if (empty($parts[3]))
1115			user_error(sprintf(self::E_Pattern,$pattern));
1116		$type=empty($parts[5])?
1117			self::REQ_SYNC|self::REQ_AJAX:
1118			constant('self::REQ_'.strtoupper($parts[5]));
1119		foreach ($this->split($parts[1]) as $verb) {
1120			if (!preg_match('/'.self::VERBS.'/',$verb))
1121				$this->error(501,$verb.' '.$this->hive['URI']);
1122			$this->hive['ROUTES'][str_replace('@',"\x00".'@',$parts[3])]
1123				[$type][strtoupper($verb)]=array($handler,$ttl,$kbps);
1124		}
1125	}
1126
1127	/**
1128	*	Reroute to specified URI
1129	*	@return NULL
1130	*	@param $url string
1131	*	@param $permanent bool
1132	**/
1133	function reroute($url,$permanent=FALSE) {
1134		if (PHP_SAPI!='cli') {
1135			if (preg_match('/^(?:@(\w+)(?:(\(.+?)\))*|https?:\/\/)/',
1136				$url,$parts)) {
1137				if (isset($parts[1])) {
1138					if (empty($this->hive['ALIASES'][$parts[1]]))
1139						user_error(sprintf(self::E_Named,$parts[1]));
1140					$url=$this->hive['BASE'].
1141						$this->hive['ALIASES'][$parts[1]];
1142					if (isset($parts[2]))
1143						$this->parse($parts[2]);
1144					$url=$this->build($url);
1145				}
1146			}
1147			else
1148				$url=$this->hive['BASE'].$url;
1149			header('Location: '.$url);
1150			$this->status($permanent?301:302);
1151			die;
1152		}
1153		$this->mock('GET '.$url);
1154	}
1155
1156	/**
1157	*	Provide ReST interface by mapping HTTP verb to class method
1158	*	@return NULL
1159	*	@param $url string
1160	*	@param $class string
1161	*	@param $ttl int
1162	*	@param $kbps int
1163	**/
1164	function map($url,$class,$ttl=0,$kbps=0) {
1165		if (is_array($url)) {
1166			foreach ($url as $item)
1167				$this->map($item,$class,$ttl,$kbps);
1168			return;
1169		}
1170		$fluid=preg_match('/@\w+/',$class);
1171		foreach (explode('|',self::VERBS) as $method)
1172			if ($fluid ||
1173				method_exists($class,$method) ||
1174				method_exists($class,'__call'))
1175				$this->route($method.' '.
1176					$url,$class.'->'.strtolower($method),$ttl,$kbps);
1177	}
1178
1179	/**
1180	*	Return TRUE if IPv4 address exists in DNSBL
1181	*	@return bool
1182	*	@param $ip string
1183	**/
1184	function blacklisted($ip) {
1185		if ($this->hive['DNSBL'] &&
1186			!in_array($ip,
1187				is_array($this->hive['EXEMPT'])?
1188					$this->hive['EXEMPT']:
1189					$this->split($this->hive['EXEMPT']))) {
1190			// Reverse IPv4 dotted quad
1191			$rev=implode('.',array_reverse(explode('.',$ip)));
1192			foreach (is_array($this->hive['DNSBL'])?
1193				$this->hive['DNSBL']:
1194				$this->split($this->hive['DNSBL']) as $server)
1195				// DNSBL lookup
1196				if (checkdnsrr($rev.'.'.$server,'A'))
1197					return TRUE;
1198		}
1199		return FALSE;
1200	}
1201
1202	/**
1203	*	Match routes against incoming URI
1204	*	@return NULL
1205	**/
1206	function run() {
1207		if ($this->blacklisted($this->hive['IP']))
1208			// Spammer detected
1209			$this->error(403);
1210		if (!$this->hive['ROUTES'])
1211			// No routes defined
1212			user_error(self::E_Routes);
1213		// Match specific routes first
1214		krsort($this->hive['ROUTES']);
1215		// Convert to BASE-relative URL
1216		$req=preg_replace(
1217			'/^'.preg_quote($this->hive['BASE'],'/').'(\/.*|$)/','\1',
1218			$this->hive['URI']
1219		);
1220		$allowed=array();
1221		$case=$this->hive['CASELESS']?'i':'';
1222		foreach ($this->hive['ROUTES'] as $url=>$routes) {
1223			$url=str_replace("\x00".'@','@',$url);
1224			if (!preg_match('/^'.
1225				preg_replace('/@(\w+\b)/','(?P<\1>[^\/\?]+)',
1226				str_replace('\*','(.*)',preg_quote($url,'/'))).
1227				'\/?(?:\?.*)?$/'.$case.'um',$req,$args))
1228				continue;
1229			$route=NULL;
1230			if (isset($routes[$this->hive['AJAX']+1]))
1231				$route=$routes[$this->hive['AJAX']+1];
1232			elseif (isset($routes[self::REQ_SYNC|self::REQ_AJAX]))
1233				$route=$routes[self::REQ_SYNC|self::REQ_AJAX];
1234			if (!$route)
1235				continue;
1236			if ($this->hive['VERB']!='OPTIONS' &&
1237				isset($route[$this->hive['VERB']])) {
1238				$parts=parse_url($req);
1239				if ($this->hive['VERB']=='GET' &&
1240					preg_match('/.+\/$/',$parts['path']))
1241					$this->reroute(substr($parts['path'],0,-1).
1242						(isset($parts['query'])?('?'.$parts['query']):''));
1243				list($handler,$ttl,$kbps)=$route[$this->hive['VERB']];
1244				if (is_bool(strpos($url,'/*')))
1245					foreach (array_keys($args) as $key)
1246						if (is_numeric($key) && $key)
1247							unset($args[$key]);
1248				if (is_string($handler)) {
1249					// Replace route pattern tokens in handler if any
1250					$handler=preg_replace_callback('/@(\w+\b)/',
1251						function($id) use($args) {
1252							return isset($args[$id[1]])?$args[$id[1]]:$id[0];
1253						},
1254						$handler
1255					);
1256					if (preg_match('/(.+)\h*(?:->|::)/',$handler,$match) &&
1257						!class_exists($match[1]))
1258						$this->error(404);
1259				}
1260				// Capture values of route pattern tokens
1261				$this->hive['PARAMS']=$args=array_map('urldecode',$args);
1262				// Save matching route
1263				$this->hive['PATTERN']=$url;
1264				// Process request
1265				$body='';
1266				$now=microtime(TRUE);
1267				if (preg_match('/GET|HEAD/',$this->hive['VERB']) &&
1268					isset($ttl)) {
1269					// Only GET and HEAD requests are cacheable
1270					$headers=$this->hive['HEADERS'];
1271					$cache=Cache::instance();
1272					$cached=$cache->exists(
1273						$hash=$this->hash($this->hive['VERB'].' '.
1274							$this->hive['URI']).'.url',$data);
1275					if ($cached && $cached[0]+$ttl>$now) {
1276						// Retrieve from cache backend
1277						list($headers,$body)=$data;
1278						if (PHP_SAPI!='cli')
1279							array_walk($headers,'header');
1280						$this->expire($cached[0]+$ttl-$now);
1281					}
1282					else
1283						// Expire HTTP client-cached page
1284						$this->expire($ttl);
1285				}
1286				else
1287					$this->expire(0);
1288				if (!strlen($body)) {
1289					if (!$this->hive['RAW'])
1290						$this->hive['BODY']=file_get_contents('php://input');
1291					ob_start();
1292					// Call route handler
1293					$this->call($handler,array($this,$args),
1294						'beforeroute,afterroute');
1295					$body=ob_get_clean();
1296					if ($ttl && !error_get_last())
1297						// Save to cache backend
1298						$cache->set($hash,array(headers_list(),$body),$ttl);
1299				}
1300				$this->hive['RESPONSE']=$body;
1301				if (!$this->hive['QUIET']) {
1302					if ($kbps) {
1303						$ctr=0;
1304						foreach (str_split($body,1024) as $part) {
1305							// Throttle output
1306							$ctr++;
1307							if ($ctr/$kbps>($elapsed=microtime(TRUE)-$now) &&
1308								!connection_aborted())
1309								usleep(1e6*($ctr/$kbps-$elapsed));
1310							echo $part;
1311						}
1312					}
1313					else
1314						echo $body;
1315				}
1316				return;
1317			}
1318			$allowed=array_keys($route);
1319			break;
1320		}
1321		if (!$allowed)
1322			// URL doesn't match any route
1323			$this->error(404);
1324		elseif (PHP_SAPI!='cli') {
1325			// Unhandled HTTP method
1326			header('Allow: '.implode(',',$allowed));
1327			if ($this->hive['VERB']!='OPTIONS')
1328				$this->error(405);
1329		}
1330	}
1331
1332	/**
1333	*	Execute callback/hooks (supports 'class->method' format)
1334	*	@return mixed|FALSE
1335	*	@param $func callback
1336	*	@param $args mixed
1337	*	@param $hooks string
1338	**/
1339	function call($func,$args=NULL,$hooks='') {
1340		if (!is_array($args))
1341			$args=array($args);
1342		// Execute function; abort if callback/hook returns FALSE
1343		if (is_string($func) &&
1344			preg_match('/(.+)\h*(->|::)\h*(.+)/s',$func,$parts)) {
1345			// Convert string to executable PHP callback
1346			if (!class_exists($parts[1]))
1347				user_error(sprintf(self::E_Class,
1348					is_string($func)?$parts[1]:$this->stringify()));
1349			if ($parts[2]=='->')
1350				$parts[1]=is_subclass_of($parts[1],'Prefab')?
1351					call_user_func($parts[1].'::instance'):
1352					new $parts[1]($this);
1353			$func=array($parts[1],$parts[3]);
1354		}
1355		if (!is_callable($func))
1356			// No route handler
1357			user_error(sprintf(self::E_Method,
1358				is_string($func)?$func:$this->stringify($func)));
1359		$obj=FALSE;
1360		if (is_array($func)) {
1361			$hooks=$this->split($hooks);
1362			$obj=TRUE;
1363		}
1364		// Execute pre-route hook if any
1365		if ($obj && $hooks && in_array($hook='beforeroute',$hooks) &&
1366			method_exists($func[0],$hook) &&
1367			call_user_func_array(array($func[0],$hook),$args)===FALSE)
1368			return FALSE;
1369		// Execute callback
1370		$out=call_user_func_array($func,$args?:array());
1371		if ($out===FALSE)
1372			return FALSE;
1373		// Execute post-route hook if any
1374		if ($obj && $hooks && in_array($hook='afterroute',$hooks) &&
1375			method_exists($func[0],$hook) &&
1376			call_user_func_array(array($func[0],$hook),$args)===FALSE)
1377			return FALSE;
1378		return $out;
1379	}
1380
1381	/**
1382	*	Execute specified callbacks in succession; Apply same arguments
1383	*	to all callbacks
1384	*	@return array
1385	*	@param $funcs array|string
1386	*	@param $args mixed
1387	**/
1388	function chain($funcs,$args=NULL) {
1389		$out=array();
1390		foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func)
1391			$out[]=$this->call($func,$args);
1392		return $out;
1393	}
1394
1395	/**
1396	*	Execute specified callbacks in succession; Relay result of
1397	*	previous callback as argument to the next callback
1398	*	@return array
1399	*	@param $funcs array|string
1400	*	@param $args mixed
1401	**/
1402	function relay($funcs,$args=NULL) {
1403		foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func)
1404			$args=array($this->call($func,$args));
1405		return array_shift($args);
1406	}
1407
1408	/**
1409	*	Configure framework according to .ini-style file settings
1410	*	@return NULL
1411	*	@param $file string
1412	**/
1413	function config($file) {
1414		preg_match_all(
1415			'/(?<=^|\n)(?:'.
1416				'\[(?<section>.+?)\]|'.
1417				'(?<lval>[^\h\r\n;].+?)\h*=\h*'.
1418				'(?<rval>(?:\\\\\h*\r?\n|.+?)*)'.
1419			')(?=\r?\n|$)/',
1420			$this->read($file),$matches,PREG_SET_ORDER);
1421		if ($matches) {
1422			$sec='globals';
1423			foreach ($matches as $match) {
1424				if ($match['section'])
1425					$sec=$match['section'];
1426				elseif (in_array($sec,array('routes','maps'))) {
1427					call_user_func_array(
1428						array($this,rtrim($sec,'s')),
1429						array_merge(array($match['lval']),
1430							str_getcsv($match['rval'])));
1431				}
1432				else {
1433					$args=array_map(
1434						function($val) {
1435							if (is_numeric($val))
1436								return $val+0;
1437							$val=ltrim($val);
1438							if (preg_match('/^\w+$/i',$val) && defined($val))
1439								return constant($val);
1440							return preg_replace('/\\\\\h*(\r?\n)/','\1',$val);
1441						},
1442						// Mark quoted strings with 0x00 whitespace
1443						str_getcsv(preg_replace('/(?<!\\\\)(")(.*?)\1/',
1444							"\\1\x00\\2\\1",$match['rval']))
1445					);
1446					call_user_func_array(array($this,'set'),
1447						array_merge(
1448							array($match['lval']),
1449							count($args)>1?array($args):$args));
1450				}
1451			}
1452		}
1453	}
1454
1455	/**
1456	*	Create mutex, invoke callback then drop ownership when done
1457	*	@return mixed
1458	*	@param $id string
1459	*	@param $func callback
1460	*	@param $args mixed
1461	**/
1462	function mutex($id,$func,$args=NULL) {
1463		if (!is_dir($tmp=$this->hive['TEMP']))
1464			mkdir($tmp,self::MODE,TRUE);
1465		// Use filesystem lock
1466		if (is_file($lock=$tmp.
1467			$this->hash($this->hive['ROOT'].$this->hive['BASE']).'.'.
1468			$this->hash($id).'.lock') &&
1469			filemtime($lock)+ini_get('max_execution_time')<microtime(TRUE))
1470			// Stale lock
1471			@unlink($lock);
1472		while (!($handle=@fopen($lock,'x')) && !connection_aborted())
1473			usleep(mt_rand(0,100));
1474		$out=$this->call($func,$args);
1475		fclose($handle);
1476		@unlink($lock);
1477		return $out;
1478	}
1479
1480	/**
1481	*	Read file (with option to apply Unix LF as standard line ending)
1482	*	@return string
1483	*	@param $file string
1484	*	@param $lf bool
1485	**/
1486	function read($file,$lf=FALSE) {
1487		$out=file_get_contents($file);
1488		return $lf?preg_replace('/\r\n|\r/',"\n",$out):$out;
1489	}
1490
1491	/**
1492	*	Exclusive file write
1493	*	@return int|FALSE
1494	*	@param $file string
1495	*	@param $data mixed
1496	*	@param $append bool
1497	**/
1498	function write($file,$data,$append=FALSE) {
1499		return file_put_contents($file,$data,LOCK_EX|($append?FILE_APPEND:0));
1500	}
1501
1502	/**
1503	*	Apply syntax highlighting
1504	*	@return string
1505	*	@param $text string
1506	**/
1507	function highlight($text) {
1508		$out='';
1509		$pre=FALSE;
1510		$text=trim($text);
1511		if (!preg_match('/^<\?php/',$text)) {
1512			$text='<?php '.$text;
1513			$pre=TRUE;
1514		}
1515		foreach (token_get_all($text) as $token)
1516			if ($pre)
1517				$pre=FALSE;
1518			else
1519				$out.='<span'.
1520					(is_array($token)?
1521						(' class="'.
1522							substr(strtolower(token_name($token[0])),2).'">'.
1523							$this->encode($token[1]).''):
1524						('>'.$this->encode($token))).
1525					'</span>';
1526		return $out?('<code>'.$out.'</code>'):$text;
1527	}
1528
1529	/**
1530	*	Dump expression with syntax highlighting
1531	*	@return NULL
1532	*	@param $expr mixed
1533	**/
1534	function dump($expr) {
1535		echo $this->highlight($this->stringify($expr));
1536	}
1537
1538	/**
1539	*	Return path relative to the base directory
1540	*	@return string
1541	*	@param $url string
1542	**/
1543	function rel($url) {
1544		return preg_replace('/(?:https?:\/\/)?'.
1545			preg_quote($this->hive['BASE'],'/').'/','',rtrim($url,'/'));
1546	}
1547
1548	/**
1549	*	Namespace-aware class autoloader
1550	*	@return mixed
1551	*	@param $class string
1552	**/
1553	protected function autoload($class) {
1554		$class=$this->fixslashes(ltrim($class,'\\'));
1555		foreach ($this->split($this->hive['PLUGINS'].';'.
1556			$this->hive['AUTOLOAD']) as $auto)
1557			if (is_file($file=$auto.$class.'.php') ||
1558				is_file($file=$auto.strtolower($class).'.php') ||
1559				is_file($file=strtolower($auto.$class).'.php'))
1560				return require($file);
1561	}
1562
1563	/**
1564	*	Execute framework/application shutdown sequence
1565	*	@return NULL
1566	*	@param $cwd string
1567	**/
1568	function unload($cwd) {
1569		chdir($cwd);
1570		if (!$error=error_get_last())
1571			@session_commit();
1572		$handler=$this->hive['UNLOAD'];
1573		if ((!$handler || $this->call($handler,$this)===FALSE) &&
1574			$error && in_array($error['type'],
1575			array(E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR)))
1576			// Fatal error detected
1577			$this->error(sprintf(self::E_Fatal,$error['message']));
1578	}
1579
1580	//! Prohibit cloning
1581	private function __clone() {
1582	}
1583
1584	//! Bootstrap
1585	function __construct() {
1586		// Managed directives
1587		ini_set('default_charset',$charset='UTF-8');
1588		if (extension_loaded('mbstring'))
1589			mb_internal_encoding($charset);
1590		ini_set('display_errors',0);
1591		// Deprecated directives
1592		@ini_set('magic_quotes_gpc',0);
1593		@ini_set('register_globals',0);
1594		// Abort on startup error
1595		// Intercept errors/exceptions; PHP5.3-compatible
1596		error_reporting(E_ALL|E_STRICT);
1597		$fw=$this;
1598		set_exception_handler(
1599			function($obj) use($fw) {
1600				$fw->error(500,$obj->getmessage(),$obj->gettrace());
1601			}
1602		);
1603		set_error_handler(
1604			function($code,$text) use($fw) {
1605				if (error_reporting())
1606					$fw->error(500,$text);
1607			}
1608		);
1609		if (!isset($_SERVER['SERVER_NAME']))
1610			$_SERVER['SERVER_NAME']=gethostname();
1611		if (PHP_SAPI=='cli') {
1612			// Emulate HTTP request
1613			if (isset($_SERVER['argc']) && $_SERVER['argc']<2) {
1614				$_SERVER['argc']++;
1615				$_SERVER['argv'][1]='/';
1616			}
1617			$_SERVER['REQUEST_METHOD']='GET';
1618			$_SERVER['REQUEST_URI']=$_SERVER['argv'][1];
1619		}
1620		$headers=array();
1621		if (PHP_SAPI!='cli')
1622			foreach (array_keys($_SERVER) as $key)
1623				if (substr($key,0,5)=='HTTP_')
1624					$headers[strtr(ucwords(strtolower(strtr(
1625						substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key];
1626		if (isset($headers['X-HTTP-Method-Override']))
1627			$_SERVER['REQUEST_METHOD']=$headers['X-HTTP-Method-Override'];
1628		elseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method']))
1629			$_SERVER['REQUEST_METHOD']=$_POST['_method'];
1630		$scheme=isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' ||
1631			isset($headers['X-Forwarded-Proto']) &&
1632			$headers['X-Forwarded-Proto']=='https'?'https':'http';
1633		if (function_exists('apache_setenv')) {
1634			// Work around Apache pre-2.4 VirtualDocumentRoot bug
1635			$_SERVER['DOCUMENT_ROOT']=str_replace($_SERVER['SCRIPT_NAME'],'',
1636				$_SERVER['SCRIPT_FILENAME']);
1637			apache_setenv("DOCUMENT_ROOT",$_SERVER['DOCUMENT_ROOT']);
1638		}
1639		$_SERVER['DOCUMENT_ROOT']=realpath($_SERVER['DOCUMENT_ROOT']);
1640		$base='';
1641		if (PHP_SAPI!='cli')
1642			$base=rtrim($this->fixslashes(
1643				dirname($_SERVER['SCRIPT_NAME'])),'/');
1644		$path=preg_replace('/^'.preg_quote($base,'/').'/','',
1645			parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH));
1646		call_user_func_array('session_set_cookie_params',
1647			$jar=array(
1648				'expire'=>0,
1649				'path'=>$base?:'/',
1650				'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) &&
1651					!filter_var($_SERVER['SERVER_NAME'],FILTER_VALIDATE_IP)?
1652					$_SERVER['SERVER_NAME']:'',
1653				'secure'=>($scheme=='https'),
1654				'httponly'=>TRUE
1655			)
1656		);
1657		// Default configuration
1658		$this->hive=array(
1659			'AGENT'=>isset($headers['X-Operamini-Phone-UA'])?
1660				$headers['X-Operamini-Phone-UA']:
1661				(isset($headers['X-Skyfire-Phone'])?
1662					$headers['X-Skyfire-Phone']:
1663					(isset($headers['User-Agent'])?
1664						$headers['User-Agent']:'')),
1665			'AJAX'=>isset($headers['X-Requested-With']) &&
1666				$headers['X-Requested-With']=='XMLHttpRequest',
1667			'ALIASES'=>array(),
1668			'AUTOLOAD'=>'./',
1669			'BASE'=>$base,
1670			'BITMASK'=>ENT_COMPAT,
1671			'BODY'=>NULL,
1672			'CACHE'=>FALSE,
1673			'CASELESS'=>TRUE,
1674			'DEBUG'=>0,
1675			'DIACRITICS'=>array(),
1676			'DNSBL'=>'',
1677			'EMOJI'=>array(),
1678			'ENCODING'=>$charset,
1679			'ERROR'=>NULL,
1680			'ESCAPE'=>TRUE,
1681			'EXEMPT'=>NULL,
1682			'FALLBACK'=>$this->fallback,
1683			'HEADERS'=>$headers,
1684			'HALT'=>TRUE,
1685			'HIGHLIGHT'=>TRUE,
1686			'HOST'=>$_SERVER['SERVER_NAME'],
1687			'IP'=>isset($headers['Client-IP'])?
1688				$headers['Client-IP']:
1689				(isset($headers['X-Forwarded-For'])?
1690					$headers['X-Forwarded-For']:
1691					(isset($_SERVER['REMOTE_ADDR'])?
1692						$_SERVER['REMOTE_ADDR']:'')),
1693			'JAR'=>$jar,
1694			'LANGUAGE'=>isset($headers['Accept-Language'])?
1695				$this->language($headers['Accept-Language']):
1696				$this->fallback,
1697			'LOCALES'=>'./',
1698			'LOGS'=>'./',
1699			'ONERROR'=>NULL,
1700			'PACKAGE'=>self::PACKAGE,
1701			'PARAMS'=>array(),
1702			'PATH'=>$path,
1703			'PATTERN'=>NULL,
1704			'PLUGINS'=>$this->fixslashes(__DIR__).'/',
1705			'PORT'=>isset($_SERVER['SERVER_PORT'])?
1706				$_SERVER['SERVER_PORT']:NULL,
1707			'PREFIX'=>NULL,
1708			'QUIET'=>FALSE,
1709			'RAW'=>FALSE,
1710			'REALM'=>$scheme.'://'.
1711				$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'],
1712			'RESPONSE'=>'',
1713			'ROOT'=>$_SERVER['DOCUMENT_ROOT'],
1714			'ROUTES'=>array(),
1715			'SCHEME'=>$scheme,
1716			'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php',
1717			'TEMP'=>'tmp/',
1718			'TIME'=>microtime(TRUE),
1719			'TZ'=>(@ini_get('date.timezone'))?:'UTC',
1720			'UI'=>'./',
1721			'UNLOAD'=>NULL,
1722			'UPLOADS'=>'./',
1723			'URI'=>&$_SERVER['REQUEST_URI'],
1724			'VERB'=>&$_SERVER['REQUEST_METHOD'],
1725			'VERSION'=>self::VERSION,
1726			'XFRAME'=>'SAMEORIGIN'
1727		);
1728		if (PHP_SAPI=='cli-server' &&
1729			preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI']))
1730			$this->reroute('/');
1731		if (ini_get('auto_globals_jit'))
1732			// Override setting
1733			$GLOBALS+=array('_ENV'=>$_ENV,'_REQUEST'=>$_REQUEST);
1734		// Sync PHP globals with corresponding hive keys
1735		$this->init=$this->hive;
1736		foreach (explode('|',self::GLOBALS) as $global) {
1737			$sync=$this->sync($global);
1738			$this->init+=array(
1739				$global=>preg_match('/SERVER|ENV/',$global)?$sync:array()
1740			);
1741		}
1742		if ($error=error_get_last())
1743			// Error detected
1744			$this->error(500,sprintf(self::E_Fatal,$error['message']),
1745				array($error));
1746		date_default_timezone_set($this->hive['TZ']);
1747		// Register framework autoloader
1748		spl_autoload_register(array($this,'autoload'));
1749		// Register shutdown handler
1750		register_shutdown_function(array($this,'unload'),getcwd());
1751	}
1752
1753}
1754
1755//! Cache engine
1756class Cache extends Prefab {
1757
1758	protected
1759		//! Cache DSN
1760		$dsn,
1761		//! Prefix for cache entries
1762		$prefix,
1763		//! MemCache or Redis object
1764		$ref;
1765
1766	/**
1767	*	Return timestamp and TTL of cache entry or FALSE if not found
1768	*	@return array|FALSE
1769	*	@param $key string
1770	*	@param $val mixed
1771	**/
1772	function exists($key,&$val=NULL) {
1773		$fw=Base::instance();
1774		if (!$this->dsn)
1775			return FALSE;
1776		$ndx=$this->prefix.'.'.$key;
1777		$parts=explode('=',$this->dsn,2);
1778		switch ($parts[0]) {
1779			case 'apc':
1780			case 'apcu':
1781				$raw=apc_fetch($ndx);
1782				break;
1783			case 'redis':
1784				$raw=$this->ref->get($ndx);
1785				break;
1786			case 'memcache':
1787				$raw=memcache_get($this->ref,$ndx);
1788				break;
1789			case 'wincache':
1790				$raw=wincache_ucache_get($ndx);
1791				break;
1792			case 'xcache':
1793				$raw=xcache_get($ndx);
1794				break;
1795			case 'folder':
1796				if (is_file($file=$parts[1].$ndx))
1797					$raw=$fw->read($file);
1798				break;
1799		}
1800		if (!empty($raw)) {
1801			list($val,$time,$ttl)=(array)$fw->unserialize($raw);
1802			if ($ttl===0 || $time+$ttl>microtime(TRUE))
1803				return array($time,$ttl);
1804			$this->clear($key);
1805		}
1806		return FALSE;
1807	}
1808
1809	/**
1810	*	Store value in cache
1811	*	@return mixed|FALSE
1812	*	@param $key string
1813	*	@param $val mixed
1814	*	@param $ttl int
1815	**/
1816	function set($key,$val,$ttl=0) {
1817		$fw=Base::instance();
1818		if (!$this->dsn)
1819			return TRUE;
1820		$ndx=$this->prefix.'.'.$key;
1821		$time=microtime(TRUE);
1822		if ($cached=$this->exists($key))
1823			list($time,$ttl)=$cached;
1824		$data=$fw->serialize(array($val,$time,$ttl));
1825		$parts=explode('=',$this->dsn,2);
1826		switch ($parts[0]) {
1827			case 'apc':
1828			case 'apcu':
1829				return apc_store($ndx,$data,$ttl);
1830			case 'redis':
1831				return $this->ref->set($ndx,$data,array('ex'=>$ttl));
1832			case 'memcache':
1833				return memcache_set($this->ref,$ndx,$data,0,$ttl);
1834			case 'wincache':
1835				return wincache_ucache_set($ndx,$data,$ttl);
1836			case 'xcache':
1837				return xcache_set($ndx,$data,$ttl);
1838			case 'folder':
1839				return $fw->write($parts[1].$ndx,$data);
1840		}
1841		return FALSE;
1842	}
1843
1844	/**
1845	*	Retrieve value of cache entry
1846	*	@return mixed|FALSE
1847	*	@param $key string
1848	**/
1849	function get($key) {
1850		return $this->dsn && $this->exists($key,$data)?$data:FALSE;
1851	}
1852
1853	/**
1854	*	Delete cache entry
1855	*	@return bool
1856	*	@param $key string
1857	**/
1858	function clear($key) {
1859		if (!$this->dsn)
1860			return;
1861		$ndx=$this->prefix.'.'.$key;
1862		$parts=explode('=',$this->dsn,2);
1863		switch ($parts[0]) {
1864			case 'apc':
1865			case 'apcu':
1866				return apc_delete($ndx);
1867			case 'redis':
1868				return $this->ref->del($ndx);
1869			case 'memcache':
1870				return memcache_delete($this->ref,$ndx);
1871			case 'wincache':
1872				return wincache_ucache_delete($ndx);
1873			case 'xcache':
1874				return xcache_unset($ndx);
1875			case 'folder':
1876				return is_file($file=$parts[1].$ndx) && @unlink($file);
1877		}
1878		return FALSE;
1879	}
1880
1881	/**
1882	*	Clear contents of cache backend
1883	*	@return bool
1884	*	@param $suffix string
1885	*	@param $lifetime int
1886	**/
1887	function reset($suffix=NULL,$lifetime=0) {
1888		if (!$this->dsn)
1889			return TRUE;
1890		$regex='/'.preg_quote($this->prefix.'.','/').'.+?'.
1891			preg_quote($suffix,'/').'/';
1892		$parts=explode('=',$this->dsn,2);
1893		switch ($parts[0]) {
1894			case 'apc':
1895				$key='info';
1896			case 'apcu':
1897				if (empty($key))
1898					$key='key';
1899				$info=apc_cache_info('user');
1900				foreach ($info['cache_list'] as $item)
1901					if (preg_match($regex,$item[$key]) &&
1902						$item['mtime']+$lifetime<time())
1903						apc_delete($item[$key]);
1904				return TRUE;
1905			case 'redis':
1906				$fw=Base::instance();
1907				$keys=$this->ref->keys($this->prefix.'.*'.$suffix);
1908				foreach($keys as $key) {
1909					$val=$fw->unserialize($this->ref->get($key));
1910					if ($val[1]+$lifetime<time())
1911						$this->ref->del($key);
1912				}
1913				return TRUE;
1914			case 'memcache':
1915				foreach (memcache_get_extended_stats(
1916					$this->ref,'slabs') as $slabs)
1917					foreach (array_filter(array_keys($slabs),'is_numeric')
1918						as $id)
1919						foreach (memcache_get_extended_stats(
1920							$this->ref,'cachedump',$id) as $data)
1921							if (is_array($data))
1922								foreach ($data as $key=>$val)
1923									if (preg_match($regex,$key) &&
1924										$val[1]+$lifetime<time())
1925										memcache_delete($this->

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