IZcontrol is a server side application that makes it possible to easily control remote appliances over the network using telnet, UDP and other protocols.
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.

3585 lines
89 KiB

4 years ago
<?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();