From f839de71cf676492540d10a13d97875938d285b2 Mon Sep 17 00:00:00 2001 From: Mark Friedrich Date: Thu, 7 Jun 2018 19:35:23 +0200 Subject: [PATCH] - upgrade PHP "Fat Free Framework" `v3.6.3` -> `v3.6.4`, #595 --- app/lib/{CHANGELOG => CHANGELOG.md} | 141 ++++++++++++++++++++++++++++ app/lib/base.php | 107 ++++++++++++++------- app/lib/db/jig.php | 24 ++++- app/lib/db/jig/mapper.php | 109 ++++++++++++++++----- app/lib/db/mongo.php | 5 +- app/lib/db/sql/mapper.php | 29 +++--- app/lib/markdown.php | 1 + app/lib/template.php | 24 ++--- app/lib/web.php | 59 ++++++++---- app/lib/web/geo.php | 4 +- 10 files changed, 394 insertions(+), 109 deletions(-) rename app/lib/{CHANGELOG => CHANGELOG.md} (77%) diff --git a/app/lib/CHANGELOG b/app/lib/CHANGELOG.md similarity index 77% rename from app/lib/CHANGELOG rename to app/lib/CHANGELOG.md index f831d7d6..04950f15 100644 --- a/app/lib/CHANGELOG +++ b/app/lib/CHANGELOG.md @@ -1,5 +1,145 @@ CHANGELOG +3.6.4 (19 April 2018) +* NEW: Added Dependency Injection support with CONTAINER variable [#221](https://github.com/bcosca/fatfree-core/issues/221) +* NEW: configurable LOGGABLE error codes [#1091](https://github.com/bcosca/fatfree/issues/1091#issuecomment-364674701) +* NEW: JAR.lifetime option, [#178](https://github.com/bcosca/fatfree-core/issues/178) +* Template: reduced Prefab calls +* Template: optimized reflection for better derivative support, [bcosca/fatfree#1088](https://github.com/bcosca/fatfree/issues/1088) +* Template: optimized parsing for template attributes and tokens +* DB\Mongo: fixed logging with mongodb extention +* DB\Jig: added lazy-loading [#7e1cd9b9b89](https://github.com/bcosca/fatfree-core/commit/7e1cd9b9b89c4175d0f6b86ced9d9bd49c04ac39) +* DB\Jig\Mapper: Added group feature, bcosca/fatfree#616 +* DB\SQL\Mapper: fix PostgreSQL RETURNING ID when no pkey is available, [bcosca/fatfree#1069](https://github.com/bcosca/fatfree/issues/1069), [#230](https://github.com/bcosca/fatfree-core/issues/230) +* DB\SQL\Mapper: disable order clause auto-quoting when it's already been quoted +* Web->location: add failsafe for geoip_region_name_by_code() [#GB:Bxyn9xn9AgAJ](https://groups.google.com/d/msg/f3-framework/APau4wnwNzE/Bxyn9xn9AgAJ) +* Web->request: Added proxy support [#e936361b](https://github.com/bcosca/fatfree-core/commit/e936361bc03010c4c7c38a396562e5e96a8a100d) +* Web->mime: Added JFIF format +* Markdown: handle line breaks in paragraph blocks, [bcosca/fatfree#1100](https://github.com/bcosca/fatfree/issues/1100) +* config: reduced cast calls on parsing config sections +* Patch empty SERVER_NAME [bcosca/fatfree#1084](https://github.com/bcosca/fatfree/issues/1084) +* Bugfix: unreliable request headers in Web->request() response [bcosca/fatfree#1092](https://github.com/bcosca/fatfree/issues/1092) +* Fixed, View->render: utilizing multiple UI paths, [bcosca/fatfree#1083](https://github.com/bcosca/fatfree/issues/1083) +* Fixed URL parsing with PHP 5.4 [#247](https://github.com/bcosca/fatfree-core/issues/247) +* Fixed PHP 7.2 warnings when session is active prematurely, [#238](https://github.com/bcosca/fatfree-core/issues/238) +* Fixed setcookie $expire variable type [#240](https://github.com/bcosca/fatfree-core/issues/240) +* Fixed expiration time when updating an existing cookie + +3.6.3 (31 December 2017) +* PHP7 fix: remove deprecated (unset) cast +* Web->request: restricted follow_location to 3XX responses only +* CLI mode: refactored arguments parsing +* CLI mode: fixed query string encoding +* SMTP: Refactor parsing of attachments +* SMTP: clean-up mail headers for multipart messages, [#1065](https://github.com/bcosca/fatfree/issues/1065) +* config: fixed performance issues on parsing config files +* config: cast command parameters in config entries to php type & constant, [#1030](https://github.com/bcosca/fatfree/issues/1030) +* config: reduced registry calls +* config: skip hive escaping when resolving dynamic config vars, [#1030](https://github.com/bcosca/fatfree/issues/1030) +* Bug fix: Incorrect cookie lifetime computation, [#1070](https://github.com/bcosca/fatfree/issues/1070), [#1016](https://github.com/bcosca/fatfree/issues/1016) +* DB\SQL\Mapper: use RETURNING option instead of a sequence query to get lastInsertId in PostgreSQL, [#1069](https://github.com/bcosca/fatfree/issues/1069), [#230](https://github.com/bcosca/fatfree-core/issues/230) +* DB\SQL\Session: check if _agent is too long for SQL based sessions [#236](https://github.com/bcosca/fatfree-core/issues/236) +* DB\SQL\Session: fix Session handler table creation issue on SQL Server, [#899](https://github.com/bcosca/fatfree/issues/899) +* DB\SQL: fix oracle db issue with empty error variable, [#1072](https://github.com/bcosca/fatfree/issues/1072) +* DB\SQL\Mapper: fix sorting issues on SQL Server, [#1052](https://github.com/bcosca/fatfree/issues/1052) [#225](https://github.com/bcosca/fatfree-core/issues/225) +* Prevent directory traversal attacks on filesystem based cache [#1073](https://github.com/bcosca/fatfree/issues/1073) +* Bug fix, Template: PHP constants used in include with attribute, [#983](https://github.com/bcosca/fatfree/issues/983) +* Bug fix, Template: Numeric value in expression alters PHP_EOL context +* Template: use existing linefeed instead of PHP_EOL, [#1048](https://github.com/bcosca/fatfree/issues/1048) +* Template: make newline interpolation handling configurable [#223](https://github.com/bcosca/fatfree-core/issues/223) +* Template: add beforerender to Preview +* fix custom FORMATS without modifiers +* Cache: Refactor Cache->reset for XCache +* Cache: loosen reset cache key pattern, [#1041](https://github.com/bcosca/fatfree/issues/1041) +* XCache: suffix reset only works if xcache.admin.enable_auth is disabled +* Added HTTP 103 as recently approved by the IETF +* LDAP changes to for AD flexibility [#227](https://github.com/bcosca/fatfree-core/issues/227) +* Hide debug trace from ajax errors when DEBUG=0 [#1071](https://github.com/bcosca/fatfree/issues/1071) +* fix View->render using potentially wrong cache entry + +3.6.2 (26 June 2017) +* Return a status code > 0 when dying on error [#220](https://github.com/bcosca/fatfree-core/issues/220) +* fix SMTP line width [#215](https://github.com/bcosca/fatfree-core/issues/215) +* Allow using a custom field for ldap user id checking [#217](https://github.com/bcosca/fatfree-core/issues/217) +* NEW: DB\SQL->exists: generic method to check if SQL table exists +* Pass handler to route handler and hooks [#1035](https://github.com/bcosca/fatfree/issues/1035) +* pass carriage return of multiline dictionary keys +* Better Web->slug customization +* fix incorrect header issue [#211](https://github.com/bcosca/fatfree-core/issues/211) +* fix schema issue on databases with case-sensitive collation, fixes [#209](https://github.com/bcosca/fatfree-core/issues/209) +* Add filter for deriving C-locale equivalent of a number +* Bug fix: @LANGUAGE remains unchanged after override +* abort: added Header pre-check +* Assemble URL after ONREROUTE +* Add reroute argument to skip script termination +* Invoke ONREROUTE after headers are sent +* SQLite switch to backtick as quote +* Bug fix: Incorrect timing in SQL query logs +* DB\SQL\Mapper: Cast return value of count to integer +* Patched $_SERVER['REQUEST_URI'] to ensure it contains a relative URI +* Tweak debug verbosity +* fix php carriage return issue in preview->build [#205](https://github.com/bcosca/fatfree-core/pull/205) +* fixed template string resolution [#205](https://github.com/bcosca/fatfree-core/pull/205) +* Fixed unexpected default seed on CACHE set [#1028](https://github.com/bcosca/fatfree/issues/1028) +* DB\SQL\Mapper: Optimized field escaping on options +* Optimize template conversion to PHP file + +3.6.1 (2 April 2017) +* NEW: Recaptcha plugin [#194](https://github.com/bcosca/fatfree-core/pull/194) +* NEW: MB variable for detecting multibyte support +* NEW: DB\SQL: Cache parsed schema for the TTL duration +* NEW: quick erase flag on Jig/Mongo/SQL mappers [#193](https://github.com/bcosca/fatfree-core/pull/193) +* NEW: Allow OPTIONS method to return a response body [#171](https://github.com/bcosca/fatfree-core/pull/171) +* NEW: Add support for Memcached (bcosca/fatfree#997) +* NEW: Rudimentary preload resource (HTTP2 server) support via template push() +* NEW: Add support for new MongoDB driver [#177](https://github.com/bcosca/fatfree-core/pull/177) +* Changed: template filter are all lowercase now +* Changed: Fix template lookup inconsistency: removed base dir from UI on render +* Changed: count() method now has an options argument [#192](https://github.com/bcosca/fatfree-core/pull/192) +* Changed: SMTP, Spit out error message if any +* \DB\SQL\Mapper: refactored row count strategy +* DB\SQL\Mapper: Allow non-scalar values to be assigned as mapper property +* DB\SQL::PARAM_FLOAT: remove cast to float (#106 and bcosca/fatfree#984) (#191) +* DB\SQL\mapper->erase: allow empty string +* DB\SQL\mapper->insert: fields reset after successful INSERT +* Add option to debounce Cursor->paginate subset [#195](https://github.com/bcosca/fatfree-core/pull/195) +* View: Don't delete sandboxed variables (#198) +* Preview: Optimize compilation of template expressions +* Preview: Use shorthand tag for direct rendering +* Preview->resolve(): new tweak to allow template persistence as option +* Web: Expose diacritics translation table +* SMTP: Enable logging of message body only when $log argument is 'verbose' +* SMTP: Convert headers to camelcase for consistency +* make cache seed more flexible, #164 +* Improve trace details for DEBUG>2 +* Enable config() to read from an array of input files +* Improved alias and reroute regex +* Make camelCase and snakeCase Unicode-aware +* format: Provision for optional whitespaces +* Break APCu-BC dependence +* Old PHP 5.3 cleanup +* Debug log must include HTTP query +* Recognize X-Forwarded-Port header (bcosca/fatfree#1002) +* Avoid use of deprecated mcrypt module +* Return only the client's IP when using the `X-Forwarded-For` header to deduce an IP address +* Remove orphan mutex locks on termination (#157) +* Use 80 as default port number to avoid issues when `$_SERVER['SERVER_PORT']` is not existing +* fread replaced with readfile() for simple send() usecase +* Bug fix: request URI with multiple leading slashes, #203 +* Bug fix: Query generates wrong adhoc field value +* Bug fix: SMTP stream context issue #200 +* Bug fix: child pseudo class selector in minify, bcosca/fatfree#1008 +* Bug fix: "Undefined index: CLI" error (#197) +* Bug fix: cast Cache-Control expire time to int, bcosca/fatfree#1004 +* Bug fix: Avoid issuance of multiple Content-Type headers for nested templates +* Bug fix: wildcard token issue with digits (bcosca/fatfree#996) +* Bug fix: afterupdate ignored when row does not change +* Bug fix: session handler read() method for PHP7 (need strict string) #184 #185 +* Bug fix: reroute mocking in CLI mode (#183) +* Bug fix: Reroute authoritative relative references (#181) +* Bug fix: locales order and charset hyphen +* Bug fix: base stripped twice in router (#176) + 3.6.0 (19 November 2016) * NEW: [cli] request type * NEW: console-friendly CLI mode @@ -89,6 +229,7 @@ CHANGELOG * 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: Image->render and Image->dump, removed unnecessary 2nd argument (#146) * 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) diff --git a/app/lib/base.php b/app/lib/base.php index 62fa14fb..e04abf40 100644 --- a/app/lib/base.php +++ b/app/lib/base.php @@ -45,7 +45,7 @@ final class Base extends Prefab implements ArrayAccess { //@{ Framework details const PACKAGE='Fat-Free Framework', - VERSION='3.6.3-Release'; + VERSION='3.6.4-Release'; //@} //@{ HTTP status codes (RFC 2616) @@ -346,17 +346,16 @@ final class Base extends Prefab implements ArrayAccess { * @param $ttl int **/ function set($key,$val,$ttl=0) { - $time=$this->hive['TIME']; + $time=(int)$this->hive['TIME']; if (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) { $this->set('REQUEST'.$expr[2],$val); if ($expr[1]=='COOKIE') { $parts=$this->cut($key); $jar=$this->unserialize($this->serialize($this->hive['JAR'])); - if (isset($_COOKIE[$parts[1]])) { - $jar['expire']=0; + unset($jar['lifetime']); + if (isset($_COOKIE[$parts[1]])) call_user_func_array('setcookie', - array_merge([$parts[1],NULL],$jar)); - } + array_merge([$parts[1],NULL],['expire'=>0]+$jar)); if ($ttl) $jar['expire']=$time+$ttl; call_user_func_array('setcookie',[$parts[1],$val]+$jar); @@ -395,9 +394,17 @@ final class Base extends Prefab implements ArrayAccess { $ref=&$this->ref($key); $ref=$val; if (preg_match('/^JAR\b/',$key)) { - $jar=$this->unserialize($this->serialize($this->hive['JAR'])); - $jar['expire']-=$time; - call_user_func_array('session_set_cookie_params',$jar); + if ($key=='JAR.lifetime') + $this->set('JAR.expire',$val==0?0: + (is_int($val)?$time+$val:strtotime($val))); + else { + if ($key=='JAR.expire') + $this->hive['JAR']['lifetime']=max(0,$val-$time); + $jar=$this->unserialize($this->serialize($this->hive['JAR'])); + unset($jar['expire']); + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + call_user_func_array('session_set_cookie_params',$jar); + } } if ($ttl) // Persist the key-value pair @@ -442,6 +449,7 @@ final class Base extends Prefab implements ArrayAccess { if ($expr[1]=='COOKIE') { $parts=$this->cut($key); $jar=$this->hive['JAR']; + unset($jar['lifetime']); $jar['expire']=0; call_user_func_array('setcookie', array_merge([$parts[1],NULL],$jar)); @@ -1231,11 +1239,18 @@ final class Base extends Prefab implements ArrayAccess { $req.='?'.$this->hive['QUERY']; if (!$text) $text='HTTP '.$code.' ('.$req.')'; - error_log($text); $trace=$this->trace($trace); - foreach (explode("\n",$trace) as $nexus) - if ($nexus) - error_log($nexus); + $loggable=$this->hive['LOGGABLE']; + if (!is_array($loggable)) + $loggable=$this->split($loggable); + foreach ($loggable as $status) + if ($status=='*' || preg_match('/^'.preg_replace('/\D/','\d',$status).'$/',$code)) { + error_log($text); + foreach (explode("\n",$trace) as $nexus) + if ($nexus) + error_log($nexus); + break; + } if ($highlight=!$this->hive['CLI'] && !$this->hive['AJAX'] && $this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS)) $trace=$this->highlight($trace); @@ -1750,6 +1765,19 @@ final class Base extends Prefab implements ArrayAccess { if ($parts[2]=='->') { if (is_subclass_of($parts[1],'Prefab')) $parts[1]=call_user_func($parts[1].'::instance'); + elseif ($container=$this->get('CONTAINER')) { + if (is_object($container) && is_callable([$container,'has']) + && $container->has($parts[1])) // PSR11 + $parts[1]=call_user_func([$container,'get'],$parts[1]); + elseif (is_callable($container)) + $parts[1]=call_user_func($container,$parts[1],$args); + elseif (is_string($container) && is_subclass_of($container,'Prefab')) + $parts[1]=call_user_func($container.'::instance')->get($parts[1]); + else + user_error(sprintf(self::E_Class, + $this->stringify($container)), + E_USER_ERROR); + } else { $ref=new ReflectionClass($parts[1]); $parts[1]=method_exists($parts[1],'__construct') && $args? @@ -1888,7 +1916,8 @@ final class Base extends Prefab implements ArrayAccess { call_user_func_array( [$this,$cmd[1]], array_merge([$match['lval']], - str_getcsv($this->cast($match['rval']))) + str_getcsv($cmd[1]=='config'?$this->cast($match['rval']): + $match['rval'])) ); } else { @@ -2190,7 +2219,7 @@ final class Base extends Prefab implements ArrayAccess { $this->error(500,$text,NULL,$level); } ); - if (!isset($_SERVER['SERVER_NAME'])) + if (!isset($_SERVER['SERVER_NAME']) || $_SERVER['SERVER_NAME']==='') $_SERVER['SERVER_NAME']=gethostname(); if ($cli=PHP_SAPI=='cli') { // Emulate HTTP request @@ -2266,14 +2295,14 @@ final class Base extends Prefab implements ArrayAccess { $base=rtrim($this->fixslashes( dirname($_SERVER['SCRIPT_NAME'])),'/'); $uri=parse_url((preg_match('/^\w+:\/\//',$_SERVER['REQUEST_URI'])?'': - '//'.$_SERVER['SERVER_NAME']).$_SERVER['REQUEST_URI']); + $scheme.'://'.$_SERVER['SERVER_NAME']).$_SERVER['REQUEST_URI']); $_SERVER['REQUEST_URI']=$uri['path']. (isset($uri['query'])?'?'.$uri['query']:''). (isset($uri['fragment'])?'#'.$uri['fragment']:''); $path=preg_replace('/^'.preg_quote($base,'/').'/','',$uri['path']); - session_cache_limiter(''); $jar=[ 'expire'=>0, + 'lifetime'=>0, 'path'=>$base?:'/', 'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) && !filter_var($_SERVER['SERVER_NAME'],FILTER_VALIDATE_IP)? @@ -2281,7 +2310,6 @@ final class Base extends Prefab implements ArrayAccess { 'secure'=>($scheme=='https'), 'httponly'=>TRUE ]; - call_user_func_array('session_set_cookie_params',$jar); $port=80; if (isset($headers['X-Forwarded-Port'])) $port=$headers['X-Forwarded-Port']; @@ -2328,6 +2356,7 @@ final class Base extends Prefab implements ArrayAccess { $this->language($headers['Accept-Language']): $this->fallback, 'LOCALES'=>'./', + 'LOGGABLE'=>'*', 'LOGS'=>'./', 'MB'=>extension_loaded('mbstring'), 'ONERROR'=>NULL, @@ -2363,6 +2392,11 @@ final class Base extends Prefab implements ArrayAccess { 'VERSION'=>self::VERSION, 'XFRAME'=>'SAMEORIGIN' ]; + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) { + unset($jar['expire']); + session_cache_limiter(''); + call_user_func_array('session_set_cookie_params',$jar); + } if (PHP_SAPI=='cli-server' && preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI'])) $this->reroute('/'); @@ -2673,16 +2707,22 @@ class View extends Prefab { //! Nesting level $level=0; + /** @var \Base Framework instance */ + protected $fw; + + function __construct() { + $this->fw=\Base::instance(); + } + /** * Encode characters to equivalent HTML entities * @return string * @param $arg mixed **/ function esc($arg) { - $fw=Base::instance(); - return $fw->recursive($arg, - function($val) use($fw) { - return is_string($val)?$fw->encode($val):$val; + return $this->fw->recursive($arg, + function($val) { + return is_string($val)?$this->fw->encode($val):$val; } ); } @@ -2693,10 +2733,9 @@ class View extends Prefab { * @param $arg mixed **/ function raw($arg) { - $fw=Base::instance(); - return $fw->recursive($arg, - function($val) use($fw) { - return is_string($val)?$fw->decode($val):$val; + return $this->fw->recursive($arg, + function($val) { + return is_string($val)?$this->fw->decode($val):$val; } ); } @@ -2708,7 +2747,7 @@ class View extends Prefab { * @param $mime string **/ protected function sandbox(array $hive=NULL,$mime=NULL) { - $fw=Base::instance(); + $fw=$this->fw; $implicit=FALSE; if (is_null($hive)) { $implicit=TRUE; @@ -2744,9 +2783,9 @@ class View extends Prefab { * @param $ttl int **/ function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { - $fw=Base::instance(); + $fw=$this->fw; $cache=Cache::instance(); - foreach ($fw->split($fw->UI) as $dir) + foreach ($fw->split($fw->UI) as $dir) { if ($cache->exists($hash=$fw->hash($dir.$file),$data)) return $data; if (is_file($this->file=$fw->fixslashes($dir.$file))) { @@ -2762,6 +2801,7 @@ class View extends Prefab { $cache->set($hash,$data,$ttl); return $data; } + } user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); } @@ -2807,7 +2847,6 @@ class Preview extends View { * @param $val int|float **/ function c($val) { - $fw=Base::instance(); $locale=setlocale(LC_NUMERIC,0); setlocale(LC_NUMERIC,'C'); $out=(string)(float)$val; @@ -2821,13 +2860,13 @@ class Preview extends View { * @param $str string **/ function token($str) { - $fw = Base::instance(); + $fw=$this->fw; $str=trim(preg_replace('/\{\{(.+?)\}\}/s',trim('\1'), $fw->compile($str))); if (preg_match('/^(.+)(?split($parts[2]) as $func) + foreach ($fw->split(trim($parts[2],"\xC2\xA0")) as $func) $str=is_string($cmd=$this->filter($func))? $cmd.'('.$str.')': 'Base::instance()->'. @@ -2890,7 +2929,7 @@ class Preview extends View { * @param $escape bool **/ function resolve($node,array $hive=NULL,$ttl=0,$persist=FALSE,$escape=NULL) { - $fw=Base::instance(); + $fw=$this->fw; $cache=Cache::instance(); if ($escape!==NULL) { $esc=$fw->ESCAPE; @@ -2951,7 +2990,7 @@ class Preview extends View { * @param $ttl int **/ function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { - $fw=Base::instance(); + $fw=$this->fw; $cache=Cache::instance(); if (!is_dir($tmp=$fw->TEMP)) mkdir($tmp,Base::MODE,TRUE); diff --git a/app/lib/db/jig.php b/app/lib/db/jig.php index d0730144..79f33fb8 100644 --- a/app/lib/db/jig.php +++ b/app/lib/db/jig.php @@ -41,7 +41,9 @@ class Jig { //! Jig log $log, //! Memory-held data - $data; + $data, + //! lazy load/save files + $lazy; /** * Read data from memory/file @@ -54,6 +56,8 @@ class Jig { $this->data[$file]=[]; return $this->data[$file]; } + if ($this->lazy && isset($this->data[$file])) + return $this->data[$file]; $fw=\Base::instance(); $raw=$fw->read($dst); switch ($this->format) { @@ -75,7 +79,7 @@ class Jig { * @param $data array **/ function write($file,array $data=NULL) { - if (!$this->dir) + if (!$this->dir || $this->lazy) return count($this->data[$file]=$data); $fw=\Base::instance(); switch ($this->format) { @@ -131,6 +135,8 @@ class Jig { * @return NULL **/ function drop() { + if ($this->lazy) // intentional + $this->data=[]; if (!$this->dir) $this->data=[]; elseif ($glob=@glob($this->dir.'/*',GLOB_NOSORT)) @@ -147,11 +153,23 @@ class Jig { * @param $dir string * @param $format int **/ - function __construct($dir=NULL,$format=self::FORMAT_JSON) { + function __construct($dir=NULL,$format=self::FORMAT_JSON,$lazy=FALSE) { if ($dir && !is_dir($dir)) mkdir($dir,\Base::MODE,TRUE); $this->uuid=\Base::instance()->hash($this->dir=$dir); $this->format=$format; + $this->lazy=$lazy; + } + + /** + * save file on destruction + **/ + function __destruct() { + if ($this->lazy) { + $this->lazy = FALSE; + foreach ($this->data as $file => $data) + $this->write($file,$data); + } } } diff --git a/app/lib/db/jig/mapper.php b/app/lib/db/jig/mapper.php index 135dd612..323f2c81 100644 --- a/app/lib/db/jig/mapper.php +++ b/app/lib/db/jig/mapper.php @@ -33,7 +33,9 @@ class Mapper extends \DB\Cursor { //! Document identifier $id, //! Document contents - $document=[]; + $document=[], + //! field map-reduce handlers + $_reduce; /** * Return database type @@ -160,7 +162,8 @@ class Mapper extends \DB\Cursor { $options+=[ 'order'=>NULL, 'limit'=>0, - 'offset'=>0 + 'offset'=>0, + 'group'=>NULL, ]; $fw=\Base::instance(); $cache=\Cache::instance(); @@ -232,30 +235,46 @@ class Mapper extends \DB\Cursor { } ); } - if (isset($options['order'])) { - $cols=$fw->split($options['order']); - uasort( - $data, - function($val1,$val2) use($cols) { - foreach ($cols as $col) { - $parts=explode(' ',$col,2); - $order=empty($parts[1])? - SORT_ASC: - constant($parts[1]); - $col=$parts[0]; - if (!array_key_exists($col,$val1)) - $val1[$col]=NULL; - if (!array_key_exists($col,$val2)) - $val2[$col]=NULL; - list($v1,$v2)=[$val1[$col],$val2[$col]]; - if ($out=strnatcmp($v1,$v2)* - (($order==SORT_ASC)*2-1)) - return $out; - } - return 0; + if (isset($options['group'])) { + $cols=array_reverse($fw->split($options['group'])); + // sort into groups + $data=$this->sort($data,$options['group']); + foreach($data as $i=>&$row) { + if (!isset($prev)) { + $prev=$row; + $prev_i=$i; + } + $drop=false; + foreach ($cols as $col) + if ($prev_i!=$i && array_key_exists($col,$row) && + array_key_exists($col,$prev) && $row[$col]==$prev[$col]) + // reduce/modify + $drop=!isset($this->_reduce[$col]) || call_user_func_array( + $this->_reduce[$col][0],[&$prev,&$row])!==FALSE; + elseif (isset($this->_reduce[$col])) { + $null=null; + // initial + call_user_func_array($this->_reduce[$col][0],[&$row,&$null]); + } + if ($drop) + unset($data[$i]); + else { + $prev=&$row; + $prev_i=$i; + } + unset($row); + } + // finalize + if ($this->_reduce[$col][1]) + foreach($data as $i=>&$row) { + $row=call_user_func($this->_reduce[$col][1],$row); + if (!$row) + unset($data[$i]); + unset($row); } - ); } + if (isset($options['order'])) + $data=$this->sort($data,$options['order']); $data=array_slice($data, $options['offset'],$options['limit']?:NULL,TRUE); if ($fw->CACHE && $ttl) @@ -281,6 +300,48 @@ class Mapper extends \DB\Cursor { return $out; } + /** + * Sort a collection + * @param $data + * @param $cond + * @return mixed + */ + protected function sort($data,$cond) { + $cols=\Base::instance()->split($cond); + uasort( + $data, + function($val1,$val2) use($cols) { + foreach ($cols as $col) { + $parts=explode(' ',$col,2); + $order=empty($parts[1])? + SORT_ASC: + constant($parts[1]); + $col=$parts[0]; + if (!array_key_exists($col,$val1)) + $val1[$col]=NULL; + if (!array_key_exists($col,$val2)) + $val2[$col]=NULL; + list($v1,$v2)=[$val1[$col],$val2[$col]]; + if ($out=strnatcmp($v1,$v2)* + (($order==SORT_ASC)*2-1)) + return $out; + } + return 0; + } + ); + return $data; + } + + /** + * Add reduce handler for grouped fields + * @param $key string + * @param $handler callback + * @param $finalize callback + */ + function reduce($key,$handler,$finalize=null){ + $this->_reduce[$key]=[$handler,$finalize]; + } + /** * Count records that match criteria * @return int diff --git a/app/lib/db/mongo.php b/app/lib/db/mongo.php index 08481ebf..86ac184c 100644 --- a/app/lib/db/mongo.php +++ b/app/lib/db/mongo.php @@ -68,8 +68,9 @@ class Mongo { $cursor=$this->db->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) '. + $this->log.=date('r',$this->legacy() ? + $frame['ts']->sec : (round((string)$frame['ts'])/1000)). + ' ('.sprintf('%.1f',$frame['millis']).'ms) '. $frame['ns'].' ['.$frame['op'].'] '. (empty($frame['query'])? '':json_encode($frame['query'])). diff --git a/app/lib/db/sql/mapper.php b/app/lib/db/sql/mapper.php index f78fc36c..afdc5c81 100644 --- a/app/lib/db/sql/mapper.php +++ b/app/lib/db/sql/mapper.php @@ -153,7 +153,7 @@ class Mapper extends \DB\Cursor { /** * Convert array to mapper object - * @return object + * @return static * @param $row array **/ protected function factory($row) { @@ -236,14 +236,15 @@ class Mapper extends \DB\Cursor { explode(',',$options['group']))); } if ($options['order']) { - $order=' ORDER BY '.implode(',',array_map( - function($str) use($db) { + $char=substr($db->quotekey(''),0,1);// quoting char + $order=' ORDER BY '.(FALSE===strpos($options['order'],$char)? + implode(',',array_map(function($str) use($db) { return preg_match('/^\h*(\w+[._\-\w]*)(?:\h+((?:ASC|DESC)[\w\h]*))?\h*$/i', $str,$parts)? ($db->quotekey($parts[1]). (isset($parts[2])?(' '.$parts[2]):'')):$str; - }, - explode(',',$options['order']))); + },explode(',',$options['order']))): + $options['order']); } // SQL Server fixes if (preg_match('/mssql|sqlsrv|odbc/', $this->engine) && @@ -285,7 +286,7 @@ class Mapper extends \DB\Cursor { /** * Build query string and execute - * @return object + * @return static[] * @param $fields string * @param $filter string|array * @param $options array @@ -360,7 +361,7 @@ class Mapper extends \DB\Cursor { /** * Return record at specified offset using same criteria as * previous load() call and make it active - * @return array + * @return static * @param $ofs int **/ function skip($ofs=1) { @@ -385,7 +386,7 @@ class Mapper extends \DB\Cursor { /** * Insert new record - * @return object + * @return static **/ function insert() { $args=[]; @@ -424,8 +425,8 @@ class Mapper extends \DB\Cursor { } } if ($fields) { - $add=''; - if ($this->engine=='pgsql') { + $add=$aik=''; + if ($this->engine=='pgsql' && !empty($pkeys)) { $names=array_keys($pkeys); $aik=end($names); $add=' RETURNING '.$this->db->quotekey($aik); @@ -437,12 +438,12 @@ class Mapper extends \DB\Cursor { 'INSERT INTO '.$this->table.' ('.$fields.') '. 'VALUES ('.$values.')'.$add,$args ); - if ($this->engine=='pgsql' && $lID) + if ($this->engine=='pgsql' && $lID && $aik) $this->_id=$lID[0][$aik]; elseif ($this->engine!='oci') $this->_id=$this->db->lastinsertid(); // Reload to obtain default and auto-increment field values - if ($reload=($inc || $filter)) + if ($reload=(($inc && $this->_id) || $filter)) $this->load($inc? [$inc.'=?',$this->db->value( $this->fields[$inc]['pdo_type'],$this->_id)]: @@ -463,7 +464,7 @@ class Mapper extends \DB\Cursor { /** * Update current record - * @return object + * @return static **/ function update() { $args=[]; @@ -652,7 +653,7 @@ class Mapper extends \DB\Cursor { /** * Instantiate class - * @param $db object + * @param $db \DB\SQL * @param $table string * @param $fields array|string * @param $ttl int|array diff --git a/app/lib/markdown.php b/app/lib/markdown.php index 18995fb7..9abcd183 100644 --- a/app/lib/markdown.php +++ b/app/lib/markdown.php @@ -306,6 +306,7 @@ class Markdown extends Prefab { }, $str ); + $str=preg_replace('/\s{2}\r?\n/','
',$str); return '

