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; }