signaturepdf/vendor/fatfree/base.php

3620 lines
91 KiB
PHP

<?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.8.1-Dev';
//@}
//@{ 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,-1,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 ($val && 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],'',['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],''],['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],'',$jar);
} else {
unset($jar['samesite']);
call_user_func_array('setcookie',
array_merge([$parts[1],''],$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
]
);
$php81=version_compare(PHP_VERSION, '8.1.0')>=0;
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 ($php81) {
$lang = $this->split($this->LANGUAGE);
// requires intl extension
$formatter = new IntlDateFormatter($lang[0],
(empty($mod) || $mod=='short')
? IntlDateFormatter::SHORT :
($mod=='full' ? IntlDateFormatter::LONG : IntlDateFormatter::MEDIUM),
IntlDateFormatter::NONE);
return $formatter->format($args[$pos]);
} else {
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 ($php81) {
$lang = $this->split($this->LANGUAGE);
// requires intl extension
$formatter = new IntlDateFormatter($lang[0],
IntlDateFormatter::NONE,
(empty($mod) || $mod=='short')
? IntlDateFormatter::SHORT :
($mod=='full' ? IntlDateFormatter::LONG : IntlDateFormatter::MEDIUM),
IntlTimeZone::createTimeZone($this->hive['TZ']));
return $formatter->format($args[$pos]);
} else {
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',round($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).'$/',(string) $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['QUIET']) {
$error=array_diff_key(
$this->hive['ERROR'],
$this->hive['DEBUG']?
[]:
['trace'=>1]
);
if ($this->hive['CLI'])
echo PHP_EOL.'==================================='.PHP_EOL.
'ERROR '.$error['code'].' - '.$error['status'].PHP_EOL.
$error['text'].PHP_EOL.PHP_EOL.(isset($error['trace']) ? $error['trace'] : '');
else
echo $this->hive['AJAX']?
json_encode($error):
('<!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(isset($url['query'])?$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(round(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
**/
#[\ReturnTypeWillChange]
function offsetexists($key) {
return $this->exists($key);
}
/**
* Convenience method for assigning hive value
* @return mixed
* @param $key string
* @param $val mixed
**/
#[\ReturnTypeWillChange]
function offsetset($key,$val) {
return $this->set($key,$val);
}
/**
* Convenience method for retrieving hive value
* @return mixed
* @param $key string
**/
#[\ReturnTypeWillChange]
function &offsetget($key) {
$val=&$this->ref($key);
return $val;
}
/**
* Convenience method for removing hive key
* @param $key string
**/
#[\ReturnTypeWillChange]
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 (!empty($headers['X-Forwarded-Port']))
$port=$headers['X-Forwarded-Port'];
elseif (!empty($_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'].
(!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;
$GLOBALS['_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();