You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							3584 lines
						
					
					
						
							89 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							3584 lines
						
					
					
						
							89 KiB
						
					
					
				| <?php | |
| 
 | |
| /* | |
|  | |
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |
|  | |
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |
|  | |
| 	This is free software: you can redistribute it and/or modify it under the | |
| 	terms of the GNU General Public License as published by the Free Software | |
| 	Foundation, either version 3 of the License, or later. | |
|  | |
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |
| 	General Public License for more details. | |
|  | |
| 	You should have received a copy of the GNU General Public License along | |
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |
|  | |
| */ | |
| 
 | |
| //! Factory class for single-instance objects | |
| abstract class Prefab { | |
| 
 | |
| 	/** | |
| 	*	Return class instance | |
| 	*	@return static | |
| 	**/ | |
| 	static function instance() { | |
| 		if (!Registry::exists($class=get_called_class())) { | |
| 			$ref=new ReflectionClass($class); | |
| 			$args=func_get_args(); | |
| 			Registry::set($class, | |
| 				$args?$ref->newinstanceargs($args):new $class); | |
| 		} | |
| 		return Registry::get($class); | |
| 	} | |
| 
 | |
| } | |
| 
 | |
| //! Base structure | |
| final class Base extends Prefab implements ArrayAccess { | |
| 
 | |
| 	//@{ Framework details | |
| 	const | |
| 		PACKAGE='Fat-Free Framework', | |
| 		VERSION='3.7.2-Release'; | |
| 	//@} | |
|  | |
| 	//@{ HTTP status codes (RFC 2616) | |
| 	const | |
| 		HTTP_100='Continue', | |
| 		HTTP_101='Switching Protocols', | |
| 		HTTP_103='Early Hints', | |
| 		HTTP_200='OK', | |
| 		HTTP_201='Created', | |
| 		HTTP_202='Accepted', | |
| 		HTTP_203='Non-Authorative Information', | |
| 		HTTP_204='No Content', | |
| 		HTTP_205='Reset Content', | |
| 		HTTP_206='Partial Content', | |
| 		HTTP_300='Multiple Choices', | |
| 		HTTP_301='Moved Permanently', | |
| 		HTTP_302='Found', | |
| 		HTTP_303='See Other', | |
| 		HTTP_304='Not Modified', | |
| 		HTTP_305='Use Proxy', | |
| 		HTTP_307='Temporary Redirect', | |
| 		HTTP_308='Permanent Redirect', | |
| 		HTTP_400='Bad Request', | |
| 		HTTP_401='Unauthorized', | |
| 		HTTP_402='Payment Required', | |
| 		HTTP_403='Forbidden', | |
| 		HTTP_404='Not Found', | |
| 		HTTP_405='Method Not Allowed', | |
| 		HTTP_406='Not Acceptable', | |
| 		HTTP_407='Proxy Authentication Required', | |
| 		HTTP_408='Request Timeout', | |
| 		HTTP_409='Conflict', | |
| 		HTTP_410='Gone', | |
| 		HTTP_411='Length Required', | |
| 		HTTP_412='Precondition Failed', | |
| 		HTTP_413='Request Entity Too Large', | |
| 		HTTP_414='Request-URI Too Long', | |
| 		HTTP_415='Unsupported Media Type', | |
| 		HTTP_416='Requested Range Not Satisfiable', | |
| 		HTTP_417='Expectation Failed', | |
| 		HTTP_421='Misdirected Request', | |
| 		HTTP_422='Unprocessable Entity', | |
| 		HTTP_423='Locked', | |
| 		HTTP_429='Too Many Requests', | |
| 		HTTP_451='Unavailable For Legal Reasons', | |
| 		HTTP_500='Internal Server Error', | |
| 		HTTP_501='Not Implemented', | |
| 		HTTP_502='Bad Gateway', | |
| 		HTTP_503='Service Unavailable', | |
| 		HTTP_504='Gateway Timeout', | |
| 		HTTP_505='HTTP Version Not Supported', | |
| 		HTTP_507='Insufficient Storage', | |
| 		HTTP_511='Network Authentication Required'; | |
| 	//@} | |
|  | |
| 	const | |
| 		//! Mapped PHP globals | |
| 		GLOBALS='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV', | |
| 		//! HTTP verbs | |
| 		VERBS='GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS', | |
| 		//! Default directory permissions | |
| 		MODE=0755, | |
| 		//! Syntax highlighting stylesheet | |
| 		CSS='code.css'; | |
| 
 | |
| 	//@{ Request types | |
| 	const | |
| 		REQ_SYNC=1, | |
| 		REQ_AJAX=2, | |
| 		REQ_CLI=4; | |
| 	//@} | |
|  | |
| 	//@{ Error messages | |
| 	const | |
| 		E_Pattern='Invalid routing pattern: %s', | |
| 		E_Named='Named route does not exist: %s', | |
| 		E_Alias='Invalid named route alias: %s', | |
| 		E_Fatal='Fatal error: %s', | |
| 		E_Open='Unable to open %s', | |
| 		E_Routes='No routes specified', | |
| 		E_Class='Invalid class %s', | |
| 		E_Method='Invalid method %s', | |
| 		E_Hive='Invalid hive key %s'; | |
| 	//@} | |
|  | |
| 	private | |
| 		//! Globals | |
| 		$hive, | |
| 		//! Initial settings | |
| 		$init, | |
| 		//! Language lookup sequence | |
| 		$languages, | |
| 		//! Mutex locks | |
| 		$locks=[], | |
| 		//! Default fallback language | |
| 		$fallback='en'; | |
| 
 | |
| 	/** | |
| 	*	Sync PHP global with corresponding hive key | |
| 	*	@return array | |
| 	*	@param $key string | |
| 	**/ | |
| 	function sync($key) { | |
| 		return $this->hive[$key]=&$GLOBALS['_'.$key]; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return the parts of specified hive key | |
| 	*	@return array | |
| 	*	@param $key string | |
| 	**/ | |
| 	private function cut($key) { | |
| 		return preg_split('/\[\h*[\'"]?(.+?)[\'"]?\h*\]|(->)|\./', | |
| 			$key,NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Replace tokenized URL with available token values | |
| 	*	@return string | |
| 	*	@param $url array|string | |
| 	*	@param $args array | |
| 	**/ | |
| 	function build($url,$args=[]) { | |
| 		$args+=$this->hive['PARAMS']; | |
| 		if (is_array($url)) | |
| 			foreach ($url as &$var) { | |
| 				$var=$this->build($var,$args); | |
| 				unset($var); | |
| 			} | |
| 		else { | |
| 			$i=0; | |
| 			$url=preg_replace_callback('/(\{)?@(\w+)(?(1)\})|(\*)/', | |
| 				function($match) use(&$i,$args) { | |
| 					if (isset($match[2]) && | |
| 						array_key_exists($match[2],$args)) | |
| 						return $args[$match[2]]; | |
| 					if (isset($match[3]) && | |
| 						array_key_exists($match[3],$args)) { | |
| 						if (!is_array($args[$match[3]])) | |
| 							return $args[$match[3]]; | |
| 						$i++; | |
| 						return $args[$match[3]][$i-1]; | |
| 					} | |
| 					return $match[0]; | |
| 				},$url); | |
| 		} | |
| 		return $url; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Parse string containing key-value pairs | |
| 	*	@return array | |
| 	*	@param $str string | |
| 	**/ | |
| 	function parse($str) { | |
| 		preg_match_all('/(\w+|\*)\h*=\h*(?:\[(.+?)\]|(.+?))(?=,|$)/', | |
| 			$str,$pairs,PREG_SET_ORDER); | |
| 		$out=[]; | |
| 		foreach ($pairs as $pair) | |
| 			if ($pair[2]) { | |
| 				$out[$pair[1]]=[]; | |
| 				foreach (explode(',',$pair[2]) as $val) | |
| 					array_push($out[$pair[1]],$val); | |
| 			} | |
| 			else | |
| 				$out[$pair[1]]=trim($pair[3]); | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	 * Cast string variable to PHP type or constant | |
| 	 * @param $val | |
| 	 * @return mixed | |
| 	 */ | |
| 	function cast($val) { | |
| 		if (preg_match('/^(?:0x[0-9a-f]+|0[0-7]+|0b[01]+)$/i',$val)) | |
| 			return intval($val,0); | |
| 		if (is_numeric($val)) | |
| 			return $val+0; | |
| 		$val=trim($val); | |
| 		if (preg_match('/^\w+$/i',$val) && defined($val)) | |
| 			return constant($val); | |
| 		return $val; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convert JS-style token to PHP expression | |
| 	*	@return string | |
| 	*	@param $str string | |
| 	*	@param $evaluate bool compile expressions as well or only convert variable access | |
| 	**/ | |
| 	function compile($str, $evaluate=TRUE) { | |
| 		return (!$evaluate) | |
| 			? preg_replace_callback( | |
| 				'/^@(\w+)((?:\..+|\[(?:(?:[^\[\]]*|(?R))*)\])*)/', | |
| 				function($expr) { | |
| 					$str='$'.$expr[1]; | |
| 					if (isset($expr[2])) | |
| 						$str.=preg_replace_callback( | |
| 							'/\.([^.\[\]]+)|\[((?:[^\[\]\'"]*|(?R))*)\]/', | |
| 							function($sub) { | |
| 								$val=isset($sub[2]) ? $sub[2] : $sub[1]; | |
| 								if (ctype_digit($val)) | |
| 									$val=(int)$val; | |
| 								$out='['.$this->export($val).']'; | |
| 								return $out; | |
| 							}, | |
| 							$expr[2] | |
| 						); | |
| 					return $str; | |
| 				}, | |
| 				$str | |
| 			) | |
| 			: preg_replace_callback( | |
| 			'/(?<!\w)@(\w+(?:(?:\->|::)\w+)?)'. | |
| 			'((?:\.\w+|\[(?:(?:[^\[\]]*|(?R))*)\]|(?:\->|::)\w+|\()*)/', | |
| 			function($expr) { | |
| 				$str='$'.$expr[1]; | |
| 				if (isset($expr[2])) | |
| 					$str.=preg_replace_callback( | |
| 						'/\.(\w+)(\()?|\[((?:[^\[\]]*|(?R))*)\]/', | |
| 						function($sub) { | |
| 							if (empty($sub[2])) { | |
| 								if (ctype_digit($sub[1])) | |
| 									$sub[1]=(int)$sub[1]; | |
| 								$out='['. | |
| 									(isset($sub[3])? | |
| 										$this->compile($sub[3]): | |
| 										$this->export($sub[1])). | |
| 								']'; | |
| 							} | |
| 							else | |
| 								$out=function_exists($sub[1])? | |
| 									$sub[0]: | |
| 									('['.$this->export($sub[1]).']'.$sub[2]); | |
| 							return $out; | |
| 						}, | |
| 						$expr[2] | |
| 					); | |
| 				return $str; | |
| 			}, | |
| 			$str | |
| 		); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Get hive key reference/contents; Add non-existent hive keys, | |
| 	*	array elements, and object properties by default | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	*	@param $add bool | |
| 	*	@param $var mixed | |
| 	**/ | |
| 	function &ref($key,$add=TRUE,&$var=NULL) { | |
| 		$null=NULL; | |
| 		$parts=$this->cut($key); | |
| 		if ($parts[0]=='SESSION') { | |
| 			if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) | |
| 				session_start(); | |
| 			$this->sync('SESSION'); | |
| 		} | |
| 		elseif (!preg_match('/^\w+$/',$parts[0])) | |
| 			user_error(sprintf(self::E_Hive,$this->stringify($key)), | |
| 				E_USER_ERROR); | |
| 		if (is_null($var)) { | |
| 			if ($add) | |
| 				$var=&$this->hive; | |
| 			else | |
| 				$var=$this->hive; | |
| 		} | |
| 		$obj=FALSE; | |
| 		foreach ($parts as $part) | |
| 			if ($part=='->') | |
| 				$obj=TRUE; | |
| 			elseif ($obj) { | |
| 				$obj=FALSE; | |
| 				if (!is_object($var)) | |
| 					$var=new stdClass; | |
| 				if ($add || property_exists($var,$part)) | |
| 					$var=&$var->$part; | |
| 				else { | |
| 					$var=&$null; | |
| 					break; | |
| 				} | |
| 			} | |
| 			else { | |
| 				if (!is_array($var)) | |
| 					$var=[]; | |
| 				if ($add || array_key_exists($part,$var)) | |
| 					$var=&$var[$part]; | |
| 				else { | |
| 					$var=&$null; | |
| 					break; | |
| 				} | |
| 			} | |
| 		return $var; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return TRUE if hive key is set | |
| 	*	(or return timestamp and TTL if cached) | |
| 	*	@return bool | |
| 	*	@param $key string | |
| 	*	@param $val mixed | |
| 	**/ | |
| 	function exists($key,&$val=NULL) { | |
| 		$val=$this->ref($key,FALSE); | |
| 		return isset($val)? | |
| 			TRUE: | |
| 			(Cache::instance()->exists($this->hash($key).'.var',$val)?:FALSE); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return TRUE if hive key is empty and not cached | |
| 	*	@param $key string | |
| 	*	@param $val mixed | |
| 	*	@return bool | |
| 	**/ | |
| 	function devoid($key,&$val=NULL) { | |
| 		$val=$this->ref($key,FALSE); | |
| 		return empty($val) && | |
| 			(!Cache::instance()->exists($this->hash($key).'.var',$val) || | |
| 				!$val); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Bind value to hive key | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	*	@param $val mixed | |
| 	*	@param $ttl int | |
| 	**/ | |
| 	function set($key,$val,$ttl=0) { | |
| 		$time=(int)$this->hive['TIME']; | |
| 		if (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) { | |
| 			$this->set('REQUEST'.$expr[2],$val); | |
| 			if ($expr[1]=='COOKIE') { | |
| 				$parts=$this->cut($key); | |
| 				$jar=$this->unserialize($this->serialize($this->hive['JAR'])); | |
| 				unset($jar['lifetime']); | |
| 				if (version_compare(PHP_VERSION, '7.3.0') >= 0) { | |
| 					unset($jar['expire']); | |
| 					if (isset($_COOKIE[$parts[1]])) | |
| 						setcookie($parts[1],NULL,['expires'=>0]+$jar); | |
| 					if ($ttl) | |
| 						$jar['expires']=$time+$ttl; | |
| 					setcookie($parts[1],$val,$jar); | |
| 				} else { | |
| 					unset($jar['samesite']); | |
| 					if (isset($_COOKIE[$parts[1]])) | |
| 						call_user_func_array('setcookie', | |
| 							array_merge([$parts[1],NULL],['expire'=>0]+$jar)); | |
| 					if ($ttl) | |
| 						$jar['expire']=$time+$ttl; | |
| 					call_user_func_array('setcookie',[$parts[1],$val]+$jar); | |
| 				} | |
| 				$_COOKIE[$parts[1]]=$val; | |
| 				return $val; | |
| 			} | |
| 		} | |
| 		else switch ($key) { | |
| 		case 'CACHE': | |
| 			$val=Cache::instance()->load($val); | |
| 			break; | |
| 		case 'ENCODING': | |
| 			ini_set('default_charset',$val); | |
| 			if (extension_loaded('mbstring')) | |
| 				mb_internal_encoding($val); | |
| 			break; | |
| 		case 'FALLBACK': | |
| 			$this->fallback=$val; | |
| 			$lang=$this->language($this->hive['LANGUAGE']); | |
| 		case 'LANGUAGE': | |
| 			if (!isset($lang)) | |
| 				$val=$this->language($val); | |
| 			$lex=$this->lexicon($this->hive['LOCALES'],$ttl); | |
| 		case 'LOCALES': | |
| 			if (isset($lex) || $lex=$this->lexicon($val,$ttl)) | |
| 				foreach ($lex as $dt=>$dd) { | |
| 					$ref=&$this->ref($this->hive['PREFIX'].$dt); | |
| 					$ref=$dd; | |
| 					unset($ref); | |
| 				} | |
| 			break; | |
| 		case 'TZ': | |
| 			date_default_timezone_set($val); | |
| 			break; | |
| 		} | |
| 		$ref=&$this->ref($key); | |
| 		$ref=$val; | |
| 		if (preg_match('/^JAR\b/',$key)) { | |
| 			if ($key=='JAR.lifetime') | |
| 				$this->set('JAR.expire',$val==0?0: | |
| 					(is_int($val)?$time+$val:strtotime($val))); | |
| 			else { | |
| 				if ($key=='JAR.expire') | |
| 					$this->hive['JAR']['lifetime']=max(0,$val-$time); | |
| 				$jar=$this->unserialize($this->serialize($this->hive['JAR'])); | |
| 				unset($jar['expire']); | |
| 				if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) | |
| 					if (version_compare(PHP_VERSION, '7.3.0') >= 0) | |
| 						session_set_cookie_params($jar); | |
| 					else { | |
| 						unset($jar['samesite']); | |
| 						call_user_func_array('session_set_cookie_params',$jar); | |
| 					} | |
| 			} | |
| 		} | |
| 		if ($ttl) | |
| 			// Persist the key-value pair | |
| 			Cache::instance()->set($this->hash($key).'.var',$val,$ttl); | |
| 		return $ref; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Retrieve contents of hive key | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	*	@param $args string|array | |
| 	**/ | |
| 	function get($key,$args=NULL) { | |
| 		if (is_string($val=$this->ref($key,FALSE)) && !is_null($args)) | |
| 			return call_user_func_array( | |
| 				[$this,'format'], | |
| 				array_merge([$val],is_array($args)?$args:[$args]) | |
| 			); | |
| 		if (is_null($val)) { | |
| 			// Attempt to retrieve from cache | |
| 			if (Cache::instance()->exists($this->hash($key).'.var',$data)) | |
| 				return $data; | |
| 		} | |
| 		return $val; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Unset hive key | |
| 	*	@param $key string | |
| 	**/ | |
| 	function clear($key) { | |
| 		// Normalize array literal | |
| 		$cache=Cache::instance(); | |
| 		$parts=$this->cut($key); | |
| 		if ($key=='CACHE') | |
| 			// Clear cache contents | |
| 			$cache->reset(); | |
| 		elseif (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) { | |
| 			$this->clear('REQUEST'.$expr[2]); | |
| 			if ($expr[1]=='COOKIE') { | |
| 				$parts=$this->cut($key); | |
| 				$jar=$this->hive['JAR']; | |
| 				unset($jar['lifetime']); | |
| 				$jar['expire']=0; | |
| 				if (version_compare(PHP_VERSION, '7.3.0') >= 0) { | |
| 					$jar['expires']=$jar['expire']; | |
| 					unset($jar['expire']); | |
| 					setcookie($parts[1],NULL,$jar); | |
| 				} else { | |
| 					unset($jar['samesite']); | |
| 					call_user_func_array('setcookie', | |
| 						array_merge([$parts[1],NULL],$jar)); | |
| 				} | |
| 				unset($_COOKIE[$parts[1]]); | |
| 			} | |
| 		} | |
| 		elseif ($parts[0]=='SESSION') { | |
| 			if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) | |
| 				session_start(); | |
| 			if (empty($parts[1])) { | |
| 				// End session | |
| 				session_unset(); | |
| 				session_destroy(); | |
| 				$this->clear('COOKIE.'.session_name()); | |
| 			} | |
| 			$this->sync('SESSION'); | |
| 		} | |
| 		if (!isset($parts[1]) && array_key_exists($parts[0],$this->init)) | |
| 			// Reset global to default value | |
| 			$this->hive[$parts[0]]=$this->init[$parts[0]]; | |
| 		else { | |
| 			$val=preg_replace('/^(\$hive)/','$this->hive', | |
| 				$this->compile('@hive.'.$key, FALSE)); | |
| 			eval('unset('.$val.');'); | |
| 			if ($parts[0]=='SESSION') { | |
| 				session_commit(); | |
| 				session_start(); | |
| 			} | |
| 			if ($cache->exists($hash=$this->hash($key).'.var')) | |
| 				// Remove from cache | |
| 				$cache->clear($hash); | |
| 		} | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return TRUE if hive variable is 'on' | |
| 	*	@return bool | |
| 	*	@param $key string | |
| 	**/ | |
| 	function checked($key) { | |
| 		$ref=&$this->ref($key); | |
| 		return $ref=='on'; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return TRUE if property has public visibility | |
| 	*	@return bool | |
| 	*	@param $obj object | |
| 	*	@param $key string | |
| 	**/ | |
| 	function visible($obj,$key) { | |
| 		if (property_exists($obj,$key)) { | |
| 			$ref=new ReflectionProperty(get_class($obj),$key); | |
| 			$out=$ref->ispublic(); | |
| 			unset($ref); | |
| 			return $out; | |
| 		} | |
| 		return FALSE; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Multi-variable assignment using associative array | |
| 	*	@param $vars array | |
| 	*	@param $prefix string | |
| 	*	@param $ttl int | |
| 	**/ | |
| 	function mset(array $vars,$prefix='',$ttl=0) { | |
| 		foreach ($vars as $key=>$val) | |
| 			$this->set($prefix.$key,$val,$ttl); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Publish hive contents | |
| 	*	@return array | |
| 	**/ | |
| 	function hive() { | |
| 		return $this->hive; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Copy contents of hive variable to another | |
| 	*	@return mixed | |
| 	*	@param $src string | |
| 	*	@param $dst string | |
| 	**/ | |
| 	function copy($src,$dst) { | |
| 		$ref=&$this->ref($dst); | |
| 		return $ref=$this->ref($src,FALSE); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Concatenate string to hive string variable | |
| 	*	@return string | |
| 	*	@param $key string | |
| 	*	@param $val string | |
| 	**/ | |
| 	function concat($key,$val) { | |
| 		$ref=&$this->ref($key); | |
| 		$ref.=$val; | |
| 		return $ref; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Swap keys and values of hive array variable | |
| 	*	@return array | |
| 	*	@param $key string | |
| 	*	@public | |
| 	**/ | |
| 	function flip($key) { | |
| 		$ref=&$this->ref($key); | |
| 		return $ref=array_combine(array_values($ref),array_keys($ref)); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Add element to the end of hive array variable | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	*	@param $val mixed | |
| 	**/ | |
| 	function push($key,$val) { | |
| 		$ref=&$this->ref($key); | |
| 		$ref[]=$val; | |
| 		return $val; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Remove last element of hive array variable | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	**/ | |
| 	function pop($key) { | |
| 		$ref=&$this->ref($key); | |
| 		return array_pop($ref); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Add element to the beginning of hive array variable | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	*	@param $val mixed | |
| 	**/ | |
| 	function unshift($key,$val) { | |
| 		$ref=&$this->ref($key); | |
| 		array_unshift($ref,$val); | |
| 		return $val; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Remove first element of hive array variable | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	**/ | |
| 	function shift($key) { | |
| 		$ref=&$this->ref($key); | |
| 		return array_shift($ref); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Merge array with hive array variable | |
| 	*	@return array | |
| 	*	@param $key string | |
| 	*	@param $src string|array | |
| 	*	@param $keep bool | |
| 	**/ | |
| 	function merge($key,$src,$keep=FALSE) { | |
| 		$ref=&$this->ref($key); | |
| 		if (!$ref) | |
| 			$ref=[]; | |
| 		$out=array_merge($ref,is_string($src)?$this->hive[$src]:$src); | |
| 		if ($keep) | |
| 			$ref=$out; | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Extend hive array variable with default values from $src | |
| 	*	@return array | |
| 	*	@param $key string | |
| 	*	@param $src string|array | |
| 	*	@param $keep bool | |
| 	**/ | |
| 	function extend($key,$src,$keep=FALSE) { | |
| 		$ref=&$this->ref($key); | |
| 		if (!$ref) | |
| 			$ref=[]; | |
| 		$out=array_replace_recursive( | |
| 			is_string($src)?$this->hive[$src]:$src,$ref); | |
| 		if ($keep) | |
| 			$ref=$out; | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convert backslashes to slashes | |
| 	*	@return string | |
| 	*	@param $str string | |
| 	**/ | |
| 	function fixslashes($str) { | |
| 		return $str?strtr($str,'\\','/'):$str; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Split comma-, semi-colon, or pipe-separated string | |
| 	*	@return array | |
| 	*	@param $str string | |
| 	*	@param $noempty bool | |
| 	**/ | |
| 	function split($str,$noempty=TRUE) { | |
| 		return array_map('trim', | |
| 			preg_split('/[,;|]/',$str,0,$noempty?PREG_SPLIT_NO_EMPTY:0)); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convert PHP expression/value to compressed exportable string | |
| 	*	@return string | |
| 	*	@param $arg mixed | |
| 	*	@param $stack array | |
| 	**/ | |
| 	function stringify($arg,array $stack=NULL) { | |
| 		if ($stack) { | |
| 			foreach ($stack as $node) | |
| 				if ($arg===$node) | |
| 					return '*RECURSION*'; | |
| 		} | |
| 		else | |
| 			$stack=[]; | |
| 		switch (gettype($arg)) { | |
| 			case 'object': | |
| 				$str=''; | |
| 				foreach (get_object_vars($arg) as $key=>$val) | |
| 					$str.=($str?',':''). | |
| 						$this->export($key).'=>'. | |
| 						$this->stringify($val, | |
| 							array_merge($stack,[$arg])); | |
| 				return get_class($arg).'::__set_state(['.$str.'])'; | |
| 			case 'array': | |
| 				$str=''; | |
| 				$num=isset($arg[0]) && | |
| 					ctype_digit(implode('',array_keys($arg))); | |
| 				foreach ($arg as $key=>$val) | |
| 					$str.=($str?',':''). | |
| 						($num?'':($this->export($key).'=>')). | |
| 						$this->stringify($val,array_merge($stack,[$arg])); | |
| 				return '['.$str.']'; | |
| 			default: | |
| 				return $this->export($arg); | |
| 		} | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Flatten array values and return as CSV string | |
| 	*	@return string | |
| 	*	@param $args array | |
| 	**/ | |
| 	function csv(array $args) { | |
| 		return implode(',',array_map('stripcslashes', | |
| 			array_map([$this,'stringify'],$args))); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convert snakecase string to camelcase | |
| 	*	@return string | |
| 	*	@param $str string | |
| 	**/ | |
| 	function camelcase($str) { | |
| 		return preg_replace_callback( | |
| 			'/_(\pL)/u', | |
| 			function($match) { | |
| 				return strtoupper($match[1]); | |
| 			}, | |
| 			$str | |
| 		); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convert camelcase string to snakecase | |
| 	*	@return string | |
| 	*	@param $str string | |
| 	**/ | |
| 	function snakecase($str) { | |
| 		return strtolower(preg_replace('/(?!^)\p{Lu}/u','_\0',$str)); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return -1 if specified number is negative, 0 if zero, | |
| 	*	or 1 if the number is positive | |
| 	*	@return int | |
| 	*	@param $num mixed | |
| 	**/ | |
| 	function sign($num) { | |
| 		return $num?($num/abs($num)):0; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Extract values of array whose keys start with the given prefix | |
| 	*	@return array | |
| 	*	@param $arr array | |
| 	*	@param $prefix string | |
| 	**/ | |
| 	function extract($arr,$prefix) { | |
| 		$out=[]; | |
| 		foreach (preg_grep('/^'.preg_quote($prefix,'/').'/',array_keys($arr)) | |
| 			as $key) | |
| 			$out[substr($key,strlen($prefix))]=$arr[$key]; | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convert class constants to array | |
| 	*	@return array | |
| 	*	@param $class object|string | |
| 	*	@param $prefix string | |
| 	**/ | |
| 	function constants($class,$prefix='') { | |
| 		$ref=new ReflectionClass($class); | |
| 		return $this->extract($ref->getconstants(),$prefix); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Generate 64bit/base36 hash | |
| 	*	@return string | |
| 	*	@param $str | |
| 	**/ | |
| 	function hash($str) { | |
| 		return str_pad(base_convert( | |
| 			substr(sha1($str),-16),16,36),11,'0',STR_PAD_LEFT); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return Base64-encoded equivalent | |
| 	*	@return string | |
| 	*	@param $data string | |
| 	*	@param $mime string | |
| 	**/ | |
| 	function base64($data,$mime) { | |
| 		return 'data:'.$mime.';base64,'.base64_encode($data); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convert special characters to HTML entities | |
| 	*	@return string | |
| 	*	@param $str string | |
| 	**/ | |
| 	function encode($str) { | |
| 		return @htmlspecialchars($str,$this->hive['BITMASK'], | |
| 			$this->hive['ENCODING'])?:$this->scrub($str); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convert HTML entities back to characters | |
| 	*	@return string | |
| 	*	@param $str string | |
| 	**/ | |
| 	function decode($str) { | |
| 		return htmlspecialchars_decode($str,$this->hive['BITMASK']); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Invoke callback recursively for all data types | |
| 	*	@return mixed | |
| 	*	@param $arg mixed | |
| 	*	@param $func callback | |
| 	*	@param $stack array | |
| 	**/ | |
| 	function recursive($arg,$func,$stack=[]) { | |
| 		if ($stack) { | |
| 			foreach ($stack as $node) | |
| 				if ($arg===$node) | |
| 					return $arg; | |
| 		} | |
| 		switch (gettype($arg)) { | |
| 			case 'object': | |
| 				$ref=new ReflectionClass($arg); | |
| 				if ($ref->iscloneable()) { | |
| 					$arg=clone($arg); | |
| 					$cast=is_a($arg,'IteratorAggregate')? | |
| 						iterator_to_array($arg):get_object_vars($arg); | |
| 					foreach ($cast as $key=>$val) | |
| 						$arg->$key=$this->recursive( | |
| 							$val,$func,array_merge($stack,[$arg])); | |
| 				} | |
| 				return $arg; | |
| 			case 'array': | |
| 				$copy=[]; | |
| 				foreach ($arg as $key=>$val) | |
| 					$copy[$key]=$this->recursive($val,$func, | |
| 						array_merge($stack,[$arg])); | |
| 				return $copy; | |
| 		} | |
| 		return $func($arg); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Remove HTML tags (except those enumerated) and non-printable | |
| 	*	characters to mitigate XSS/code injection attacks | |
| 	*	@return mixed | |
| 	*	@param $arg mixed | |
| 	*	@param $tags string | |
| 	**/ | |
| 	function clean($arg,$tags=NULL) { | |
| 		return $this->recursive($arg, | |
| 			function($val) use($tags) { | |
| 				if ($tags!='*') | |
| 					$val=trim(strip_tags($val, | |
| 						'<'.implode('><',$this->split($tags)).'>')); | |
| 				return trim(preg_replace( | |
| 					'/[\x00-\x08\x0B\x0C\x0E-\x1F]/','',$val)); | |
| 			} | |
| 		); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Similar to clean(), except that variable is passed by reference | |
| 	*	@return mixed | |
| 	*	@param $var mixed | |
| 	*	@param $tags string | |
| 	**/ | |
| 	function scrub(&$var,$tags=NULL) { | |
| 		return $var=$this->clean($var,$tags); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return locale-aware formatted string | |
| 	*	@return string | |
| 	**/ | |
| 	function format() { | |
| 		$args=func_get_args(); | |
| 		$val=array_shift($args); | |
| 		// Get formatting rules | |
| 		$conv=localeconv(); | |
| 		return preg_replace_callback( | |
| 			'/\{\s*(?P<pos>\d+)\s*(?:,\s*(?P<type>\w+)\s*'. | |
| 			'(?:,\s*(?P<mod>(?:\w+(?:\s*\{.+?\}\s*,?\s*)?)*)'. | |
| 			'(?:,\s*(?P<prop>.+?))?)?)?\s*\}/', | |
| 			function($expr) use($args,$conv) { | |
| 				/** | |
| 				 * @var string $pos | |
| 				 * @var string $mod | |
| 				 * @var string $type | |
| 				 * @var string $prop | |
| 				 */ | |
| 				extract($expr); | |
| 				/** | |
| 				 * @var string $thousands_sep | |
| 				 * @var string $negative_sign | |
| 				 * @var string $positive_sign | |
| 				 * @var string $frac_digits | |
| 				 * @var string $decimal_point | |
| 				 * @var string $int_curr_symbol | |
| 				 * @var string $currency_symbol | |
| 				 */ | |
| 				extract($conv); | |
| 				if (!array_key_exists($pos,$args)) | |
| 					return $expr[0]; | |
| 				if (isset($type)) { | |
| 					if (isset($this->hive['FORMATS'][$type])) | |
| 						return $this->call( | |
| 							$this->hive['FORMATS'][$type], | |
| 							[ | |
| 								$args[$pos], | |
| 								isset($mod)?$mod:null, | |
| 								isset($prop)?$prop:null | |
| 							] | |
| 						); | |
| 					switch ($type) { | |
| 						case 'plural': | |
| 							preg_match_all('/(?<tag>\w+)'. | |
| 								'(?:\s*\{\s*(?<data>.+?)\s*\})/', | |
| 								$mod,$matches,PREG_SET_ORDER); | |
| 							$ord=['zero','one','two']; | |
| 							foreach ($matches as $match) { | |
| 								/** @var string $tag */ | |
| 								/** @var string $data */ | |
| 								extract($match); | |
| 								if (isset($ord[$args[$pos]]) && | |
| 									$tag==$ord[$args[$pos]] || $tag=='other') | |
| 									return str_replace('#',$args[$pos],$data); | |
| 							} | |
| 						case 'number': | |
| 							if (isset($mod)) | |
| 								switch ($mod) { | |
| 									case 'integer': | |
| 										return number_format( | |
| 											$args[$pos],0,'',$thousands_sep); | |
| 									case 'currency': | |
| 										$int=$cstm=FALSE; | |
| 										if (isset($prop) && | |
| 											$cstm=!$int=($prop=='int')) | |
| 											$currency_symbol=$prop; | |
| 										if (!$cstm && | |
| 											function_exists('money_format') && | |
| 											version_compare(PHP_VERSION,'7.4.0')<0) | |
| 											return money_format( | |
| 												'%'.($int?'i':'n'),$args[$pos]); | |
| 										$fmt=[ | |
| 											0=>'(nc)',1=>'(n c)', | |
| 											2=>'(nc)',10=>'+nc', | |
| 											11=>'+n c',12=>'+ nc', | |
| 											20=>'nc+',21=>'n c+', | |
| 											22=>'nc +',30=>'n+c', | |
| 											31=>'n +c',32=>'n+ c', | |
| 											40=>'nc+',41=>'n c+', | |
| 											42=>'nc +',100=>'(cn)', | |
| 											101=>'(c n)',102=>'(cn)', | |
| 											110=>'+cn',111=>'+c n', | |
| 											112=>'+ cn',120=>'cn+', | |
| 											121=>'c n+',122=>'cn +', | |
| 											130=>'+cn',131=>'+c n', | |
| 											132=>'+ cn',140=>'c+n', | |
| 											141=>'c+ n',142=>'c +n' | |
| 										]; | |
| 										if ($args[$pos]<0) { | |
| 											$sgn=$negative_sign; | |
| 											$pre='n'; | |
| 										} | |
| 										else { | |
| 											$sgn=$positive_sign; | |
| 											$pre='p'; | |
| 										} | |
| 										return str_replace( | |
| 											['+','n','c'], | |
| 											[$sgn,number_format( | |
| 												abs($args[$pos]), | |
| 												$frac_digits, | |
| 												$decimal_point, | |
| 												$thousands_sep), | |
| 												$int?$int_curr_symbol | |
| 													:$currency_symbol], | |
| 											$fmt[(int)( | |
| 												(${$pre.'_cs_precedes'}%2). | |
| 												(${$pre.'_sign_posn'}%5). | |
| 												(${$pre.'_sep_by_space'}%3) | |
| 											)] | |
| 										); | |
| 									case 'percent': | |
| 										return number_format( | |
| 											$args[$pos]*100,0,$decimal_point, | |
| 											$thousands_sep).'%'; | |
| 								} | |
| 							$frac=$args[$pos]-(int)$args[$pos]; | |
| 							return number_format( | |
| 								$args[$pos], | |
| 								isset($prop)? | |
| 									$prop: | |
| 									($frac?strlen($frac)-2:0), | |
| 								$decimal_point,$thousands_sep); | |
| 						case 'date': | |
| 							if (empty($mod) || $mod=='short') | |
| 								$prop='%x'; | |
| 							elseif ($mod=='full') | |
| 								$prop='%A, %d %B %Y'; | |
| 							elseif ($mod!='custom') | |
| 								$prop='%d %B %Y'; | |
| 							return strftime($prop,$args[$pos]); | |
| 						case 'time': | |
| 							if (empty($mod) || $mod=='short') | |
| 								$prop='%X'; | |
| 							elseif ($mod!='custom') | |
| 								$prop='%r'; | |
| 							return strftime($prop,$args[$pos]); | |
| 						default: | |
| 							return $expr[0]; | |
| 					} | |
| 				} | |
| 				return $args[$pos]; | |
| 			}, | |
| 			$val | |
| 		); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return string representation of expression | |
| 	*	@return string | |
| 	*	@param $expr mixed | |
| 	**/ | |
| 	function export($expr) { | |
| 		return var_export($expr,TRUE); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Assign/auto-detect language | |
| 	*	@return string | |
| 	*	@param $code string | |
| 	**/ | |
| 	function language($code) { | |
| 		$code=preg_replace('/\h+|;q=[0-9.]+/','',$code); | |
| 		$code.=($code?',':'').$this->fallback; | |
| 		$this->languages=[]; | |
| 		foreach (array_reverse(explode(',',$code)) as $lang) | |
| 			if (preg_match('/^(\w{2})(?:-(\w{2}))?\b/i',$lang,$parts)) { | |
| 				// Generic language | |
| 				array_unshift($this->languages,$parts[1]); | |
| 				if (isset($parts[2])) { | |
| 					// Specific language | |
| 					$parts[0]=$parts[1].'-'.($parts[2]=strtoupper($parts[2])); | |
| 					array_unshift($this->languages,$parts[0]); | |
| 				} | |
| 			} | |
| 		$this->languages=array_unique($this->languages); | |
| 		$locales=[]; | |
| 		$windows=preg_match('/^win/i',PHP_OS); | |
| 		// Work around PHP's Turkish locale bug | |
| 		foreach (preg_grep('/^(?!tr)/i',$this->languages) as $locale) { | |
| 			if ($windows) { | |
| 				$parts=explode('-',$locale); | |
| 				$locale=@constant('ISO::LC_'.$parts[0]); | |
| 				if (isset($parts[1]) && | |
| 					$country=@constant('ISO::CC_'.strtolower($parts[1]))) | |
| 					$locale.='-'.$country; | |
| 			} | |
| 			$locale=str_replace('-','_',$locale); | |
| 			$locales[]=$locale.'.'.ini_get('default_charset'); | |
| 			$locales[]=$locale; | |
| 		} | |
| 		setlocale(LC_ALL,$locales); | |
| 		return $this->hive['LANGUAGE']=implode(',',$this->languages); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return lexicon entries | |
| 	*	@return array | |
| 	*	@param $path string | |
| 	*	@param $ttl int | |
| 	**/ | |
| 	function lexicon($path,$ttl=0) { | |
| 		$languages=$this->languages?:explode(',',$this->fallback); | |
| 		$cache=Cache::instance(); | |
| 		if ($ttl && $cache->exists( | |
| 			$hash=$this->hash(implode(',',$languages).$path).'.dic',$lex)) | |
| 			return $lex; | |
| 		$lex=[]; | |
| 		foreach ($languages as $lang) | |
| 			foreach ($this->split($path) as $dir) | |
| 				if ((is_file($file=($base=$dir.$lang).'.php') || | |
| 					is_file($file=$base.'.php')) && | |
| 					is_array($dict=require($file))) | |
| 					$lex+=$dict; | |
| 				elseif (is_file($file=$base.'.json') && | |
| 					is_array($dict=json_decode(file_get_contents($file), true))) | |
| 					$lex+=$dict; | |
| 				elseif (is_file($file=$base.'.ini')) { | |
| 					preg_match_all( | |
| 						'/(?<=^|\n)(?:'. | |
| 							'\[(?<prefix>.+?)\]|'. | |
| 							'(?<lval>[^\h\r\n;].*?)\h*=\h*'. | |
| 							'(?<rval>(?:\\\\\h*\r?\n|.+?)*)'. | |
| 						')(?=\r?\n|$)/', | |
| 						$this->read($file),$matches,PREG_SET_ORDER); | |
| 					if ($matches) { | |
| 						$prefix=''; | |
| 						foreach ($matches as $match) | |
| 							if ($match['prefix']) | |
| 								$prefix=$match['prefix'].'.'; | |
| 							elseif (!array_key_exists( | |
| 								$key=$prefix.$match['lval'],$lex)) | |
| 								$lex[$key]=trim(preg_replace( | |
| 									'/\\\\\h*\r?\n/',"\n",$match['rval'])); | |
| 					} | |
| 				} | |
| 		if ($ttl) | |
| 			$cache->set($hash,$lex,$ttl); | |
| 		return $lex; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return string representation of PHP value | |
| 	*	@return string | |
| 	*	@param $arg mixed | |
| 	**/ | |
| 	function serialize($arg) { | |
| 		switch (strtolower($this->hive['SERIALIZER'])) { | |
| 			case 'igbinary': | |
| 				return igbinary_serialize($arg); | |
| 			default: | |
| 				return serialize($arg); | |
| 		} | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return PHP value derived from string | |
| 	*	@return string | |
| 	*	@param $arg mixed | |
| 	**/ | |
| 	function unserialize($arg) { | |
| 		switch (strtolower($this->hive['SERIALIZER'])) { | |
| 			case 'igbinary': | |
| 				return igbinary_unserialize($arg); | |
| 			default: | |
| 				return unserialize($arg); | |
| 		} | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Send HTTP status header; Return text equivalent of status code | |
| 	*	@return string | |
| 	*	@param $code int | |
| 	**/ | |
| 	function status($code) { | |
| 		$reason=@constant('self::HTTP_'.$code); | |
| 		if (!$this->hive['CLI'] && !headers_sent()) | |
| 			header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason); | |
| 		return $reason; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Send cache metadata to HTTP client | |
| 	*	@param $secs int | |
| 	**/ | |
| 	function expire($secs=0) { | |
| 		if (!$this->hive['CLI'] && !headers_sent()) { | |
| 			$secs=(int)$secs; | |
| 			if ($this->hive['PACKAGE']) | |
| 				header('X-Powered-By: '.$this->hive['PACKAGE']); | |
| 			if ($this->hive['XFRAME']) | |
| 				header('X-Frame-Options: '.$this->hive['XFRAME']); | |
| 			header('X-XSS-Protection: 1; mode=block'); | |
| 			header('X-Content-Type-Options: nosniff'); | |
| 			if ($this->hive['VERB']=='GET' && $secs) { | |
| 				$time=microtime(TRUE); | |
| 				header_remove('Pragma'); | |
| 				header('Cache-Control: max-age='.$secs); | |
| 				header('Expires: '.gmdate('r',$time+$secs)); | |
| 				header('Last-Modified: '.gmdate('r')); | |
| 			} | |
| 			else { | |
| 				header('Pragma: no-cache'); | |
| 				header('Cache-Control: no-cache, no-store, must-revalidate'); | |
| 				header('Expires: '.gmdate('r',0)); | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return HTTP user agent | |
| 	*	@return string | |
| 	**/ | |
| 	function agent() { | |
| 		$headers=$this->hive['HEADERS']; | |
| 		return isset($headers['X-Operamini-Phone-UA'])? | |
| 			$headers['X-Operamini-Phone-UA']: | |
| 			(isset($headers['X-Skyfire-Phone'])? | |
| 				$headers['X-Skyfire-Phone']: | |
| 				(isset($headers['User-Agent'])? | |
| 					$headers['User-Agent']:'')); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return TRUE if XMLHttpRequest detected | |
| 	*	@return bool | |
| 	**/ | |
| 	function ajax() { | |
| 		$headers=$this->hive['HEADERS']; | |
| 		return isset($headers['X-Requested-With']) && | |
| 			$headers['X-Requested-With']=='XMLHttpRequest'; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Sniff IP address | |
| 	*	@return string | |
| 	**/ | |
| 	function ip() { | |
| 		$headers=$this->hive['HEADERS']; | |
| 		return isset($headers['Client-IP'])? | |
| 			$headers['Client-IP']: | |
| 			(isset($headers['X-Forwarded-For'])? | |
| 				explode(',',$headers['X-Forwarded-For'])[0]: | |
| 				(isset($_SERVER['REMOTE_ADDR'])? | |
| 					$_SERVER['REMOTE_ADDR']:'')); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return filtered stack trace as a formatted string (or array) | |
| 	*	@return string|array | |
| 	*	@param $trace array|NULL | |
| 	*	@param $format bool | |
| 	**/ | |
| 	function trace(array $trace=NULL,$format=TRUE) { | |
| 		if (!$trace) { | |
| 			$trace=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); | |
| 			$frame=$trace[0]; | |
| 			if (isset($frame['file']) && $frame['file']==__FILE__) | |
| 				array_shift($trace); | |
| 		} | |
| 		$debug=$this->hive['DEBUG']; | |
| 		$trace=array_filter( | |
| 			$trace, | |
| 			function($frame) use($debug) { | |
| 				return isset($frame['file']) && | |
| 					($debug>1 || | |
| 					($frame['file']!=__FILE__ || $debug) && | |
| 					(empty($frame['function']) || | |
| 					!preg_match('/^(?:(?:trigger|user)_error|'. | |
| 						'__call|call_user_func)/',$frame['function']))); | |
| 			} | |
| 		); | |
| 		if (!$format) | |
| 			return $trace; | |
| 		$out=''; | |
| 		$eol="\n"; | |
| 		// Analyze stack trace | |
| 		foreach ($trace as $frame) { | |
| 			$line=''; | |
| 			if (isset($frame['class'])) | |
| 				$line.=$frame['class'].$frame['type']; | |
| 			if (isset($frame['function'])) | |
| 				$line.=$frame['function'].'('. | |
| 					($debug>2 && isset($frame['args'])? | |
| 						$this->csv($frame['args']):'').')'; | |
| 			$src=$this->fixslashes(str_replace($_SERVER['DOCUMENT_ROOT']. | |
| 				'/','',$frame['file'])).':'.$frame['line']; | |
| 			$out.='['.$src.'] '.$line.$eol; | |
| 		} | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Log error; Execute ONERROR handler if defined, else display | |
| 	*	default error page (HTML for synchronous requests, JSON string | |
| 	*	for AJAX requests) | |
| 	*	@param $code int | |
| 	*	@param $text string | |
| 	*	@param $trace array | |
| 	*	@param $level int | |
| 	**/ | |
| 	function error($code,$text='',array $trace=NULL,$level=0) { | |
| 		$prior=$this->hive['ERROR']; | |
| 		$header=$this->status($code); | |
| 		$req=$this->hive['VERB'].' '.$this->hive['PATH']; | |
| 		if ($this->hive['QUERY']) | |
| 			$req.='?'.$this->hive['QUERY']; | |
| 		if (!$text) | |
| 			$text='HTTP '.$code.' ('.$req.')'; | |
| 		$trace=$this->trace($trace); | |
| 		$loggable=$this->hive['LOGGABLE']; | |
| 		if (!is_array($loggable)) | |
| 			$loggable=$this->split($loggable); | |
| 		foreach ($loggable as $status) | |
| 			if ($status=='*' || | |
| 				preg_match('/^'.preg_replace('/\D/','\d',$status).'$/',$code)) { | |
| 				error_log($text); | |
| 				foreach (explode("\n",$trace) as $nexus) | |
| 					if ($nexus) | |
| 						error_log($nexus); | |
| 				break; | |
| 			} | |
| 		if ($highlight=!$this->hive['CLI'] && !$this->hive['AJAX'] && | |
| 			$this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS)) | |
| 			$trace=$this->highlight($trace); | |
| 		$this->hive['ERROR']=[ | |
| 			'status'=>$header, | |
| 			'code'=>$code, | |
| 			'text'=>$text, | |
| 			'trace'=>$trace, | |
| 			'level'=>$level | |
| 		]; | |
| 		$this->expire(-1); | |
| 		$handler=$this->hive['ONERROR']; | |
| 		$this->hive['ONERROR']=NULL; | |
| 		$eol="\n"; | |
| 		if ((!$handler || | |
| 			$this->call($handler,[$this,$this->hive['PARAMS']], | |
| 				'beforeroute,afterroute')===FALSE) && | |
| 			!$prior && !$this->hive['CLI'] && !$this->hive['QUIET']) | |
| 			echo $this->hive['AJAX']? | |
| 				json_encode( | |
| 					array_diff_key( | |
| 						$this->hive['ERROR'], | |
| 						$this->hive['DEBUG']? | |
| 							[]: | |
| 							['trace'=>1] | |
| 					) | |
| 				): | |
| 				('<!DOCTYPE html>'.$eol. | |
| 				'<html>'.$eol. | |
| 				'<head>'. | |
| 					'<title>'.$code.' '.$header.'</title>'. | |
| 					($highlight? | |
| 						('<style>'.$this->read($css).'</style>'):''). | |
| 				'</head>'.$eol. | |
| 				'<body>'.$eol. | |
| 					'<h1>'.$header.'</h1>'.$eol. | |
| 					'<p>'.$this->encode($text?:$req).'</p>'.$eol. | |
| 					($this->hive['DEBUG']?('<pre>'.$trace.'</pre>'.$eol):''). | |
| 				'</body>'.$eol. | |
| 				'</html>'); | |
| 		if ($this->hive['HALT']) | |
| 			die(1); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Mock HTTP request | |
| 	*	@return mixed | |
| 	*	@param $pattern string | |
| 	*	@param $args array | |
| 	*	@param $headers array | |
| 	*	@param $body string | |
| 	**/ | |
| 	function mock($pattern, | |
| 		array $args=NULL,array $headers=NULL,$body=NULL) { | |
| 		if (!$args) | |
| 			$args=[]; | |
| 		$types=['sync','ajax','cli']; | |
| 		preg_match('/([\|\w]+)\h+(?:@(\w+)(?:(\(.+?)\))*|([^\h]+))'. | |
| 			'(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts); | |
| 		$verb=strtoupper($parts[1]); | |
| 		if ($parts[2]) { | |
| 			if (empty($this->hive['ALIASES'][$parts[2]])) | |
| 				user_error(sprintf(self::E_Named,$parts[2]),E_USER_ERROR); | |
| 			$parts[4]=$this->hive['ALIASES'][$parts[2]]; | |
| 			$parts[4]=$this->build($parts[4], | |
| 				isset($parts[3])?$this->parse($parts[3]):[]); | |
| 		} | |
| 		if (empty($parts[4])) | |
| 			user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR); | |
| 		$url=parse_url($parts[4]); | |
| 		parse_str(@$url['query'],$GLOBALS['_GET']); | |
| 		if (preg_match('/GET|HEAD/',$verb)) | |
| 			$GLOBALS['_GET']=array_merge($GLOBALS['_GET'],$args); | |
| 		$GLOBALS['_POST']=$verb=='POST'?$args:[]; | |
| 		$GLOBALS['_REQUEST']=array_merge($GLOBALS['_GET'],$GLOBALS['_POST']); | |
| 		foreach ($headers?:[] as $key=>$val) | |
| 			$_SERVER['HTTP_'.strtr(strtoupper($key),'-','_')]=$val; | |
| 		$this->hive['VERB']=$verb; | |
| 		$this->hive['PATH']=$url['path']; | |
| 		$this->hive['URI']=$this->hive['BASE'].$url['path']; | |
| 		if ($GLOBALS['_GET']) | |
| 			$this->hive['URI'].='?'.http_build_query($GLOBALS['_GET']); | |
| 		$this->hive['BODY']=''; | |
| 		if (!preg_match('/GET|HEAD/',$verb)) | |
| 			$this->hive['BODY']=$body?:http_build_query($args); | |
| 		$this->hive['AJAX']=isset($parts[5]) && | |
| 			preg_match('/ajax/i',$parts[5]); | |
| 		$this->hive['CLI']=isset($parts[5]) && | |
| 			preg_match('/cli/i',$parts[5]); | |
| 		return $this->run(); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Assemble url from alias name | |
| 	*	@return string | |
| 	*	@param $name string | |
| 	*	@param $params array|string | |
| 	*	@param $query string|array | |
| 	*	@param $fragment string | |
| 	**/ | |
| 	function alias($name,$params=[],$query=NULL,$fragment=NULL) { | |
| 		if (!is_array($params)) | |
| 			$params=$this->parse($params); | |
| 		if (empty($this->hive['ALIASES'][$name])) | |
| 			user_error(sprintf(self::E_Named,$name),E_USER_ERROR); | |
| 		$url=$this->build($this->hive['ALIASES'][$name],$params); | |
| 		if (is_array($query)) | |
| 			$query=http_build_query($query); | |
| 		return $url.($query?('?'.$query):'').($fragment?'#'.$fragment:''); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Bind handler to route pattern | |
| 	*	@return NULL | |
| 	*	@param $pattern string|array | |
| 	*	@param $handler callback | |
| 	*	@param $ttl int | |
| 	*	@param $kbps int | |
| 	**/ | |
| 	function route($pattern,$handler,$ttl=0,$kbps=0) { | |
| 		$types=['sync','ajax','cli']; | |
| 		$alias=null; | |
| 		if (is_array($pattern)) { | |
| 			foreach ($pattern as $item) | |
| 				$this->route($item,$handler,$ttl,$kbps); | |
| 			return; | |
| 		} | |
| 		preg_match('/([\|\w]+)\h+(?:(?:@?(.+?)\h*:\h*)?(@(\w+)|[^\h]+))'. | |
| 			'(?:\h+\[('.implode('|',$types).')\])?/u',$pattern,$parts); | |
| 		if (isset($parts[2]) && $parts[2]) { | |
| 			if (!preg_match('/^\w+$/',$parts[2])) | |
| 				user_error(sprintf(self::E_Alias,$parts[2]),E_USER_ERROR); | |
| 			$this->hive['ALIASES'][$alias=$parts[2]]=$parts[3]; | |
| 		} | |
| 		elseif (!empty($parts[4])) { | |
| 			if (empty($this->hive['ALIASES'][$parts[4]])) | |
| 				user_error(sprintf(self::E_Named,$parts[4]),E_USER_ERROR); | |
| 			$parts[3]=$this->hive['ALIASES'][$alias=$parts[4]]; | |
| 		} | |
| 		if (empty($parts[3])) | |
| 			user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR); | |
| 		$type=empty($parts[5])?0:constant('self::REQ_'.strtoupper($parts[5])); | |
| 		foreach ($this->split($parts[1]) as $verb) { | |
| 			if (!preg_match('/'.self::VERBS.'/',$verb)) | |
| 				$this->error(501,$verb.' '.$this->hive['URI']); | |
| 			$this->hive['ROUTES'][$parts[3]][$type][strtoupper($verb)]= | |
| 				[$handler,$ttl,$kbps,$alias]; | |
| 		} | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Reroute to specified URI | |
| 	*	@return NULL | |
| 	*	@param $url array|string | |
| 	*	@param $permanent bool | |
| 	*	@param $die bool | |
| 	**/ | |
| 	function reroute($url=NULL,$permanent=FALSE,$die=TRUE) { | |
| 		if (!$url) | |
| 			$url=$this->hive['REALM']; | |
| 		if (is_array($url)) | |
| 			$url=call_user_func_array([$this,'alias'],$url); | |
| 		elseif (preg_match('/^(?:@([^\/()?#]+)(?:\((.+?)\))*(\?[^#]+)*(#.+)*)/', | |
| 			$url,$parts) && isset($this->hive['ALIASES'][$parts[1]])) | |
| 			$url=$this->build($this->hive['ALIASES'][$parts[1]], | |
| 					isset($parts[2])?$this->parse($parts[2]):[]). | |
| 				(isset($parts[3])?$parts[3]:'').(isset($parts[4])?$parts[4]:''); | |
| 		else | |
| 			$url=$this->build($url); | |
| 		if (($handler=$this->hive['ONREROUTE']) && | |
| 			$this->call($handler,[$url,$permanent,$die])!==FALSE) | |
| 			return; | |
| 		if ($url[0]!='/' && !preg_match('/^\w+:\/\//i',$url)) | |
| 			$url='/'.$url; | |
| 		if ($url[0]=='/' && (empty($url[1]) || $url[1]!='/')) { | |
| 			$port=$this->hive['PORT']; | |
| 			$port=in_array($port,[80,443])?'':(':'.$port); | |
| 			$url=$this->hive['SCHEME'].'://'. | |
| 				$this->hive['HOST'].$port.$this->hive['BASE'].$url; | |
| 		} | |
| 		if ($this->hive['CLI']) | |
| 			$this->mock('GET '.$url.' [cli]'); | |
| 		else { | |
| 			header('Location: '.$url); | |
| 			$this->status($permanent?301:302); | |
| 			if ($die) | |
| 				die; | |
| 		} | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Provide ReST interface by mapping HTTP verb to class method | |
| 	*	@return NULL | |
| 	*	@param $url string | |
| 	*	@param $class string|object | |
| 	*	@param $ttl int | |
| 	*	@param $kbps int | |
| 	**/ | |
| 	function map($url,$class,$ttl=0,$kbps=0) { | |
| 		if (is_array($url)) { | |
| 			foreach ($url as $item) | |
| 				$this->map($item,$class,$ttl,$kbps); | |
| 			return; | |
| 		} | |
| 		foreach (explode('|',self::VERBS) as $method) | |
| 			$this->route($method.' '.$url,is_string($class)? | |
| 				$class.'->'.$this->hive['PREMAP'].strtolower($method): | |
| 				[$class,$this->hive['PREMAP'].strtolower($method)], | |
| 				$ttl,$kbps); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Redirect a route to another URL | |
| 	*	@return NULL | |
| 	*	@param $pattern string|array | |
| 	*	@param $url string | |
| 	*	@param $permanent bool | |
| 	*/ | |
| 	function redirect($pattern,$url,$permanent=TRUE) { | |
| 		if (is_array($pattern)) { | |
| 			foreach ($pattern as $item) | |
| 				$this->redirect($item,$url,$permanent); | |
| 			return; | |
| 		} | |
| 		$this->route($pattern,function($fw) use($url,$permanent) { | |
| 			$fw->reroute($url,$permanent); | |
| 		}); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return TRUE if IPv4 address exists in DNSBL | |
| 	*	@return bool | |
| 	*	@param $ip string | |
| 	**/ | |
| 	function blacklisted($ip) { | |
| 		if ($this->hive['DNSBL'] && | |
| 			!in_array($ip, | |
| 				is_array($this->hive['EXEMPT'])? | |
| 					$this->hive['EXEMPT']: | |
| 					$this->split($this->hive['EXEMPT']))) { | |
| 			// Reverse IPv4 dotted quad | |
| 			$rev=implode('.',array_reverse(explode('.',$ip))); | |
| 			foreach (is_array($this->hive['DNSBL'])? | |
| 				$this->hive['DNSBL']: | |
| 				$this->split($this->hive['DNSBL']) as $server) | |
| 				// DNSBL lookup | |
| 				if (checkdnsrr($rev.'.'.$server,'A')) | |
| 					return TRUE; | |
| 		} | |
| 		return FALSE; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Applies the specified URL mask and returns parameterized matches | |
| 	*	@return $args array | |
| 	*	@param $pattern string | |
| 	*	@param $url string|NULL | |
| 	**/ | |
| 	function mask($pattern,$url=NULL) { | |
| 		if (!$url) | |
| 			$url=$this->rel($this->hive['URI']); | |
| 		$case=$this->hive['CASELESS']?'i':''; | |
| 		$wild=preg_quote($pattern,'/'); | |
| 		$i=0; | |
| 		while (is_int($pos=strpos($wild,'\*'))) { | |
| 			$wild=substr_replace($wild,'(?P<_'.$i.'>[^\?]*)',$pos,2); | |
| 			$i++; | |
| 		} | |
| 		preg_match('/^'. | |
| 			preg_replace( | |
| 				'/((\\\{)?@(\w+\b)(?(2)\\\}))/', | |
| 				'(?P<\3>[^\/\?]+)', | |
| 				$wild).'\/?$/'.$case.'um',$url,$args); | |
| 		foreach (array_keys($args) as $key) { | |
| 			if (preg_match('/^_\d+$/',$key)) { | |
| 				if (empty($args['*'])) | |
| 					$args['*']=$args[$key]; | |
| 				else { | |
| 					if (is_string($args['*'])) | |
| 						$args['*']=[$args['*']]; | |
| 					array_push($args['*'],$args[$key]); | |
| 				} | |
| 				unset($args[$key]); | |
| 			} | |
| 			elseif (is_numeric($key) && $key) | |
| 				unset($args[$key]); | |
| 		} | |
| 		return $args; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Match routes against incoming URI | |
| 	*	@return mixed | |
| 	**/ | |
| 	function run() { | |
| 		if ($this->blacklisted($this->hive['IP'])) | |
| 			// Spammer detected | |
| 			$this->error(403); | |
| 		if (!$this->hive['ROUTES']) | |
| 			// No routes defined | |
| 			user_error(self::E_Routes,E_USER_ERROR); | |
| 		// Match specific routes first | |
| 		$paths=[]; | |
| 		foreach ($keys=array_keys($this->hive['ROUTES']) as $key) { | |
| 			$path=preg_replace('/@\w+/','*@',$key); | |
| 			if (substr($path,-1)!='*') | |
| 				$path.='+'; | |
| 			$paths[]=$path; | |
| 		} | |
| 		$vals=array_values($this->hive['ROUTES']); | |
| 		array_multisort($paths,SORT_DESC,$keys,$vals); | |
| 		$this->hive['ROUTES']=array_combine($keys,$vals); | |
| 		// Convert to BASE-relative URL | |
| 		$req=urldecode($this->hive['PATH']); | |
| 		$preflight=FALSE; | |
| 		if ($cors=(isset($this->hive['HEADERS']['Origin']) && | |
| 			$this->hive['CORS']['origin'])) { | |
| 			$cors=$this->hive['CORS']; | |
| 			header('Access-Control-Allow-Origin: '.$cors['origin']); | |
| 			header('Access-Control-Allow-Credentials: '. | |
| 				$this->export($cors['credentials'])); | |
| 			$preflight= | |
| 				isset($this->hive['HEADERS']['Access-Control-Request-Method']); | |
| 		} | |
| 		$allowed=[]; | |
| 		foreach ($this->hive['ROUTES'] as $pattern=>$routes) { | |
| 			if (!$args=$this->mask($pattern,$req)) | |
| 				continue; | |
| 			ksort($args); | |
| 			$route=NULL; | |
| 			$ptr=$this->hive['CLI']?self::REQ_CLI:$this->hive['AJAX']+1; | |
| 			if (isset($routes[$ptr][$this->hive['VERB']]) || | |
| 				isset($routes[$ptr=0])) | |
| 				$route=$routes[$ptr]; | |
| 			if (!$route) | |
| 				continue; | |
| 			if (isset($route[$this->hive['VERB']]) && !$preflight) { | |
| 				if ($this->hive['VERB']=='GET' && | |
| 					preg_match('/.+\/$/',$this->hive['PATH'])) | |
| 					$this->reroute(substr($this->hive['PATH'],0,-1). | |
| 						($this->hive['QUERY']?('?'.$this->hive['QUERY']):'')); | |
| 				list($handler,$ttl,$kbps,$alias)=$route[$this->hive['VERB']]; | |
| 				// Capture values of route pattern tokens | |
| 				$this->hive['PARAMS']=$args; | |
| 				// Save matching route | |
| 				$this->hive['ALIAS']=$alias; | |
| 				$this->hive['PATTERN']=$pattern; | |
| 				if ($cors && $cors['expose']) | |
| 					header('Access-Control-Expose-Headers: '. | |
| 						(is_array($cors['expose'])? | |
| 							implode(',',$cors['expose']):$cors['expose'])); | |
| 				if (is_string($handler)) { | |
| 					// Replace route pattern tokens in handler if any | |
| 					$handler=preg_replace_callback('/({)?@(\w+\b)(?(1)})/', | |
| 						function($id) use($args) { | |
| 							$pid=count($id)>2?2:1; | |
| 							return isset($args[$id[$pid]])? | |
| 								$args[$id[$pid]]: | |
| 								$id[0]; | |
| 						}, | |
| 						$handler | |
| 					); | |
| 					if (preg_match('/(.+)\h*(?:->|::)/',$handler,$match) && | |
| 						!class_exists($match[1])) | |
| 						$this->error(404); | |
| 				} | |
| 				// Process request | |
| 				$result=NULL; | |
| 				$body=''; | |
| 				$now=microtime(TRUE); | |
| 				if (preg_match('/GET|HEAD/',$this->hive['VERB']) && $ttl) { | |
| 					// Only GET and HEAD requests are cacheable | |
| 					$headers=$this->hive['HEADERS']; | |
| 					$cache=Cache::instance(); | |
| 					$cached=$cache->exists( | |
| 						$hash=$this->hash($this->hive['VERB'].' '. | |
| 							$this->hive['URI']).'.url',$data); | |
| 					if ($cached) { | |
| 						if (isset($headers['If-Modified-Since']) && | |
| 							strtotime($headers['If-Modified-Since'])+ | |
| 								$ttl>$now) { | |
| 							$this->status(304); | |
| 							die; | |
| 						} | |
| 						// Retrieve from cache backend | |
| 						list($headers,$body,$result)=$data; | |
| 						if (!$this->hive['CLI']) | |
| 							array_walk($headers,'header'); | |
| 						$this->expire($cached[0]+$ttl-$now); | |
| 					} | |
| 					else | |
| 						// Expire HTTP client-cached page | |
| 						$this->expire($ttl); | |
| 				} | |
| 				else | |
| 					$this->expire(0); | |
| 				if (!strlen($body)) { | |
| 					if (!$this->hive['RAW'] && !$this->hive['BODY']) | |
| 						$this->hive['BODY']=file_get_contents('php://input'); | |
| 					ob_start(); | |
| 					// Call route handler | |
| 					$result=$this->call($handler,[$this,$args,$handler], | |
| 						'beforeroute,afterroute'); | |
| 					$body=ob_get_clean(); | |
| 					if (isset($cache) && !error_get_last()) { | |
| 						// Save to cache backend | |
| 						$cache->set($hash,[ | |
| 							// Remove cookies | |
| 							preg_grep('/Set-Cookie\:/',headers_list(), | |
| 								PREG_GREP_INVERT),$body,$result],$ttl); | |
| 					} | |
| 				} | |
| 				$this->hive['RESPONSE']=$body; | |
| 				if (!$this->hive['QUIET']) { | |
| 					if ($kbps) { | |
| 						$ctr=0; | |
| 						foreach (str_split($body,1024) as $part) { | |
| 							// Throttle output | |
| 							$ctr++; | |
| 							if ($ctr/$kbps>($elapsed=microtime(TRUE)-$now) && | |
| 								!connection_aborted()) | |
| 								usleep(1e6*($ctr/$kbps-$elapsed)); | |
| 							echo $part; | |
| 						} | |
| 					} | |
| 					else | |
| 						echo $body; | |
| 				} | |
| 				if ($result || $this->hive['VERB']!='OPTIONS') | |
| 					return $result; | |
| 			} | |
| 			$allowed=array_merge($allowed,array_keys($route)); | |
| 		} | |
| 		if (!$allowed) | |
| 			// URL doesn't match any route | |
| 			$this->error(404); | |
| 		elseif (!$this->hive['CLI']) { | |
| 			if (!preg_grep('/Allow:/',$headers_send=headers_list())) | |
| 				// Unhandled HTTP method | |
| 				header('Allow: '.implode(',',array_unique($allowed))); | |
| 			if ($cors) { | |
| 				if (!preg_grep('/Access-Control-Allow-Methods:/',$headers_send)) | |
| 					header('Access-Control-Allow-Methods: OPTIONS,'. | |
| 						implode(',',$allowed)); | |
| 				if ($cors['headers'] && | |
| 					!preg_grep('/Access-Control-Allow-Headers:/',$headers_send)) | |
| 					header('Access-Control-Allow-Headers: '. | |
| 						(is_array($cors['headers'])? | |
| 							implode(',',$cors['headers']): | |
| 							$cors['headers'])); | |
| 				if ($cors['ttl']>0) | |
| 					header('Access-Control-Max-Age: '.$cors['ttl']); | |
| 			} | |
| 			if ($this->hive['VERB']!='OPTIONS') | |
| 				$this->error(405); | |
| 		} | |
| 		return FALSE; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Loop until callback returns TRUE (for long polling) | |
| 	*	@return mixed | |
| 	*	@param $func callback | |
| 	*	@param $args array | |
| 	*	@param $timeout int | |
| 	**/ | |
| 	function until($func,$args=NULL,$timeout=60) { | |
| 		if (!$args) | |
| 			$args=[]; | |
| 		$time=time(); | |
| 		$max=ini_get('max_execution_time'); | |
| 		$limit=max(0,($max?min($timeout,$max):$timeout)-1); | |
| 		$out=''; | |
| 		// Turn output buffering on | |
| 		ob_start(); | |
| 		// Not for the weak of heart | |
| 		while ( | |
| 			// No error occurred | |
| 			!$this->hive['ERROR'] && | |
| 			// Got time left? | |
| 			time()-$time+1<$limit && | |
| 			// Still alive? | |
| 			!connection_aborted() && | |
| 			// Restart session | |
| 			!headers_sent() && | |
| 			(session_status()==PHP_SESSION_ACTIVE || session_start()) && | |
| 			// CAUTION: Callback will kill host if it never becomes truthy! | |
| 			!$out=$this->call($func,$args)) { | |
| 			if (!$this->hive['CLI']) | |
| 				session_commit(); | |
| 			// Hush down | |
| 			sleep(1); | |
| 		} | |
| 		ob_flush(); | |
| 		flush(); | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Disconnect HTTP client; | |
| 	*	Set FcgidOutputBufferSize to zero if server uses mod_fcgid; | |
| 	*	Disable mod_deflate when rendering text/html output | |
| 	**/ | |
| 	function abort() { | |
| 		if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) | |
| 			session_start(); | |
| 		$out=''; | |
| 		while (ob_get_level()) | |
| 			$out=ob_get_clean().$out; | |
| 		if (!headers_sent()) { | |
| 			header('Content-Length: '.strlen($out)); | |
| 			header('Connection: close'); | |
| 		} | |
| 		session_commit(); | |
| 		echo $out; | |
| 		flush(); | |
| 		if (function_exists('fastcgi_finish_request')) | |
| 			fastcgi_finish_request(); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Grab the real route handler behind the string expression | |
| 	*	@return string|array | |
| 	*	@param $func string | |
| 	*	@param $args array | |
| 	**/ | |
| 	function grab($func,$args=NULL) { | |
| 		if (preg_match('/(.+)\h*(->|::)\h*(.+)/s',$func,$parts)) { | |
| 			// Convert string to executable PHP callback | |
| 			if (!class_exists($parts[1])) | |
| 				user_error(sprintf(self::E_Class,$parts[1]),E_USER_ERROR); | |
| 			if ($parts[2]=='->') { | |
| 				if (is_subclass_of($parts[1],'Prefab')) | |
| 					$parts[1]=call_user_func($parts[1].'::instance'); | |
| 				elseif (isset($this->hive['CONTAINER'])) { | |
| 					$container=$this->hive['CONTAINER']; | |
| 					if (is_object($container) && is_callable([$container,'has']) | |
| 						&& $container->has($parts[1])) // PSR11 | |
| 						$parts[1]=call_user_func([$container,'get'],$parts[1]); | |
| 					elseif (is_callable($container)) | |
| 						$parts[1]=call_user_func($container,$parts[1],$args); | |
| 					elseif (is_string($container) && | |
| 						is_subclass_of($container,'Prefab')) | |
| 						$parts[1]=call_user_func($container.'::instance')-> | |
| 							get($parts[1]); | |
| 					else | |
| 						user_error(sprintf(self::E_Class, | |
| 							$this->stringify($parts[1])), | |
| 							E_USER_ERROR); | |
| 				} | |
| 				else { | |
| 					$ref=new ReflectionClass($parts[1]); | |
| 					$parts[1]=method_exists($parts[1],'__construct') && $args? | |
| 						$ref->newinstanceargs($args): | |
| 						$ref->newinstance(); | |
| 				} | |
| 			} | |
| 			$func=[$parts[1],$parts[3]]; | |
| 		} | |
| 		return $func; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Execute callback/hooks (supports 'class->method' format) | |
| 	*	@return mixed|FALSE | |
| 	*	@param $func callback | |
| 	*	@param $args mixed | |
| 	*	@param $hooks string | |
| 	**/ | |
| 	function call($func,$args=NULL,$hooks='') { | |
| 		if (!is_array($args)) | |
| 			$args=[$args]; | |
| 		// Grab the real handler behind the string representation | |
| 		if (is_string($func)) | |
| 			$func=$this->grab($func,$args); | |
| 		// Execute function; abort if callback/hook returns FALSE | |
| 		if (!is_callable($func)) | |
| 			// No route handler | |
| 			if ($hooks=='beforeroute,afterroute') { | |
| 				$allowed=[]; | |
| 				if (is_array($func)) | |
| 					$allowed=array_intersect( | |
| 						array_map('strtoupper',get_class_methods($func[0])), | |
| 						explode('|',self::VERBS) | |
| 					); | |
| 				header('Allow: '.implode(',',$allowed)); | |
| 				$this->error(405); | |
| 			} | |
| 			else | |
| 				user_error(sprintf(self::E_Method, | |
| 					is_string($func)?$func:$this->stringify($func)), | |
| 					E_USER_ERROR); | |
| 		$obj=FALSE; | |
| 		if (is_array($func)) { | |
| 			$hooks=$this->split($hooks); | |
| 			$obj=TRUE; | |
| 		} | |
| 		// Execute pre-route hook if any | |
| 		if ($obj && $hooks && in_array($hook='beforeroute',$hooks) && | |
| 			method_exists($func[0],$hook) && | |
| 			call_user_func_array([$func[0],$hook],$args)===FALSE) | |
| 			return FALSE; | |
| 		// Execute callback | |
| 		$out=call_user_func_array($func,$args?:[]); | |
| 		if ($out===FALSE) | |
| 			return FALSE; | |
| 		// Execute post-route hook if any | |
| 		if ($obj && $hooks && in_array($hook='afterroute',$hooks) && | |
| 			method_exists($func[0],$hook) && | |
| 			call_user_func_array([$func[0],$hook],$args)===FALSE) | |
| 			return FALSE; | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Execute specified callbacks in succession; Apply same arguments | |
| 	*	to all callbacks | |
| 	*	@return array | |
| 	*	@param $funcs array|string | |
| 	*	@param $args mixed | |
| 	**/ | |
| 	function chain($funcs,$args=NULL) { | |
| 		$out=[]; | |
| 		foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func) | |
| 			$out[]=$this->call($func,$args); | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Execute specified callbacks in succession; Relay result of | |
| 	*	previous callback as argument to the next callback | |
| 	*	@return array | |
| 	*	@param $funcs array|string | |
| 	*	@param $args mixed | |
| 	**/ | |
| 	function relay($funcs,$args=NULL) { | |
| 		foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func) | |
| 			$args=[$this->call($func,$args)]; | |
| 		return array_shift($args); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Configure framework according to .ini-style file settings; | |
| 	*	If optional 2nd arg is provided, template strings are interpreted | |
| 	*	@return object | |
| 	*	@param $source string|array | |
| 	*	@param $allow bool | |
| 	**/ | |
| 	function config($source,$allow=FALSE) { | |
| 		if (is_string($source)) | |
| 			$source=$this->split($source); | |
| 		if ($allow) | |
| 			$preview=Preview::instance(); | |
| 		foreach ($source as $file) { | |
| 			preg_match_all( | |
| 				'/(?<=^|\n)(?:'. | |
| 					'\[(?<section>.+?)\]|'. | |
| 					'(?<lval>[^\h\r\n;].*?)\h*=\h*'. | |
| 					'(?<rval>(?:\\\\\h*\r?\n|.+?)*)'. | |
| 				')(?=\r?\n|$)/', | |
| 				$this->read($file), | |
| 				$matches,PREG_SET_ORDER); | |
| 			if ($matches) { | |
| 				$sec='globals'; | |
| 				$cmd=[]; | |
| 				foreach ($matches as $match) { | |
| 					if ($match['section']) { | |
| 						$sec=$match['section']; | |
| 						if (preg_match( | |
| 							'/^(?!(?:global|config|route|map|redirect)s\b)'. | |
| 							'(.*?)(?:\s*[:>])/i',$sec,$msec) && | |
| 							!$this->exists($msec[1])) | |
| 							$this->set($msec[1],NULL); | |
| 						preg_match('/^(config|route|map|redirect)s\b|'. | |
| 							'^(.+?)\s*\>\s*(.*)/i',$sec,$cmd); | |
| 						continue; | |
| 					} | |
| 					if ($allow) | |
| 						foreach (['lval','rval'] as $ndx) | |
| 							$match[$ndx]=$preview-> | |
| 								resolve($match[$ndx],NULL,0,FALSE,FALSE); | |
| 					if (!empty($cmd)) { | |
| 						isset($cmd[3])? | |
| 						$this->call($cmd[3], | |
| 							[$match['lval'],$match['rval'],$cmd[2]]): | |
| 						call_user_func_array( | |
| 							[$this,$cmd[1]], | |
| 							array_merge([$match['lval']], | |
| 								str_getcsv($cmd[1]=='config'? | |
| 								$this->cast($match['rval']): | |
| 									$match['rval'])) | |
| 						); | |
| 					} | |
| 					else { | |
| 						$rval=preg_replace( | |
| 							'/\\\\\h*(\r?\n)/','\1',$match['rval']); | |
| 						$ttl=NULL; | |
| 						if (preg_match('/^(.+)\|\h*(\d+)$/',$rval,$tmp)) { | |
| 							array_shift($tmp); | |
| 							list($rval,$ttl)=$tmp; | |
| 						} | |
| 						$args=array_map( | |
| 							function($val) { | |
| 								$val=$this->cast($val); | |
| 								if (is_string($val)) | |
| 									$val=strlen($val)? | |
| 										preg_replace('/\\\\"/','"',$val): | |
| 										NULL; | |
| 								return $val; | |
| 							}, | |
| 							// Mark quoted strings with 0x00 whitespace | |
| 							str_getcsv(preg_replace( | |
| 								'/(?<!\\\\)(")(.*?)\1/', | |
| 								"\\1\x00\\2\\1",trim($rval))) | |
| 						); | |
| 						preg_match('/^(?<section>[^:]+)(?:\:(?<func>.+))?/', | |
| 							$sec,$parts); | |
| 						$func=isset($parts['func'])?$parts['func']:NULL; | |
| 						$custom=(strtolower($parts['section'])!='globals'); | |
| 						if ($func) | |
| 							$args=[$this->call($func,$args)]; | |
| 						if (count($args)>1) | |
| 							$args=[$args]; | |
| 						if (isset($ttl)) | |
| 							$args=array_merge($args,[$ttl]); | |
| 						call_user_func_array( | |
| 							[$this,'set'], | |
| 							array_merge( | |
| 								[ | |
| 									($custom?($parts['section'].'.'):''). | |
| 									$match['lval'] | |
| 								], | |
| 								$args | |
| 							) | |
| 						); | |
| 					} | |
| 				} | |
| 			} | |
| 		} | |
| 		return $this; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Create mutex, invoke callback then drop ownership when done | |
| 	*	@return mixed | |
| 	*	@param $id string | |
| 	*	@param $func callback | |
| 	*	@param $args mixed | |
| 	**/ | |
| 	function mutex($id,$func,$args=NULL) { | |
| 		if (!is_dir($tmp=$this->hive['TEMP'])) | |
| 			mkdir($tmp,self::MODE,TRUE); | |
| 		// Use filesystem lock | |
| 		if (is_file($lock=$tmp. | |
| 			$this->hive['SEED'].'.'.$this->hash($id).'.lock') && | |
| 			filemtime($lock)+ini_get('max_execution_time')<microtime(TRUE)) | |
| 			// Stale lock | |
| 			@unlink($lock); | |
| 		while (!($handle=@fopen($lock,'x')) && !connection_aborted()) | |
| 			usleep(mt_rand(0,100)); | |
| 		$this->locks[$id]=$lock; | |
| 		$out=$this->call($func,$args); | |
| 		fclose($handle); | |
| 		@unlink($lock); | |
| 		unset($this->locks[$id]); | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Read file (with option to apply Unix LF as standard line ending) | |
| 	*	@return string | |
| 	*	@param $file string | |
| 	*	@param $lf bool | |
| 	**/ | |
| 	function read($file,$lf=FALSE) { | |
| 		$out=@file_get_contents($file); | |
| 		return $lf?preg_replace('/\r\n|\r/',"\n",$out):$out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Exclusive file write | |
| 	*	@return int|FALSE | |
| 	*	@param $file string | |
| 	*	@param $data mixed | |
| 	*	@param $append bool | |
| 	**/ | |
| 	function write($file,$data,$append=FALSE) { | |
| 		return file_put_contents($file,$data,$this->hive['LOCK']|($append?FILE_APPEND:0)); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Apply syntax highlighting | |
| 	*	@return string | |
| 	*	@param $text string | |
| 	**/ | |
| 	function highlight($text) { | |
| 		$out=''; | |
| 		$pre=FALSE; | |
| 		$text=trim($text); | |
| 		if ($text && !preg_match('/^<\?php/',$text)) { | |
| 			$text='<?php '.$text; | |
| 			$pre=TRUE; | |
| 		} | |
| 		foreach (token_get_all($text) as $token) | |
| 			if ($pre) | |
| 				$pre=FALSE; | |
| 			else | |
| 				$out.='<span'. | |
| 					(is_array($token)? | |
| 						(' class="'. | |
| 							substr(strtolower(token_name($token[0])),2).'">'. | |
| 							$this->encode($token[1]).''): | |
| 						('>'.$this->encode($token))). | |
| 					'</span>'; | |
| 		return $out?('<code>'.$out.'</code>'):$text; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Dump expression with syntax highlighting | |
| 	*	@param $expr mixed | |
| 	**/ | |
| 	function dump($expr) { | |
| 		echo $this->highlight($this->stringify($expr)); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return path (and query parameters) relative to the base directory | |
| 	*	@return string | |
| 	*	@param $url string | |
| 	**/ | |
| 	function rel($url) { | |
| 		return preg_replace('/^(?:https?:\/\/)?'. | |
| 			preg_quote($this->hive['BASE'],'/').'(\/.*|$)/','\1',$url); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Namespace-aware class autoloader | |
| 	*	@return mixed | |
| 	*	@param $class string | |
| 	**/ | |
| 	protected function autoload($class) { | |
| 		$class=$this->fixslashes(ltrim($class,'\\')); | |
| 		/** @var callable $func */ | |
| 		$func=NULL; | |
| 		if (is_array($path=$this->hive['AUTOLOAD']) && | |
| 			isset($path[1]) && is_callable($path[1])) | |
| 			list($path,$func)=$path; | |
| 		foreach ($this->split($this->hive['PLUGINS'].';'.$path) as $auto) | |
| 			if ($func && is_file($file=$func($auto.$class).'.php') || | |
| 				is_file($file=$auto.$class.'.php') || | |
| 				is_file($file=$auto.strtolower($class).'.php') || | |
| 				is_file($file=strtolower($auto.$class).'.php')) | |
| 				return require($file); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Execute framework/application shutdown sequence | |
| 	*	@param $cwd string | |
| 	**/ | |
| 	function unload($cwd) { | |
| 		chdir($cwd); | |
| 		if (!($error=error_get_last()) && | |
| 			session_status()==PHP_SESSION_ACTIVE) | |
| 			session_commit(); | |
| 		foreach ($this->locks as $lock) | |
| 			@unlink($lock); | |
| 		$handler=$this->hive['UNLOAD']; | |
| 		if ((!$handler || $this->call($handler,$this)===FALSE) && | |
| 			$error && in_array($error['type'], | |
| 			[E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR])) | |
| 			// Fatal error detected | |
| 			$this->error(500, | |
| 				sprintf(self::E_Fatal,$error['message']),[$error]); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convenience method for checking hive key | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	**/ | |
| 	function offsetexists($key) { | |
| 		return $this->exists($key); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convenience method for assigning hive value | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	*	@param $val mixed | |
| 	**/ | |
| 	function offsetset($key,$val) { | |
| 		return $this->set($key,$val); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convenience method for retrieving hive value | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	**/ | |
| 	function &offsetget($key) { | |
| 		$val=&$this->ref($key); | |
| 		return $val; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convenience method for removing hive key | |
| 	*	@param $key string | |
| 	**/ | |
| 	function offsetunset($key) { | |
| 		$this->clear($key); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Alias for offsetexists() | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	**/ | |
| 	function __isset($key) { | |
| 		return $this->offsetexists($key); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Alias for offsetset() | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	*	@param $val mixed | |
| 	**/ | |
| 	function __set($key,$val) { | |
| 		return $this->offsetset($key,$val); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Alias for offsetget() | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	**/ | |
| 	function &__get($key) { | |
| 		$val=&$this->offsetget($key); | |
| 		return $val; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Alias for offsetunset() | |
| 	*	@param $key string | |
| 	**/ | |
| 	function __unset($key) { | |
| 		$this->offsetunset($key); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Call function identified by hive key | |
| 	*	@return mixed | |
| 	*	@param $key string | |
| 	*	@param $args array | |
| 	**/ | |
| 	function __call($key,array $args) { | |
| 		if ($this->exists($key,$val)) | |
| 			return call_user_func_array($val,$args); | |
| 		user_error(sprintf(self::E_Method,$key),E_USER_ERROR); | |
| 	} | |
| 
 | |
| 	//! Prohibit cloning | |
| 	private function __clone() { | |
| 	} | |
| 
 | |
| 	//! Bootstrap | |
| 	function __construct() { | |
| 		// Managed directives | |
| 		ini_set('default_charset',$charset='UTF-8'); | |
| 		if (extension_loaded('mbstring')) | |
| 			mb_internal_encoding($charset); | |
| 		ini_set('display_errors',0); | |
| 		// Deprecated directives | |
| 		@ini_set('magic_quotes_gpc',0); | |
| 		@ini_set('register_globals',0); | |
| 		// Intercept errors/exceptions; PHP5.3-compatible | |
| 		$check=error_reporting((E_ALL|E_STRICT)&~(E_NOTICE|E_USER_NOTICE)); | |
| 		set_exception_handler( | |
| 			function($obj) { | |
| 				/** @var Exception $obj */ | |
| 				$this->hive['EXCEPTION']=$obj; | |
| 				$this->error(500, | |
| 					$obj->getmessage().' '. | |
| 					'['.$obj->getFile().':'.$obj->getLine().']', | |
| 					$obj->gettrace()); | |
| 			} | |
| 		); | |
| 		set_error_handler( | |
| 			function($level,$text,$file,$line) { | |
| 				if ($level & error_reporting()) | |
| 					$this->error(500,$text,NULL,$level); | |
| 			} | |
| 		); | |
| 		if (!isset($_SERVER['SERVER_NAME']) || $_SERVER['SERVER_NAME']==='') | |
| 			$_SERVER['SERVER_NAME']=gethostname(); | |
| 		$headers=[]; | |
| 		if ($cli=PHP_SAPI=='cli') { | |
| 			// Emulate HTTP request | |
| 			$_SERVER['REQUEST_METHOD']='GET'; | |
| 			if (!isset($_SERVER['argv'][1])) { | |
| 				$_SERVER['argc']++; | |
| 				$_SERVER['argv'][1]='/'; | |
| 			} | |
| 			$req=$query=''; | |
| 			if (substr($_SERVER['argv'][1],0,1)=='/') { | |
| 				$req=$_SERVER['argv'][1]; | |
| 				$query=parse_url($req,PHP_URL_QUERY); | |
| 			} else { | |
| 				foreach($_SERVER['argv'] as $i=>$arg) { | |
| 					if (!$i) continue; | |
| 					if (preg_match('/^\-(\-)?(\w+)(?:\=(.*))?$/',$arg,$m)) { | |
| 						foreach($m[1]?[$m[2]]:str_split($m[2]) as $k) | |
| 							$query.=($query?'&':'').urlencode($k).'='; | |
| 						if (isset($m[3])) | |
| 							$query.=urlencode($m[3]); | |
| 					} else | |
| 						$req.='/'.$arg; | |
| 				} | |
| 				if (!$req) | |
| 					$req='/'; | |
| 				if ($query) | |
| 					$req.='?'.$query; | |
| 			} | |
| 			$_SERVER['REQUEST_URI']=$req; | |
| 			parse_str($query,$GLOBALS['_GET']); | |
| 		} | |
| 		elseif (function_exists('getallheaders')) { | |
| 			foreach (getallheaders() as $key=>$val) { | |
| 				$tmp=strtoupper(strtr($key,'-','_')); | |
| 				// TODO: use ucwords delimiters for php 5.4.32+ & 5.5.16+ | |
| 				$key=strtr(ucwords(strtolower(strtr($key,'-',' '))),' ','-'); | |
| 				$headers[$key]=$val; | |
| 				if (isset($_SERVER['HTTP_'.$tmp])) | |
| 					$headers[$key]=&$_SERVER['HTTP_'.$tmp]; | |
| 			} | |
| 		} | |
| 		else { | |
| 			if (isset($_SERVER['CONTENT_LENGTH'])) | |
| 				$headers['Content-Length']=&$_SERVER['CONTENT_LENGTH']; | |
| 			if (isset($_SERVER['CONTENT_TYPE'])) | |
| 				$headers['Content-Type']=&$_SERVER['CONTENT_TYPE']; | |
| 			foreach (array_keys($_SERVER) as $key) | |
| 				if (substr($key,0,5)=='HTTP_') | |
| 					$headers[strtr(ucwords(strtolower(strtr( | |
| 						substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key]; | |
| 		} | |
| 		if (isset($headers['X-HTTP-Method-Override'])) | |
| 			$_SERVER['REQUEST_METHOD']=$headers['X-HTTP-Method-Override']; | |
| 		elseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method'])) | |
| 			$_SERVER['REQUEST_METHOD']=strtoupper($_POST['_method']); | |
| 		$scheme=isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' || | |
| 			isset($headers['X-Forwarded-Proto']) && | |
| 			$headers['X-Forwarded-Proto']=='https'?'https':'http'; | |
| 		// Create hive early on to expose header methods | |
| 		$this->hive=['HEADERS'=>&$headers]; | |
| 		if (function_exists('apache_setenv')) { | |
| 			// Work around Apache pre-2.4 VirtualDocumentRoot bug | |
| 			$_SERVER['DOCUMENT_ROOT']=str_replace($_SERVER['SCRIPT_NAME'],'', | |
| 				$_SERVER['SCRIPT_FILENAME']); | |
| 			apache_setenv("DOCUMENT_ROOT",$_SERVER['DOCUMENT_ROOT']); | |
| 		} | |
| 		$_SERVER['DOCUMENT_ROOT']=realpath($_SERVER['DOCUMENT_ROOT']); | |
| 		$base=''; | |
| 		if (!$cli) | |
| 			$base=rtrim($this->fixslashes( | |
| 				dirname($_SERVER['SCRIPT_NAME'])),'/'); | |
| 		$uri=parse_url((preg_match('/^\w+:\/\//',$_SERVER['REQUEST_URI'])?'': | |
| 				$scheme.'://'.$_SERVER['SERVER_NAME']).$_SERVER['REQUEST_URI']); | |
| 		$_SERVER['REQUEST_URI']=$uri['path']. | |
| 			(isset($uri['query'])?'?'.$uri['query']:''). | |
| 			(isset($uri['fragment'])?'#'.$uri['fragment']:''); | |
| 		$path=preg_replace('/^'.preg_quote($base,'/').'/','',$uri['path']); | |
| 		$jar=[ | |
| 			'expire'=>0, | |
| 			'lifetime'=>0, | |
| 			'path'=>$base?:'/', | |
| 			'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) && | |
| 				!filter_var($_SERVER['SERVER_NAME'],FILTER_VALIDATE_IP)? | |
| 				$_SERVER['SERVER_NAME']:'', | |
| 			'secure'=>($scheme=='https'), | |
| 			'httponly'=>TRUE, | |
| 			'samesite'=>'Lax', | |
| 		]; | |
| 		$port=80; | |
| 		if (isset($headers['X-Forwarded-Port'])) | |
| 			$port=$headers['X-Forwarded-Port']; | |
| 		elseif (isset($_SERVER['SERVER_PORT'])) | |
| 			$port=$_SERVER['SERVER_PORT']; | |
| 		// Default configuration | |
| 		$this->hive+=[ | |
| 			'AGENT'=>$this->agent(), | |
| 			'AJAX'=>$this->ajax(), | |
| 			'ALIAS'=>NULL, | |
| 			'ALIASES'=>[], | |
| 			'AUTOLOAD'=>'./', | |
| 			'BASE'=>$base, | |
| 			'BITMASK'=>ENT_COMPAT, | |
| 			'BODY'=>NULL, | |
| 			'CACHE'=>FALSE, | |
| 			'CASELESS'=>TRUE, | |
| 			'CLI'=>$cli, | |
| 			'CORS'=>[ | |
| 				'headers'=>'', | |
| 				'origin'=>FALSE, | |
| 				'credentials'=>FALSE, | |
| 				'expose'=>FALSE, | |
| 				'ttl'=>0 | |
| 			], | |
| 			'DEBUG'=>0, | |
| 			'DIACRITICS'=>[], | |
| 			'DNSBL'=>'', | |
| 			'EMOJI'=>[], | |
| 			'ENCODING'=>$charset, | |
| 			'ERROR'=>NULL, | |
| 			'ESCAPE'=>TRUE, | |
| 			'EXCEPTION'=>NULL, | |
| 			'EXEMPT'=>NULL, | |
| 			'FALLBACK'=>$this->fallback, | |
| 			'FORMATS'=>[], | |
| 			'FRAGMENT'=>isset($uri['fragment'])?$uri['fragment']:'', | |
| 			'HALT'=>TRUE, | |
| 			'HIGHLIGHT'=>FALSE, | |
| 			'HOST'=>$_SERVER['SERVER_NAME'], | |
| 			'IP'=>$this->ip(), | |
| 			'JAR'=>$jar, | |
| 			'LANGUAGE'=>isset($headers['Accept-Language'])? | |
| 				$this->language($headers['Accept-Language']): | |
| 				$this->fallback, | |
| 			'LOCALES'=>'./', | |
| 			'LOCK'=>LOCK_EX, | |
| 			'LOGGABLE'=>'*', | |
| 			'LOGS'=>'./', | |
| 			'MB'=>extension_loaded('mbstring'), | |
| 			'ONERROR'=>NULL, | |
| 			'ONREROUTE'=>NULL, | |
| 			'PACKAGE'=>self::PACKAGE, | |
| 			'PARAMS'=>[], | |
| 			'PATH'=>$path, | |
| 			'PATTERN'=>NULL, | |
| 			'PLUGINS'=>$this->fixslashes(__DIR__).'/', | |
| 			'PORT'=>$port, | |
| 			'PREFIX'=>NULL, | |
| 			'PREMAP'=>'', | |
| 			'QUERY'=>isset($uri['query'])?$uri['query']:'', | |
| 			'QUIET'=>FALSE, | |
| 			'RAW'=>FALSE, | |
| 			'REALM'=>$scheme.'://'.$_SERVER['SERVER_NAME']. | |
| 				($port && !in_array($port,[80,443])?(':'.$port):''). | |
| 				$_SERVER['REQUEST_URI'], | |
| 			'RESPONSE'=>'', | |
| 			'ROOT'=>$_SERVER['DOCUMENT_ROOT'], | |
| 			'ROUTES'=>[], | |
| 			'SCHEME'=>$scheme, | |
| 			'SEED'=>$this->hash($_SERVER['SERVER_NAME'].$base), | |
| 			'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php', | |
| 			'TEMP'=>'tmp/', | |
| 			'TIME'=>&$_SERVER['REQUEST_TIME_FLOAT'], | |
| 			'TZ'=>@date_default_timezone_get(), | |
| 			'UI'=>'./', | |
| 			'UNLOAD'=>NULL, | |
| 			'UPLOADS'=>'./', | |
| 			'URI'=>&$_SERVER['REQUEST_URI'], | |
| 			'VERB'=>&$_SERVER['REQUEST_METHOD'], | |
| 			'VERSION'=>self::VERSION, | |
| 			'XFRAME'=>'SAMEORIGIN' | |
| 		]; | |
| 		if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) { | |
| 			unset($jar['expire']); | |
| 			session_cache_limiter(''); | |
| 			if (version_compare(PHP_VERSION, '7.3.0') >= 0) | |
| 				session_set_cookie_params($jar); | |
| 			else { | |
| 				unset($jar['samesite']); | |
| 				call_user_func_array('session_set_cookie_params',$jar); | |
| 			} | |
| 		} | |
| 		if (PHP_SAPI=='cli-server' && | |
| 			preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI'])) | |
| 			$this->reroute('/'); | |
| 		if (ini_get('auto_globals_jit')) | |
| 			// Override setting | |
| 			$GLOBALS+=['_ENV'=>$_ENV,'_REQUEST'=>$_REQUEST]; | |
| 		// Sync PHP globals with corresponding hive keys | |
| 		$this->init=$this->hive; | |
| 		foreach (explode('|',self::GLOBALS) as $global) { | |
| 			$sync=$this->sync($global); | |
| 			$this->init+=[ | |
| 				$global=>preg_match('/SERVER|ENV/',$global)?$sync:[] | |
| 			]; | |
| 		} | |
| 		if ($check && $error=error_get_last()) | |
| 			// Error detected | |
| 			$this->error(500, | |
| 				sprintf(self::E_Fatal,$error['message']),[$error]); | |
| 		date_default_timezone_set($this->hive['TZ']); | |
| 		// Register framework autoloader | |
| 		spl_autoload_register([$this,'autoload']); | |
| 		// Register shutdown handler | |
| 		register_shutdown_function([$this,'unload'],getcwd()); | |
| 	} | |
| 
 | |
| } | |
| 
 | |
| //! Cache engine | |
| class Cache extends Prefab { | |
| 
 | |
| 	protected | |
| 		//! Cache DSN | |
| 		$dsn, | |
| 		//! Prefix for cache entries | |
| 		$prefix, | |
| 		//! MemCache or Redis object | |
| 		$ref; | |
| 
 | |
| 	/** | |
| 	*	Return timestamp and TTL of cache entry or FALSE if not found | |
| 	*	@return array|FALSE | |
| 	*	@param $key string | |
| 	*	@param $val mixed | |
| 	**/ | |
| 	function exists($key,&$val=NULL) { | |
| 		$fw=Base::instance(); | |
| 		if (!$this->dsn) | |
| 			return FALSE; | |
| 		$ndx=$this->prefix.'.'.$key; | |
| 		$parts=explode('=',$this->dsn,2); | |
| 		switch ($parts[0]) { | |
| 			case 'apc': | |
| 			case 'apcu': | |
| 				$raw=call_user_func($parts[0].'_fetch',$ndx); | |
| 				break; | |
| 			case 'redis': | |
| 				$raw=$this->ref->get($ndx); | |
| 				break; | |
| 			case 'memcache': | |
| 				$raw=memcache_get($this->ref,$ndx); | |
| 				break; | |
| 			case 'memcached': | |
| 				$raw=$this->ref->get($ndx); | |
| 				break; | |
| 			case 'wincache': | |
| 				$raw=wincache_ucache_get($ndx); | |
| 				break; | |
| 			case 'xcache': | |
| 				$raw=xcache_get($ndx); | |
| 				break; | |
| 			case 'folder': | |
| 				$raw=$fw->read($parts[1].$ndx); | |
| 				break; | |
| 		} | |
| 		if (!empty($raw)) { | |
| 			list($val,$time,$ttl)=(array)$fw->unserialize($raw); | |
| 			if ($ttl===0 || $time+$ttl>microtime(TRUE)) | |
| 				return [$time,$ttl]; | |
| 			$val=null; | |
| 			$this->clear($key); | |
| 		} | |
| 		return FALSE; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Store value in cache | |
| 	*	@return mixed|FALSE | |
| 	*	@param $key string | |
| 	*	@param $val mixed | |
| 	*	@param $ttl int | |
| 	**/ | |
| 	function set($key,$val,$ttl=0) { | |
| 		$fw=Base::instance(); | |
| 		if (!$this->dsn) | |
| 			return TRUE; | |
| 		$ndx=$this->prefix.'.'.$key; | |
| 		if ($cached=$this->exists($key)) | |
| 			$ttl=$cached[1]; | |
| 		$data=$fw->serialize([$val,microtime(TRUE),$ttl]); | |
| 		$parts=explode('=',$this->dsn,2); | |
| 		switch ($parts[0]) { | |
| 			case 'apc': | |
| 			case 'apcu': | |
| 				return call_user_func($parts[0].'_store',$ndx,$data,$ttl); | |
| 			case 'redis': | |
| 				return $this->ref->set($ndx,$data,$ttl?['ex'=>$ttl]:[]); | |
| 			case 'memcache': | |
| 				return memcache_set($this->ref,$ndx,$data,0,$ttl); | |
| 			case 'memcached': | |
| 				return $this->ref->set($ndx,$data,$ttl); | |
| 			case 'wincache': | |
| 				return wincache_ucache_set($ndx,$data,$ttl); | |
| 			case 'xcache': | |
| 				return xcache_set($ndx,$data,$ttl); | |
| 			case 'folder': | |
| 				return $fw->write($parts[1]. | |
| 					str_replace(['/','\\'],'',$ndx),$data); | |
| 		} | |
| 		return FALSE; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Retrieve value of cache entry | |
| 	*	@return mixed|FALSE | |
| 	*	@param $key string | |
| 	**/ | |
| 	function get($key) { | |
| 		return $this->dsn && $this->exists($key,$data)?$data:FALSE; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Delete cache entry | |
| 	*	@return bool | |
| 	*	@param $key string | |
| 	**/ | |
| 	function clear($key) { | |
| 		if (!$this->dsn) | |
| 			return; | |
| 		$ndx=$this->prefix.'.'.$key; | |
| 		$parts=explode('=',$this->dsn,2); | |
| 		switch ($parts[0]) { | |
| 			case 'apc': | |
| 			case 'apcu': | |
| 				return call_user_func($parts[0].'_delete',$ndx); | |
| 			case 'redis': | |
| 				return $this->ref->del($ndx); | |
| 			case 'memcache': | |
| 				return memcache_delete($this->ref,$ndx); | |
| 			case 'memcached': | |
| 				return $this->ref->delete($ndx); | |
| 			case 'wincache': | |
| 				return wincache_ucache_delete($ndx); | |
| 			case 'xcache': | |
| 				return xcache_unset($ndx); | |
| 			case 'folder': | |
| 				return @unlink($parts[1].$ndx); | |
| 		} | |
| 		return FALSE; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Clear contents of cache backend | |
| 	*	@return bool | |
| 	*	@param $suffix string | |
| 	**/ | |
| 	function reset($suffix=NULL) { | |
| 		if (!$this->dsn) | |
| 			return TRUE; | |
| 		$regex='/'.preg_quote($this->prefix.'.','/').'.*'. | |
| 			preg_quote($suffix,'/').'/'; | |
| 		$parts=explode('=',$this->dsn,2); | |
| 		switch ($parts[0]) { | |
| 			case 'apc': | |
| 			case 'apcu': | |
| 				$info=call_user_func($parts[0].'_cache_info', | |
| 					$parts[0]=='apcu'?FALSE:'user'); | |
| 				if (!empty($info['cache_list'])) { | |
| 					$key=array_key_exists('info', | |
| 						$info['cache_list'][0])?'info':'key'; | |
| 					foreach ($info['cache_list'] as $item) | |
| 						if (preg_match($regex,$item[$key])) | |
| 							call_user_func($parts[0].'_delete',$item[$key]); | |
| 				} | |
| 				return TRUE; | |
| 			case 'redis': | |
| 				$keys=$this->ref->keys($this->prefix.'.*'.$suffix); | |
| 				foreach($keys as $key) | |
| 					$this->ref->del($key); | |
| 				return TRUE; | |
| 			case 'memcache': | |
| 				foreach (memcache_get_extended_stats( | |
| 					$this->ref,'slabs') as $slabs) | |
| 					foreach (array_filter(array_keys($slabs),'is_numeric') | |
| 						as $id) | |
| 						foreach (memcache_get_extended_stats( | |
| 							$this->ref,'cachedump',$id) as $data) | |
| 							if (is_array($data)) | |
| 								foreach (array_keys($data) as $key) | |
| 									if (preg_match($regex,$key)) | |
| 										memcache_delete($this->ref,$key); | |
| 				return TRUE; | |
| 			case 'memcached': | |
| 				foreach ($this->ref->getallkeys()?:[] as $key) | |
| 					if (preg_match($regex,$key)) | |
| 						$this->ref->delete($key); | |
| 				return TRUE; | |
| 			case 'wincache': | |
| 				$info=wincache_ucache_info(); | |
| 				foreach ($info['ucache_entries'] as $item) | |
| 					if (preg_match($regex,$item['key_name'])) | |
| 						wincache_ucache_delete($item['key_name']); | |
| 				return TRUE; | |
| 			case 'xcache': | |
| 				if ($suffix && !ini_get('xcache.admin.enable_auth')) { | |
| 					$cnt=xcache_count(XC_TYPE_VAR); | |
| 					for ($i=0;$i<$cnt;$i++) { | |
| 						$list=xcache_list(XC_TYPE_VAR,$i); | |
| 						foreach ($list['cache_list'] as $item) | |
| 							if (preg_match($regex,$item['name'])) | |
| 								xcache_unset($item['name']); | |
| 					} | |
| 				} else | |
| 					xcache_unset_by_prefix($this->prefix.'.'); | |
| 				return TRUE; | |
| 			case 'folder': | |
| 				if ($glob=@glob($parts[1].'*')) | |
| 					foreach ($glob as $file) | |
| 						if (preg_match($regex,basename($file))) | |
| 							@unlink($file); | |
| 				return TRUE; | |
| 		} | |
| 		return FALSE; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Load/auto-detect cache backend | |
| 	*	@return string | |
| 	*	@param $dsn bool|string | |
| 	*	@param $seed bool|string | |
| 	**/ | |
| 	function load($dsn,$seed=NULL) { | |
| 		$fw=Base::instance(); | |
| 		if ($dsn=trim($dsn)) { | |
| 			if (preg_match('/^redis=(.+)/',$dsn,$parts) && | |
| 				extension_loaded('redis')) { | |
| 				list($host,$port,$db,$password)=explode(':',$parts[1])+[1=>6379,2=>NULL,3=>NULL]; | |
| 				$this->ref=new Redis; | |
| 				if(!$this->ref->connect($host,$port,2)) | |
| 					$this->ref=NULL; | |
| 				if(!empty($password)) | |
| 					$this->ref->auth($password); | |
| 				if(isset($db)) | |
| 					$this->ref->select($db); | |
| 			} | |
| 			elseif (preg_match('/^memcache=(.+)/',$dsn,$parts) && | |
| 				extension_loaded('memcache')) | |
| 				foreach ($fw->split($parts[1]) as $server) { | |
| 					list($host,$port)=explode(':',$server)+[1=>11211]; | |
| 					if (empty($this->ref)) | |
| 						$this->ref=@memcache_connect($host,$port)?:NULL; | |
| 					else | |
| 						memcache_add_server($this->ref,$host,$port); | |
| 				} | |
| 			elseif (preg_match('/^memcached=(.+)/',$dsn,$parts) && | |
| 				extension_loaded('memcached')) | |
| 				foreach ($fw->split($parts[1]) as $server) { | |
| 					list($host,$port)=explode(':',$server)+[1=>11211]; | |
| 					if (empty($this->ref)) | |
| 						$this->ref=new Memcached(); | |
| 					$this->ref->addServer($host,$port); | |
| 				} | |
| 			if (empty($this->ref) && !preg_match('/^folder\h*=/',$dsn)) | |
| 				$dsn=($grep=preg_grep('/^(apc|wincache|xcache)/', | |
| 					array_map('strtolower',get_loaded_extensions())))? | |
| 						// Auto-detect | |
| 						current($grep): | |
| 						// Use filesystem as fallback | |
| 						('folder='.$fw->TEMP.'cache/'); | |
| 			if (preg_match('/^folder\h*=\h*(.+)/',$dsn,$parts) && | |
| 				!is_dir($parts[1])) | |
| 				mkdir($parts[1],Base::MODE,TRUE); | |
| 		} | |
| 		$this->prefix=$seed?:$fw->SEED; | |
| 		return $this->dsn=$dsn; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Class constructor | |
| 	*	@param $dsn bool|string | |
| 	**/ | |
| 	function __construct($dsn=FALSE) { | |
| 		if ($dsn) | |
| 			$this->load($dsn); | |
| 	} | |
| 
 | |
| } | |
| 
 | |
| //! View handler | |
| class View extends Prefab { | |
| 
 | |
| 	private | |
| 		//! Temporary hive | |
| 		$temp; | |
| 
 | |
| 	protected | |
| 		//! Template file | |
| 		$file, | |
| 		//! Post-rendering handler | |
| 		$trigger, | |
| 		//! Nesting level | |
| 		$level=0; | |
| 
 | |
| 	/** @var \Base Framework instance */ | |
| 	protected $fw; | |
| 
 | |
| 	function __construct() { | |
| 		$this->fw=\Base::instance(); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Encode characters to equivalent HTML entities | |
| 	*	@return string | |
| 	*	@param $arg mixed | |
| 	**/ | |
| 	function esc($arg) { | |
| 		return $this->fw->recursive($arg, | |
| 			function($val) { | |
| 				return is_string($val)?$this->fw->encode($val):$val; | |
| 			} | |
| 		); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Decode HTML entities to equivalent characters | |
| 	*	@return string | |
| 	*	@param $arg mixed | |
| 	**/ | |
| 	function raw($arg) { | |
| 		return $this->fw->recursive($arg, | |
| 			function($val) { | |
| 				return is_string($val)?$this->fw->decode($val):$val; | |
| 			} | |
| 		); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Create sandbox for template execution | |
| 	*	@return string | |
| 	*	@param $hive array | |
| 	*	@param $mime string | |
| 	**/ | |
| 	protected function sandbox(array $hive=NULL,$mime=NULL) { | |
| 		$fw=$this->fw; | |
| 		$implicit=FALSE; | |
| 		if (is_null($hive)) { | |
| 			$implicit=TRUE; | |
| 			$hive=$fw->hive(); | |
| 		} | |
| 		if ($this->level<1 || $implicit) { | |
| 			if (!$fw->CLI && $mime && !headers_sent() && | |
| 				!preg_grep ('/^Content-Type:/',headers_list())) | |
| 				header('Content-Type: '.$mime.'; '. | |
| 					'charset='.$fw->ENCODING); | |
| 			if ($fw->ESCAPE && (!$mime || | |
| 					preg_match('/^(text\/html|(application|text)\/(.+\+)?xml)$/i',$mime))) | |
| 				$hive=$this->esc($hive); | |
| 			if (isset($hive['ALIASES'])) | |
| 				$hive['ALIASES']=$fw->build($hive['ALIASES']); | |
| 		} | |
| 		$this->temp=$hive; | |
| 		unset($fw,$hive,$implicit,$mime); | |
| 		extract($this->temp); | |
| 		$this->temp=NULL; | |
| 		$this->level++; | |
| 		ob_start(); | |
| 		require($this->file); | |
| 		$this->level--; | |
| 		return ob_get_clean(); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Render template | |
| 	*	@return string | |
| 	*	@param $file string | |
| 	*	@param $mime string | |
| 	*	@param $hive array | |
| 	*	@param $ttl int | |
| 	**/ | |
| 	function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { | |
| 		$fw=$this->fw; | |
| 		$cache=Cache::instance(); | |
| 		foreach ($fw->split($fw->UI) as $dir) { | |
| 			if ($cache->exists($hash=$fw->hash($dir.$file),$data)) | |
| 				return $data; | |
| 			if (is_file($this->file=$fw->fixslashes($dir.$file))) { | |
| 				if (isset($_COOKIE[session_name()]) && | |
| 					!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) | |
| 					session_start(); | |
| 				$fw->sync('SESSION'); | |
| 				$data=$this->sandbox($hive,$mime); | |
| 				if (isset($this->trigger['afterrender'])) | |
| 					foreach($this->trigger['afterrender'] as $func) | |
| 						$data=$fw->call($func,[$data, $dir.$file]); | |
| 				if ($ttl) | |
| 					$cache->set($hash,$data,$ttl); | |
| 				return $data; | |
| 			} | |
| 		} | |
| 		user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	post rendering handler | |
| 	*	@param $func callback | |
| 	*/ | |
| 	function afterrender($func) { | |
| 		$this->trigger['afterrender'][]=$func; | |
| 	} | |
| 
 | |
| } | |
| 
 | |
| //! Lightweight template engine | |
| class Preview extends View { | |
| 
 | |
| 	protected | |
| 		//! token filter | |
| 		$filter=[ | |
| 			'c'=>'$this->c', | |
| 			'esc'=>'$this->esc', | |
| 			'raw'=>'$this->raw', | |
| 			'export'=>'Base::instance()->export', | |
| 			'alias'=>'Base::instance()->alias', | |
| 			'format'=>'Base::instance()->format' | |
| 		]; | |
| 
 | |
| 	protected | |
| 		//! newline interpolation | |
| 		$interpolation=true; | |
| 
 | |
| 	/** | |
| 	 * Enable/disable markup parsing interpolation | |
| 	 * mainly used for adding appropriate newlines | |
| 	 * @param $bool bool | |
| 	 */ | |
| 	function interpolation($bool) { | |
| 		$this->interpolation=$bool; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return C-locale equivalent of number | |
| 	*	@return string | |
| 	*	@param $val int|float | |
| 	**/ | |
| 	function c($val) { | |
| 		$locale=setlocale(LC_NUMERIC,0); | |
| 		setlocale(LC_NUMERIC,'C'); | |
| 		$out=(string)(float)$val; | |
| 		$locale=setlocale(LC_NUMERIC,$locale); | |
| 		return $out; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Convert token to variable | |
| 	*	@return string | |
| 	*	@param $str string | |
| 	**/ | |
| 	function token($str) { | |
| 		$str=trim(preg_replace('/\{\{(.+?)\}\}/s','\1',$this->fw->compile($str))); | |
| 		if (preg_match('/^(.+)(?<!\|)\|((?:\h*\w+(?:\h*[,;]?))+)$/s', | |
| 			$str,$parts)) { | |
| 			$str=trim($parts[1]); | |
| 			foreach ($this->fw->split(trim($parts[2],"\xC2\xA0")) as $func) | |
| 				$str=((empty($this->filter[$cmd=$func]) && | |
| 					function_exists($cmd)) || | |
| 					is_string($cmd=$this->filter($func)))? | |
| 					$cmd.'('.$str.')': | |
| 					'Base::instance()->'. | |
| 						'call($this->filter(\''.$func.'\'),['.$str.'])'; | |
| 		} | |
| 		return $str; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Register or get (one specific or all) token filters | |
| 	*	@param string $key | |
| 	*	@param string|closure $func | |
| 	*	@return array|closure|string | |
| 	*/ | |
| 	function filter($key=NULL,$func=NULL) { | |
| 		if (!$key) | |
| 			return array_keys($this->filter); | |
| 		$key=strtolower($key); | |
| 		if (!$func) | |
| 			return $this->filter[$key]; | |
| 		$this->filter[$key]=$func; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Assemble markup | |
| 	*	@return string | |
| 	*	@param $node string | |
| 	**/ | |
| 	protected function build($node) { | |
| 		return preg_replace_callback( | |
| 			'/\{~(.+?)~\}|\{\*(.+?)\*\}|\{\-(.+?)\-\}|'. | |
| 			'\{\{(.+?)\}\}((\r?\n)*)/s', | |
| 			function($expr) { | |
| 				if ($expr[1]) | |
| 					$str='<?php '.$this->token($expr[1]).' ?>'; | |
| 				elseif ($expr[2]) | |
| 					return ''; | |
| 				elseif ($expr[3]) | |
| 					$str=$expr[3]; | |
| 				else { | |
| 					$str='<?= ('.trim($this->token($expr[4])).')'. | |
| 						($this->interpolation? | |
| 							(!empty($expr[6])?'."'.$expr[6].'"':''):'').' ?>'; | |
| 					if (isset($expr[5])) | |
| 						$str.=$expr[5]; | |
| 				} | |
| 				return $str; | |
| 			}, | |
| 			$node | |
| 		); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Render template string | |
| 	*	@return string | |
| 	*	@param $node string|array | |
| 	*	@param $hive array | |
| 	*	@param $ttl int | |
| 	*	@param $persist bool | |
| 	*	@param $escape bool | |
| 	**/ | |
| 	function resolve($node,array $hive=NULL,$ttl=0,$persist=FALSE,$escape=NULL) { | |
| 		$fw=$this->fw; | |
| 		$cache=Cache::instance(); | |
| 		if ($escape!==NULL) { | |
| 			$esc=$fw->ESCAPE; | |
| 			$fw->ESCAPE=$escape; | |
| 		} | |
| 		if ($ttl || $persist) | |
| 			$hash=$fw->hash($fw->serialize($node)); | |
| 		if ($ttl && $cache->exists($hash,$data)) | |
| 			return $data; | |
| 		if ($persist) { | |
| 			if (!is_dir($tmp=$fw->TEMP)) | |
| 				mkdir($tmp,Base::MODE,TRUE); | |
| 			if (!is_file($this->file=($tmp. | |
| 				$fw->SEED.'.'.$hash.'.php'))) | |
| 				$fw->write($this->file,$this->build($node)); | |
| 			if (isset($_COOKIE[session_name()]) && | |
| 				!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) | |
| 				session_start(); | |
| 			$fw->sync('SESSION'); | |
| 			$data=$this->sandbox($hive); | |
| 		} | |
| 		else { | |
| 			if (!$hive) | |
| 				$hive=$fw->hive(); | |
| 			if ($fw->ESCAPE) | |
| 				$hive=$this->esc($hive); | |
| 			extract($hive); | |
| 			unset($hive); | |
| 			ob_start(); | |
| 			eval(' ?>'.$this->build($node).'<?php '); | |
| 			$data=ob_get_clean(); | |
| 		} | |
| 		if ($ttl) | |
| 			$cache->set($hash,$data,$ttl); | |
| 		if ($escape!==NULL) | |
| 			$fw->ESCAPE=$esc; | |
| 		return $data; | |
| 	} | |
| 
 | |
| 	/** | |
| 	 *	Parse template string | |
| 	 *	@return string | |
| 	 *	@param $text string | |
| 	 **/ | |
| 	function parse($text) { | |
| 		// Remove PHP code and comments | |
| 		return preg_replace( | |
| 			'/\h*<\?(?!xml)(?:php|\s*=)?.+?\?>\h*|'. | |
| 			'\{\*.+?\*\}/is','', $text); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Render template | |
| 	*	@return string | |
| 	*	@param $file string | |
| 	*	@param $mime string | |
| 	*	@param $hive array | |
| 	*	@param $ttl int | |
| 	**/ | |
| 	function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { | |
| 		$fw=$this->fw; | |
| 		$cache=Cache::instance(); | |
| 		if (!is_dir($tmp=$fw->TEMP)) | |
| 			mkdir($tmp,Base::MODE,TRUE); | |
| 		foreach ($fw->split($fw->UI) as $dir) { | |
| 			if ($cache->exists($hash=$fw->hash($dir.$file),$data)) | |
| 				return $data; | |
| 			if (is_file($view=$fw->fixslashes($dir.$file))) { | |
| 				if (!is_file($this->file=($tmp. | |
| 					$fw->SEED.'.'.$fw->hash($view).'.php')) || | |
| 					filemtime($this->file)<filemtime($view)) { | |
| 					$contents=$fw->read($view); | |
| 					if (isset($this->trigger['beforerender'])) | |
| 						foreach ($this->trigger['beforerender'] as $func) | |
| 							$contents=$fw->call($func, [$contents, $view]); | |
| 					$text=$this->parse($contents); | |
| 					$fw->write($this->file,$this->build($text)); | |
| 				} | |
| 				if (isset($_COOKIE[session_name()]) && | |
| 					!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) | |
| 					session_start(); | |
| 				$fw->sync('SESSION'); | |
| 				$data=$this->sandbox($hive,$mime); | |
| 				if(isset($this->trigger['afterrender'])) | |
| 					foreach ($this->trigger['afterrender'] as $func) | |
| 						$data=$fw->call($func, [$data, $view]); | |
| 				if ($ttl) | |
| 					$cache->set($hash,$data,$ttl); | |
| 				return $data; | |
| 			} | |
| 		} | |
| 		user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); | |
| 	} | |
| 
 | |
| 	/** | |
| 	 *	post rendering handler | |
| 	 *	@param $func callback | |
| 	 */ | |
| 	function beforerender($func) { | |
| 		$this->trigger['beforerender'][]=$func; | |
| 	} | |
| 
 | |
| } | |
| 
 | |
| //! ISO language/country codes | |
| class ISO extends Prefab { | |
| 
 | |
| 	//@{ ISO 3166-1 country codes | |
| 	const | |
| 		CC_af='Afghanistan', | |
| 		CC_ax='Åland Islands', | |
| 		CC_al='Albania', | |
| 		CC_dz='Algeria', | |
| 		CC_as='American Samoa', | |
| 		CC_ad='Andorra', | |
| 		CC_ao='Angola', | |
| 		CC_ai='Anguilla', | |
| 		CC_aq='Antarctica', | |
| 		CC_ag='Antigua and Barbuda', | |
| 		CC_ar='Argentina', | |
| 		CC_am='Armenia', | |
| 		CC_aw='Aruba', | |
| 		CC_au='Australia', | |
| 		CC_at='Austria', | |
| 		CC_az='Azerbaijan', | |
| 		CC_bs='Bahamas', | |
| 		CC_bh='Bahrain', | |
| 		CC_bd='Bangladesh', | |
| 		CC_bb='Barbados', | |
| 		CC_by='Belarus', | |
| 		CC_be='Belgium', | |
| 		CC_bz='Belize', | |
| 		CC_bj='Benin', | |
| 		CC_bm='Bermuda', | |
| 		CC_bt='Bhutan', | |
| 		CC_bo='Bolivia', | |
| 		CC_bq='Bonaire, Sint Eustatius and Saba', | |
| 		CC_ba='Bosnia and Herzegovina', | |
| 		CC_bw='Botswana', | |
| 		CC_bv='Bouvet Island', | |
| 		CC_br='Brazil', | |
| 		CC_io='British Indian Ocean Territory', | |
| 		CC_bn='Brunei Darussalam', | |
| 		CC_bg='Bulgaria', | |
| 		CC_bf='Burkina Faso', | |
| 		CC_bi='Burundi', | |
| 		CC_kh='Cambodia', | |
| 		CC_cm='Cameroon', | |
| 		CC_ca='Canada', | |
| 		CC_cv='Cape Verde', | |
| 		CC_ky='Cayman Islands', | |
| 		CC_cf='Central African Republic', | |
| 		CC_td='Chad', | |
| 		CC_cl='Chile', | |
| 		CC_cn='China', | |
| 		CC_cx='Christmas Island', | |
| 		CC_cc='Cocos (Keeling) Islands', | |
| 		CC_co='Colombia', | |
| 		CC_km='Comoros', | |
| 		CC_cg='Congo', | |
| 		CC_cd='Congo, The Democratic Republic of', | |
| 		CC_ck='Cook Islands', | |
| 		CC_cr='Costa Rica', | |
| 		CC_ci='Côte d\'ivoire', | |
| 		CC_hr='Croatia', | |
| 		CC_cu='Cuba', | |
| 		CC_cw='Curaçao', | |
| 		CC_cy='Cyprus', | |
| 		CC_cz='Czech Republic', | |
| 		CC_dk='Denmark', | |
| 		CC_dj='Djibouti', | |
| 		CC_dm='Dominica', | |
| 		CC_do='Dominican Republic', | |
| 		CC_ec='Ecuador', | |
| 		CC_eg='Egypt', | |
| 		CC_sv='El Salvador', | |
| 		CC_gq='Equatorial Guinea', | |
| 		CC_er='Eritrea', | |
| 		CC_ee='Estonia', | |
| 		CC_et='Ethiopia', | |
| 		CC_fk='Falkland Islands (Malvinas)', | |
| 		CC_fo='Faroe Islands', | |
| 		CC_fj='Fiji', | |
| 		CC_fi='Finland', | |
| 		CC_fr='France', | |
| 		CC_gf='French Guiana', | |
| 		CC_pf='French Polynesia', | |
| 		CC_tf='French Southern Territories', | |
| 		CC_ga='Gabon', | |
| 		CC_gm='Gambia', | |
| 		CC_ge='Georgia', | |
| 		CC_de='Germany', | |
| 		CC_gh='Ghana', | |
| 		CC_gi='Gibraltar', | |
| 		CC_gr='Greece', | |
| 		CC_gl='Greenland', | |
| 		CC_gd='Grenada', | |
| 		CC_gp='Guadeloupe', | |
| 		CC_gu='Guam', | |
| 		CC_gt='Guatemala', | |
| 		CC_gg='Guernsey', | |
| 		CC_gn='Guinea', | |
| 		CC_gw='Guinea-Bissau', | |
| 		CC_gy='Guyana', | |
| 		CC_ht='Haiti', | |
| 		CC_hm='Heard Island and McDonald Islands', | |
| 		CC_va='Holy See (Vatican City State)', | |
| 		CC_hn='Honduras', | |
| 		CC_hk='Hong Kong', | |
| 		CC_hu='Hungary', | |
| 		CC_is='Iceland', | |
| 		CC_in='India', | |
| 		CC_id='Indonesia', | |
| 		CC_ir='Iran, Islamic Republic of', | |
| 		CC_iq='Iraq', | |
| 		CC_ie='Ireland', | |
| 		CC_im='Isle of Man', | |
| 		CC_il='Israel', | |
| 		CC_it='Italy', | |
| 		CC_jm='Jamaica', | |
| 		CC_jp='Japan', | |
| 		CC_je='Jersey', | |
| 		CC_jo='Jordan', | |
| 		CC_kz='Kazakhstan', | |
| 		CC_ke='Kenya', | |
| 		CC_ki='Kiribati', | |
| 		CC_kp='Korea, Democratic People\'s Republic of', | |
| 		CC_kr='Korea, Republic of', | |
| 		CC_kw='Kuwait', | |
| 		CC_kg='Kyrgyzstan', | |
| 		CC_la='Lao People\'s Democratic Republic', | |
| 		CC_lv='Latvia', | |
| 		CC_lb='Lebanon', | |
| 		CC_ls='Lesotho', | |
| 		CC_lr='Liberia', | |
| 		CC_ly='Libya', | |
| 		CC_li='Liechtenstein', | |
| 		CC_lt='Lithuania', | |
| 		CC_lu='Luxembourg', | |
| 		CC_mo='Macao', | |
| 		CC_mk='Macedonia, The Former Yugoslav Republic of', | |
| 		CC_mg='Madagascar', | |
| 		CC_mw='Malawi', | |
| 		CC_my='Malaysia', | |
| 		CC_mv='Maldives', | |
| 		CC_ml='Mali', | |
| 		CC_mt='Malta', | |
| 		CC_mh='Marshall Islands', | |
| 		CC_mq='Martinique', | |
| 		CC_mr='Mauritania', | |
| 		CC_mu='Mauritius', | |
| 		CC_yt='Mayotte', | |
| 		CC_mx='Mexico', | |
| 		CC_fm='Micronesia, Federated States of', | |
| 		CC_md='Moldova, Republic of', | |
| 		CC_mc='Monaco', | |
| 		CC_mn='Mongolia', | |
| 		CC_me='Montenegro', | |
| 		CC_ms='Montserrat', | |
| 		CC_ma='Morocco', | |
| 		CC_mz='Mozambique', | |
| 		CC_mm='Myanmar', | |
| 		CC_na='Namibia', | |
| 		CC_nr='Nauru', | |
| 		CC_np='Nepal', | |
| 		CC_nl='Netherlands', | |
| 		CC_nc='New Caledonia', | |
| 		CC_nz='New Zealand', | |
| 		CC_ni='Nicaragua', | |
| 		CC_ne='Niger', | |
| 		CC_ng='Nigeria', | |
| 		CC_nu='Niue', | |
| 		CC_nf='Norfolk Island', | |
| 		CC_mp='Northern Mariana Islands', | |
| 		CC_no='Norway', | |
| 		CC_om='Oman', | |
| 		CC_pk='Pakistan', | |
| 		CC_pw='Palau', | |
| 		CC_ps='Palestinian Territory, Occupied', | |
| 		CC_pa='Panama', | |
| 		CC_pg='Papua New Guinea', | |
| 		CC_py='Paraguay', | |
| 		CC_pe='Peru', | |
| 		CC_ph='Philippines', | |
| 		CC_pn='Pitcairn', | |
| 		CC_pl='Poland', | |
| 		CC_pt='Portugal', | |
| 		CC_pr='Puerto Rico', | |
| 		CC_qa='Qatar', | |
| 		CC_re='Réunion', | |
| 		CC_ro='Romania', | |
| 		CC_ru='Russian Federation', | |
| 		CC_rw='Rwanda', | |
| 		CC_bl='Saint Barthélemy', | |
| 		CC_sh='Saint Helena, Ascension and Tristan da Cunha', | |
| 		CC_kn='Saint Kitts and Nevis', | |
| 		CC_lc='Saint Lucia', | |
| 		CC_mf='Saint Martin (French Part)', | |
| 		CC_pm='Saint Pierre and Miquelon', | |
| 		CC_vc='Saint Vincent and The Grenadines', | |
| 		CC_ws='Samoa', | |
| 		CC_sm='San Marino', | |
| 		CC_st='Sao Tome and Principe', | |
| 		CC_sa='Saudi Arabia', | |
| 		CC_sn='Senegal', | |
| 		CC_rs='Serbia', | |
| 		CC_sc='Seychelles', | |
| 		CC_sl='Sierra Leone', | |
| 		CC_sg='Singapore', | |
| 		CC_sk='Slovakia', | |
| 		CC_sx='Sint Maarten (Dutch Part)', | |
| 		CC_si='Slovenia', | |
| 		CC_sb='Solomon Islands', | |
| 		CC_so='Somalia', | |
| 		CC_za='South Africa', | |
| 		CC_gs='South Georgia and The South Sandwich Islands', | |
| 		CC_ss='South Sudan', | |
| 		CC_es='Spain', | |
| 		CC_lk='Sri Lanka', | |
| 		CC_sd='Sudan', | |
| 		CC_sr='Suriname', | |
| 		CC_sj='Svalbard and Jan Mayen', | |
| 		CC_sz='Swaziland', | |
| 		CC_se='Sweden', | |
| 		CC_ch='Switzerland', | |
| 		CC_sy='Syrian Arab Republic', | |
| 		CC_tw='Taiwan, Province of China', | |
| 		CC_tj='Tajikistan', | |
| 		CC_tz='Tanzania, United Republic of', | |
| 		CC_th='Thailand', | |
| 		CC_tl='Timor-Leste', | |
| 		CC_tg='Togo', | |
| 		CC_tk='Tokelau', | |
| 		CC_to='Tonga', | |
| 		CC_tt='Trinidad and Tobago', | |
| 		CC_tn='Tunisia', | |
| 		CC_tr='Turkey', | |
| 		CC_tm='Turkmenistan', | |
| 		CC_tc='Turks and Caicos Islands', | |
| 		CC_tv='Tuvalu', | |
| 		CC_ug='Uganda', | |
| 		CC_ua='Ukraine', | |
| 		CC_ae='United Arab Emirates', | |
| 		CC_gb='United Kingdom', | |
| 		CC_us='United States', | |
| 		CC_um='United States Minor Outlying Islands', | |
| 		CC_uy='Uruguay', | |
| 		CC_uz='Uzbekistan', | |
| 		CC_vu='Vanuatu', | |
| 		CC_ve='Venezuela', | |
| 		CC_vn='Viet Nam', | |
| 		CC_vg='Virgin Islands, British', | |
| 		CC_vi='Virgin Islands, U.S.', | |
| 		CC_wf='Wallis and Futuna', | |
| 		CC_eh='Western Sahara', | |
| 		CC_ye='Yemen', | |
| 		CC_zm='Zambia', | |
| 		CC_zw='Zimbabwe'; | |
| 	//@} | |
|  | |
| 	//@{ ISO 639-1 language codes (Windows-compatibility subset) | |
| 	const | |
| 		LC_af='Afrikaans', | |
| 		LC_am='Amharic', | |
| 		LC_ar='Arabic', | |
| 		LC_as='Assamese', | |
| 		LC_ba='Bashkir', | |
| 		LC_be='Belarusian', | |
| 		LC_bg='Bulgarian', | |
| 		LC_bn='Bengali', | |
| 		LC_bo='Tibetan', | |
| 		LC_br='Breton', | |
| 		LC_ca='Catalan', | |
| 		LC_co='Corsican', | |
| 		LC_cs='Czech', | |
| 		LC_cy='Welsh', | |
| 		LC_da='Danish', | |
| 		LC_de='German', | |
| 		LC_dv='Divehi', | |
| 		LC_el='Greek', | |
| 		LC_en='English', | |
| 		LC_es='Spanish', | |
| 		LC_et='Estonian', | |
| 		LC_eu='Basque', | |
| 		LC_fa='Persian', | |
| 		LC_fi='Finnish', | |
| 		LC_fo='Faroese', | |
| 		LC_fr='French', | |
| 		LC_gd='Scottish Gaelic', | |
| 		LC_gl='Galician', | |
| 		LC_gu='Gujarati', | |
| 		LC_he='Hebrew', | |
| 		LC_hi='Hindi', | |
| 		LC_hr='Croatian', | |
| 		LC_hu='Hungarian', | |
| 		LC_hy='Armenian', | |
| 		LC_id='Indonesian', | |
| 		LC_ig='Igbo', | |
| 		LC_is='Icelandic', | |
| 		LC_it='Italian', | |
| 		LC_ja='Japanese', | |
| 		LC_ka='Georgian', | |
| 		LC_kk='Kazakh', | |
| 		LC_km='Khmer', | |
| 		LC_kn='Kannada', | |
| 		LC_ko='Korean', | |
| 		LC_lb='Luxembourgish', | |
| 		LC_lo='Lao', | |
| 		LC_lt='Lithuanian', | |
| 		LC_lv='Latvian', | |
| 		LC_mi='Maori', | |
| 		LC_ml='Malayalam', | |
| 		LC_mr='Marathi', | |
| 		LC_ms='Malay', | |
| 		LC_mt='Maltese', | |
| 		LC_ne='Nepali', | |
| 		LC_nl='Dutch', | |
| 		LC_no='Norwegian', | |
| 		LC_oc='Occitan', | |
| 		LC_or='Oriya', | |
| 		LC_pl='Polish', | |
| 		LC_ps='Pashto', | |
| 		LC_pt='Portuguese', | |
| 		LC_qu='Quechua', | |
| 		LC_ro='Romanian', | |
| 		LC_ru='Russian', | |
| 		LC_rw='Kinyarwanda', | |
| 		LC_sa='Sanskrit', | |
| 		LC_si='Sinhala', | |
| 		LC_sk='Slovak', | |
| 		LC_sl='Slovenian', | |
| 		LC_sq='Albanian', | |
| 		LC_sv='Swedish', | |
| 		LC_ta='Tamil', | |
| 		LC_te='Telugu', | |
| 		LC_th='Thai', | |
| 		LC_tk='Turkmen', | |
| 		LC_tr='Turkish', | |
| 		LC_tt='Tatar', | |
| 		LC_uk='Ukrainian', | |
| 		LC_ur='Urdu', | |
| 		LC_vi='Vietnamese', | |
| 		LC_wo='Wolof', | |
| 		LC_yo='Yoruba', | |
| 		LC_zh='Chinese'; | |
| 	//@} | |
|  | |
| 	/** | |
| 	*	Return list of languages indexed by ISO 639-1 language code | |
| 	*	@return array | |
| 	**/ | |
| 	function languages() { | |
| 		return \Base::instance()->constants($this,'LC_'); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Return list of countries indexed by ISO 3166-1 country code | |
| 	*	@return array | |
| 	**/ | |
| 	function countries() { | |
| 		return \Base::instance()->constants($this,'CC_'); | |
| 	} | |
| 
 | |
| } | |
| 
 | |
| //! Container for singular object instances | |
| final class Registry { | |
| 
 | |
| 	private static | |
| 		//! Object catalog | |
| 		$table; | |
| 
 | |
| 	/** | |
| 	*	Return TRUE if object exists in catalog | |
| 	*	@return bool | |
| 	*	@param $key string | |
| 	**/ | |
| 	static function exists($key) { | |
| 		return isset(self::$table[$key]); | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Add object to catalog | |
| 	*	@return object | |
| 	*	@param $key string | |
| 	*	@param $obj object | |
| 	**/ | |
| 	static function set($key,$obj) { | |
| 		return self::$table[$key]=$obj; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Retrieve object from catalog | |
| 	*	@return object | |
| 	*	@param $key string | |
| 	**/ | |
| 	static function get($key) { | |
| 		return self::$table[$key]; | |
| 	} | |
| 
 | |
| 	/** | |
| 	*	Delete object from catalog | |
| 	*	@param $key string | |
| 	**/ | |
| 	static function clear($key) { | |
| 		self::$table[$key]=NULL; | |
| 		unset(self::$table[$key]); | |
| 	} | |
| 
 | |
| 	//! Prohibit cloning | |
| 	private function __clone() { | |
| 	} | |
| 
 | |
| 	//! Prohibit instantiation | |
| 	private function __construct() { | |
| 	} | |
| 
 | |
| } | |
| 
 | |
| return Base::instance();
 | |
| 
 |