PageRenderTime 23ms CodeModel.GetById 4ms app.highlight 65ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/base.php

https://gitlab.com/f3/f3-skeleton
PHP | 2360 lines | 1605 code | 83 blank | 672 comment | 235 complexity | 42f0a5aa4f309174e75d51f6267049c3 MD5 | raw file

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

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

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