diff --git a/app/lib/CHANGELOG b/app/lib/CHANGELOG index cf2dab51..f831d7d6 100644 --- a/app/lib/CHANGELOG +++ b/app/lib/CHANGELOG @@ -1,5 +1,112 @@ CHANGELOG +3.6.0 (19 November 2016) +* NEW: [cli] request type +* NEW: console-friendly CLI mode +* NEW: lexicon caching +* NEW: Silent operator skips startup error check (#125) +* NEW: DB\SQL->trans() +* NEW: custom config section parser, i.e. [conf > Foo::bar] +* NEW: support for cache tags in SQL +* NEW: custom FORMATS +* NEW: Mongo mapper fields whitelist +* NEW: WebSocket server +* NEW: Base->extend method (#158) +* NEW: Implement framework variable caching via config, i.e. FOO = "bar" | 3600 +* NEW: Lightweight OAuth2 client +* NEW: SEED variable, configurable app-specific hashing prefix (#149, bcosca/fatfree#951, bcosca/fatfree#884, bcosca/fatfree#629) +* NEW: CLI variable +* NEW: Web->send, specify custom filename (#124) +* NEW: Web->send, added flushing flag (#131) +* NEW: Indexed route wildcards, now exposed in PARAMS['*'] +* Changed: PHP 5.4 is now the minimum version requirement +* Changed: Prevent database wrappers from being cloned +* Changed: Router works on PATH instead of URI (#126) NB: PARAMS.0 no longer contains the query string +* Changed: Removed ALIASES autobuilding (#118) +* Changed: Route wildcards match empty strings (#119) +* Changed: Disable default debug highlighting, HIGHLIGHT is false now +* General PHP 5.4 optimizations +* Optimized config parsing +* Optimized Base->recursive +* Optimized header extraction +* Optimized cache/expire headers +* Optimized session_start behaviour (bcosca/fatfree#673) +* Optimized reroute regex +* Tweaked cookie removal +* Better route precedence order +* Performance tweak: reduced cache calls +* Refactored lexicon (LOCALES) build-up, much faster now +* Added turkish locale bug workaround +* Geo->tzinfo Update to UTC +* Added Xcache reset (bcosca/fatfree#928) +* Redis cache: allow db name in dsn +* SMTP: Improve server emulation responses +* SMTP: Optimize transmission envelope +* SMTP: Implement mock transmission +* SMTP: Various bug fixes and feature improvements +* SMTP: quit on failed authentication +* Geo->weather: force metric units +* Base->until: Implement CLI interoperability +* Base->format: looser plural syntax +* Base->format: Force decimal as default number format +* Base->merge: Added $keep flag to save result to the hive key +* Base->reroute: Allow array as URL argument for aliasing +* Base->alias: Allow query string (or array) to be appended to alias +* Permit reroute to named routes with URL query segment +* Sync COOKIE global on set() +* Permit non-hive variables to use JS dot notation +* RFC2616: Use absolute URIs for Location header +* Matrix->calendar: Check if calendar extension is loaded +* Markdown: require start of line/whitespace for text processing (#136) +* DB\[SQL|Jig|Mongo]->log(FALSE) disables logging +* DB\SQL->exec: Added timestamp toggle to db log +* DB\SQL->schema: Remove unnecessary line terminators +* DB\SQL\Mapper: allow array filter with empty string +* DB\SQL\Mapper: optimized handling for key-less tables +* DB\SQL\Mapper: added float support (#106) +* DB\SQL\Session: increased default column sizes (#148, bcosca/fatfree#931, bcosca/fatfree#950) +* Web: Catch cURL errors +* Optimize Web->receive (bcosca/fatfree#930) +* Web->minify: fix arbitrary file download vulnerability +* Web->request: fix cache control max-age detection (bcosca/fatfree#908) +* Web->request: Add request headers & error message to return value (bcosca/fatfree#737) +* Web->request: Refactored response to HTTP request +* Web->send flush while sending big files +* Image->rgb: allow hex strings +* Image->captcha: Check if GD module supports TrueType +* Image->load: Return FALSE on load failure +* Image->resize: keep aspect ratio when only width or height was given +* Updated OpenID lib (bcosca/fatfree#965) +* Audit->card: add new mastercard "2" BIN range (bcosca/fatfree#954) +* Deprecated: Bcrypt class +* Preview->render: optimized detection to remove short open PHP tags and allow xml tags (#133) +* Display file and line number in exception handler (bcosca/fatfree#967) +* Added error reporting level to Base->error and ERROR.level (bcosca/fatfree#957) +* Added optional custom cache instance to Session (#141) +* CLI-aware mock() +* XFRAME and PACKAGE can be switched off now (#128) +* Bug fix: wrong time calculation on memcache reset (#170) +* Bug fix: encode CLI parameters +* Bug fix: Close connection on abort explicitly (#162) +* Bug fix: Image->identicon, Avoid double-size sprite rotation (and possible segfault) +* Bug fix: Magic->offsetset, access property as array element (#147) +* Bug fix: multi-line custom template tag parsing (bcosca/fatfree#935) +* Bug fix: cache headers on errors (bcosca/fatfree#885) +* Bug fix: Web, deprecated CURLOPT_SSL_VERIFYHOST in curl +* Bug fix: Web, Invalid user error constant (bcosca/fatfree#962) +* Bug fix: Web->request, redirections for domain-less location (#135) +* Bug fix: DB\SQL\Mapper, reset changed flag after update (#142, #152) +* Bug fix: DB\SQL\Mapper, fix changed flag when using assignment operator #143 #150 #151 +* Bug fix: DB\SQL\Mapper, revival of the HAVING clause +* Bug fix: DB\SQL\Mapper, pgsql with non-integer primary keys (bcosca/fatfree#916) +* Bug fix: DB\SQL\Session, quote table name (bcosca/fatfree#977) +* Bug fix: snakeCase returns word starting with underscore (bcosca/fatfree#927) +* Bug fix: mock does not populate PATH variable +* Bug fix: Geo->weather API key (#129) +* Bug fix: Incorrect compilation of array element with zero index +* Bug fix: Compilation of array construct is incorrect +* Bug fix: Trailing slash redirection on UTF-8 paths (#121) + 3.5.1 (31 December 2015) * NEW: ttl attribute in template tag * NEW: allow anonymous function for template filter diff --git a/app/lib/audit.php b/app/lib/audit.php index 1338ca8c..4c91740d 100644 --- a/app/lib/audit.php +++ b/app/lib/audit.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -47,7 +47,7 @@ class Audit extends Prefab { * @param $mx boolean **/ function email($str,$mx=TRUE) { - $hosts=array(); + $hosts=[]; return is_string(filter_var($str,FILTER_VALIDATE_EMAIL)) && (!$mx || getmxrr(substr($str,strrpos($str,'@')+1),$hosts)); } @@ -166,7 +166,8 @@ class Audit extends Prefab { return 'Discover'; if (preg_match('/^(?:2131|1800|35\d{3})\d{11}$/',$id)) return 'JCB'; - if (preg_match('/^5[1-5][0-9]{14}$/',$id)) + if (preg_match('/^5[1-5][0-9]{14}$|'. + '^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)\d{12}$/',$id)) return 'MasterCard'; if (preg_match('/^4[0-9]{12}(?:[0-9]{3})?$/',$id)) return 'Visa'; diff --git a/app/lib/auth.php b/app/lib/auth.php index 5f7e0508..56f89ed5 100644 --- a/app/lib/auth.php +++ b/app/lib/auth.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -20,7 +20,6 @@ */ - //! Authorization/authentication plug-in class Auth { @@ -48,19 +47,19 @@ class Auth { protected function _jig($id,$pw,$realm) { return (bool) call_user_func_array( - array($this->mapper,'load'), - array( + [$this->mapper,'load'], + [ array_merge( - array( + [ '@'.$this->args['id'].'==? AND '. '@'.$this->args['pw'].'==?'. (isset($this->args['realm'])? (' AND @'.$this->args['realm'].'==?'):''), $id,$pw - ), - (isset($this->args['realm'])?array($realm):array()) + ], + (isset($this->args['realm'])?[$realm]:[]) ) - ) + ] ); } @@ -74,12 +73,12 @@ class Auth { protected function _mongo($id,$pw,$realm) { return (bool) $this->mapper->load( - array( + [ $this->args['id']=>$id, $this->args['pw']=>$pw - )+ + ]+ (isset($this->args['realm'])? - array($this->args['realm']=>$realm):array()) + [$this->args['realm']=>$realm]:[]) ); } @@ -93,19 +92,19 @@ class Auth { protected function _sql($id,$pw,$realm) { return (bool) call_user_func_array( - array($this->mapper,'load'), - array( + [$this->mapper,'load'], + [ array_merge( - array( + [ $this->args['id'].'=? AND '. $this->args['pw'].'=?'. (isset($this->args['realm'])? (' AND '.$this->args['realm'].'=?'):''), $id,$pw - ), - (isset($this->args['realm'])?array($realm):array()) + ], + (isset($this->args['realm'])?[$realm]:[]) ) - ) + ] ); } diff --git a/app/lib/base.php b/app/lib/base.php index ea1a1f09..b5a7bdca 100644 --- a/app/lib/base.php +++ b/app/lib/base.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -45,7 +45,7 @@ final class Base extends Prefab implements ArrayAccess { //@{ Framework details const PACKAGE='Fat-Free Framework', - VERSION='3.5.1-Release'; + VERSION='3.6.0-Release'; //@} //@{ HTTP status codes (RFC 2616) @@ -102,10 +102,11 @@ final class Base extends Prefab implements ArrayAccess { //! Syntax highlighting stylesheet CSS='code.css'; - //@{ HTTP request types + //@{ Request types const REQ_SYNC=1, - REQ_AJAX=2; + REQ_AJAX=2, + REQ_CLI=4; //@} //@{ Error messages @@ -153,26 +154,30 @@ final class Base extends Prefab implements ArrayAccess { * Replace tokenized URL with available token values * @return string * @param $url array|string - * @param $params array + * @param $args array **/ - function build($url,$params=array()) { - $params+=$this->hive['PARAMS']; + function build($url,$args=[]) { + $args+=$this->hive['PARAMS']; if (is_array($url)) foreach ($url as &$var) { - $var=$this->build($var,$params); + $var=$this->build($var,$args); unset($var); } else { $i=0; - $url=preg_replace_callback('/@(\w+)|\*/', - function($match) use(&$i,$params) { - $i++; + $url=preg_replace_callback('/@(\w+)|(\*)/', + function($match) use(&$i,$args) { if (isset($match[1]) && - array_key_exists($match[1],$params)) - return $params[$match[1]]; - return array_key_exists($i,$params)? - $params[$i]: - $match[0]; + array_key_exists($match[1],$args)) + return $args[$match[1]]; + if (isset($match[2]) && + array_key_exists($match[2],$args)) { + if (!is_array($args[$match[2]])) + return $args[$match[2]]; + $i++; + return $args[$match[2]][$i-1]; + } + return $match[0]; },$url); } return $url; @@ -183,14 +188,17 @@ final class Base extends Prefab implements ArrayAccess { * @return string * @param $name string * @param $params array|string + * @param $query string|array **/ - function alias($name,$params=array()) { + function alias($name,$params=[],$query=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); - return $url; + if (is_array($query)) + $query=http_build_query($query); + return $url.($query?('?'.$query):''); } /** @@ -199,11 +207,17 @@ final class Base extends Prefab implements ArrayAccess { * @param $str string **/ function parse($str) { - preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/', + preg_match_all('/(\w+|\*)\h*=\h*(?:\[(.+?)\]|(.+?))(?=,|$)/', $str,$pairs,PREG_SET_ORDER); - $out=array(); + $out=[]; foreach ($pairs as $pair) - $out[$pair[1]]=trim($pair[2]); + 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; } @@ -224,12 +238,17 @@ final class Base extends Prefab implements ArrayAccess { ((function_exists($expr[1])? ('.'.$expr[1]): ('['.var_export($expr[1],TRUE).']')).'('): - ('['.var_export( - isset($expr[3])? - trim($fw->compile($expr[3])): - (ctype_digit($expr[2])? + ('['. + (isset($expr[3])? + (strlen($expr[3])? + var_export( + trim($fw->compile($expr[3])), + TRUE): + ''): + var_export(ctype_digit($expr[2])? (int)$expr[2]: - $expr[2]),TRUE).']'); + $expr[2],TRUE)). + ']'); }, $var[1] ); @@ -244,21 +263,25 @@ final class Base extends Prefab implements ArrayAccess { * @return mixed * @param $key string * @param $add bool + * @param $var mixed **/ - function &ref($key,$add=TRUE) { + function &ref($key,$add=TRUE,&$var=NULL) { $null=NULL; $parts=$this->cut($key); if ($parts[0]=='SESSION') { - @session_start(); + 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 ($add) - $var=&$this->hive; - else - $var=$this->hive; + if (is_null($var)) { + if ($add) + $var=&$this->hive; + else + $var=$this->hive; + } $obj=FALSE; foreach ($parts as $part) if ($part=='->') @@ -276,7 +299,7 @@ final class Base extends Prefab implements ArrayAccess { } else { if (!is_array($var)) - $var=array(); + $var=[]; if ($add || array_key_exists($part,$var)) $var=&$var[$part]; else { @@ -284,8 +307,6 @@ final class Base extends Prefab implements ArrayAccess { break; } } - if ($parts[0]=='ALIASES') - $var=$this->build($var); return $var; } @@ -330,9 +351,15 @@ final class Base extends Prefab implements ArrayAccess { if ($expr[1]=='COOKIE') { $parts=$this->cut($key); $jar=$this->unserialize($this->serialize($this->hive['JAR'])); + if (isset($_COOKIE[$parts[1]])) { + $jar['expire']=strtotime('-1 year'); + call_user_func_array('setcookie', + array_merge([$parts[1],NULL],$jar)); + } if ($ttl) $jar['expire']=$time+$ttl; - call_user_func_array('setcookie',array($parts[1],$val)+$jar); + call_user_func_array('setcookie',[$parts[1],$val]+$jar); + $_COOKIE[$parts[1]]=$val; return $val; } } @@ -351,10 +378,14 @@ final class Base extends Prefab implements ArrayAccess { case 'LANGUAGE': if (!isset($lang)) $val=$this->language($val); - $lex=$this->lexicon($this->hive['LOCALES']); + $lex=$this->lexicon($this->hive['LOCALES'],$ttl); case 'LOCALES': - if (isset($lex) || $lex=$this->lexicon($val)) - $this->mset($lex,$this->hive['PREFIX'],$ttl); + 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); @@ -367,10 +398,9 @@ final class Base extends Prefab implements ArrayAccess { $jar['expire']-=$time; call_user_func_array('session_set_cookie_params',$jar); } - $cache=Cache::instance(); - if ($cache->exists($hash=$this->hash($key).'.var') || $ttl) + if ($ttl) // Persist the key-value pair - $cache->set($hash,$val,$ttl); + Cache::instance()->set($this->hash($key).'.var',$val,$ttl); return $ref; } @@ -383,8 +413,8 @@ final class Base extends Prefab implements ArrayAccess { function get($key,$args=NULL) { if (is_string($val=$this->ref($key,FALSE)) && !is_null($args)) return call_user_func_array( - array($this,'format'), - array_merge(array($val),is_array($args)?$args:array($args)) + [$this,'format'], + array_merge([$val],is_array($args)?$args:[$args]) ); if (is_null($val)) { // Attempt to retrieve from cache @@ -413,12 +443,13 @@ final class Base extends Prefab implements ArrayAccess { $jar=$this->hive['JAR']; $jar['expire']=strtotime('-1 year'); call_user_func_array('setcookie', - array_merge(array($parts[1],''),$jar)); + array_merge([$parts[1],NULL],$jar)); unset($_COOKIE[$parts[1]]); } } elseif ($parts[0]=='SESSION') { - @session_start(); + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); if (empty($parts[1])) { // End session session_unset(); @@ -571,10 +602,33 @@ final class Base extends Prefab implements ArrayAccess { * @return array * @param $key string * @param $src string|array + * @param $keep bool **/ - function merge($key,$src) { + function merge($key,$src,$keep=FALSE) { $ref=&$this->ref($key); - return array_merge($ref,is_string($src)?$this->hive[$src]:$src); + 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; } /** @@ -610,7 +664,7 @@ final class Base extends Prefab implements ArrayAccess { return '*RECURSION*'; } else - $stack=array(); + $stack=[]; switch (gettype($arg)) { case 'object': $str=''; @@ -618,8 +672,8 @@ final class Base extends Prefab implements ArrayAccess { $str.=($str?',':''). var_export($key,TRUE).'=>'. $this->stringify($val, - array_merge($stack,array($arg))); - return get_class($arg).'::__set_state(array('.$str.'))'; + array_merge($stack,[$arg])); + return get_class($arg).'::__set_state(['.$str.'])'; case 'array': $str=''; $num=isset($arg[0]) && @@ -627,9 +681,8 @@ final class Base extends Prefab implements ArrayAccess { foreach ($arg as $key=>$val) $str.=($str?',':''). ($num?'':(var_export($key,TRUE).'=>')). - $this->stringify($val, - array_merge($stack,array($arg))); - return 'array('.$str.')'; + $this->stringify($val,array_merge($stack,[$arg])); + return '['.$str.']'; default: return var_export($arg,TRUE); } @@ -642,7 +695,7 @@ final class Base extends Prefab implements ArrayAccess { **/ function csv(array $args) { return implode(',',array_map('stripcslashes', - array_map(array($this,'stringify'),$args))); + array_map([$this,'stringify'],$args))); } /** @@ -666,7 +719,7 @@ final class Base extends Prefab implements ArrayAccess { * @param $str string **/ function snakecase($str) { - return strtolower(preg_replace('/[[:upper:]]/','_\0',$str)); + return strtolower(preg_replace('/(?!^)[[:upper:]]/','_\0',$str)); } /** @@ -680,14 +733,15 @@ final class Base extends Prefab implements ArrayAccess { } /** - * Extract values of an associative array whose keys start with the given prefix + * Extract values of array whose keys start with the given prefix * @return array * @param $arr array * @param $prefix string **/ function extract($arr,$prefix) { - $out=array(); - foreach (preg_grep('/^'.preg_quote($prefix,'/').'/',array_keys($arr)) as $key) + $out=[]; + foreach (preg_grep('/^'.preg_quote($prefix,'/').'/',array_keys($arr)) + as $key) $out[substr($key,strlen($prefix))]=$arr[$key]; return $out; } @@ -749,33 +803,29 @@ final class Base extends Prefab implements ArrayAccess { * @param $func callback * @param $stack array **/ - function recursive($arg,$func,$stack=NULL) { + function recursive($arg,$func,$stack=[]) { if ($stack) { foreach ($stack as $node) if ($arg===$node) return $arg; } - else - $stack=array(); switch (gettype($arg)) { case 'object': - if (method_exists('ReflectionClass','iscloneable')) { - $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,array($arg))); - } + $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=array(); + $copy=[]; foreach ($arg as $key=>$val) $copy[$key]=$this->recursive($val,$func, - array_merge($stack,array($arg))); + array_merge($stack,[$arg])); return $copy; } return $func($arg); @@ -822,20 +872,23 @@ final class Base extends Prefab implements ArrayAccess { $conv=localeconv(); return preg_replace_callback( '/\{(?P\d+)\s*(?:,\s*(?P\w+)\s*'. - '(?:,\s*(?P(?:\w+(?:\s*\{.+?\}\s*,?)?)*)'. + '(?:,\s*(?P(?:\w+(?:\s*\{.+?\}\s*,?\s*)?)*)'. '(?:,\s*(?P.+?))?)?)?\}/', function($expr) use($args,$conv) { extract($expr); extract($conv); if (!array_key_exists($pos,$args)) return $expr[0]; - if (isset($type)) + if (isset($type)) { + if (isset($this->hive['FORMATS'][$type])) + return $this->call($this->hive['FORMATS'][$type], + [$args[$pos],$mod,isset($prop)?$prop:null]); switch ($type) { case 'plural': preg_match_all('/(?\w+)'. '(?:\s*\{\s*(?.+?)\s*\})/', $mod,$matches,PREG_SET_ORDER); - $ord=array('zero','one','two'); + $ord=['zero','one','two']; foreach ($matches as $match) { extract($match); if (isset($ord[$args[$pos]]) && @@ -850,12 +903,14 @@ final class Base extends Prefab implements ArrayAccess { $args[$pos],0,'',$thousands_sep); case 'currency': $int=$cstm=false; - if (isset($prop) && $cstm=!$int=($prop=='int')) + if (isset($prop) && + $cstm=!$int=($prop=='int')) $currency_symbol=$prop; - if (!$cstm && function_exists('money_format')) + if (!$cstm && + function_exists('money_format')) return money_format( '%'.($int?'i':'n'),$args[$pos]); - $fmt=array( + $fmt=[ 0=>'(nc)',1=>'(n c)', 2=>'(nc)',10=>'+nc', 11=>'+n c',12=>'+ nc', @@ -871,7 +926,7 @@ final class Base extends Prefab implements ArrayAccess { 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'; @@ -881,14 +936,14 @@ final class Base extends Prefab implements ArrayAccess { $pre='p'; } return str_replace( - array('+','n','c'), - array($sgn,number_format( + ['+','n','c'], + [$sgn,number_format( abs($args[$pos]), $frac_digits, $decimal_point, $thousands_sep), $int?$int_curr_symbol - :$currency_symbol), + :$currency_symbol], $fmt[(int)( (${$pre.'_cs_precedes'}%2). (${$pre.'_sign_posn'}%5). @@ -899,12 +954,10 @@ final class Base extends Prefab implements ArrayAccess { return number_format( $args[$pos]*100,0,$decimal_point, $thousands_sep).'%'; - case 'decimal': - return number_format( - $args[$pos],isset($prop)?$prop:2, - $decimal_point,$thousands_sep); } - break; + return number_format( + $args[$pos],isset($prop)?$prop:2, + $decimal_point,$thousands_sep); case 'date': if (empty($mod) || $mod=='short') $prop='%x'; @@ -918,6 +971,7 @@ final class Base extends Prefab implements ArrayAccess { default: return $expr[0]; } + } return $args[$pos]; }, $val @@ -932,8 +986,8 @@ final class Base extends Prefab implements ArrayAccess { function language($code) { $code=preg_replace('/\h+|;q=[0-9.]+/','',$code); $code.=($code?',':'').$this->fallback; - $this->languages=array(); - foreach (array_reverse(explode(',',$code)) as $lang) { + $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]); @@ -943,11 +997,11 @@ final class Base extends Prefab implements ArrayAccess { array_unshift($this->languages,$parts[0]); } } - } $this->languages=array_unique($this->languages); - $locales=array(); + $locales=[]; $windows=preg_match('/^win/i',PHP_OS); - foreach ($this->languages as $locale) { + // 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]); @@ -966,10 +1020,16 @@ final class Base extends Prefab implements ArrayAccess { * Return lexicon entries * @return array * @param $path string + * @param $ttl int **/ - function lexicon($path) { - $lex=array(); - foreach ($this->languages?:explode(',',$this->fallback) as $lang) + function lexicon($path,$ttl=0) { + $languages=$this->languages?:explode(',',$this->fallback); + $cache=Cache::instance(); + if ($cache->exists( + $hash=$this->hash(implode(',',$languages)).'.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')) && @@ -994,6 +1054,8 @@ final class Base extends Prefab implements ArrayAccess { '/\\\\\h*\r?\n/','',$match['rval'])); } } + if ($ttl) + $cache->set($hash,$lex,$ttl); return $lex; } @@ -1032,7 +1094,7 @@ final class Base extends Prefab implements ArrayAccess { **/ function status($code) { $reason=@constant('self::HTTP_'.$code); - if (PHP_SAPI!='cli' && !headers_sent()) + if (!$this->hive['CLI'] && !headers_sent()) header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason); return $reason; } @@ -1043,20 +1105,25 @@ final class Base extends Prefab implements ArrayAccess { * @param $secs int **/ function expire($secs=0) { - if (PHP_SAPI!='cli') { - header('X-Content-Type-Options: nosniff'); - header('X-Frame-Options: '.$this->hive['XFRAME']); - header('X-Powered-By: '.$this->hive['PACKAGE']); + if (!$this->hive['CLI'] && !headers_sent()) { + 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'); - if ($secs) { + header('X-Content-Type-Options: nosniff'); + if ($this->hive['VERB']=='GET' && $secs) { $time=microtime(TRUE); header_remove('Pragma'); + header('Cache-Control: max-age='.(int)$secs); header('Expires: '.gmdate('r',$time+$secs)); - header('Cache-Control: max-age='.$secs); header('Last-Modified: '.gmdate('r')); } - else + else { + header('Pragma: no-cache'); header('Cache-Control: no-cache, no-store, must-revalidate'); + header('Expires: '.gmdate('r',0)); + } } } @@ -1099,14 +1166,14 @@ final class Base extends Prefab implements ArrayAccess { } /** - * Return filtered, formatted stack trace + * 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) { + function trace(array $trace=NULL,$format=TRUE) { if (!$trace) { - $trace=debug_backtrace(FALSE); + $trace=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); $frame=$trace[0]; if (isset($frame['file']) && $frame['file']==__FILE__) array_shift($trace); @@ -1150,8 +1217,9 @@ final class Base extends Prefab implements ArrayAccess { * @param $code int * @param $text string * @param $trace array + * @param $level int **/ - function error($code,$text='',array $trace=NULL) { + function error($code,$text='',array $trace=NULL,$level=0) { $prior=$this->hive['ERROR']; $header=$this->status($code); $req=$this->hive['VERB'].' '.$this->hive['PATH']; @@ -1162,22 +1230,24 @@ final class Base extends Prefab implements ArrayAccess { foreach (explode("\n",$trace) as $nexus) if ($nexus) error_log($nexus); - if ($highlight=PHP_SAPI!='cli' && !$this->hive['AJAX'] && + if ($highlight=!$this->hive['CLI'] && !$this->hive['AJAX'] && $this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS)) $trace=$this->highlight($trace); - $this->hive['ERROR']=array( + $this->hive['ERROR']=[ 'status'=>$header, 'code'=>$code, 'text'=>$text, - 'trace'=>$trace - ); + 'trace'=>$trace, + 'level'=>$level + ]; + $this->expire(-1); $handler=$this->hive['ONERROR']; $this->hive['ONERROR']=NULL; $eol="\n"; if ((!$handler || - $this->call($handler,array($this,$this->hive['PARAMS']), + $this->call($handler,[$this,$this->hive['PARAMS']], 'beforeroute,afterroute')===FALSE) && - !$prior && PHP_SAPI!='cli' && !$this->hive['QUIET']) + !$prior && !$this->hive['CLI'] && !$this->hive['QUIET']) echo $this->hive['AJAX']? json_encode($this->hive['ERROR']): (''.$eol. @@ -1208,8 +1278,8 @@ final class Base extends Prefab implements ArrayAccess { function mock($pattern, array $args=NULL,array $headers=NULL,$body=NULL) { if (!$args) - $args=array(); - $types=array('sync','ajax'); + $args=[]; + $types=['sync','ajax','cli']; preg_match('/([\|\w]+)\h+(?:@(\w+)(?:(\(.+?)\))*|([^\h]+))'. '(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts); $verb=strtoupper($parts[1]); @@ -1218,7 +1288,7 @@ final class Base extends Prefab implements ArrayAccess { 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]):array()); + isset($parts[3])?$this->parse($parts[3]):[]); } if (empty($parts[4])) user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR); @@ -1226,11 +1296,12 @@ final class Base extends Prefab implements ArrayAccess { parse_str(@$url['query'],$GLOBALS['_GET']); if (preg_match('/GET|HEAD/',$verb)) $GLOBALS['_GET']=array_merge($GLOBALS['_GET'],$args); - $GLOBALS['_POST']=$verb=='POST'?$args:array(); + $GLOBALS['_POST']=$verb=='POST'?$args:[]; $GLOBALS['_REQUEST']=array_merge($GLOBALS['_GET'],$GLOBALS['_POST']); - foreach ($headers?:array() as $key=>$val) + 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']); @@ -1239,6 +1310,8 @@ final class Base extends Prefab implements ArrayAccess { $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(); } @@ -1251,7 +1324,7 @@ final class Base extends Prefab implements ArrayAccess { * @param $kbps int **/ function route($pattern,$handler,$ttl=0,$kbps=0) { - $types=array('sync','ajax'); + $types=['sync','ajax','cli']; $alias=null; if (is_array($pattern)) { foreach ($pattern as $item) @@ -1269,39 +1342,43 @@ final class Base extends Prefab implements ArrayAccess { } if (empty($parts[3])) user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR); - $type=empty($parts[5])? - self::REQ_SYNC|self::REQ_AJAX: - constant('self::REQ_'.strtoupper($parts[5])); + $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)]= - array($handler,$ttl,$kbps,$alias); + [$handler,$ttl,$kbps,$alias]; } } /** * Reroute to specified URI * @return NULL - * @param $url string + * @param $url array|string * @param $permanent bool **/ function reroute($url=NULL,$permanent=FALSE) { if (!$url) $url=$this->hive['REALM']; - if (preg_match('/^(?:@(\w+)(?:(\(.+?)\))*)/',$url,$parts)) { + if (is_array($url)) + $url=call_user_func_array([$this,'alias'],$url); + elseif (preg_match('/^(?:@(\w+)(?:(\(.+?)\))*(\?.+)*)/',$url,$parts)) { if (empty($this->hive['ALIASES'][$parts[1]])) user_error(sprintf(self::E_Named,$parts[1]),E_USER_ERROR); $url=$this->hive['ALIASES'][$parts[1]]; } - $url=$this->build($url, - isset($parts[2])?$this->parse($parts[2]):array()); + $url=$this->build($url,isset($parts[2])?$this->parse($parts[2]):[]). + (isset($parts[3])?$parts[3]:''); if (($handler=$this->hive['ONREROUTE']) && - $this->call($handler,array($url,$permanent))!==FALSE) + $this->call($handler,[$url,$permanent])!==FALSE) return; - if ($url[0]=='/') - $url=$this->hive['BASE'].$url; - if (PHP_SAPI!='cli') { + if ($url[0]=='/') { + $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']) { header('Location: '.$url); $this->status($permanent?301:302); die; @@ -1326,7 +1403,7 @@ final class Base extends Prefab implements ArrayAccess { foreach (explode('|',self::VERBS) as $method) $this->route($method.' '.$url,is_string($class)? $class.'->'.$this->hive['PREMAP'].strtolower($method): - array($class,$this->hive['PREMAP'].strtolower($method)), + [$class,$this->hive['PREMAP'].strtolower($method)], $ttl,$kbps); } @@ -1381,10 +1458,31 @@ final class Base extends Prefab implements ArrayAccess { 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>[^\/\?]+)', - str_replace('\*','([^\?]+)',preg_quote($pattern,'/'))). - '\/?(?:\?.*)?$/'.$case.'um',$url,$args); + 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; } @@ -1400,14 +1498,18 @@ final class Base extends Prefab implements ArrayAccess { // No routes defined user_error(self::E_Routes,E_USER_ERROR); // Match specific routes first - $paths=array(); - foreach ($keys=array_keys($this->hive['ROUTES']) as $key) - $paths[]=str_replace('@','*@',$key); + $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=$this->rel(urldecode($this->hive['URI'])); + $req=$this->rel(urldecode($this->hive['PATH'])); if ($cors=(isset($this->hive['HEADERS']['Origin']) && $this->hive['CORS']['origin'])) { $cors=$this->hive['CORS']; @@ -1415,45 +1517,42 @@ final class Base extends Prefab implements ArrayAccess { header('Access-Control-Allow-Credentials: '. ($cors['credentials']?'true':'false')); } - $allowed=array(); + $allowed=[]; foreach ($this->hive['ROUTES'] as $pattern=>$routes) { if (!$args=$this->mask($pattern,$req)) continue; ksort($args); $route=NULL; - if (isset( - $routes[$ptr=$this->hive['AJAX']+1][$this->hive['VERB']])) + $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]; - elseif (isset($routes[self::REQ_SYNC|self::REQ_AJAX])) - $route=$routes[self::REQ_SYNC|self::REQ_AJAX]; if (!$route) continue; if ($this->hive['VERB']!='OPTIONS' && isset($route[$this->hive['VERB']])) { - $parts=parse_url($req); if ($this->hive['VERB']=='GET' && - preg_match('/.+\/$/',$parts['path'])) - $this->reroute(substr($parts['path'],0,-1). - (isset($parts['query'])?('?'.$parts['query']):'')); + 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']]; - if (is_bool(strpos($pattern,'/*'))) - foreach (array_keys($args) as $key) - if (is_numeric($key) && $key) - unset($args[$key]); // 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'])); + 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]; + return isset($args[$id[$pid]])? + $args[$id[$pid]]: + $id[0]; }, $handler ); @@ -1481,7 +1580,7 @@ final class Base extends Prefab implements ArrayAccess { } // Retrieve from cache backend list($headers,$body,$result)=$data; - if (PHP_SAPI!='cli') + if (!$this->hive['CLI']) array_walk($headers,'header'); $this->expire($cached[0]+$ttl-$now); } @@ -1496,15 +1595,15 @@ final class Base extends Prefab implements ArrayAccess { $this->hive['BODY']=file_get_contents('php://input'); ob_start(); // Call route handler - $result=$this->call($handler,array($this,$args), + $result=$this->call($handler,[$this,$args], 'beforeroute,afterroute'); $body=ob_get_clean(); if (isset($cache) && !error_get_last()) { // Save to cache backend - $cache->set($hash,array( + $cache->set($hash,[ // Remove cookies preg_grep('/Set-Cookie\:/',headers_list(), - PREG_GREP_INVERT),$body,$result),$ttl); + PREG_GREP_INVERT),$body,$result],$ttl); } } $this->hive['RESPONSE']=$body; @@ -1530,7 +1629,7 @@ final class Base extends Prefab implements ArrayAccess { if (!$allowed) // URL doesn't match any route $this->error(404); - elseif (PHP_SAPI!='cli') { + elseif (!$this->hive['CLI']) { // Unhandled HTTP method header('Allow: '.implode(',',array_unique($allowed))); if ($cors) { @@ -1559,9 +1658,10 @@ final class Base extends Prefab implements ArrayAccess { **/ function until($func,$args=NULL,$timeout=60) { if (!$args) - $args=array(); + $args=[]; $time=time(); - $limit=max(0,min($timeout,$max=ini_get('max_execution_time')-1)); + $max=ini_get('max_execution_time'); + $limit=max(0,($max?min($timeout,$max):$timeout)-1); $out=''; // Turn output buffering on ob_start(); @@ -1569,15 +1669,17 @@ final class Base extends Prefab implements ArrayAccess { while ( // No error occurred !$this->hive['ERROR'] && + // Got time left? + time()-$time+1<$limit && // Still alive? !connection_aborted() && - // Got time left? - (time()-$time+1<$limit) && // Restart session - @session_start() && + !headers_sent() && + (session_status()==PHP_SESSION_ACTIVE || session_start()) && // CAUTION: Callback will kill host if it never becomes truthy! - !($out=$this->call($func,$args))) { - session_commit(); + !$out=$this->call($func,$args)) { + if (!$this->hive['CLI']) + session_commit(); // Hush down sleep(1); } @@ -1590,12 +1692,14 @@ final class Base extends Prefab implements ArrayAccess { * Disconnect HTTP client **/ function abort() { - @session_start(); + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); session_commit(); $out=''; while (ob_get_level()) $out=ob_get_clean().$out; header('Content-Length: '.strlen($out)); + header('Connection: close'); echo $out; flush(); if (function_exists('fastcgi_finish_request')) @@ -1618,12 +1722,12 @@ final class Base extends Prefab implements ArrayAccess { $parts[1]=call_user_func($parts[1].'::instance'); else { $ref=new ReflectionClass($parts[1]); - $parts[1]=method_exists($parts[1],'__construct')? + $parts[1]=method_exists($parts[1],'__construct') && $args? $ref->newinstanceargs($args): $ref->newinstance(); } } - $func=array($parts[1],$parts[3]); + $func=[$parts[1],$parts[3]]; } return $func; } @@ -1637,7 +1741,7 @@ final class Base extends Prefab implements ArrayAccess { **/ function call($func,$args=NULL,$hooks='') { if (!is_array($args)) - $args=array($args); + $args=[$args]; // Grab the real handler behind the string representation if (is_string($func)) $func=$this->grab($func,$args); @@ -1645,7 +1749,7 @@ final class Base extends Prefab implements ArrayAccess { if (!is_callable($func)) // No route handler if ($hooks=='beforeroute,afterroute') { - $allowed=array(); + $allowed=[]; if (is_array($func)) $allowed=array_intersect( array_map('strtoupper',get_class_methods($func[0])), @@ -1666,16 +1770,16 @@ final class Base extends Prefab implements ArrayAccess { // Execute pre-route hook if any if ($obj && $hooks && in_array($hook='beforeroute',$hooks) && method_exists($func[0],$hook) && - call_user_func_array(array($func[0],$hook),$args)===FALSE) + call_user_func_array([$func[0],$hook],$args)===FALSE) return FALSE; // Execute callback - $out=call_user_func_array($func,$args?:array()); + $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(array($func[0],$hook),$args)===FALSE) + call_user_func_array([$func[0],$hook],$args)===FALSE) return FALSE; return $out; } @@ -1688,7 +1792,7 @@ final class Base extends Prefab implements ArrayAccess { * @param $args mixed **/ function chain($funcs,$args=NULL) { - $out=array(); + $out=[]; foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func) $out[]=$this->call($func,$args); return $out; @@ -1703,7 +1807,7 @@ final class Base extends Prefab implements ArrayAccess { **/ function relay($funcs,$args=NULL) { foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func) - $args=array($this->call($func,$args)); + $args=[$this->call($func,$args)]; return array_shift($args); } @@ -1725,12 +1829,17 @@ final class Base extends Prefab implements ArrayAccess { $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)'. - '((?:\.?\w)+)/i',$sec,$msec) && !$this->exists($msec[0])) + if (preg_match( + '/^(?!(?:global|config|route|map|redirect)s\b)'. + '((?:\.?\w)+)/i',$sec,$msec) && + !$this->exists($msec[0])) $this->set($msec[0],NULL); + preg_match('/^(config|route|map|redirect)s\b|'. + '^((?:\.?\w)+)\s*\>\s*(.*)/i',$sec,$cmd); } else { if ($allow) { @@ -1739,45 +1848,54 @@ final class Base extends Prefab implements ArrayAccess { $match['rval']=Preview::instance()-> resolve($match['rval']); } - if (preg_match('/^(config|route|map|redirect)s\b/i', - $sec,$cmd)) { - call_user_func_array( - array($this,$cmd[1]), - array_merge(array($match['lval']), + 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($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) { if (is_numeric($val)) return $val+0; - $val=ltrim($val); + $val=trim($val); if (preg_match('/^\w+$/i',$val) && defined($val)) return constant($val); - return trim(preg_replace( - array('/\\\\"/','/\\\\\h*(\r?\n)/'), - array('"','\1'),$val)); + return preg_replace('/\\\\"/','"',$val); }, // Mark quoted strings with 0x00 whitespace str_getcsv(preg_replace('/(?[^:]+)(?:\:(?.+))?/', $sec,$parts); $func=isset($parts['func'])?$parts['func']:NULL; $custom=(strtolower($parts['section'])!='globals'); if ($func) - $args=array($this->call($func, - count($args)>1?array($args):$args)); + $args=[$this->call($func,$args)]; + if (count($args)>1) + $args=[$args]; + if (isset($ttl)) + $args=array_merge($args,[$ttl]); call_user_func_array( - array($this,'set'), + [$this,'set'], array_merge( - array( + [ ($custom?($parts['section'].'.'):''). $match['lval'] - ), - count($args)>1?array($args):$args + ], + $args ) ); } @@ -1799,8 +1917,7 @@ final class Base extends Prefab implements ArrayAccess { mkdir($tmp,self::MODE,TRUE); // Use filesystem lock if (is_file($lock=$tmp. - $this->hash($this->hive['ROOT'].$this->hive['BASE']).'.'. - $this->hash($id).'.lock') && + $this->get('SEED').'.'.$this->hash($id).'.lock') && filemtime($lock)+ini_get('max_execution_time')hive['UNLOAD']; if ((!$handler || $this->call($handler,$this)===FALSE) && $error && in_array($error['type'], - array(E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR))) + [E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR])) // Fatal error detected - $this->error(500,sprintf(self::E_Fatal,$error['message']), - array($error)); + $this->error(500, + sprintf(self::E_Fatal,$error['message']),[$error]); } /** @@ -2018,37 +2135,73 @@ final class Base extends Prefab implements ArrayAccess { @ini_set('magic_quotes_gpc',0); @ini_set('register_globals',0); // Intercept errors/exceptions; PHP5.3-compatible - error_reporting((E_ALL|E_STRICT)&~(E_NOTICE|E_USER_NOTICE)); + $check=error_reporting((E_ALL|E_STRICT)&~(E_NOTICE|E_USER_NOTICE)); $fw=$this; set_exception_handler( function($obj) use($fw) { $fw->hive['EXCEPTION']=$obj; - $fw->error(500,$obj->getmessage(),$obj->gettrace()); + $fw->error(500, + $obj->getmessage().' '. + '['.$obj->getFile().':'.$obj->getLine().']', + $obj->gettrace()); } ); set_error_handler( - function($code,$text) use($fw) { - if ($code & error_reporting()) - $fw->error(500,$text); + function($level,$text) use($fw) { + if ($level & error_reporting()) + $fw->error(500,$text,NULL,$level); } ); if (!isset($_SERVER['SERVER_NAME'])) $_SERVER['SERVER_NAME']=gethostname(); - if (PHP_SAPI=='cli') { + if ($cli=PHP_SAPI=='cli') { // Emulate HTTP request - if (isset($_SERVER['argc']) && $_SERVER['argc']<2) { + $_SERVER['REQUEST_METHOD']='GET'; + if (!isset($_SERVER['argv'][1])) { $_SERVER['argc']++; $_SERVER['argv'][1]='/'; } - $_SERVER['REQUEST_METHOD']='GET'; - $_SERVER['REQUEST_URI']=$_SERVER['argv'][1]; + if (substr($_SERVER['argv'][1],0,1)=='/') + $_SERVER['REQUEST_URI']=$_SERVER['argv'][1]; + else { + $req=$opts=''; + 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) + $opts.=($opts?'&':'').$k.'='; + if (isset($m[3])) + $opts.=$m[3]; + } else + $req.='/'.$arg; + } + $_SERVER['REQUEST_URI']=($req?:'/').'?'.urlencode($opts); + parse_str($opts,$GLOBALS['_GET']); + } + } + $headers=[]; + if (!$cli) { + if (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]; + } } - $headers=array(); - if (PHP_SAPI!='cli') - 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'])) @@ -2057,7 +2210,7 @@ final class Base extends Prefab implements ArrayAccess { isset($headers['X-Forwarded-Proto']) && $headers['X-Forwarded-Proto']=='https'?'https':'http'; // Create hive early on to expose header methods - $this->hive=array('HEADERS'=>$headers); + $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'],'', @@ -2066,13 +2219,14 @@ final class Base extends Prefab implements ArrayAccess { } $_SERVER['DOCUMENT_ROOT']=realpath($_SERVER['DOCUMENT_ROOT']); $base=''; - if (PHP_SAPI!='cli') + if (!$cli) $base=rtrim($this->fixslashes( dirname($_SERVER['SCRIPT_NAME'])),'/'); $uri=parse_url($_SERVER['REQUEST_URI']); $path=preg_replace('/^'.preg_quote($base,'/').'/','',$uri['path']); + session_cache_limiter(''); call_user_func_array('session_set_cookie_params', - $jar=array( + $jar=[ 'expire'=>0, 'path'=>$base?:'/', 'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) && @@ -2080,43 +2234,45 @@ final class Base extends Prefab implements ArrayAccess { $_SERVER['SERVER_NAME']:'', 'secure'=>($scheme=='https'), 'httponly'=>TRUE - ) + ] ); $port=0; if (isset($_SERVER['SERVER_PORT'])) $port=$_SERVER['SERVER_PORT']; // Default configuration - $this->hive+=array( + $this->hive+=[ 'AGENT'=>$this->agent(), 'AJAX'=>$this->ajax(), 'ALIAS'=>NULL, - 'ALIASES'=>array(), + 'ALIASES'=>[], 'AUTOLOAD'=>'./', 'BASE'=>$base, 'BITMASK'=>ENT_COMPAT, 'BODY'=>NULL, 'CACHE'=>FALSE, 'CASELESS'=>TRUE, - 'CONFIG'=>NULL, - 'CORS'=>array( + 'CLI'=>$cli, + 'CORS'=>[ 'headers'=>'', 'origin'=>FALSE, 'credentials'=>FALSE, 'expose'=>FALSE, - 'ttl'=>0), + 'ttl'=>0 + ], 'DEBUG'=>0, - 'DIACRITICS'=>array(), + 'DIACRITICS'=>[], 'DNSBL'=>'', - 'EMOJI'=>array(), + 'EMOJI'=>[], 'ENCODING'=>$charset, 'ERROR'=>NULL, 'ESCAPE'=>TRUE, 'EXCEPTION'=>NULL, 'EXEMPT'=>NULL, 'FALLBACK'=>$this->fallback, + 'FORMATS'=>[], 'FRAGMENT'=>isset($uri['fragment'])?$uri['fragment']:'', 'HALT'=>TRUE, - 'HIGHLIGHT'=>TRUE, + 'HIGHLIGHT'=>FALSE, 'HOST'=>$_SERVER['SERVER_NAME'], 'IP'=>$this->ip(), 'JAR'=>$jar, @@ -2128,7 +2284,7 @@ final class Base extends Prefab implements ArrayAccess { 'ONERROR'=>NULL, 'ONREROUTE'=>NULL, 'PACKAGE'=>self::PACKAGE, - 'PARAMS'=>array(), + 'PARAMS'=>[], 'PATH'=>$path, 'PATTERN'=>NULL, 'PLUGINS'=>$this->fixslashes(__DIR__).'/', @@ -2143,11 +2299,12 @@ final class Base extends Prefab implements ArrayAccess { (':'.$port):'').$_SERVER['REQUEST_URI'], 'RESPONSE'=>'', 'ROOT'=>$_SERVER['DOCUMENT_ROOT'], - 'ROUTES'=>array(), + 'ROUTES'=>[], 'SCHEME'=>$scheme, + 'SEED'=>$this->hash($_SERVER['SERVER_NAME'].$base), 'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php', 'TEMP'=>'tmp/', - 'TIME'=>microtime(TRUE), + 'TIME'=>&$_SERVER['REQUEST_TIME_FLOAT'], 'TZ'=>@date_default_timezone_get(), 'UI'=>'./', 'UNLOAD'=>NULL, @@ -2156,30 +2313,30 @@ final class Base extends Prefab implements ArrayAccess { 'VERB'=>&$_SERVER['REQUEST_METHOD'], 'VERSION'=>self::VERSION, 'XFRAME'=>'SAMEORIGIN' - ); + ]; if (PHP_SAPI=='cli-server' && preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI'])) $this->reroute('/'); if (ini_get('auto_globals_jit')) // Override setting - $GLOBALS+=array('_ENV'=>$_ENV,'_REQUEST'=>$_REQUEST); + $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+=array( - $global=>preg_match('/SERVER|ENV/',$global)?$sync:array() - ); + $this->init+=[ + $global=>preg_match('/SERVER|ENV/',$global)?$sync:[] + ]; } - if ($error=error_get_last()) + if ($check && $error=error_get_last()) // Error detected - $this->error(500,sprintf(self::E_Fatal,$error['message']), - array($error)); + $this->error(500, + sprintf(self::E_Fatal,$error['message']),[$error]); date_default_timezone_set($this->hive['TZ']); // Register framework autoloader - spl_autoload_register(array($this,'autoload')); + spl_autoload_register([$this,'autoload']); // Register shutdown handler - register_shutdown_function(array($this,'unload'),getcwd()); + register_shutdown_function([$this,'unload'],getcwd()); } } @@ -2231,7 +2388,7 @@ class Cache extends Prefab { if (!empty($raw)) { list($val,$time,$ttl)=(array)$fw->unserialize($raw); if ($ttl===0 || $time+$ttl>microtime(TRUE)) - return array($time,$ttl); + return [$time,$ttl]; $val=null; $this->clear($key); } @@ -2253,14 +2410,14 @@ class Cache extends Prefab { $time=microtime(TRUE); if ($cached=$this->exists($key)) list($time,$ttl)=$cached; - $data=$fw->serialize(array($val,$time,$ttl)); + $data=$fw->serialize([$val,$time,$ttl]); $parts=explode('=',$this->dsn,2); switch ($parts[0]) { case 'apc': case 'apcu': return apc_store($ndx,$data,$ttl); case 'redis': - return $this->ref->set($ndx,$data,array('ex'=>$ttl)); + return $this->ref->set($ndx,$data, $ttl ? ['ex'=>$ttl] : []); case 'memcache': return memcache_set($this->ref,$ndx,$data,0,$ttl); case 'wincache': @@ -2327,7 +2484,8 @@ class Cache extends Prefab { case 'apcu': $info=apc_cache_info('user'); if (!empty($info['cache_list'])) { - $key=array_key_exists('info',$info['cache_list'][0])?'info':'key'; + $key=array_key_exists('info', + $info['cache_list'][0])?'info':'key'; $mtkey=array_key_exists('mtime',$info['cache_list'][0])? 'mtime':'modification_time'; foreach ($info['cache_list'] as $item) @@ -2346,6 +2504,7 @@ class Cache extends Prefab { } return TRUE; case 'memcache': + $fw=Base::instance(); foreach (memcache_get_extended_stats( $this->ref,'slabs') as $slabs) foreach (array_filter(array_keys($slabs),'is_numeric') @@ -2353,8 +2512,9 @@ class Cache extends Prefab { foreach (memcache_get_extended_stats( $this->ref,'cachedump',$id) as $data) if (is_array($data)) - foreach ($data as $key=>$val) + foreach (array_keys($data) as $key) if (preg_match($regex,$key) && + ($val=$fw->unserialize(memcache_get($this->ref,$key))) && $val[1]+$lifetimeref,$key); return TRUE; @@ -2366,7 +2526,8 @@ class Cache extends Prefab { wincache_ucache_delete($item['key_name']); return TRUE; case 'xcache': - return TRUE; /* Not supported */ + xcache_unset_by_prefix($this->prefix.'.'); + return TRUE; case 'folder': if ($glob=@glob($parts[1].'*')) foreach ($glob as $file) @@ -2388,25 +2549,17 @@ class Cache extends Prefab { if ($dsn=trim($dsn)) { if (preg_match('/^redis=(.+)/',$dsn,$parts) && extension_loaded('redis')) { - $port=6379; - $parts=explode(':',$parts[1],2); - if (count($parts)>1) - list($host,$port)=$parts; - else - $host=$parts[0]; + list($host,$port,$db)=explode(':',$parts[1])+[1=>6379,2=>NULL]; $this->ref=new Redis; if(!$this->ref->connect($host,$port,2)) $this->ref=NULL; + if(isset($db)) + $this->ref->select($db); } elseif (preg_match('/^memcache=(.+)/',$dsn,$parts) && extension_loaded('memcache')) foreach ($fw->split($parts[1]) as $server) { - $port=11211; - $parts=explode(':',$server,2); - if (count($parts)>1) - list($host,$port)=$parts; - else - $host=$parts[0]; + list($host,$port)=explode(':',$server)+[1=>11211]; if (empty($this->ref)) $this->ref=@memcache_connect($host,$port)?:NULL; else @@ -2423,7 +2576,7 @@ class Cache extends Prefab { !is_dir($parts[1])) mkdir($parts[1],Base::MODE,TRUE); } - $this->prefix=$fw->hash($_SERVER['SERVER_NAME'].$fw->get('BASE')); + $this->prefix=$fw->get('SEED'); return $this->dsn=$dsn; } @@ -2486,9 +2639,9 @@ class View extends Prefab { protected function sandbox(array $hive=NULL) { $this->level++; $fw=Base::instance(); - $implicit=false; - if ($hive === null) { - $implicit=true; + $implicit=FALSE; + if (is_null($hive)) { + $implicit=TRUE; $hive=$fw->hive(); } if ($this->level<2 || $implicit) { @@ -2497,7 +2650,7 @@ class View extends Prefab { if (isset($hive['ALIASES'])) $hive['ALIASES']=$fw->build($hive['ALIASES']); } - unset($fw, $implicit); + unset($fw,$implicit); extract($hive); unset($hive); ob_start(); @@ -2521,10 +2674,11 @@ class View extends Prefab { return $data; foreach ($fw->split($fw->get('UI').';./') as $dir) if (is_file($this->view=$fw->fixslashes($dir.$file))) { - if (isset($_COOKIE[session_name()])) - @session_start(); + if (isset($_COOKIE[session_name()]) && + !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); $fw->sync('SESSION'); - if ($mime && PHP_SAPI!='cli' && !headers_sent()) + if ($mime && !$fw->get('CLI') && !headers_sent()) header('Content-Type: '.$mime.'; '. 'charset='.$fw->get('ENCODING')); $data=$this->sandbox($hive); @@ -2555,12 +2709,12 @@ class Preview extends View { //! MIME type $mime, //! token filter - $filter=array( + $filter=[ 'esc'=>'$this->esc', 'raw'=>'$this->raw', 'alias'=>'\Base::instance()->alias', 'format'=>'\Base::instance()->format' - ); + ]; /** * Convert token to variable @@ -2574,9 +2728,10 @@ class Preview extends View { $str,$parts)) { $str=trim($parts[1]); foreach (Base::instance()->split($parts[2]) as $func) - $str=is_string($cmd=$this->filter($func))?$cmd.'('.$str.')': + $str=is_string($cmd=$this->filter($func))? + $cmd.'('.$str.')': '\Base::instance()->call('. - '$this->filter(\''.$func.'\'),array('.$str.'))'; + '$this->filter(\''.$func.'\'),['.$str.'])'; } return $str; } @@ -2646,9 +2801,13 @@ class Preview extends View { * @param $hive array * @param $ttl int **/ - function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { + function render($file,$mime=NULL,array $hive=NULL,$ttl=0) { $fw=Base::instance(); $cache=Cache::instance(); + if ($mime) + $this->mime=$mime; + elseif (!$this->mime) + $this->mime='text/html'; if (!is_dir($tmp=$fw->get('TEMP'))) mkdir($tmp,Base::MODE,TRUE); foreach ($fw->split($fw->get('UI')) as $dir) { @@ -2656,23 +2815,23 @@ class Preview extends View { return $data; if (is_file($view=$fw->fixslashes($dir.$file))) { if (!is_file($this->view=($tmp. - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash($view).'.php')) || + $fw->get('SEED').'.'.$fw->hash($view).'.php')) || filemtime($this->view)\h*'. - '(?!["\'])|\{\*.+?\*\}/is','', + '/\h*<\?(?!xml)(?:php|\s*=)?.+?\?>\h*'. + '|\{\*.+?\*\}/is','', $fw->read($view)); if (method_exists($this,'parse')) $text=$this->parse($text); $fw->write($this->view,$this->build($text)); } - if (isset($_COOKIE[session_name()])) - @session_start(); + if (isset($_COOKIE[session_name()]) && + !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); $fw->sync('SESSION'); - if ($mime && PHP_SAPI!='cli' && !headers_sent()) - header('Content-Type: '.($this->mime=$mime).'; '. + if (!$fw->get('CLI') && !headers_sent()) + header('Content-Type: '.$this->mime.'; '. 'charset='.$fw->get('ENCODING')); $data=$this->sandbox($hive); if(isset($this->trigger['afterrender'])) diff --git a/app/lib/basket.php b/app/lib/basket.php index 94e03063..ffb498e7 100644 --- a/app/lib/basket.php +++ b/app/lib/basket.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -34,7 +34,7 @@ class Basket extends Magic { //! Current item identifier $id, //! Current item contents - $item=array(); + $item=[]; /** * Return TRUE if field is defined @@ -86,7 +86,7 @@ class Basket extends Magic { * @param $val mixed **/ function find($key=NULL,$val=NULL) { - $out=array(); + $out=[]; if (isset($_SESSION[$this->key])) { foreach ($_SESSION[$this->key] as $id=>$item) if (!isset($key) || @@ -122,7 +122,7 @@ class Basket extends Magic { return $this->item=$found[0]->item; } $this->reset(); - return array(); + return []; } /** @@ -175,7 +175,7 @@ class Basket extends Magic { **/ function reset() { $this->id=NULL; - $this->item=array(); + $this->item=[]; } /** @@ -219,7 +219,7 @@ class Basket extends Magic { unset($_SESSION[$this->key]); return $out; } - return array(); + return []; } /** @@ -229,7 +229,8 @@ class Basket extends Magic { **/ function __construct($key='basket') { $this->key=$key; - @session_start(); + if (session_status()!=PHP_SESSION_ACTIVE) + session_start(); Base::instance()->sync('SESSION'); $this->reset(); } diff --git a/app/lib/bcrypt.php b/app/lib/bcrypt.php index 6ecd61e4..2f44d39b 100644 --- a/app/lib/bcrypt.php +++ b/app/lib/bcrypt.php @@ -1,26 +1,27 @@ . +* +* @deprecated use http://php.net/manual/en/ref.password.php instead (PHP 5.5+ only) +**/ - Copyright (c) 2009-2015 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 . - -*/ - -//! Lightweight password hashing library class Bcrypt extends Prefab { //@{ Error messages diff --git a/app/lib/cli/ws.php b/app/lib/cli/ws.php new file mode 100644 index 00000000..390fa733 --- /dev/null +++ b/app/lib/cli/ws.php @@ -0,0 +1,516 @@ +. + +*/ + +namespace CLI; + +//! RFC6455 WebSocket server +class WS { + + const + //! UUID magic string + Magic='258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + //! Max packet size + Packet=65536; + + //@{ Mask bits for first byte of header + const + Text=0x01, + Binary=0x02, + Close=0x08, + Ping=0x09, + Pong=0x0a, + OpCode=0x0f, + Finale=0x80; + //@} + + //@{ Mask bits for second byte of header + const + Length=0x7f; + //@} + + protected + $addr, + $ctx, + $wait, + $sockets, + $agents=[], + $events=[]; + + /** + * Allocate stream socket + * @return NULL + * @param $socket resource + **/ + function alloc($socket) { + if (is_bool($str=$this->read($socket))) { + $this->close($socket); + return; + } + // Get WebSocket headers + $hdrs=[]; + $CRLF="\r\n"; + $verb=NULL; + $uri=NULL; + foreach (explode($CRLF,trim($str)) as $line) + if (preg_match('/^(\w+)\s(.+)\sHTTP\/1\.\d$/', + trim($line),$match)) { + $verb=$match[1]; + $uri=$match[2]; + } + else + if (preg_match('/^(.+): (.+)/',trim($line),$match)) + // Standardize header + $hdrs[ + strtr( + ucwords( + strtolower( + strtr($match[1],'-',' ') + ) + ),' ','-' + ) + ]=$match[2]; + else { + $this->close($socket); + return; + } + if (empty($hdrs['Upgrade']) && + empty($hdrs['Sec-Websocket-Key'])) { + // Not a WebSocket request + if ($verb && $uri) + $this->write( + $socket, + $str='HTTP/1.1 400 Bad Request'.$CRLF. + 'Connection: close'.$CRLF.$CRLF + ); + $this->close($socket); + return; + } + // Handshake + $bytes=$this->write( + $socket, + $str='HTTP/1.1 101 Switching Protocols'.$CRLF. + 'Upgrade: websocket'.$CRLF. + 'Connection: Upgrade'.$CRLF. + 'Sec-WebSocket-Accept: '. + base64_encode( + sha1( + $hdrs['Sec-Websocket-Key']. + self::Magic, + TRUE + ) + ).$CRLF.$CRLF + ); + if ($bytes) { + // Connect agent to server + $this->sockets[]=$socket; + $this->agents[(int)$socket]= + new Agent($this,$socket,$verb,$uri,$hdrs); + } + else + $this->close($socket); + } + + /** + * Close stream socket + * @return NULL + * @param $socket resource + **/ + function close($socket) { + stream_socket_shutdown($socket,STREAM_SHUT_WR); + @fclose($socket); + } + + /** + * Free stream socket + * @return bool + * @param $socket resource + **/ + function free($socket) { + unset($this->sockets[array_search($socket,$this->sockets)]); + unset($this->agents[(int)$socket]); + $this->close($socket); + } + + /** + * Read from stream socket + * @return string|FALSE + * @param $socket resource + **/ + function read($socket) { + if (is_string($str=@fread($socket,self::Packet)) && + strlen($str) && + strlen($str)events['error']) && + is_callable($func=$this->events['error'])) + $func($this); + return FALSE; + } + + /** + * Write to stream socket + * @return int|FALSE + * @param $socket resource + * @param $str string + **/ + function write($socket,$str) { + for ($i=0,$bytes=0;$ievents['error']) && + is_callable($func=$this->events['error'])) + $func($this); + return FALSE; + } + return $bytes; + } + + /** + * Return socket agents + * @return array + * @param $uri string + ***/ + function agents($uri=NULL) { + return array_filter( + $this->agents, + function($val) use($uri) { + return $uri?($val->uri()==$uri):TRUE; + } + ); + } + + /** + * Return event handlers + * @return array + **/ + function events() { + return $this->events; + } + + /** + * Bind function to event handler + * @return object + * @param $event string + * @param $func callable + **/ + function on($event,$func) { + $this->events[$event]=$func; + return $this; + } + + /** + * Terminate server + * @return NULL + * @param $signal int + **/ + function kill($signal) { + die; + } + + /** + * Execute the server process + * @return object + **/ + function run() { + $fw=\Base::instance(); + // Assign signal handlers + declare(ticks=1); + pcntl_signal(SIGINT,[$this,'kill']); + pcntl_signal(SIGTERM,[$this,'kill']); + gc_enable(); + // Activate WebSocket listener + $listen=stream_socket_server( + $this->addr,$errno,$errstr, + STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, + $this->ctx + ); + $socket=socket_import_stream($listen); + register_shutdown_function(function() use($listen) { + foreach ($this->sockets as $socket) + if ($socket!=$listen) + $this->free($socket); + $this->close($listen); + if (isset($this->events['stop']) && + is_callable($func=$this->events['stop'])) + $func($this); + }); + if ($errstr) + user_error($errstr,E_USER_ERROR); + if (isset($this->events['start']) && + is_callable($func=$this->events['start'])) + $func($this); + $this->sockets=[$listen]; + $empty=[]; + $wait=$this->wait; + while (TRUE) { + $active=$this->sockets; + $mark=microtime(TRUE); + $count=@stream_select( + $active,$empty,$empty,(int)$wait,round(1e6*($wait-(int)$wait)) + ); + if (is_bool($count) && $wait) { + if (isset($this->events['error']) && + is_callable($func=$this->events['error'])) + $func($this); + die; + } + if ($count) { + // Process active connections + foreach ($active as $socket) { + if (!is_resource($socket)) + continue; + if ($socket==$listen) { + if ($socket=@stream_socket_accept($listen,0)) + $this->alloc($socket); + else + if (isset($this->events['error']) && + is_callable($func=$this->events['error'])) + $func($this); + } + else { + $id=(int)$socket; + if (isset($this->agents[$id]) && + $raw=$this->agents[$id]->fetch()) { + list($op,$data)=$raw; + // Dispatch + switch ($op & self::OpCode) { + case self::Ping: + $this->agents[$id]->send(self::Pong); + break; + case self::Close: + $this->free($socket); + break; + case self::Text: + $data=trim($data); + case self::Binary: + if (isset($this->events['receive']) && + is_callable($func=$this->events['receive'])) + $func($this->agents[$id],$op,$data); + break; + } + } + } + } + $wait-=microtime(TRUE)-$mark; + while ($wait<1e-6) { + $wait+=$this->wait; + $count=0; + } + } + if (!$count) { + $mark=microtime(TRUE); + foreach ($this->sockets as $socket) { + if (!is_resource($socket)) + continue; + $id=(int)$socket; + if ($socket!=$listen && + isset($this->agents[$id]) && + isset($this->events['idle']) && + is_callable($func=$this->events['idle'])) + $func($this->agents[$id]); + } + $wait=$this->wait-microtime(TRUE)+$mark; + } + gc_collect_cycles(); + } + } + + /** + * Instantiate object + * @return object + * @param $addr string + * @param $ctx resource + * @param $wait int + **/ + function __construct($addr,$ctx=NULL,$wait=60) { + $this->addr=$addr; + $this->ctx=$ctx?:stream_context_create(); + $this->wait=$wait; + $this->events=[]; + } + +} + +//! RFC6455 remote socket +class Agent { + + protected + $server, + $id, + $socket, + $flag, + $verb, + $uri, + $headers, + $events, + $buffer; + + /** + * Return server instance + * @return object + **/ + function server() { + return $this->server; + } + + /** + * Return socket ID + * @return string + **/ + function id() { + return $this->id; + } + + /** + * Return request method + * @return string + **/ + function verb() { + return $this->verb; + } + + /** + * Return request URI + * @return string + **/ + function uri() { + return $this->uri; + } + + /** + * Return socket headers + * @return string + **/ + function headers() { + return $this->headers; + } + + /** + * Frame and transmit payload + * @return string|FALSE + * @param $socket resource + * @param $op int + * @param $payload string + **/ + function send($op,$data='') { + $mask=WS::Finale | $op & WS::OpCode; + $len=strlen($data); + $str=''; + if ($len>0xffff) + $str=pack('CCNN',$mask,0x7f,$len); + else + if ($len>0x7d) + $str=pack('CCn',$mask,0x7e,$len); + else + $str=pack('CC',$mask,$len); + $str.=$data; + $server=$this->server(); + if (is_bool($server->write($this->socket,$str))) { + $this->free(); + return FALSE; + } + if (!in_array($op,[WS::Pong,WS::Close]) && + isset($this->events['send']) && + is_callable($func=$this->events['send'])) + $func($this,$op,$data); + return $data; + } + + /** + * Retrieve and unmask payload + * @return array|FALSE + **/ + function fetch() { + // Unmask payload + $server=$this->server(); + if (is_bool($buf=$server->read($this->socket))) { + $this->free(); + return FALSE; + } + $buf=($this->buffer.=$buf); + $op=ord($buf[0]) & WS::OpCode; + $len=ord($buf[1]) & WS::Length; + $pos=2; + if ($len==0x7e) { + $len=ord($buf[2])*256+ord($buf[3]); + $pos+=2; + } + else + if ($len==0x7f) { + for ($i=0,$len=0;$i<8;$i++) + $len=$len*256+ord($buf[$i+2]); + $pos+=8; + } + for ($i=0,$mask=[];$i<4;$i++) + $mask[$i]=ord($buf[$pos+$i]); + $pos+=4; + if (strlen($buf)<$len+$pos) + return FALSE; + for ($i=0,$data='';$i<$len;$i++) + $data.=chr(ord($buf[$pos+$i])^$mask[$i%4]); + $this->buffer=''; + return [$op,$data]; + } + + /** + * Free stream socket + * @return NULL + **/ + function free() { + $this->server->free($this->socket); + } + + /** + * Destroy object + * @return NULL + **/ + function __destruct() { + if (isset($this->events['disconnect']) && + is_callable($func=$this->events['disconnect'])) + $func($this); + } + + /** + * Instantiate object + * @return object + * @param $server object + * @param $socket resource + * @param $verb string + * @param $uri string + * @param $hdrs array + **/ + function __construct($server,$socket,$verb,$uri,array $hdrs) { + $this->server=$server; + $this->id=stream_socket_get_name($socket,TRUE); + $this->socket=$socket; + $this->verb=$verb; + $this->uri=$uri; + $this->headers=$hdrs; + $this->events=$server->events(); + $this->buffer=''; + if (isset($this->events['connect']) && + is_callable($func=$this->events['connect'])) + $func($this); + } + +} diff --git a/app/lib/db/cursor.php b/app/lib/db/cursor.php index baef9531..9ea7cdc9 100644 --- a/app/lib/db/cursor.php +++ b/app/lib/db/cursor.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -32,11 +32,11 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { protected //! Query results - $query=array(), + $query=[], //! Current position $ptr=0, //! Event listeners - $trigger=array(); + $trigger=[]; /** * Return database type @@ -126,7 +126,7 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { **/ function findone($filter=NULL,array $options=NULL,$ttl=0) { if (!$options) - $options=array(); + $options=[]; // Override limit $options['limit']=1; return ($data=$this->find($filter,$options,$ttl))?$data[0]:FALSE; @@ -148,11 +148,11 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { $total=$this->count($filter,$ttl); $count=ceil($total/$size); $pos=max(0,min($pos,$count-1)); - return array( + return [ 'subset'=>$this->find($filter, array_merge( - $options?:array(), - array('limit'=>$size,'offset'=>$pos*$size) + $options?:[], + ['limit'=>$size,'offset'=>$pos*$size] ), $ttl ), @@ -160,7 +160,7 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { 'limit'=>$size, 'count'=>$count, 'pos'=>$pos<$count?$pos:0 - ); + ]; } /** @@ -378,7 +378,7 @@ abstract class Cursor extends \Magic implements \IteratorAggregate { * @return NULL **/ function reset() { - $this->query=array(); + $this->query=[]; $this->ptr=0; } diff --git a/app/lib/db/jig.php b/app/lib/db/jig.php index 736735c4..d4337933 100644 --- a/app/lib/db/jig.php +++ b/app/lib/db/jig.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -51,7 +51,7 @@ class Jig { function &read($file) { if (!$this->dir || !is_file($dst=$this->dir.$file)) { if (!isset($this->data[$file])) - $this->data[$file]=array(); + $this->data[$file]=[]; return $this->data[$file]; } $fw=\Base::instance(); @@ -106,11 +106,14 @@ class Jig { } /** - * Return profiler results + * Return profiler results (or disable logging) + * @param $flag bool * @return string **/ - function log() { - return $this->log; + function log($flag=TRUE) { + if ($flag) + return $this->log; + $this->log=FALSE; } /** @@ -129,12 +132,16 @@ class Jig { **/ function drop() { if (!$this->dir) - $this->data=array(); + $this->data=[]; elseif ($glob=@glob($this->dir.'/*',GLOB_NOSORT)) foreach ($glob as $file) @unlink($file); } + //! Prohibit cloning + private function __clone() { + } + /** * Instantiate class * @param $dir string diff --git a/app/lib/db/jig/mapper.php b/app/lib/db/jig/mapper.php index f0f3953c..74335fcd 100644 --- a/app/lib/db/jig/mapper.php +++ b/app/lib/db/jig/mapper.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -33,7 +33,7 @@ class Mapper extends \DB\Cursor { //! Document identifier $id, //! Document contents - $document=array(); + $document=[]; /** * Return database type @@ -97,7 +97,7 @@ class Mapper extends \DB\Cursor { $mapper->id=$id; foreach ($row as $field=>$val) $mapper->document[$field]=$val; - $mapper->query=array(clone($mapper)); + $mapper->query=[clone($mapper)]; if (isset($mapper->trigger['load'])) \Base::instance()->call($mapper->trigger['load'],$mapper); return $mapper; @@ -111,7 +111,7 @@ class Mapper extends \DB\Cursor { function cast($obj=NULL) { if (!$obj) $obj=$this; - return $obj->document+array('_id'=>$this->id); + return $obj->document+['_id'=>$this->id]; } /** @@ -157,20 +157,20 @@ class Mapper extends \DB\Cursor { **/ function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) { if (!$options) - $options=array(); - $options+=array( + $options=[]; + $options+=[ 'order'=>NULL, 'limit'=>0, 'offset'=>0 - ); + ]; $fw=\Base::instance(); $cache=\Cache::instance(); $db=$this->db; $now=microtime(TRUE); - $data=array(); + $data=[]; if (!$fw->get('CACHE') || !$ttl || !($cached=$cache->exists( $hash=$fw->hash($this->db->dir(). - $fw->stringify(array($filter,$options))).'.jig',$data)) || + $fw->stringify([$filter,$options])).'.jig',$data)) || $cached[0]+$ttlread($this->file); if (is_null($data)) @@ -188,8 +188,8 @@ class Mapper extends \DB\Cursor { $args=isset($filter[1]) && is_array($filter[1])? $filter[1]: array_slice($filter,1,NULL,TRUE); - $args=is_array($args)?$args:array(1=>$args); - $keys=$vals=array(); + $args=is_array($args)?$args:[1=>$args]; + $keys=$vals=[]; $tokens=array_slice( token_get_all('token($expr)),1); $data=array_filter($data, @@ -248,7 +248,7 @@ class Mapper extends \DB\Cursor { $val1[$col]=NULL; if (!array_key_exists($col,$val2)) $val2[$col]=NULL; - list($v1,$v2)=array($val1[$col],$val2[$col]); + list($v1,$v2)=[$val1[$col],$val2[$col]]; if ($out=strnatcmp($v1,$v2)* (($order==SORT_ASC)*2-1)) return $out; @@ -263,7 +263,7 @@ class Mapper extends \DB\Cursor { // Save to cache backend $cache->set($hash,$data,$ttl); } - $out=array(); + $out=[]; foreach ($data as $id=>&$doc) { unset($doc['_id']); $out[]=$this->factory($id,$doc); @@ -303,7 +303,7 @@ class Mapper extends \DB\Cursor { * @param $ofs int **/ function skip($ofs=1) { - $this->document=($out=parent::skip($ofs))?$out->document:array(); + $this->document=($out=parent::skip($ofs))?$out->document:[]; $this->id=$out?$out->id:NULL; if ($this->document && isset($this->trigger['load'])) \Base::instance()->call($this->trigger['load'],$this); @@ -324,10 +324,10 @@ class Mapper extends \DB\Cursor { !connection_aborted()) usleep(mt_rand(0,100)); $this->id=$id; - $pkey=array('_id'=>$this->id); + $pkey=['_id'=>$this->id]; if (isset($this->trigger['beforeinsert']) && \Base::instance()->call($this->trigger['beforeinsert'], - array($this,$pkey))===FALSE) + [$this,$pkey])===FALSE) return $this->document; $data[$id]=$this->document; $db->write($this->file,$data); @@ -335,8 +335,8 @@ class Mapper extends \DB\Cursor { $this->file.' [insert] '.json_encode($this->document)); if (isset($this->trigger['afterinsert'])) \Base::instance()->call($this->trigger['afterinsert'], - array($this,$pkey)); - $this->load(array('@_id=?',$this->id)); + [$this,$pkey]); + $this->load(['@_id=?',$this->id]); return $this->document; } @@ -350,7 +350,7 @@ class Mapper extends \DB\Cursor { $data=&$db->read($this->file); if (isset($this->trigger['beforeupdate']) && \Base::instance()->call($this->trigger['beforeupdate'], - array($this,array('_id'=>$this->id)))===FALSE) + [$this,['_id'=>$this->id]])===FALSE) return $this->document; $data[$this->id]=$this->document; $db->write($this->file,$data); @@ -358,7 +358,7 @@ class Mapper extends \DB\Cursor { $this->file.' [update] '.json_encode($this->document)); if (isset($this->trigger['afterupdate'])) \Base::instance()->call($this->trigger['afterupdate'], - array($this,array('_id'=>$this->id))); + [$this,['_id'=>$this->id]]); return $this->document; } @@ -371,7 +371,7 @@ class Mapper extends \DB\Cursor { $db=$this->db; $now=microtime(TRUE); $data=&$db->read($this->file); - $pkey=array('_id'=>$this->id); + $pkey=['_id'=>$this->id]; if ($filter) { foreach ($this->find($filter,NULL,FALSE) as $mapper) if (!$mapper->erase()) @@ -386,14 +386,14 @@ class Mapper extends \DB\Cursor { return FALSE; if (isset($this->trigger['beforeerase']) && \Base::instance()->call($this->trigger['beforeerase'], - array($this,$pkey))===FALSE) + [$this,$pkey])===FALSE) return FALSE; $db->write($this->file,$data); if ($filter) { $args=isset($filter[1]) && is_array($filter[1])? $filter[1]: array_slice($filter,1,NULL,TRUE); - $args=is_array($args)?$args:array(1=>$args); + $args=is_array($args)?$args:[1=>$args]; foreach ($args as $key=>$val) { $vals[]=\Base::instance()-> stringify(is_array($val)?$val[0]:$val); @@ -405,7 +405,7 @@ class Mapper extends \DB\Cursor { ($filter?preg_replace($keys,$vals,$filter[0],1):'')); if (isset($this->trigger['aftererase'])) \Base::instance()->call($this->trigger['aftererase'], - array($this,$pkey)); + [$this,$pkey]); return TRUE; } @@ -415,7 +415,7 @@ class Mapper extends \DB\Cursor { **/ function reset() { $this->id=NULL; - $this->document=array(); + $this->document=[]; parent::reset(); } diff --git a/app/lib/db/jig/session.php b/app/lib/db/jig/session.php index e4dca2fe..d3a92d9b 100644 --- a/app/lib/db/jig/session.php +++ b/app/lib/db/jig/session.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -63,12 +63,13 @@ class Session extends Mapper { * @param $id string **/ function read($id) { - $this->load(array('@session_id=?',$this->sid=$id)); + $this->load(['@session_id=?',$this->sid=$id]); if ($this->dry()) return FALSE; if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { $fw=\Base::instance(); - if (!isset($this->onsuspect) || FALSE===$fw->call($this->onsuspect,array($this,$id))) { + if (!isset($this->onsuspect) || + $fw->call($this->onsuspect,[$this,$id])===FALSE) { //NB: `session_destroy` can't be called at that stage (`session_start` not completed) $this->destroy($id); $this->close(); @@ -101,7 +102,7 @@ class Session extends Mapper { * @param $id string **/ function destroy($id) { - $this->erase(array('@session_id=?',$id)); + $this->erase(['@session_id=?',$id]); return TRUE; } @@ -111,7 +112,7 @@ class Session extends Mapper { * @param $max int **/ function cleanup($max) { - $this->erase(array('@stamp+?erase(['@stamp+?onsuspect=$onsuspect; session_set_save_handler( - array($this,'open'), - array($this,'close'), - array($this,'read'), - array($this,'write'), - array($this,'destroy'), - array($this,'cleanup') + [$this,'open'], + [$this,'close'], + [$this,'read'], + [$this,'write'], + [$this,'destroy'], + [$this,'cleanup'] ); register_shutdown_function('session_commit'); $fw=\Base::instance(); $headers=$fw->get('HEADERS'); - $this->_csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); + $this->_csrf=$fw->get('SEED').'.'.$fw->hash(mt_rand()); if ($key) $fw->set($key,$this->_csrf); $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; diff --git a/app/lib/db/mongo.php b/app/lib/db/mongo.php index ffe772e0..fedec0cd 100644 --- a/app/lib/db/mongo.php +++ b/app/lib/db/mongo.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -57,21 +57,27 @@ class Mongo { } /** - * Return MongoDB profiler results + * Return MongoDB profiler results (or disable logging) + * @param $flag bool * @return string **/ - function log() { - $cursor=$this->selectcollection('system.profile')->find(); - foreach (iterator_to_array($cursor) as $frame) - if (!preg_match('/\.system\..+$/',$frame['ns'])) - $this->log.=date('r',$frame['ts']->sec).' ('. - sprintf('%.1f',$frame['millis']).'ms) '. - $frame['ns'].' ['.$frame['op'].'] '. - (empty($frame['query'])? - '':json_encode($frame['query'])). - (empty($frame['command'])? - '':json_encode($frame['command'])). - PHP_EOL; + function log($flag=TRUE) { + if ($flag) { + $cursor=$this->selectcollection('system.profile')->find(); + foreach (iterator_to_array($cursor) as $frame) + if (!preg_match('/\.system\..+$/',$frame['ns'])) + $this->log.=date('r',$frame['ts']->sec).' ('. + sprintf('%.1f',$frame['millis']).'ms) '. + $frame['ns'].' ['.$frame['op'].'] '. + (empty($frame['query'])? + '':json_encode($frame['query'])). + (empty($frame['command'])? + '':json_encode($frame['command'])). + PHP_EOL; + } else { + $this->log=FALSE; + $this->setprofilinglevel(-1); + } return $this->log; } @@ -81,7 +87,8 @@ class Mongo { **/ function drop() { $out=$this->db->drop(); - $this->setprofilinglevel(2); + if ($this->log!==FALSE) + $this->setprofilinglevel(2); return $out; } @@ -92,7 +99,11 @@ class Mongo { * @param $args array **/ function __call($func,array $args) { - return call_user_func_array(array($this->db,$func),$args); + return call_user_func_array([$this->db,$func],$args); + } + + //! Prohibit cloning + private function __clone() { } /** @@ -104,7 +115,7 @@ class Mongo { function __construct($dsn,$dbname,array $options=NULL) { $this->uuid=\Base::instance()->hash($this->dsn=$dsn); $class=class_exists('\MongoClient')?'\MongoClient':'\Mongo'; - $this->db=new \MongoDB(new $class($dsn,$options?:array()),$dbname); + $this->db=new \MongoDB(new $class($dsn,$options?:[]),$dbname); $this->setprofilinglevel(2); } diff --git a/app/lib/db/mongo/mapper.php b/app/lib/db/mongo/mapper.php index e814b293..411c524b 100644 --- a/app/lib/db/mongo/mapper.php +++ b/app/lib/db/mongo/mapper.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -31,9 +31,11 @@ class Mapper extends \DB\Cursor { //! Mongo collection $collection, //! Mongo document - $document=array(), + $document=[], //! Mongo cursor - $cursor; + $cursor, + //! Defined fields + $fields; /** * Return database type @@ -92,7 +94,7 @@ class Mapper extends \DB\Cursor { $mapper->reset(); foreach ($row as $key=>$val) $mapper->document[$key]=$val; - $mapper->query=array(clone($mapper)); + $mapper->query=[clone($mapper)]; if (isset($mapper->trigger['load'])) \Base::instance()->call($mapper->trigger['load'],$mapper); return $mapper; @@ -119,48 +121,48 @@ class Mapper extends \DB\Cursor { **/ function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) { if (!$options) - $options=array(); - $options+=array( + $options=[]; + $options+=[ 'group'=>NULL, 'order'=>NULL, 'limit'=>0, 'offset'=>0 - ); + ]; $fw=\Base::instance(); $cache=\Cache::instance(); if (!($cached=$cache->exists($hash=$fw->hash($this->db->dsn(). - $fw->stringify(array($fields,$filter,$options))).'.mongo', + $fw->stringify([$fields,$filter,$options])).'.mongo', $result)) || !$ttl || $cached[0]+$ttlcollection->group( $options['group']['keys'], $options['group']['initial'], $options['group']['reduce'], - array( + [ 'condition'=>$filter, 'finalize'=>$options['group']['finalize'] - ) + ] ); $tmp=$this->db->selectcollection( $fw->get('HOST').'.'.$fw->get('BASE').'.'. uniqid(NULL,TRUE).'.tmp' ); - $tmp->batchinsert($grp['retval'],array('w'=>1)); - $filter=array(); + $tmp->batchinsert($grp['retval'],['w'=>1]); + $filter=[]; $collection=$tmp; } else { - $filter=$filter?:array(); + $filter=$filter?:[]; $collection=$this->collection; } - $this->cursor=$collection->find($filter,$fields?:array()); + $this->cursor=$collection->find($filter,$fields?:[]); if ($options['order']) $this->cursor=$this->cursor->sort($options['order']); if ($options['limit']) $this->cursor=$this->cursor->limit($options['limit']); if ($options['offset']) $this->cursor=$this->cursor->skip($options['offset']); - $result=array(); + $result=[]; while ($this->cursor->hasnext()) $result[]=$this->cursor->getnext(); if ($options['group']) @@ -169,7 +171,7 @@ class Mapper extends \DB\Cursor { // Save to cache backend $cache->set($hash,$result,$ttl); } - $out=array(); + $out=[]; foreach ($result as $doc) $out[]=$this->factory($doc); return $out; @@ -184,14 +186,14 @@ class Mapper extends \DB\Cursor { **/ function find($filter=NULL,array $options=NULL,$ttl=0) { if (!$options) - $options=array(); - $options+=array( + $options=[]; + $options+=[ 'group'=>NULL, 'order'=>NULL, 'limit'=>0, 'offset'=>0 - ); - return $this->select(NULL,$filter,$options,$ttl); + ]; + return $this->select($this->fields,$filter,$options,$ttl); } /** @@ -204,9 +206,9 @@ class Mapper extends \DB\Cursor { $fw=\Base::instance(); $cache=\Cache::instance(); if (!($cached=$cache->exists($hash=$fw->hash($fw->stringify( - array($filter))).'.mongo',$result)) || !$ttl || + [$filter])).'.mongo',$result)) || !$ttl || $cached[0]+$ttlcollection->count($filter?:array()); + $result=$this->collection->count($filter?:[]); if ($fw->get('CACHE') && $ttl) // Save to cache backend $cache->set($hash,$result,$ttl); @@ -221,7 +223,7 @@ class Mapper extends \DB\Cursor { * @param $ofs int **/ function skip($ofs=1) { - $this->document=($out=parent::skip($ofs))?$out->document:array(); + $this->document=($out=parent::skip($ofs))?$out->document:[]; if ($this->document && isset($this->trigger['load'])) \Base::instance()->call($this->trigger['load'],$this); return $out; @@ -236,13 +238,13 @@ class Mapper extends \DB\Cursor { return $this->update(); if (isset($this->trigger['beforeinsert']) && \Base::instance()->call($this->trigger['beforeinsert'], - array($this,array('_id'=>$this->document['_id'])))===FALSE) + [$this,['_id'=>$this->document['_id']]])===FALSE) return $this->document; $this->collection->insert($this->document); - $pkey=array('_id'=>$this->document['_id']); + $pkey=['_id'=>$this->document['_id']]; if (isset($this->trigger['afterinsert'])) \Base::instance()->call($this->trigger['afterinsert'], - array($this,$pkey)); + [$this,$pkey]); $this->load($pkey); return $this->document; } @@ -252,16 +254,16 @@ class Mapper extends \DB\Cursor { * @return array **/ function update() { - $pkey=array('_id'=>$this->document['_id']); + $pkey=['_id'=>$this->document['_id']]; if (isset($this->trigger['beforeupdate']) && \Base::instance()->call($this->trigger['beforeupdate'], - array($this,$pkey))===FALSE) + [$this,$pkey])===FALSE) return $this->document; $this->collection->update( - $pkey,$this->document,array('upsert'=>TRUE)); + $pkey,$this->document,['upsert'=>TRUE]); if (isset($this->trigger['afterupdate'])) \Base::instance()->call($this->trigger['afterupdate'], - array($this,$pkey)); + [$this,$pkey]); return $this->document; } @@ -273,17 +275,17 @@ class Mapper extends \DB\Cursor { function erase($filter=NULL) { if ($filter) return $this->collection->remove($filter); - $pkey=array('_id'=>$this->document['_id']); + $pkey=['_id'=>$this->document['_id']]; if (isset($this->trigger['beforeerase']) && \Base::instance()->call($this->trigger['beforeerase'], - array($this,$pkey))===FALSE) + [$this,$pkey])===FALSE) return FALSE; $result=$this->collection-> - remove(array('_id'=>$this->document['_id'])); + remove(['_id'=>$this->document['_id']]); parent::erase(); if (isset($this->trigger['aftererase'])) \Base::instance()->call($this->trigger['aftererase'], - array($this,$pkey)); + [$this,$pkey]); return $result; } @@ -292,7 +294,7 @@ class Mapper extends \DB\Cursor { * @return NULL **/ function reset() { - $this->document=array(); + $this->document=[]; parent::reset(); } @@ -351,10 +353,12 @@ class Mapper extends \DB\Cursor { * @return void * @param $db object * @param $collection string + * @param $fields array **/ - function __construct(\DB\Mongo $db,$collection) { + function __construct(\DB\Mongo $db,$collection,$fields=NULL) { $this->db=$db; $this->collection=$db->selectcollection($collection); + $this->fields=$fields; $this->reset(); } diff --git a/app/lib/db/mongo/session.php b/app/lib/db/mongo/session.php index 0b510f13..5ea894b8 100644 --- a/app/lib/db/mongo/session.php +++ b/app/lib/db/mongo/session.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -63,12 +63,13 @@ class Session extends Mapper { * @param $id string **/ function read($id) { - $this->load(array('session_id'=>$this->sid=$id)); + $this->load(['session_id'=>$this->sid=$id]); if ($this->dry()) return FALSE; if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { $fw=\Base::instance(); - if (!isset($this->onsuspect) || FALSE===$fw->call($this->onsuspect,array($this,$id))) { + if (!isset($this->onsuspect) || + $fw->call($this->onsuspect,[$this,$id])===FALSE) { //NB: `session_destroy` can't be called at that stage (`session_start` not completed) $this->destroy($id); $this->close(); @@ -101,7 +102,7 @@ class Session extends Mapper { * @param $id string **/ function destroy($id) { - $this->erase(array('session_id'=>$id)); + $this->erase(['session_id'=>$id]); return TRUE; } @@ -111,7 +112,7 @@ class Session extends Mapper { * @param $max int **/ function cleanup($max) { - $this->erase(array('$where'=>'this.stamp+'.$max.'<'.time())); + $this->erase(['$where'=>'this.stamp+'.$max.'<'.time()]); return TRUE; } @@ -168,18 +169,17 @@ class Session extends Mapper { parent::__construct($db,$table); $this->onsuspect=$onsuspect; session_set_save_handler( - array($this,'open'), - array($this,'close'), - array($this,'read'), - array($this,'write'), - array($this,'destroy'), - array($this,'cleanup') + [$this,'open'], + [$this,'close'], + [$this,'read'], + [$this,'write'], + [$this,'destroy'], + [$this,'cleanup'] ); register_shutdown_function('session_commit'); $fw=\Base::instance(); $headers=$fw->get('HEADERS'); - $this->_csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); + $this->_csrf=$fw->get('SEED').'.'.$fw->hash(mt_rand()); if ($key) $fw->set($key,$this->_csrf); $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; diff --git a/app/lib/db/sql.php b/app/lib/db/sql.php index 60089446..48b49cc1 100644 --- a/app/lib/db/sql.php +++ b/app/lib/db/sql.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -30,6 +30,9 @@ class SQL { E_PKey='Table %s does not have a primary key'; //@} + const + PARAM_FLOAT='float'; + protected //! UUID $uuid, @@ -78,6 +81,14 @@ class SQL { return $out; } + /** + * Return transaction flag + * @return bool + **/ + function trans() { + return $this->trans; + } + /** * Map data type of argument to a PDO constant * @return int @@ -93,6 +104,8 @@ class SQL { return \PDO::PARAM_INT; case 'resource': return \PDO::PARAM_LOB; + case 'float': + return self::PARAM_FLOAT; default: return \PDO::PARAM_STR; } @@ -106,6 +119,10 @@ class SQL { **/ function value($type,$val) { switch ($type) { + case self::PARAM_FLOAT: + return (float)(is_string($val) + ? str_replace(',','.',preg_replace('/([.,])(?!\d+$)/','',$val)) + : $val); case \PDO::PARAM_NULL: return (unset)$val; case \PDO::PARAM_INT: @@ -124,15 +141,19 @@ class SQL { * @return array|int|FALSE * @param $cmds string|array * @param $args string|array - * @param $ttl int + * @param $ttl int|array * @param $log bool + * @param $stamp bool **/ - function exec($cmds,$args=NULL,$ttl=0,$log=TRUE) { + function exec($cmds,$args=NULL,$ttl=0,$log=TRUE,$stamp=FALSE) { + $tag=''; + if (is_array($ttl)) + list($ttl,$tag)=$ttl; $auto=FALSE; if (is_null($args)) - $args=array(); + $args=[]; elseif (is_scalar($args)) - $args=array(1=>$args); + $args=[1=>$args]; if (is_array($cmds)) { if (count($args)<($count=count($cmds))) // Apply arguments to SQL commands @@ -144,9 +165,11 @@ class SQL { } else { $count=1; - $cmds=array($cmds); - $args=array($args); + $cmds=[$cmds]; + $args=[$args]; } + if ($this->log===FALSE) + $log=FALSE; $fw=\Base::instance(); $cache=\Cache::instance(); $result=FALSE; @@ -161,10 +184,10 @@ class SQL { if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd)) continue; $now=microtime(TRUE); - $keys=$vals=array(); + $keys=$vals=[]; if ($fw->get('CACHE') && $ttl && ($cached=$cache->exists( $hash=$fw->hash($this->dsn.$cmd. - $fw->stringify($arg)).'.sql',$result)) && + $fw->stringify($arg)).($tag?'.'.$tag:'').'.sql',$result)) && $cached[0]+$ttl>microtime(TRUE)) { foreach ($arg as $key=>$val) { $vals[]=$fw->stringify(is_array($val)?$val[0]:$val); @@ -172,7 +195,7 @@ class SQL { '/'; } if ($log) - $this->log.=date('r').' ('. + $this->log.=($stamp?(date('r').' '):'').'('. sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. '[CACHED] '. preg_replace($keys,$vals, @@ -182,20 +205,22 @@ class SQL { foreach ($arg as $key=>$val) { if (is_array($val)) { // User-specified data type - $query->bindvalue($key,$val[0],$val[1]); + $query->bindvalue($key,$val[0], + $val[1]==self::PARAM_FLOAT?\PDO::PARAM_STR:$val[1]); $vals[]=$fw->stringify($this->value($val[1],$val[0])); } else { // Convert to PDO data type $query->bindvalue($key,$val, - $type=$this->type($val)); + ($type=$this->type($val))==self::PARAM_FLOAT? + \PDO::PARAM_STR:$type); $vals[]=$fw->stringify($this->value($type,$val)); } $keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). '/'; } if ($log) - $this->log.=date('r').' ('. + $this->log.=($stamp?(date('r').' '):'').'('. sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. preg_replace($keys,$vals, str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; @@ -216,7 +241,7 @@ class SQL { if (preg_match('/sqlite2?/',$this->engine)) foreach ($result as $pos=>$rec) { unset($result[$pos]); - $result[$pos]=array(); + $result[$pos]=[]; foreach ($rec as $key=>$val) $result[$pos][trim($key,'\'"[]`')]=$val; } @@ -269,20 +294,20 @@ class SQL { * @return array|FALSE * @param $table string * @param $fields array|string - * @param $ttl int + * @param $ttl int|array **/ function schema($table,$fields=NULL,$ttl=0) { if (strpos($table,'.')) list($schema,$table)=explode('.',$table); // Supported engines - $cmd=array( - 'sqlite2?'=>array( - 'PRAGMA table_info("'.$table.'");', - 'name','type','dflt_value','notnull',0,'pk',TRUE), - 'mysql'=>array( - 'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`;', - 'Field','Type','Default','Null','YES','Key','PRI'), - 'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>array( + $cmd=[ + 'sqlite2?'=>[ + 'PRAGMA table_info("'.$table.'")', + 'name','type','dflt_value','notnull',0,'pk',TRUE], + 'mysql'=>[ + 'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`', + 'Field','Type','Default','Null','YES','Key','PRI'], + 'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>[ 'SELECT '. 'c.column_name AS field,'. 'c.data_type AS type,'. @@ -309,10 +334,9 @@ class SQL { 'c.table_name='.$this->quote($table). ($this->dbname? (' AND c.table_catalog='. - $this->quote($this->dbname)):''). - ';', - 'field','type','defval','nullable','YES','pkey','PRIMARY KEY'), - 'oci'=>array( + $this->quote($this->dbname)):''), + 'field','type','defval','nullable','YES','pkey','PRIMARY KEY'], + 'oci'=>[ 'SELECT c.column_name AS field, '. 'c.data_type AS type, '. 'c.data_default AS defval, '. @@ -326,35 +350,35 @@ class SQL { 'AND constraint_type='.$this->quote('P').') AS pkey '. 'FROM all_tab_cols c '. 'WHERE c.table_name='.$this->quote($table), - 'FIELD','TYPE','DEFVAL','NULLABLE','Y','PKEY','P') - ); + 'FIELD','TYPE','DEFVAL','NULLABLE','Y','PKEY','P'] + ]; if (is_string($fields)) $fields=\Base::instance()->split($fields); foreach ($cmd as $key=>$val) if (preg_match('/'.$key.'/',$this->engine)) { - // Improve InnoDB performance on MySQL with - // SET GLOBAL innodb_stats_on_metadata=0; - // This requires SUPER privilege! - $rows=array(); + $rows=[]; foreach ($this->exec($val[0],NULL,$ttl) as $row) { if (!$fields || in_array($row[$val[1]],$fields)) - $rows[$row[$val[1]]]=array( + $rows[$row[$val[1]]]=[ 'type'=>$row[$val[2]], 'pdo_type'=> preg_match('/int\b|integer/i',$row[$val[2]])? \PDO::PARAM_INT: (preg_match('/bool/i',$row[$val[2]])? \PDO::PARAM_BOOL: - (preg_match('/blob|bytea|image|binary/i', - $row[$val[2]])? - \PDO::PARAM_LOB: - \PDO::PARAM_STR)), + (preg_match( + '/blob|bytea|image|binary/i', + $row[$val[2]])?\PDO::PARAM_LOB: + (preg_match( + '/float|decimal|real|numeric|double/i', + $row[$val[2]])?self::PARAM_FLOAT: + \PDO::PARAM_STR))), 'default'=>is_string($row[$val[3]])? preg_replace('/^\s*([\'"])(.*)\1\s*/','\2', $row[$val[3]]):$row[$val[3]], 'nullable'=>$row[$val[4]]==$val[5], 'pkey'=>$row[$val[6]]==$val[7] - ); + ]; } return $rows; } @@ -420,20 +444,22 @@ class SQL { * Return quoted identifier name * @return string * @param $key - **/ - function quotekey($key) { - $delims=array( + * @param bool $split + **/ + function quotekey($key, $split=TRUE) { + $delims=[ 'mysql'=>'``', 'sqlite2?|pgsql|oci'=>'""', 'mssql|sqlsrv|odbc|sybase|dblib'=>'[]' - ); + ]; $use=''; foreach ($delims as $engine=>$delim) if (preg_match('/'.$engine.'/',$this->engine)) { $use=$delim; break; } - return $use[0].implode($use[1].'.'.$use[0],explode('.',$key)).$use[1]; + return $use[0].($split ? implode($use[1].'.'.$use[0],explode('.',$key)) + : $key).$use[1]; } /** @@ -443,7 +469,11 @@ class SQL { * @param $args array **/ function __call($func,array $args) { - return call_user_func_array(array($this->pdo,$func),$args); + return call_user_func_array([$this->pdo,$func],$args); + } + + //! Prohibit cloning + private function __clone() { } /** @@ -459,10 +489,10 @@ class SQL { if (preg_match('/^.+?(?:dbname|database)=(.+?)(?=;|$)/is',$dsn,$parts)) $this->dbname=$parts[1]; if (!$options) - $options=array(); + $options=[]; if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql') - $options+=array(\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '. - strtolower(str_replace('-','',$fw->get('ENCODING'))).';'); + $options+=[\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '. + strtolower(str_replace('-','',$fw->get('ENCODING'))).';']; $this->pdo=new \PDO($dsn,$user,$pw,$options); $this->engine=$this->pdo->getattribute(\PDO::ATTR_DRIVER_NAME); } diff --git a/app/lib/db/sql/mapper.php b/app/lib/db/sql/mapper.php index ca6f8493..ac4dd87c 100644 --- a/app/lib/db/sql/mapper.php +++ b/app/lib/db/sql/mapper.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -39,7 +39,7 @@ class Mapper extends \DB\Cursor { //! Defined fields $fields, //! Adhoc fields - $adhoc=array(); + $adhoc=[]; /** * Return database type @@ -90,7 +90,7 @@ class Mapper extends \DB\Cursor { if (array_key_exists($key,$this->fields)) { $val=is_null($val) && $this->fields[$key]['nullable']? NULL:$this->db->value($this->fields[$key]['pdo_type'],$val); - if ($this->fields[$key]['value']!==$val || + if ($this->fields[$key]['initial']!==$val || $this->fields[$key]['default']!==$val && is_null($val)) $this->fields[$key]['changed']=TRUE; return $this->fields[$key]['value']=$val; @@ -100,7 +100,7 @@ class Mapper extends \DB\Cursor { $this->adhoc[$key]['value']=$val; else // Parenthesize expression in case it's a subquery - $this->adhoc[$key]=array('expr'=>'('.$val.')','value'=>NULL); + $this->adhoc[$key]=['expr'=>'('.$val.')','value'=>NULL]; return $val; } @@ -144,6 +144,8 @@ class Mapper extends \DB\Cursor { return 'bool'; case \PDO::PARAM_STR: return 'string'; + case \DB\SQL::PARAM_FLOAT: + return 'float'; } } @@ -163,10 +165,11 @@ class Mapper extends \DB\Cursor { else continue; $mapper->{$var}[$key]['value']=$val; + $mapper->{$var}[$key]['initial']=$val; if ($var=='fields' && $mapper->{$var}[$key]['pkey']) $mapper->{$var}[$key]['previous']=$val; } - $mapper->query=array(clone($mapper)); + $mapper->query=[clone($mapper)]; if (isset($mapper->trigger['load'])) \Base::instance()->call($mapper->trigger['load'],$mapper); return $mapper; @@ -194,37 +197,36 @@ class Mapper extends \DB\Cursor { * @param $fields string * @param $filter string|array * @param $options array - * @param $ttl int + * @param $ttl int|array **/ function select($fields,$filter=NULL,array $options=NULL,$ttl=0) { if (!$options) - $options=array(); - $options+=array( + $options=[]; + $options+=[ 'group'=>NULL, 'order'=>NULL, 'limit'=>0, 'offset'=>0 - ); + ]; $db=$this->db; $sql='SELECT '.$fields.' FROM '.$this->table; - $args=array(); - if ($filter) { - if (is_array($filter)) { - $args=isset($filter[1]) && is_array($filter[1])? - $filter[1]: - array_slice($filter,1,NULL,TRUE); - $args=is_array($args)?$args:array(1=>$args); - list($filter)=$filter; - } - $sql.=' WHERE '.$filter; + $args=[]; + if (is_array($filter)) { + $args=isset($filter[1]) && is_array($filter[1])? + $filter[1]: + array_slice($filter,1,NULL,TRUE); + $args=is_array($args)?$args:[1=>$args]; + list($filter)=$filter; } + if ($filter) + $sql.=' WHERE '.$filter; if ($options['group']) { $sql.=' GROUP BY '.implode(',',array_map( function($str) use($db) { return preg_replace_callback( '/\b(\w+)\h*(HAVING.+|$)/i', function($parts) use($db) { - return $db->quotekey($parts[1]); + return $db->quotekey($parts[1]).(isset($parts[2])?(' '.$parts[2]):''); }, $str ); @@ -243,7 +245,7 @@ class Mapper extends \DB\Cursor { } if (preg_match('/mssql|sqlsrv|odbc/', $this->engine) && ($options['limit'] || $options['offset'])) { - $pkeys=array(); + $pkeys=[]; foreach ($this->fields as $key=>$field) if ($field['pkey']) $pkeys[]=$key; @@ -274,7 +276,7 @@ class Mapper extends \DB\Cursor { $sql.=' OFFSET '.(int)$options['offset']; } $result=$this->db->exec($sql,$args,$ttl); - $out=array(); + $out=[]; foreach ($result as &$row) { foreach ($row as $field=>&$val) { if (array_key_exists($field,$this->fields)) { @@ -297,24 +299,24 @@ class Mapper extends \DB\Cursor { * @return static[] * @param $filter string|array * @param $options array - * @param $ttl int + * @param $ttl int|array **/ function find($filter=NULL,array $options=NULL,$ttl=0) { if (!$options) - $options=array(); - $options+=array( + $options=[]; + $options+=[ 'group'=>NULL, 'order'=>NULL, 'limit'=>0, 'offset'=>0 - ); + ]; $adhoc=''; foreach ($this->adhoc as $key=>$field) $adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key); return $this->select( ($options['group'] && !preg_match('/mysql|sqlite/',$this->engine)? $options['group']: - implode(',',array_map(array($this->db,'quotekey'), + implode(',',array_map([$this->db,'quotekey'], array_keys($this->fields)))).$adhoc,$filter,$options,$ttl); } @@ -322,18 +324,18 @@ class Mapper extends \DB\Cursor { * Count records that match criteria * @return int * @param $filter string|array - * @param $ttl int + * @param $ttl int|array **/ function count($filter=NULL,$ttl=0) { $sql='SELECT COUNT(*) AS '. $this->db->quotekey('rows').' FROM '.$this->table; - $args=array(); + $args=[]; if ($filter) { if (is_array($filter)) { $args=isset($filter[1]) && is_array($filter[1])? $filter[1]: array_slice($filter,1,NULL,TRUE); - $args=is_array($args)?$args:array(1=>$args); + $args=is_array($args)?$args:[1=>$args]; list($filter)=$filter; } $sql.=' WHERE '.$filter; @@ -353,6 +355,7 @@ class Mapper extends \DB\Cursor { $dry=$this->dry(); foreach ($this->fields as $key=>&$field) { $field['value']=$dry?NULL:$out->fields[$key]['value']; + $field['initial']=$field['value']; $field['changed']=FALSE; if ($field['pkey']) $field['previous']=$dry?NULL:$out->fields[$key]['value']; @@ -372,22 +375,22 @@ class Mapper extends \DB\Cursor { * @return object **/ function insert() { - $args=array(); + $args=[]; $actr=0; $nctr=0; $fields=''; $values=''; $filter=''; - $pkeys=array(); - $nkeys=array(); - $ckeys=array(); + $pkeys=[]; + $nkeys=[]; + $ckeys=[]; $inc=NULL; foreach ($this->fields as $key=>$field) if ($field['pkey']) $pkeys[$key]=$field['previous']; if (isset($this->trigger['beforeinsert']) && \Base::instance()->call($this->trigger['beforeinsert'], - array($this,$pkeys))===FALSE) + [$this,$pkeys])===FALSE) return $this; foreach ($this->fields as $key=>&$field) { if ($field['pkey']) { @@ -396,13 +399,13 @@ class Mapper extends \DB\Cursor { empty($field['value']) && !$field['nullable']) $inc=$key; $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; - $nkeys[$nctr+1]=array($field['value'],$field['pdo_type']); + $nkeys[$nctr+1]=[$field['value'],$field['pdo_type']]; $nctr++; } if ($field['changed'] && $key!=$inc) { $fields.=($actr?',':'').$this->db->quotekey($key); $values.=($actr?',':'').'?'; - $args[$actr+1]=array($field['value'],$field['pdo_type']); + $args[$actr+1]=[$field['value'],$field['pdo_type']]; $actr++; $ckeys[]=$key; } @@ -420,18 +423,21 @@ class Mapper extends \DB\Cursor { $seq=NULL; if ($this->engine=='pgsql') { $names=array_keys($pkeys); - $seq=$this->source.'_'.end($names).'_seq'; + $aik=end($names); + if ($this->fields[$aik]['pdo_type']==\PDO::PARAM_INT) + $seq=$this->source.'_'.$aik.'_seq'; } - if ($this->engine!='oci') + if ($this->engine!='oci' && !($this->engine=='pgsql' && !$seq)) $this->_id=$this->db->lastinsertid($seq); // Reload to obtain default and auto-increment field values - $this->load($inc? - array($inc.'=?',$this->db->value( - $this->fields[$inc]['pdo_type'],$this->_id)): - array($filter,$nkeys)); + if ($inc || $filter) + $this->load($inc? + [$inc.'=?',$this->db->value( + $this->fields[$inc]['pdo_type'],$this->_id)]: + [$filter,$nkeys]); if (isset($this->trigger['afterinsert'])) \Base::instance()->call($this->trigger['afterinsert'], - array($this,$pkeys)); + [$this,$pkeys]); } return $this; } @@ -441,38 +447,42 @@ class Mapper extends \DB\Cursor { * @return object **/ function update() { - $args=array(); + $args=[]; $ctr=0; $pairs=''; $filter=''; - $pkeys=array(); + $pkeys=[]; foreach ($this->fields as $key=>$field) if ($field['pkey']) $pkeys[$key]=$field['previous']; if (isset($this->trigger['beforeupdate']) && \Base::instance()->call($this->trigger['beforeupdate'], - array($this,$pkeys))===FALSE) + [$this,$pkeys])===FALSE) return $this; foreach ($this->fields as $key=>$field) if ($field['changed']) { $pairs.=($pairs?',':'').$this->db->quotekey($key).'=?'; - $args[$ctr+1]=array($field['value'],$field['pdo_type']); - $ctr++; + $args[++$ctr]=[$field['value'],$field['pdo_type']]; } foreach ($this->fields as $key=>$field) if ($field['pkey']) { $filter.=($filter?' AND ':' WHERE '). $this->db->quotekey($key).'=?'; - $args[$ctr+1]=array($field['previous'],$field['pdo_type']); - $ctr++; + $args[++$ctr]=[$field['previous'],$field['pdo_type']]; } if ($pairs) { $sql='UPDATE '.$this->table.' SET '.$pairs.$filter; $this->db->exec($sql,$args); if (isset($this->trigger['afterupdate'])) \Base::instance()->call($this->trigger['afterupdate'], - array($this,$pkeys)); + [$this,$pkeys]); } + // reset changed flag after calling afterupdate + foreach ($this->fields as $key=>&$field) { + $field['changed']=FALSE; + $field['initial']=$field['value']; + unset($field); + } return $this; } @@ -483,25 +493,25 @@ class Mapper extends \DB\Cursor { **/ function erase($filter=NULL) { if ($filter) { - $args=array(); + $args=[]; if (is_array($filter)) { $args=isset($filter[1]) && is_array($filter[1])? $filter[1]: array_slice($filter,1,NULL,TRUE); - $args=is_array($args)?$args:array(1=>$args); + $args=is_array($args)?$args:[1=>$args]; list($filter)=$filter; } return $this->db-> exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args); } - $args=array(); + $args=[]; $ctr=0; $filter=''; - $pkeys=array(); + $pkeys=[]; foreach ($this->fields as $key=>&$field) { if ($field['pkey']) { $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; - $args[$ctr+1]=array($field['previous'],$field['pdo_type']); + $args[$ctr+1]=[$field['previous'],$field['pdo_type']]; $pkeys[$key]=$field['previous']; $ctr++; } @@ -518,13 +528,13 @@ class Mapper extends \DB\Cursor { parent::erase(); if (isset($this->trigger['beforeerase']) && \Base::instance()->call($this->trigger['beforeerase'], - array($this,$pkeys))===FALSE) + [$this,$pkeys])===FALSE) return 0; $out=$this->db-> exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args); if (isset($this->trigger['aftererase'])) \Base::instance()->call($this->trigger['aftererase'], - array($this,$pkeys)); + [$this,$pkeys]); return $out; } @@ -535,6 +545,7 @@ class Mapper extends \DB\Cursor { function reset() { foreach ($this->fields as &$field) { $field['value']=NULL; + $field['initial']=NULL; $field['changed']=FALSE; if ($field['pkey']) $field['previous']=NULL; @@ -591,7 +602,7 @@ class Mapper extends \DB\Cursor { * @param $adhoc bool **/ function fields($adhoc=TRUE) { - return array_keys($this->fields+($adhoc?$this->adhoc:array())); + return array_keys($this->fields+($adhoc?$this->adhoc:[])); } /** @@ -617,7 +628,7 @@ class Mapper extends \DB\Cursor { * @param $db object * @param $table string * @param $fields array|string - * @param $ttl int + * @param $ttl int|array **/ function __construct(\DB\SQL $db,$table,$fields=NULL,$ttl=60) { $this->db=$db; diff --git a/app/lib/db/sql/session.php b/app/lib/db/sql/session.php index 86be69f2..56c1f430 100644 --- a/app/lib/db/sql/session.php +++ b/app/lib/db/sql/session.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -63,12 +63,13 @@ class Session extends Mapper { * @param $id string **/ function read($id) { - $this->load(array('session_id=?',$this->sid=$id)); + $this->load(['session_id=?',$this->sid=$id]); if ($this->dry()) return FALSE; if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { $fw=\Base::instance(); - if (!isset($this->onsuspect) || FALSE===$fw->call($this->onsuspect,array($this,$id))) { + if (!isset($this->onsuspect) || + $fw->call($this->onsuspect,[$this,$id])===FALSE) { //NB: `session_destroy` can't be called at that stage (`session_start` not completed) $this->destroy($id); $this->close(); @@ -101,7 +102,7 @@ class Session extends Mapper { * @param $id string **/ function destroy($id) { - $this->erase(array('session_id=?',$id)); + $this->erase(['session_id=?',$id]); return TRUE; } @@ -111,7 +112,7 @@ class Session extends Mapper { * @param $max int **/ function cleanup($max) { - $this->erase(array('stamp+?erase(['stamp+?name())&&$db->driver()!='pgsql')? - ($name.'.'):''))). - $table.' ('.$eol. + ($db->quotekey($name,FALSE).'.'):''))). + $db->quotekey($table,FALSE).' ('.$eol. $tab.$db->quotekey('session_id').' VARCHAR(255),'.$eol. $tab.$db->quotekey('data').' TEXT,'.$eol. $tab.$db->quotekey('ip').' VARCHAR(45),'.$eol. - $tab.$db->quotekey('agent').' VARCHAR(255),'.$eol. + $tab.$db->quotekey('agent').' VARCHAR(300),'.$eol. $tab.$db->quotekey('stamp').' INTEGER,'.$eol. $tab.'PRIMARY KEY ('.$db->quotekey('session_id').')'.$eol. ');' @@ -190,18 +191,17 @@ class Session extends Mapper { parent::__construct($db,$table); $this->onsuspect=$onsuspect; session_set_save_handler( - array($this,'open'), - array($this,'close'), - array($this,'read'), - array($this,'write'), - array($this,'destroy'), - array($this,'cleanup') + [$this,'open'], + [$this,'close'], + [$this,'read'], + [$this,'write'], + [$this,'destroy'], + [$this,'cleanup'] ); register_shutdown_function('session_commit'); $fw=\Base::instance(); $headers=$fw->get('HEADERS'); - $this->_csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); + $this->_csrf=$fw->get('SEED').'.'.$fw->hash(mt_rand()); if ($key) $fw->set($key,$this->_csrf); $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; diff --git a/app/lib/f3.php b/app/lib/f3.php index 96c7845e..d68cd1fb 100644 --- a/app/lib/f3.php +++ b/app/lib/f3.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -36,7 +36,7 @@ class F3 { static function __callstatic($func,array $args) { if (!self::$fw) self::$fw=Base::instance(); - return call_user_func_array(array(self::$fw,$func),$args); + return call_user_func_array([self::$fw,$func],$args); } } diff --git a/app/lib/image.php b/app/lib/image.php index e1acd483..a8514c22 100644 --- a/app/lib/image.php +++ b/app/lib/image.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -28,6 +28,7 @@ class Image { E_Color='Invalid color specified: %s', E_File='File not found', E_Font='CAPTCHA font not found', + E_TTF='No TrueType support in GD module', E_Length='Invalid CAPTCHA length: %s'; //@} @@ -54,9 +55,11 @@ class Image { /** * Convert RGB hex triad to array * @return array|FALSE - * @param $color int + * @param $color int|string **/ function rgb($color) { + if (is_string($color)) + $color=hexdec($color); $hex=str_pad($hex=dechex($color),$color<4096?3:6,'0',STR_PAD_LEFT); if (($len=strlen($hex))>6) user_error(sprintf(self::E_Color,'0x'.$hex),E_USER_ERROR); @@ -225,9 +228,17 @@ class Image { * @param $crop bool * @param $enlarge bool **/ - function resize($width,$height,$crop=TRUE,$enlarge=TRUE) { + function resize($width=NULL,$height=NULL,$crop=TRUE,$enlarge=TRUE) { + if (is_null($width) && is_null($height)) + return $this; + $origw=$this->width(); + $origh=$this->height(); + if (is_null($width)) + $width=round(($height/$origh)*$origw); + if (is_null($height)) + $height=round(($width/$origw)*$origh); // Adjust dimensions; retain aspect ratio - $ratio=($origw=imagesx($this->data))/($origh=imagesy($this->data)); + $ratio=$origw/$origh; if (!$crop) { if ($width/$ratio<=$height) $height=round($width/$ratio); @@ -331,24 +342,24 @@ class Image { * @param $blocks int **/ function identicon($str,$size=64,$blocks=4) { - $sprites=array( - array(.5,1,1,0,1,1), - array(.5,0,1,0,.5,1,0,1), - array(.5,0,1,0,1,1,.5,1,1,.5), - array(0,.5,.5,0,1,.5,.5,1,.5,.5), - array(0,.5,1,0,1,1,0,1,1,.5), - array(1,0,1,1,.5,1,1,.5,.5,.5), - array(0,0,1,0,1,.5,0,0,.5,1,0,1), - array(0,0,.5,0,1,.5,.5,1,0,1,.5,.5), - array(.5,0,.5,.5,1,.5,1,1,.5,1,.5,.5,0,.5), - array(0,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1), - array(0,.5,.5,1,1,.5,.5,0,1,0,1,1,0,1), - array(.5,0,1,0,1,1,.5,1,1,.75,.5,.5,1,.25), - array(0,.5,.5,0,.5,.5,1,0,1,.5,.5,1,.5,.5,0,1), - array(0,0,1,0,1,1,0,1,1,.5,.5,.25,.5,.75,0,.5,.5,.25), - array(0,.5,.5,.5,.5,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1), - array(0,0,1,0,.5,.5,.5,0,0,.5,1,.5,.5,1,.5,.5,0,1) - ); + $sprites=[ + [.5,1,1,0,1,1], + [.5,0,1,0,.5,1,0,1], + [.5,0,1,0,1,1,.5,1,1,.5], + [0,.5,.5,0,1,.5,.5,1,.5,.5], + [0,.5,1,0,1,1,0,1,1,.5], + [1,0,1,1,.5,1,1,.5,.5,.5], + [0,0,1,0,1,.5,0,0,.5,1,0,1], + [0,0,.5,0,1,.5,.5,1,0,1,.5,.5], + [.5,0,.5,.5,1,.5,1,1,.5,1,.5,.5,0,.5], + [0,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1], + [0,.5,.5,1,1,.5,.5,0,1,0,1,1,0,1], + [.5,0,1,0,1,1,.5,1,1,.75,.5,.5,1,.25], + [0,.5,.5,0,.5,.5,1,0,1,.5,.5,1,.5,.5,0,1], + [0,0,1,0,1,1,0,1,1,.5,.5,.25,.5,.75,0,.5,.5,.25], + [0,.5,.5,.5,.5,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1], + [0,0,1,0,.5,.5,.5,0,0,.5,1,.5,.5,1,.5,.5,0,1] + ]; $hash=sha1($str); $this->data=imagecreatetruecolor($size,$size); list($r,$g,$b)=$this->rgb(hexdec(substr($hash,-3))); @@ -360,15 +371,10 @@ class Image { for ($i=$j,$x=$blocks-1-$j;$i<$x;$i++) { $sprite=imagecreatetruecolor($dim,$dim); imagefill($sprite,0,0,IMG_COLOR_TRANSPARENT); - if ($block=$sprites[ - hexdec($hash[($j*$blocks+$i)*2])%$ctr]) { - for ($k=0,$pts=count($block);$k<$pts;$k++) - $block[$k]*=$dim; - imagefilledpolygon($sprite,$block,$pts/2,$fg); - } - $sprite=imagerotate($sprite, - 90*(hexdec($hash[($j*$blocks+$i)*2+1])%4), - imagecolorallocatealpha($sprite,0,0,0,127)); + $block=$sprites[hexdec($hash[($j*$blocks+$i)*2])%$ctr]; + for ($k=0,$pts=count($block);$k<$pts;$k++) + $block[$k]*=$dim; + imagefilledpolygon($sprite,$block,$pts/2,$fg); for ($k=0;$k<4;$k++) { imagecopyresampled($this->data,$sprite, $i*$dim/2,$j*$dim/2,0,0,$dim/2,$dim/2,$dim,$dim); @@ -398,6 +404,10 @@ class Image { user_error(sprintf(self::E_Length,$len),E_USER_ERROR); return FALSE; } + if (!function_exists('imagettftext')) { + user_error(self::E_TTF,E_USER_ERROR); + return FALSE; + } $fw=Base::instance(); foreach ($fw->split($path?:$fw->get('UI').';./') as $dir) if (is_file($path=$dir.$font)) { @@ -405,7 +415,7 @@ class Image { $ssl?bin2hex(openssl_random_pseudo_bytes($len)):uniqid(), -$len)); $block=$size*3; - $tmp=array(); + $tmp=[]; for ($i=0,$width=0,$height=0;$i<$len;$i++) { // Process at 2x magnification $box=imagettfbbox($size*2,0,$path,$seed[$i]); @@ -472,8 +482,10 @@ class Image { header('Content-Type: image/'.$format); header('X-Powered-By: '.Base::instance()->get('PACKAGE')); } - call_user_func_array('image'.$format, - array_merge(array($this->data),$args)); + call_user_func_array( + 'image'.$format, + array_merge([$this->data,NULL],$args) + ); } /** @@ -484,8 +496,10 @@ class Image { $args=func_get_args(); $format=$args?array_shift($args):'png'; ob_start(); - call_user_func_array('image'.$format, - array_merge(array($this->data),$args)); + call_user_func_array( + 'image'.$format, + array_merge([$this->data,NULL],$args) + ); return ob_get_clean(); } @@ -507,8 +521,7 @@ class Image { if (!is_dir($dir=$fw->get('TEMP'))) mkdir($dir,Base::MODE,TRUE); $this->count++; - $fw->write($dir.'/'. - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + $fw->write($dir.'/'.$fw->get('SEED').'.'. $fw->hash($this->file).'-'.$this->count.'.png', $this->dump()); } @@ -523,8 +536,7 @@ class Image { function restore($state=1) { $fw=Base::instance(); if ($this->flag && is_file($file=($path=$fw->get('TEMP'). - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash($this->file).'-').$state.'.png')) { + $fw->get('SEED').'.'.$fw->hash($this->file).'-').$state.'.png')) { if (is_resource($this->data)) imagedestroy($this->data); $this->data=imagecreatefromstring($fw->read($file)); @@ -553,11 +565,12 @@ class Image { /** * Load string - * @return object + * @return object|FALSE * @param $str string **/ function load($str) { - $this->data=imagecreatefromstring($str); + if (!$this->data=@imagecreatefromstring($str)) + return FALSE; imagesavealpha($this->data,TRUE); $this->save(); return $this; @@ -592,9 +605,7 @@ class Image { if (is_resource($this->data)) { imagedestroy($this->data); $fw=Base::instance(); - $path=$fw->get('TEMP'). - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash($this->file); + $path=$fw->get('TEMP').$fw->get('SEED').'.'.$fw->hash($this->file); if ($glob=@glob($path.'*.png',GLOB_NOSORT)) foreach ($glob as $match) if (preg_match('/-(\d+)\.png/',$match)) diff --git a/app/lib/log.php b/app/lib/log.php index 6583cedb..2bbe4c27 100644 --- a/app/lib/log.php +++ b/app/lib/log.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). diff --git a/app/lib/magic.php b/app/lib/magic.php index 5669908a..76ea221e 100644 --- a/app/lib/magic.php +++ b/app/lib/magic.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -70,7 +70,7 @@ abstract class Magic implements ArrayAccess { **/ function offsetset($key,$val) { return Base::instance()->visible($this,$key)? - ($this->key=$val):$this->set($key,$val); + ($this->$key=$val):$this->set($key,$val); } /** diff --git a/app/lib/markdown.php b/app/lib/markdown.php index 9863d911..1c3fb163 100644 --- a/app/lib/markdown.php +++ b/app/lib/markdown.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -321,7 +321,7 @@ class Markdown extends Prefab { $tmp=''; while ($str!=$tmp) $str=preg_replace_callback( - '/(?special) - $this->special=array( + $this->special=[ '...'=>'…', '(tm)'=>'™', '(r)'=>'®', '(c)'=>'©' - ); + ]; foreach ($this->special as $key=>$val) $str=preg_replace('/'.preg_quote($key,'/').'/i',$val,$str); return htmlspecialchars($str,ENT_COMPAT, @@ -454,7 +454,7 @@ class Markdown extends Prefab { * @param $str string **/ function scan($str) { - $inline=array('img','a','text','auto','code'); + $inline=['img','a','text','auto','code']; foreach ($inline as $func) $str=$this->{'_'.$func}($str); return $str; @@ -468,7 +468,7 @@ class Markdown extends Prefab { protected function build($str) { if (!$this->blocks) { // Regexes for capturing entire blocks - $this->blocks=array( + $this->blocks=[ 'blockquote'=>'/^(?:\h?>\h?.*?(?:\n+|$))+/', 'pre'=>'/^(?:(?: {4}|\t).+?(?:\n+|$))+/', 'fence'=>'/^`{3}\h*(\w+)?.*?[^\n]*\n+(.+?)`{3}[^\n]*'. @@ -486,7 +486,7 @@ class Markdown extends Prefab { '(?:\/>|>(?:(?>[^><]+)|(?R))*<\/\2>))'. '\h*(?:\n{2,}|\n*$)|<[\?%].+?[\?%]>\h*(?:\n?$|\n*))/s', 'p'=>'/^(.+?(?:\n{2,}|\n*$))/s' - ); + ]; } $self=$this; // Treat lines with nothing but whitespaces as empty lines @@ -546,7 +546,7 @@ class Markdown extends Prefab { if (preg_match($regex,substr($str,$ptr),$match)) { $ptr+=strlen($match[0]); $dst.=call_user_func_array( - array($this,'_'.$func), + [$this,'_'.$func], count($match)>1?array_slice($match,1):$match ); break; diff --git a/app/lib/matrix.php b/app/lib/matrix.php index 1ebce6b9..f20e9890 100644 --- a/app/lib/matrix.php +++ b/app/lib/matrix.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -45,7 +45,7 @@ class Matrix extends Prefab { * @param $var array **/ function transpose(array &$var) { - $out=array(); + $out=[]; foreach ($var as $keyx=>$cols) foreach ($cols as $keyy=>$valy) $out[$keyy][$keyx]=$valy; @@ -63,7 +63,7 @@ class Matrix extends Prefab { uasort( $var, function($val1,$val2) use($col,$order) { - list($v1,$v2)=array($val1[$col],$val2[$col]); + list($v1,$v2)=[$val1[$col],$val2[$col]]; $out=is_numeric($v1) && is_numeric($v2)? Base::instance()->sign($v1-$v2):strcmp($v1,$v2); if ($order==SORT_DESC) @@ -96,12 +96,15 @@ class Matrix extends Prefab { * @param $first int **/ function calendar($date='now',$first=0) { - $parts=getdate(strtotime($date)); - $days=cal_days_in_month(CAL_GREGORIAN,$parts['mon'],$parts['year']); - $ref=date('w',strtotime(date('Y-m',$parts[0]).'-01'))+(7-$first)%7; - $out=array(); - for ($i=0;$i<$days;$i++) - $out[floor(($ref+$i)/7)][($ref+$i)%7]=$i+1; + $out=FALSE; + if (extension_loaded('calendar')) { + $parts=getdate(strtotime($date)); + $days=cal_days_in_month(CAL_GREGORIAN,$parts['mon'],$parts['year']); + $ref=date('w',strtotime(date('Y-m',$parts[0]).'-01'))+(7-$first)%7; + $out=[]; + for ($i=0;$i<$days;$i++) + $out[floor(($ref+$i)/7)][($ref+$i)%7]=$i+1; + } return $out; } diff --git a/app/lib/session.php b/app/lib/session.php index 8b28c936..583193d9 100644 --- a/app/lib/session.php +++ b/app/lib/session.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -33,7 +33,9 @@ class Session { //! IP, $_ip, //! Suspect callback - $onsuspect; + $onsuspect, + //! Cache instance + $_cache; /** * Open session @@ -61,11 +63,12 @@ class Session { **/ function read($id) { $this->sid=$id; - if (!$data=Cache::instance()->get($id.'.@')) + if (!$data=$this->_cache->get($id.'.@')) return FALSE; if ($data['ip']!=$this->_ip || $data['agent']!=$this->_agent) { $fw=Base::instance(); - if (!isset($this->onsuspect) || FALSE===$fw->call($this->onsuspect,array($this,$id))) { + if (!isset($this->onsuspect) || + $fw->call($this->onsuspect,[$this,$id])===FALSE) { //NB: `session_destroy` can't be called at that stage (`session_start` not completed) $this->destroy($id); $this->close(); @@ -85,13 +88,13 @@ class Session { function write($id,$data) { $fw=Base::instance(); $jar=$fw->get('JAR'); - Cache::instance()->set($id.'.@', - array( + $this->_cache->set($id.'.@', + [ 'data'=>$data, 'ip'=>$this->_ip, 'agent'=>$this->_agent, 'stamp'=>time() - ), + ], $jar['expire']?($jar['expire']-time()):0 ); return TRUE; @@ -103,7 +106,7 @@ class Session { * @param $id string **/ function destroy($id) { - Cache::instance()->clear($id.'.@'); + $this->_cache->clear($id.'.@'); return TRUE; } @@ -113,7 +116,7 @@ class Session { * @param $max int **/ function cleanup($max) { - Cache::instance()->reset('.@',$max); + $this->_cache->reset('.@',$max); return TRUE; } @@ -148,7 +151,7 @@ class Session { function stamp() { if (!$this->sid) session_start(); - return Cache::instance()->exists($this->sid.'.@',$data)? + return $this->_cache->exists($this->sid.'.@',$data)? $data['stamp']:FALSE; } @@ -165,21 +168,21 @@ class Session { * @param $onsuspect callback * @param $key string **/ - function __construct($onsuspect=NULL,$key=NULL) { + function __construct($onsuspect=NULL,$key=NULL,$cache=null) { $this->onsuspect=$onsuspect; + $this->_cache=$cache?:Cache::instance(); session_set_save_handler( - array($this,'open'), - array($this,'close'), - array($this,'read'), - array($this,'write'), - array($this,'destroy'), - array($this,'cleanup') + [$this,'open'], + [$this,'close'], + [$this,'read'], + [$this,'write'], + [$this,'destroy'], + [$this,'cleanup'] ); register_shutdown_function('session_commit'); $fw=\Base::instance(); $headers=$fw->get('HEADERS'); - $this->_csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); + $this->_csrf=$fw->get('SEED').'.'.$fw->hash(mt_rand()); if ($key) $fw->set($key,$this->_csrf); $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; diff --git a/app/lib/smtp.php b/app/lib/smtp.php index 3c9e964f..00ee9b2d 100644 --- a/app/lib/smtp.php +++ b/app/lib/smtp.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -118,20 +118,41 @@ class SMTP extends Magic { * @return string * @param $cmd string * @param $log bool + * @param $mock bool **/ - protected function dialog($cmd=NULL,$log=TRUE) { - $socket=&$this->socket; - if (!is_null($cmd)) - fputs($socket,$cmd."\r\n"); + protected function dialog($cmd=NULL,$log=TRUE,$mock=FALSE) { $reply=''; - while (!feof($socket) && ($info=stream_get_meta_data($socket)) && - !$info['timed_out'] && $str=fgets($socket,4096)) { - $reply.=$str; - if (preg_match('/(?:^|\n)\d{3} .+?\r\n/s',$reply)) + if ($mock) { + $host=str_replace('ssl://','',$this->host); + switch ($cmd) { + case NULL: + $reply='220 '.$host.' ESMTP ready'."\n"; break; + case 'DATA': + $reply='354 Go ahead'."\n"; + break; + case 'QUIT': + $reply='221 '.$host.' closing connection'."\n"; + break; + default: + $reply='250 OK'."\n"; + break; + } + } + else { + $socket=&$this->socket; + if ($cmd) + fputs($socket,$cmd."\r\n"); + while (!feof($socket) && ($info=stream_get_meta_data($socket)) && + !$info['timed_out'] && $str=fgets($socket,4096)) { + $reply.=$str; + if (preg_match('/(?:^|\n)\d{3} .+?\r\n/s',$reply)) + break; + } } if ($log) { - $this->log.=$cmd."\n"; + if ($cmd) + $this->log.=$cmd."\n"; $this->log.=str_replace("\r",'',$reply); } return $reply; @@ -147,9 +168,9 @@ class SMTP extends Magic { function attach($file,$alias=NULL,$cid=NULL) { if (!is_file($file)) user_error(sprintf(self::E_Attach,$file),E_USER_ERROR); - if (is_string($alias)) - $file=array($alias=>$file); - $this->attachments[]=array('filename'=>$file,'cid'=>$cid); + if ($alias) + $file=[$alias=>$file]; + $this->attachments[]=['filename'=>$file,'cid'=>$cid]; } /** @@ -157,8 +178,9 @@ class SMTP extends Magic { * @return bool * @param $message string * @param $log bool + * @param $mock bool **/ - function send($message,$log=TRUE) { + function send($message,$log=TRUE,$mock=FALSE) { if ($this->scheme=='ssl' && !extension_loaded('openssl')) return FALSE; // Message should not be blank @@ -168,20 +190,25 @@ class SMTP extends Magic { // Retrieve headers $headers=$this->headers; // Connect to the server - $socket=&$this->socket; - $socket=@fsockopen($this->host,$this->port); - if (!$socket) - return FALSE; - stream_set_blocking($socket,TRUE); + if (!$mock) { + $socket=&$this->socket; + $socket=@fsockopen($this->host,$this->port,$errno,$errstr); + if (!$socket) { + $fw->error(500,$errstr); + return FALSE; + } + stream_set_blocking($socket,TRUE); + } // Get server's initial response - $this->dialog(NULL,FALSE); + $this->dialog(NULL,TRUE,$mock); // Announce presence - $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log); + $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log,$mock); if (strtolower($this->scheme)=='tls') { - $this->dialog('STARTTLS',$log); - stream_socket_enable_crypto( - $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); - $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log); + $this->dialog('STARTTLS',$log,$mock); + if (!$mock) + stream_socket_enable_crypto( + $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); + $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log,$mock); } if (preg_match('/8BITMIME/',$reply)) $headers['Content-Transfer-Encoding']='8bit'; @@ -192,12 +219,22 @@ class SMTP extends Magic { } if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) { // Authenticate - $this->dialog('AUTH LOGIN',$log); - $this->dialog(base64_encode($this->user),$log); - $this->dialog(base64_encode($this->pw),$log); + $this->dialog('AUTH LOGIN',$log,$mock); + $this->dialog(base64_encode($this->user),$log,$mock); + $auth_rply=$this->dialog(base64_encode($this->pw),$log,$mock); + if (!preg_match('/^235\s.*/',$auth_rply)) { + $this->dialog('QUIT',$log,$mock); + if (!$mock && $socket) + fclose($socket); + return FALSE; + } } + if (empty($headers['Message-ID'])) + $headers['Message-ID']='<'.uniqid('',TRUE).'@'.$this->host.'>'; + if (empty($headers['Date'])) + $headers['Date']=date('r'); // Required headers - $reqd=array('From','To','Subject'); + $reqd=['From','To','Subject']; foreach ($reqd as $id) if (empty($headers[$id])) user_error(sprintf(self::E_Header,$id),E_USER_ERROR); @@ -205,21 +242,32 @@ class SMTP extends Magic { $str=''; // Stringify headers foreach ($headers as $key=>&$val) { - if (!in_array($key,$reqd) && (!$this->attachments || - $key!='Content-Type' && $key!='Content-Transfer-Encoding')) + if (!in_array($key,$reqd) && + (!$this->attachments || + $key!='Content-Type' && + $key!='Content-Transfer-Encoding')) $str.=$key.': '.$val.$eol; - if (in_array($key,array('From','To','Cc','Bcc')) && - !preg_match('/[<>]/',$val)) - $val='<'.$val.'>'; + if (in_array($key,['From','To','Cc','Bcc'])) { + $email=''; + preg_match_all('/(?:".+?" )?(?:<.+?>|[^ ,]+)/', + $val,$matches,PREG_SET_ORDER); + foreach ($matches as $raw) + $email.=($email?', ':''). + (preg_match('/<.+?>/',$raw[0])? + $raw[0]: + ('<'.$raw[0].'>')); + $val=$email; + } unset($val); } // Start message dialog - $this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),$log); + $this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),$log,$mock); foreach ($fw->split($headers['To']. (isset($headers['Cc'])?(';'.$headers['Cc']):''). - (isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst) - $this->dialog('RCPT TO: '.strstr($dst,'<'),$log); - $this->dialog('DATA',$log); + (isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst) { + $this->dialog('RCPT TO: '.strstr($dst,'<'),$log,$mock); + } + $this->dialog('DATA',$log,$mock); if ($this->attachments) { // Replace Content-Type $type=$headers['Content-Type']; @@ -241,28 +289,25 @@ class SMTP extends Magic { $out.=$str.$eol; $out.=$message.$eol; foreach ($this->attachments as $attachment) { - if (is_array($attachment['filename'])) { + if (is_array($attachment['filename'])) list($alias,$file)=each($attachment['filename']); - $filename=$alias; - $attachment['filename']=$file; - } else - $filename=basename($attachment['filename']); + $alias=basename($file=$attachment['filename']); $out.='--'.$hash.$eol; $out.='Content-Type: application/octet-stream'.$eol; $out.='Content-Transfer-Encoding: base64'.$eol; if ($attachment['cid']) $out.='Content-ID: '.$attachment['cid'].$eol; $out.='Content-Disposition: attachment; '. - 'filename="'.$filename.'"'.$eol; + 'filename="'.$alias.'"'.$eol; $out.=$eol; $out.=chunk_split(base64_encode( - file_get_contents($attachment['filename']))).$eol; + file_get_contents($file))).$eol; } $out.=$eol; $out.='--'.$hash.'--'.$eol; $out.='.'; - $this->dialog($out,FALSE); + $this->dialog($out,TRUE,$mock); } else { // Send mail headers @@ -274,10 +319,10 @@ class SMTP extends Magic { $out.=$message.$eol; $out.='.'; // Send message - $this->dialog($out); + $this->dialog($out,TRUE,$mock); } - $this->dialog('QUIT',$log); - if ($socket) + $this->dialog('QUIT',$log,$mock); + if (!$mock && $socket) fclose($socket); return TRUE; } @@ -290,12 +335,13 @@ class SMTP extends Magic { * @param $user string * @param $pw string **/ - function __construct($host='localhost',$port=25,$scheme=null,$user=null,$pw=null) { - $this->headers=array( + function __construct( + $host='localhost',$port=25,$scheme=NULL,$user=NULL,$pw=NULL) { + $this->headers=[ 'MIME-Version'=>'1.0', 'Content-Type'=>'text/plain; '. 'charset='.Base::instance()->get('ENCODING') - ); + ]; $this->host=$host; if (strtolower($this->scheme=strtolower($scheme))=='ssl') $this->host='ssl://'.$host; diff --git a/app/lib/template.php b/app/lib/template.php index 8b985cde..c9c0d61b 100644 --- a/app/lib/template.php +++ b/app/lib/template.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -32,7 +32,7 @@ class Template extends Preview { //! Template tags $tags, //! Custom tag handlers - $custom=array(); + $custom=[]; /** * Template -set- tag handler @@ -60,14 +60,14 @@ class Template extends Preview { ($attrib['with']=$this->token($attrib['with'])) && preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/', $attrib['with'],$pairs,PREG_SET_ORDER)? - 'array('.implode(',', + ('['.implode(',', array_map(function($pair) { return '\''.$pair[1].'\'=>'. (preg_match('/^\'.*\'$/',$pair[2]) || preg_match('/\$/',$pair[2])? $pair[2]: \Base::instance()->stringify($pair[2])); - },$pairs)).')+get_defined_vars()': + },$pairs)).']+get_defined_vars()'): 'get_defined_vars()'; $ttl=isset($attrib['ttl'])?(int)$attrib['ttl']:0; return @@ -127,7 +127,7 @@ class Template extends Preview { (isset($attrib['counter'])? (($ctr=$this->token($attrib['counter'])).'=0; '):''). 'foreach (('. - $this->token($attrib['group']).'?:array()) as '. + $this->token($attrib['group']).'?:[]) as '. (isset($attrib['key'])? ($this->token($attrib['key']).'=>'):''). $this->token($attrib['value']).'):'. @@ -147,12 +147,12 @@ class Template extends Preview { // Grab and blocks foreach ($node as $pos=>$block) if (isset($block['true'])) - $true=array($pos,$block); + $true=[$pos,$block]; elseif (isset($block['false'])) - $false=array($pos,$block); + $false=[$pos,$block]; if (isset($true,$false) && $true[0]>$false[0]) // Reverse and blocks - list($node[$true[0]],$node[$false[0]])=array($false[1],$true[1]); + list($node[$true[0]],$node[$false[0]])=[$false[1],$true[1]]; return 'token($attrib['if']).'): ?>'. $this->build($node). @@ -229,7 +229,7 @@ class Template extends Preview { * @return string * @param $node array|string **/ - protected function build($node) { + function build($node) { if (is_string($node)) return parent::build($node); $out=''; @@ -259,7 +259,7 @@ class Template extends Preview { if ($func[0]=='_') return call_user_func_array($this->custom[$func],$args); if (method_exists($this,$func)) - return call_user_func_array(array($this,$func),$args); + return call_user_func_array([$this,$func],$args); user_error(sprintf(self::E_Method,$func),E_USER_ERROR); } @@ -270,9 +270,9 @@ class Template extends Preview { **/ function parse($text) { // Build tree structure - for ($ptr=0,$w=5,$len=strlen($text),$tree=array(),$tmp='';$ptr<$len;) + for ($ptr=0,$w=5,$len=strlen($text),$tree=[],$tmp='';$ptr<$len;) if (preg_match('/^(.{0,'.$w.'}?)<(\/?)(?:F3:)?'. - '('.$this->tags.')\b((?:\h+[\w-]+'. + '('.$this->tags.')\b((?:\s+[\w-]+'. '(?:\h*=\h*(?:"(?:.*?)"|\'(?:.*?)\'))?|'. '\h*\{\{.+?\}\})*)\h*(\/?)>/is', substr($text,$ptr),$match)) { @@ -281,7 +281,7 @@ class Template extends Preview { // Element node if ($match[2]) { // Find matching start tag - $stack=array(); + $stack=[]; for($i=count($tree)-1;$i>=0;$i--) { $item = $tree[$i]; if (is_array($item) && array_key_exists($match[3],$item) @@ -296,7 +296,7 @@ class Template extends Preview { else { // Start tag $node=&$tree[][$match[3]]; - $node=array(); + $node=[]; if ($match[4]) { // Process attributes preg_match_all( diff --git a/app/lib/test.php b/app/lib/test.php index 05e6e6c5..2070dab4 100644 --- a/app/lib/test.php +++ b/app/lib/test.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -32,7 +32,7 @@ class Test { protected //! Test results - $data=array(), + $data=[], //! Success indicator $passed=TRUE; @@ -61,7 +61,7 @@ class Test { function expect($cond,$text=NULL) { $out=(bool)$cond; if ($this->level==$out || $this->level==self::FLAG_Both) { - $data=array('status'=>$out,'text'=>$text,'source'=>NULL); + $data=['status'=>$out,'text'=>$text,'source'=>NULL]; foreach (debug_backtrace() as $frame) if (isset($frame['file'])) { $data['source']=Base::instance()-> diff --git a/app/lib/utf.php b/app/lib/utf.php index fbfe0005..22b41c3d 100644 --- a/app/lib/utf.php +++ b/app/lib/utf.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -180,7 +180,7 @@ class UTF extends Prefab { * @param $str string **/ function emojify($str) { - $map=array( + $map=[ ':('=>'\u2639', // frown ':)'=>'\u263a', // smile '<3'=>'\u2665', // heart @@ -191,7 +191,7 @@ class UTF extends Prefab { ':,'=>'\u1f60f', // think ':/'=>'\u1f623', // skeptic '8O'=>'\u1f632', // oops - )+Base::instance()->get('EMOJI'); + ]+Base::instance()->get('EMOJI'); return $this->translate(str_replace(array_keys($map), array_values($map),$str)); } diff --git a/app/lib/web.php b/app/lib/web.php index 57fbf937..22864898 100644 --- a/app/lib/web.php +++ b/app/lib/web.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -39,7 +39,7 @@ class Web extends Prefab { **/ function mime($file) { if (preg_match('/\w+$/',$file,$ext)) { - $map=array( + $map=[ 'au'=>'audio/basic', 'avi'=>'video/avi', 'bmp'=>'image/bmp', @@ -77,7 +77,7 @@ class Web extends Prefab { 'xls'=>'application/vnd.ms-excel', 'xml'=>'application/xml', 'zip'=>'application/x-zip-compressed' - ); + ]; foreach ($map as $key=>$val) if (preg_match('/'.$key.'/',strtolower($ext[0]))) return $val; @@ -93,7 +93,7 @@ class Web extends Prefab { * @param $list string|array **/ function acceptable($list=NULL) { - $accept=array(); + $accept=[]; foreach (explode(',',str_replace(' ','',@$_SERVER['HTTP_ACCEPT'])) as $mime) if (preg_match('/(.+?)(?:;q=([\d\.]+)|$)/',$mime,$parts)) @@ -124,8 +124,10 @@ class Web extends Prefab { * @param $mime string * @param $kbps int * @param $force bool + * @param $name string + * @param $flush bool **/ - function send($file,$mime=NULL,$kbps=0,$force=TRUE) { + function send($file,$mime=NULL,$kbps=0,$force=TRUE,$name=NULL,$flush=TRUE) { if (!is_file($file)) return FALSE; $size=filesize($file); @@ -133,7 +135,7 @@ class Web extends Prefab { header('Content-Type: '.($mime?:$this->mime($file))); if ($force) header('Content-Disposition: attachment; '. - 'filename="'.basename($file).'"'); + 'filename="'.($name!==NULL?$name:basename($file)).'"'); header('Accept-Ranges: bytes'); header('Content-Length: '.$size); header('X-Powered-By: '.Base::instance()->get('PACKAGE')); @@ -152,6 +154,10 @@ class Web extends Prefab { } // Send 1KiB and reset timer echo fread($handle,1024); + if ($flush) { + ob_flush(); + flush(); + } } fclose($handle); return $size; @@ -170,9 +176,7 @@ class Web extends Prefab { if (!is_dir($dir)) mkdir($dir,Base::MODE,TRUE); if ($fw->get('VERB')=='PUT') { - $tmp=$fw->get('TEMP'). - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(uniqid()); + $tmp=$fw->get('TEMP').$fw->get('SEED').'.'.$fw->hash(uniqid()); if (!$fw->get('RAW')) $fw->write($tmp,$fw->get('BODY')); else { @@ -188,7 +192,7 @@ class Web extends Prefab { fclose($src); } $base=basename($fw->get('URI')); - $file=array( + $file=[ 'name'=>$dir. ($slug && preg_match('/(.+?)(\.\w+)?$/',$base,$parts)? (is_callable($slug)? @@ -199,24 +203,24 @@ class Web extends Prefab { 'tmp_name'=>$tmp, 'type'=>$this->mime($base), 'size'=>filesize($tmp) - ); + ]; return (!file_exists($file['name']) || $overwrite) && - (!$func || $fw->call($func,array($file))!==FALSE) && + (!$func || $fw->call($func,[$file])!==FALSE) && rename($tmp,$file['name']); } - $fetch=function($arr)use(&$fetch){ + $fetch=function($arr) use(&$fetch) { if (!is_array($arr)) - return array($arr); - $data=array(); + return [$arr]; + $data=[]; foreach($arr as $k=>$sub) $data=array_merge($data,$fetch($sub)); return $data; }; - $out=array(); + $out=[]; foreach ($_FILES as $name=>$item) { - $files=array(); - foreach($item as $k=>$mix) - foreach($fetch($mix) as $i=>$val) + $files=[]; + foreach ($item as $k=>$mix) + foreach ($fetch($mix) as $i=>$val) $files[$i][$k]=$val; foreach ($files as $file) { if (empty($file['name'])) @@ -230,9 +234,8 @@ class Web extends Prefab { (isset($parts[2])?$parts[2]:''))): $base); $out[$file['name']]=!$file['error'] && - is_uploaded_file($file['tmp_name']) && (!file_exists($file['name']) || $overwrite) && - (!$func || $fw->call($func,array($file,$name))!==FALSE) && + (!$func || $fw->call($func,[$file,$name])!==FALSE) && move_uploaded_file($file['tmp_name'],$file['name']); } } @@ -277,7 +280,7 @@ class Web extends Prefab { ini_get('default_socket_timeout'); curl_setopt($curl,CURLOPT_CONNECTTIMEOUT,$timeout); curl_setopt($curl,CURLOPT_TIMEOUT,$timeout); - $headers=array(); + $headers=[]; curl_setopt($curl,CURLOPT_HEADERFUNCTION, // Callback for response headers function($curl,$line) use(&$headers) { @@ -286,22 +289,32 @@ class Web extends Prefab { return strlen($line); } ); + curl_setopt($curl,CURLOPT_SSL_VERIFYHOST,2); curl_setopt($curl,CURLOPT_SSL_VERIFYPEER,FALSE); ob_start(); curl_exec($curl); + $err=curl_error($curl); curl_close($curl); $body=ob_get_clean(); - if ($options['follow_location'] && + if (!$err && + $options['follow_location'] && preg_match('/^Location: (.+)$/m',implode(PHP_EOL,$headers),$loc)) { $options['max_redirects']--; + if($loc[1][0] == '/') { + $parts=parse_url($url); + $loc[1]=$parts['scheme'].'://'.$parts['host']. + ((isset($parts['port']) && !in_array($parts['port'],[80,443])) + ?':'.$parts['port']:'').$loc[1]; + } return $this->request($loc[1],$options); } - return array( + return [ 'body'=>$body, 'headers'=>$headers, 'engine'=>'cURL', - 'cached'=>FALSE - ); + 'cached'=>FALSE, + 'error'=>$err + ]; } /** @@ -314,28 +327,36 @@ class Web extends Prefab { $eol="\r\n"; $options['header']=implode($eol,$options['header']); $body=@file_get_contents($url,FALSE, - stream_context_create(array('http'=>$options))); + stream_context_create(['http'=>$options])); $headers=isset($http_response_header)? - $http_response_header:array(); - $match=NULL; - foreach ($headers as $header) - if (preg_match('/Content-Encoding: (.+)/',$header,$match)) - break; - if ($match) - switch ($match[1]) { - case 'gzip': - $body=gzdecode($body); + $http_response_header:[]; + $err=''; + if (is_string($body)) { + $match=NULL; + foreach ($headers as $header) + if (preg_match('/Content-Encoding: (.+)/',$header,$match)) break; - case 'deflate': - $body=gzuncompress($body); - break; - } - return array( + if ($match) + switch ($match[1]) { + case 'gzip': + $body=gzdecode($body); + break; + case 'deflate': + $body=gzuncompress($body); + break; + } + } + else { + $tmp=error_get_last(); + $err=$tmp['message']; + } + return [ 'body'=>$body, 'headers'=>$headers, 'engine'=>'stream', - 'cached'=>FALSE - ); + 'cached'=>FALSE, + 'error'=>$err + ]; } /** @@ -346,7 +367,7 @@ class Web extends Prefab { **/ protected function _socket($url,$options) { $eol="\r\n"; - $headers=array(); + $headers=[]; $body=''; $parts=parse_url($url); $empty=empty($parts['port']); @@ -361,54 +382,54 @@ class Web extends Prefab { $parts['path']='/'; if (empty($parts['query'])) $parts['query']=''; - $socket=@fsockopen($parts['host'],$parts['port']); - if (!$socket) - return FALSE; - stream_set_blocking($socket,TRUE); - stream_set_timeout($socket,isset($options['timeout'])? - $options['timeout']:ini_get('default_socket_timeout')); - fputs($socket,$options['method'].' '.$parts['path']. - ($parts['query']?('?'.$parts['query']):'').' HTTP/1.0'.$eol - ); - fputs($socket,implode($eol,$options['header']).$eol.$eol); - if (isset($options['content'])) - fputs($socket,$options['content'].$eol); - // Get response - $content=''; - while (!feof($socket) && - ($info=stream_get_meta_data($socket)) && - !$info['timed_out'] && !connection_aborted() && - $str=fgets($socket,4096)) - $content.=$str; - fclose($socket); - $html=explode($eol.$eol,$content,2); - $body=isset($html[1])?$html[1]:''; - $headers=array_merge($headers,$current=explode($eol,$html[0])); - $match=NULL; - foreach ($current as $header) - if (preg_match('/Content-Encoding: (.+)/',$header,$match)) - break; - if ($match) - switch ($match[1]) { - case 'gzip': - $body=gzdecode($body); - break; - case 'deflate': - $body=gzuncompress($body); + if ($socket=@fsockopen($parts['host'],$parts['port'],$code,$err)) { + stream_set_blocking($socket,TRUE); + stream_set_timeout($socket,isset($options['timeout'])? + $options['timeout']:ini_get('default_socket_timeout')); + fputs($socket,$options['method'].' '.$parts['path']. + ($parts['query']?('?'.$parts['query']):'').' HTTP/1.0'.$eol + ); + fputs($socket,implode($eol,$options['header']).$eol.$eol); + if (isset($options['content'])) + fputs($socket,$options['content'].$eol); + // Get response + $content=''; + while (!feof($socket) && + ($info=stream_get_meta_data($socket)) && + !$info['timed_out'] && !connection_aborted() && + $str=fgets($socket,4096)) + $content.=$str; + fclose($socket); + $html=explode($eol.$eol,$content,2); + $body=isset($html[1])?$html[1]:''; + $headers=array_merge($headers,$current=explode($eol,$html[0])); + $match=NULL; + foreach ($current as $header) + if (preg_match('/Content-Encoding: (.+)/',$header,$match)) break; + if ($match) + switch ($match[1]) { + case 'gzip': + $body=gzdecode($body); + break; + case 'deflate': + $body=gzuncompress($body); + break; + } + if ($options['follow_location'] && + preg_match('/Location: (.+?)'.preg_quote($eol).'/', + $html[0],$loc)) { + $options['max_redirects']--; + return $this->request($loc[1],$options); } - if ($options['follow_location'] && - preg_match('/Location: (.+?)'.preg_quote($eol).'/', - $html[0],$loc)) { - $options['max_redirects']--; - return $this->request($loc[1],$options); } - return array( + return [ 'body'=>$body, 'headers'=>$headers, 'engine'=>'socket', - 'cached'=>FALSE - ); + 'cached'=>FALSE, + 'error'=>$err + ]; } /** @@ -419,17 +440,17 @@ class Web extends Prefab { **/ function engine($arg='curl') { $arg=strtolower($arg); - $flags=array( + $flags=[ 'curl'=>extension_loaded('curl'), 'stream'=>ini_get('allow_url_fopen'), 'socket'=>function_exists('fsockopen') - ); + ]; if ($flags[$arg]) return $this->wrapper=$arg; foreach ($flags as $key=>$val) if ($val) return $this->wrapper=$key; - user_error(E_Request,E_USER_ERROR); + user_error(self::E_Request,E_USER_ERROR); } /** @@ -440,7 +461,7 @@ class Web extends Prefab { **/ function subst(array &$old,$new) { if (is_string($new)) - $new=array($new); + $new=[$new]; foreach ($new as $hdr) { $old=preg_grep('/'.preg_quote(strstr($hdr,':',TRUE),'/').':.+/', $old,PREG_GREP_INVERT); @@ -469,11 +490,11 @@ class Web extends Prefab { elseif (!preg_match('/https?/',$parts['scheme'])) return FALSE; if (!is_array($options)) - $options=array(); + $options=[]; if (empty($options['header'])) - $options['header']=array(); + $options['header']=[]; elseif (is_string($options['header'])) - $options['header']=array($options['header']); + $options['header']=[$options['header']]; if (!$this->wrapper) $this->engine(); if ($this->wrapper!='stream') { @@ -487,13 +508,13 @@ class Web extends Prefab { $this->subst($options['header'],'Host: '.$parts['host']); } $this->subst($options['header'], - array( + [ 'Accept-Encoding: gzip,deflate', 'User-Agent: '.(isset($options['user_agent'])? $options['user_agent']: 'Mozilla/5.0 (compatible; '.php_uname('s').')'), 'Connection: close' - ) + ] ); if (isset($options['content']) && is_string($options['content'])) { if ($options['method']=='POST' && @@ -508,13 +529,13 @@ class Web extends Prefab { 'Authorization: Basic '. base64_encode($parts['user'].':'.$parts['pass']) ); - $options+=array( + $options+=[ 'method'=>'GET', 'header'=>$options['header'], 'follow_location'=>TRUE, 'max_redirects'=>20, 'ignore_errors'=>FALSE - ); + ]; $eol="\r\n"; if ($fw->get('CACHE') && preg_match('/GET|HEAD/',$options['method'])) { @@ -534,11 +555,14 @@ class Web extends Prefab { $result=$cache->get($hash); $result['cached']=TRUE; } - elseif (preg_match('/Cache-Control: max-age=(.+?)'. - preg_quote($eol).'/',implode($eol,$result['headers']),$exp)) + elseif (preg_match('/Cache-Control:(?:.*)max-age=(\d+)(?:,?.*'. + preg_quote($eol).')/',implode($eol,$result['headers']),$exp)) $cache->set($hash,$result,$exp[1]); } - return $result; + $req=[$options['method'].' '.$url]; + foreach ($options['header'] as $header) + array_push($req,$header); + return array_merge(['request'=>$req],$result); } /** @@ -563,7 +587,9 @@ class Web extends Prefab { $path=$fw->get('UI').';./'; foreach ($fw->split($path,FALSE) as $dir) foreach ($files as $file) - if (is_file($save=$fw->fixslashes($dir.$file))) { + if (is_file($save=$fw->fixslashes($dir.$file)) && + is_bool(strpos($save,'../')) && + preg_match('/\.(css|js)$/i',$file)) { if ($fw->get('CACHE') && ($cached=$cache->exists( $hash=$fw->hash($save).'.'.$ext[0],$data)) && @@ -637,7 +663,7 @@ class Web extends Prefab { } continue; } - if (in_array($src[$ptr],array('\'','"'))) { + if (in_array($src[$ptr],['\'','"'])) { $match=$src[$ptr]; $data.=$match; $ptr++; @@ -693,14 +719,14 @@ class Web extends Prefab { NULL,LIBXML_NOBLANKS|LIBXML_NOERROR); if (!is_object($xml)) return FALSE; - $out=array(); + $out=[]; if (isset($xml->channel)) { $out['source']=(string)$xml->channel->title; $max=min($max,count($xml->channel->item)); for ($i=0;$i<$max;$i++) { $item=$xml->channel->item[$i]; - $list=array(''=>NULL)+$item->getnamespaces(TRUE); - $fields=array(); + $list=[''=>NULL]+$item->getnamespaces(TRUE); + $fields=[]; foreach ($list as $ns=>$uri) foreach ($item->children($uri) as $key=>$val) $fields[$ns.($ns?':':'').$key]=(string)$val; @@ -748,7 +774,7 @@ class Web extends Prefab { function slug($text) { return trim(strtolower(preg_replace('/([^\pL\pN])+/u','-', trim(strtr(str_replace('\'','',$text), - array( + [ 'Ǎ'=>'A','А'=>'A','Ā'=>'A','Ă'=>'A','Ą'=>'A','Å'=>'A', 'Ǻ'=>'A','Ä'=>'Ae','Á'=>'A','À'=>'A','Ã'=>'A','Â'=>'A', 'Æ'=>'AE','Ǽ'=>'AE','Б'=>'B','Ç'=>'C','Ć'=>'C','Ĉ'=>'C', @@ -796,7 +822,7 @@ class Web extends Prefab { 'ǜ'=>'u','ǔ'=>'u','ǖ'=>'u','ũ'=>'u','ü'=>'ue','в'=>'v', 'ŵ'=>'w','ы'=>'y','ÿ'=>'y','ý'=>'y','ŷ'=>'y','ź'=>'z', 'ž'=>'z','з'=>'z','ż'=>'z','ж'=>'zh','ь'=>'','ъ'=>'' - )+Base::instance()->get('DIACRITICS'))))),'-'); + ]+Base::instance()->get('DIACRITICS'))))),'-'); } /** @@ -850,8 +876,7 @@ if (!function_exists('gzdecode')) { $fw=Base::instance(); if (!is_dir($tmp=$fw->get('TEMP'))) mkdir($tmp,Base::MODE,TRUE); - file_put_contents($file=$tmp.'/'. - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. + file_put_contents($file=$tmp.'/'.$fw->get('SEED').'.'. $fw->hash(uniqid(NULL,TRUE)).'.gz',$str,LOCK_EX); ob_start(); readgzfile($file); diff --git a/app/lib/web/geo.php b/app/lib/web/geo.php index 498f857d..7d7a0bce 100644 --- a/app/lib/web/geo.php +++ b/app/lib/web/geo.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -34,14 +34,14 @@ class Geo extends \Prefab { $ref=new \DateTimeZone($zone); $loc=$ref->getLocation(); $trn=$ref->getTransitions($now=time(),$now); - $out=array( + $out=[ 'offset'=>$ref-> - getOffset(new \DateTime('now',new \DateTimeZone('GMT')))/3600, + getOffset(new \DateTime('now',new \DateTimeZone('UTC')))/3600, 'country'=>$loc['country_code'], 'latitude'=>$loc['latitude'], 'longitude'=>$loc['longitude'], 'dst'=>$trn[0]['isdst'] - ); + ]; unset($ref); return $out; } @@ -72,7 +72,7 @@ class Geo extends \Prefab { if (($req=$web->request('http://www.geoplugin.net/json.gp'. ($public?('?ip='.$ip):''))) && $data=json_decode($req['body'],TRUE)) { - $out=array(); + $out=[]; foreach ($data as $key=>$val) if (!strpos($key,'currency') && $key!=='geoplugin_status' && $key!=='geoplugin_region') @@ -87,17 +87,17 @@ class Geo extends \Prefab { * @return array|FALSE * @param $latitude float * @param $longitude float + * @param $key string **/ - function weather($latitude,$longitude) { + function weather($latitude,$longitude,$key) { $fw=\Base::instance(); $web=\Web::instance(); - $query=array( + $query=[ 'lat'=>$latitude, - 'lon'=>$longitude - ); - $req=$web->request( - 'http://api.openweathermap.org/data/2.5/weather?'. - http_build_query($query)); + 'lon'=>$longitude, + 'APPID'=>$key, + 'units'=>'metric' + ]; return ($req=$web->request( 'http://api.openweathermap.org/data/2.5/weather?'. http_build_query($query)))? diff --git a/app/lib/web/google/staticmap.php b/app/lib/web/google/staticmap.php index 71acc550..6a3f1601 100644 --- a/app/lib/web/google/staticmap.php +++ b/app/lib/web/google/staticmap.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). diff --git a/app/lib/web/oauth2.php b/app/lib/web/oauth2.php new file mode 100644 index 00000000..a41f9fb3 --- /dev/null +++ b/app/lib/web/oauth2.php @@ -0,0 +1,134 @@ +. + +*/ + +namespace Web; + +//! Lightweight OAuth2 client +class OAuth2 extends \Magic { + + protected + //! Scopes and claims + $args=[]; + + /** + * Return OAuth2 authentication URI + * @return string + * @param $endpoint string + **/ + function uri($endpoint) { + return $endpoint.'?'.http_build_query($this->args); + } + + /** + * Send request to API/token endpoint + * @return string|FALSE + * @param $uri string + * @param $method string + * @param $token array + **/ + function request($uri,$method,$token=NULL) { + $web=\Web::instance(); + $options=[ + 'method'=>$method, + 'content'=>http_build_query($this->args), + 'header'=>['Accept: application/json'] + ]; + if ($token) + array_push($options['header'],'Authorization: Bearer '.$token); + elseif ($method=='POST') + array_push($options['header'],'Authorization: Basic '. + base64_encode( + $this->args['client_id'].':'. + $this->args['client_secret'] + ) + ); + $response=$web->request($uri,$options); + return $response['body'] && + preg_grep('/HTTP\/1\.\d 200/',$response['headers'])? + json_decode($response['body'],TRUE): + FALSE; + } + + /** + * Parse JSON Web token + * @return array + * @param $token string + **/ + function jwt($token) { + return json_decode( + base64_decode( + str_replace( + ['-','_'], + ['+','/'], + explode('.',$token)[1] + ) + ), + TRUE + ); + } + + /** + * Return TRUE if scope/claim exists + * @return bool + * @param $key string + **/ + function exists($key) { + return isset($this->args[$key]); + } + + /** + * Bind value to scope/claim + * @return string + * @param $key string + * @param $val string + **/ + function set($key,$val) { + return $this->args[$key]=$val; + } + + /** + * Return value of scope/claim + * @return mixed + * @param $key string + **/ + function &get($key) { + if (isset($this->args[$key])) + $val=&$this->args[$key]; + else + $val=NULL; + return $val; + } + + /** + * Remove scope/claim + * @return NULL + * @param $key + **/ + function clear($key=NULL) { + if ($key) + unset($this->args[$key]); + else + $this->args=[]; + } + +} + diff --git a/app/lib/web/openid.php b/app/lib/web/openid.php index 89173d0b..f795cb37 100644 --- a/app/lib/web/openid.php +++ b/app/lib/web/openid.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -29,7 +29,7 @@ class OpenID extends \Magic { //! OpenID provider endpoint URL $url, //! HTTP request parameters - $args=array(); + $args=[]; /** * Determine OpenID provider @@ -38,11 +38,11 @@ class OpenID extends \Magic { **/ protected function discover($proxy) { // Normalize - if (!preg_match('/https?:\/\//i',$this->args['identity'])) - $this->args['identity']='http://'.$this->args['identity']; - $url=parse_url($this->args['identity']); + if (!preg_match('/https?:\/\//i',$this->args['endpoint'])) + $this->args['endpoint']='http://'.$this->args['endpoint']; + $url=parse_url($this->args['endpoint']); // Remove fragment; reconnect parts - $this->args['identity']=$url['scheme'].'://'. + $this->args['endpoint']=$url['scheme'].'://'. (isset($url['user'])? ($url['user']. (isset($url['pass'])?(':'.$url['pass']):'').'@'):''). @@ -50,7 +50,7 @@ class OpenID extends \Magic { (isset($url['query'])?('?'.$url['query']):''); // HTML-based discovery of OpenID provider $req=\Web::instance()-> - request($this->args['identity'],array('proxy'=>$proxy)); + request($this->args['endpoint'],['proxy'=>$proxy]); if (!$req) return FALSE; $type=array_values(preg_grep('/Content-Type:/',$req['headers'])); @@ -63,8 +63,9 @@ class OpenID extends \Magic { $svc=$xrds['XRD']['Service']; if (isset($svc[0])) $svc=$svc[0]; + $svc_type=is_array($svc['Type'])?$svc['Type']:array($svc['Type']); if (preg_grep('/http:\/\/specs\.openid\.net\/auth\/2.0\/'. - '(?:server|signon)/',$svc['Type'])) { + '(?:server|signon)/',$svc_type)) { $this->args['provider']=$svc['URI']; if (isset($svc['LocalID'])) $this->args['localidentity']=$svc['LocalID']; @@ -89,7 +90,7 @@ class OpenID extends \Magic { preg_match_all('/\b(rel|href)\h*=\h*'. '(?:"(.+?)"|\'(.+?)\')/s',$parts[1],$attr, PREG_SET_ORDER)) { - $node=array(); + $node=[]; foreach ($attr as $kv) $node[$kv[1]]=isset($kv[2])?$kv[2]:$kv[3]; if (isset($node['rel']) && @@ -140,7 +141,7 @@ class OpenID extends \Magic { * @param $attr array * @param $reqd string|array **/ - function auth($proxy=NULL,$attr=array(),array $reqd=NULL) { + function auth($proxy=NULL,$attr=[],array $reqd=NULL) { $fw=\Base::instance(); $root=$fw->get('SCHEME').'://'.$fw->get('HOST'); if (empty($this->args['trust_root'])) @@ -157,7 +158,7 @@ class OpenID extends \Magic { $this->args['ax.required']=is_string($reqd)? $reqd:implode(',',$reqd); } - $var=array(); + $var=[]; foreach ($this->args as $key=>$val) $var['openid.'.$key]=$val; $fw->reroute($this->url.'?'.http_build_query($var)); @@ -179,16 +180,16 @@ class OpenID extends \Magic { $this->args['mode']!='error' && $this->url=$this->discover($proxy)) { $this->args['mode']='check_authentication'; - $var=array(); + $var=[]; foreach ($this->args as $key=>$val) $var['openid.'.$key]=$val; $req=\Web::instance()->request( $this->url, - array( + [ 'method'=>'POST', 'content'=>http_build_query($var), 'proxy'=>$proxy - ) + ] ); return (bool)preg_match('/is_valid:true/i',$req['body']); } @@ -245,4 +246,3 @@ class OpenID extends \Magic { } } - diff --git a/app/lib/web/pingback.php b/app/lib/web/pingback.php index 9dd750a3..f063f104 100644 --- a/app/lib/web/pingback.php +++ b/app/lib/web/pingback.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2016 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -38,7 +38,7 @@ class Pingback extends \Prefab { $web=\Web::instance(); $req=$web->request($url); $found=FALSE; - if ($req && $req['body']) { + if ($req['body']) { // Look for pingback header foreach ($req['headers'] as $header) if (preg_match('/^X-Pingback:\h*(.+)/',$header,$href)) { @@ -71,7 +71,7 @@ class Pingback extends \Prefab { $doc=new \DOMDocument('1.0',$fw->get('ENCODING')); $doc->stricterrorchecking=FALSE; $doc->recover=TRUE; - if ($req && @$doc->loadhtml($req['body'])) { + if (@$doc->loadhtml($req['body'])) { // Parse anchor tags $links=$doc->getelementsbytagname('a'); foreach ($links as $link) { @@ -79,17 +79,17 @@ class Pingback extends \Prefab { // Find pingback-enabled resources if ($permalink && $found=$this->enabled($permalink)) { $req=$web->request($found, - array( + [ 'method'=>'POST', 'header'=>'Content-Type: application/xml', 'content'=>xmlrpc_encode_request( 'pingback.ping', - array($source,$permalink), - array('encoding'=>$fw->get('ENCODING')) + [$source,$permalink], + ['encoding'=>$fw->get('ENCODING')] ) - ) + ] ); - if ($req && $req['body']) + if ($req['body']) $this->log.=date('r').' '. $permalink.' [permalink:'.$found.']'.PHP_EOL. $req['body'].PHP_EOL; @@ -118,7 +118,7 @@ class Pingback extends \Prefab { $path=$fw->get('BASE'); $web=\Web::instance(); $args=xmlrpc_decode_request($fw->get('BODY'),$method,$charset); - $options=array('encoding'=>$charset); + $options=['encoding'=>$charset]; if ($method=='pingback.ping' && isset($args[0],$args[1])) { list($source,$permalink)=$args; $doc=new \DOMDocument('1.0',$fw->get('ENCODING')); @@ -138,8 +138,7 @@ class Pingback extends \Prefab { $links=$doc->getelementsbytagname('a'); foreach ($links as $link) { if ($link->getattribute('href')==$permalink) { - call_user_func_array($func, - array($source,$req['body'])); + call_user_func_array($func,[$source,$req['body']]); // Success die(xmlrpc_encode_request(NULL,$source,$options)); }