'.$this->scan($str).'

'."\n\n"; } return ''; diff --git a/app/lib/template.php b/app/lib/template.php index 5d4c74b0..919d1c6d 100644 --- a/app/lib/template.php +++ b/app/lib/template.php @@ -273,7 +273,7 @@ class Template extends Preview { // Build tree structure for ($ptr=0,$w=5,$len=strlen($text),$tree=[],$tmp='';$ptr<$len;) if (preg_match('/^(.{0,'.$w.'}?)<(\/?)(?:F3:)?'. - '('.$this->tags.')\b((?:\s+[\w-]+'. + '('.$this->tags.')\b((?:\s+[\w-.:@!]+'. '(?:\h*=\h*(?:"(?:.*?)"|\'(?:.*?)\'))?|'. '\h*\{\{.+?\}\})*)\h*(\/?)>/is', substr($text,$ptr),$match)) { @@ -303,19 +303,18 @@ class Template extends Preview { if ($match[4]) { // Process attributes preg_match_all( - '/(?:\b([\w-]+)\h*'. - '(?:=\h*(?:"(.*?)"|\'(.*?)\'))?|'. - '(\{\{.+?\}\}))/s', + '/(?:(\{\{.+?\}\})|([^\s\/"\'=]+))'. + '\h*(?:=\h*(?:"(.*?)"|\'(.*?)\'))?/s', $match[4],$attr,PREG_SET_ORDER); foreach ($attr as $kv) - if (isset($kv[4])) - $node['@attrib'][]=$kv[4]; + if (!empty($kv[1]) && !isset($kv[3]) && !isset($kv[4])) + $node['@attrib'][]=$kv[1]; else - $node['@attrib'][$kv[1]]= - (isset($kv[2]) && $kv[2]!==''? - $kv[2]: - (isset($kv[3]) && $kv[3]!==''? - $kv[3]:NULL)); + $node['@attrib'][$kv[1]?:$kv[2]]= + (isset($kv[3]) && $kv[3]!==''? + $kv[3]: + (isset($kv[4]) && $kv[4]!==''? + $kv[4]:NULL)); } } $tmp=''; @@ -342,12 +341,13 @@ class Template extends Preview { * return object **/ function __construct() { - $ref=new ReflectionClass(__CLASS__); + $ref=new ReflectionClass(get_called_class()); $this->tags=''; foreach ($ref->getmethods() as $method) if (preg_match('/^_(?=[[:alpha:]])/',$method->name)) $this->tags.=(strlen($this->tags)?'|':''). substr($method->name,1); + parent::__construct(); } } diff --git a/app/lib/web.php b/app/lib/web.php index dd817ce2..36812ee1 100644 --- a/app/lib/web.php +++ b/app/lib/web.php @@ -52,7 +52,7 @@ class Web extends Prefab { 'hqx'=>'application/mac-binhex40', 'html?'=>'text/html', 'jar'=>'application/java-archive', - 'jpe?g'=>'image/jpeg', + 'jpe?g|jfif?'=>'image/jpeg', 'js'=>'application/x-javascript', 'midi'=>'audio/x-midi', 'mp3'=>'audio/mpeg', @@ -281,6 +281,8 @@ class Web extends Prefab { curl_setopt($curl,CURLOPT_HTTPHEADER,$options['header']); if (isset($options['content'])) curl_setopt($curl,CURLOPT_POSTFIELDS,$options['content']); + if (isset($options['proxy'])) + curl_setopt($curl,CURLOPT_PROXY,$options['proxy']); curl_setopt($curl,CURLOPT_ENCODING,'gzip,deflate'); $timeout=isset($options['timeout'])? $options['timeout']: @@ -333,6 +335,12 @@ class Web extends Prefab { **/ protected function _stream($url,$options) { $eol="\r\n"; + if (isset($options['proxy'])) { + $options['proxy']=preg_replace('/https?/i','tcp',$options['proxy']); + $options['request_fulluri']=true; + if (preg_match('/socks4?/i',$options['proxy'])) + return $this->_socket($url,$options); + } $options['header']=implode($eol,$options['header']); $body=@file_get_contents($url,FALSE, stream_context_create(['http'=>$options])); @@ -378,25 +386,46 @@ class Web extends Prefab { $headers=[]; $body=''; $parts=parse_url($url); - $empty=empty($parts['port']); - if ($parts['scheme']=='https') { + $hostname=$parts['host']; + $proxy=false; + if ($parts['scheme']=='https') $parts['host']='ssl://'.$parts['host']; - if ($empty) - $parts['port']=443; - } - elseif ($empty) - $parts['port']=80; + if (empty($parts['port'])) + $parts['port']=$parts['scheme']=='https'?443:80; if (empty($parts['path'])) $parts['path']='/'; if (empty($parts['query'])) $parts['query']=''; - if ($socket=@fsockopen($parts['host'],$parts['port'],$code,$err)) { + if (isset($options['proxy'])) { + $req=$url; + $pp=parse_url($options['proxy']); + $proxy=$pp['scheme']; + if ($pp['scheme']=='https') + $pp['host']='ssl://'.$pp['host']; + if (empty($pp['port'])) + $pp['port']=$pp['scheme']=='https'?443:80; + $socket=@fsockopen($pp['host'],$pp['port'],$code,$err); + } else { + $req=$parts['path'].($parts['query']?('?'.$parts['query']):''); + $socket=@fsockopen($parts['host'],$parts['port'],$code,$err); + } + if ($socket) { 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 - ); + if ($proxy=='socks4') { + // SOCKS4; http://en.wikipedia.org/wiki/SOCKS#Protocol + $packet="\x04\x01".pack("n", $parts['port']). + pack("H*",dechex(ip2long(gethostbyname($hostname))))."\0"; + fputs($socket, $packet, strlen($packet)); + $response=fread($socket, 9); + if (strlen($response)==8 && (ord($response[0])==0 || ord($response[0])==4) + && ord($response[1])==90) { + $options['header'][]='Host: '.$hostname; + } else + $err='Socket Status '.ord($response[1]); + } + fputs($socket,$options['method'].' '.$req.' HTTP/1.0'.$eol); fputs($socket,implode($eol,$options['header']).$eol.$eol); if (isset($options['content'])) fputs($socket,$options['content'].$eol); @@ -508,12 +537,6 @@ class Web extends Prefab { $this->engine(); if ($this->wrapper!='stream') { // PHP streams can't cope with redirects when Host header is set - foreach ($options['header'] as &$header) - if (preg_match('/^Host:/',$header)) { - $header='Host: '.$parts['host']; - unset($header); - break; - } $this->subst($options['header'],'Host: '.$parts['host']); } $this->subst($options['header'], diff --git a/app/lib/web/geo.php b/app/lib/web/geo.php index c669024d..9d680025 100644 --- a/app/lib/web/geo.php +++ b/app/lib/web/geo.php @@ -64,8 +64,8 @@ class Geo extends \Prefab { $out=@geoip_record_by_name($ip)) { $out['request']=$ip; $out['region_code']=$out['region']; - $out['region_name']=geoip_region_name_by_code( - $out['country_code'],$out['region']); + $out['region_name']=(!empty($out['country_code']) && !empty($out['region'])) + ? geoip_region_name_by_code($out['country_code'],$out['region']) : ''; unset($out['country_code3'],$out['region'],$out['postal_code']); return $out; }