diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md index d7c078836..04950f153 100644 --- a/lib/CHANGELOG.md +++ b/lib/CHANGELOG.md @@ -1,5 +1,30 @@ 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 diff --git a/lib/base.php b/lib/base.php index 62fa14fb2..e04abf40c 100644 --- a/lib/base.php +++ b/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 @@ function devoid($key,&$val=NULL) { * @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 @@ function set($key,$val,$ttl=0) { $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 @@ function clear($key) { 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 @@ function error($code,$text='',array $trace=NULL,$level=0) { $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 @@ function grab($func,$args=NULL) { 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 @@ function config($source,$allow=FALSE) { 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 @@ function($level,$text,$file,$line) { $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 @@ function($level,$text,$file,$line) { $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 @@ function($level,$text,$file,$line) { '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 @@ function($level,$text,$file,$line) { $this->language($headers['Accept-Language']): $this->fallback, 'LOCALES'=>'./', + 'LOGGABLE'=>'*', 'LOGS'=>'./', 'MB'=>extension_loaded('mbstring'), 'ONERROR'=>NULL, @@ -2363,6 +2392,11 @@ function($level,$text,$file,$line) { '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 @@ function($val) use($fw) { * @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 @@ function($val) use($fw) { * @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 @@ protected function sandbox(array $hive=NULL,$mime=NULL) { * @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 @@ function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { $cache->set($hash,$data,$ttl); return $data; } + } user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); } @@ -2807,7 +2847,6 @@ function interpolation($bool) { * @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 @@ function c($val) { * @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 @@ function($expr) { * @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 @@ function parse($text) { * @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/lib/db/jig.php b/lib/db/jig.php index d0730144a..79f33fb8e 100644 --- a/lib/db/jig.php +++ b/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 @@ function &read($file) { $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 @@ function &read($file) { * @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 @@ function jot($frame) { * @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 @@ private function __clone() { * @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/lib/db/jig/mapper.php b/lib/db/jig/mapper.php index 135dd6123..323f2c815 100644 --- a/lib/db/jig/mapper.php +++ b/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 @@ function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) { $options+=[ 'order'=>NULL, 'limit'=>0, - 'offset'=>0 + 'offset'=>0, + 'group'=>NULL, ]; $fw=\Base::instance(); $cache=\Cache::instance(); @@ -232,30 +235,46 @@ function($_row) use($fw,$args,$tokens) { } ); } - 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; + 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]); } - return 0; + 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 @@ function($val1,$val2) use($cols) { 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/lib/db/mongo.php b/lib/db/mongo.php index 08481ebff..86ac184c9 100644 --- a/lib/db/mongo.php +++ b/lib/db/mongo.php @@ -68,8 +68,9 @@ function log($flag=TRUE) { $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/lib/db/sql/mapper.php b/lib/db/sql/mapper.php index f78fc36c6..afdc5c81a 100644 --- a/lib/db/sql/mapper.php +++ b/lib/db/sql/mapper.php @@ -153,7 +153,7 @@ function __call($func,$args) { /** * Convert array to mapper object - * @return object + * @return static * @param $row array **/ protected function factory($row) { @@ -236,14 +236,15 @@ function($parts) use($db) { 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 @@ function($str) use($db) { /** * Build query string and execute - * @return object + * @return static[] * @param $fields string * @param $filter string|array * @param $options array @@ -360,7 +361,7 @@ function count($filter=NULL,array $options=NULL,$ttl=0) { /** * 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 @@ function skip($ofs=1) { /** * Insert new record - * @return object + * @return static **/ function insert() { $args=[]; @@ -424,8 +425,8 @@ function insert() { } } 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 @@ function insert() { '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 @@ function insert() { /** * Update current record - * @return object + * @return static **/ function update() { $args=[]; @@ -652,7 +653,7 @@ function getiterator() { /** * Instantiate class - * @param $db object + * @param $db \DB\SQL * @param $table string * @param $fields array|string * @param $ttl int|array diff --git a/lib/markdown.php b/lib/markdown.php index 18995fb7b..9abcd183c 100644 --- a/lib/markdown.php +++ b/lib/markdown.php @@ -306,6 +306,7 @@ function($expr) { }, $str ); + $str=preg_replace('/\s{2}\r?\n/','
',$str); return '

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

'."\n\n"; } return ''; diff --git a/lib/template.php b/lib/template.php index 5d4c74b03..919d1c6de 100644 --- a/lib/template.php +++ b/lib/template.php @@ -273,7 +273,7 @@ function parse($text) { // 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 @@ function parse($text) { 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 @@ function parse($text) { * 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/lib/web.php b/lib/web.php index dd817ce24..36812ee15 100644 --- a/lib/web.php +++ b/lib/web.php @@ -52,7 +52,7 @@ function mime($file) { '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 @@ protected function _curl($url,$options) { 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 @@ function($curl,$line) use(&$headers) { **/ 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 @@ protected function _socket($url,$options) { $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 @@ function request($url,array $options=NULL) { $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/lib/web/geo.php b/lib/web/geo.php index c669024db..9d680025e 100644 --- a/lib/web/geo.php +++ b/lib/web/geo.php @@ -64,8 +64,8 @@ function location($ip=NULL) { $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; }