PageRenderTime 6ms CodeModel.GetById 5ms app.highlight 64ms RepoModel.GetById 1ms app.codeStats 0ms

/libs/f3/base.php

https://github.com/floviolleau/selfoss
PHP | 2619 lines | 2032 code | 99 blank | 488 comment | 205 complexity | 27cc1f68dd0bf80a18223f724f56fd49 MD5 | raw file

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

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

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