diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md index 04950f153..4960ea2c3 100644 --- a/lib/CHANGELOG.md +++ b/lib/CHANGELOG.md @@ -1,5 +1,48 @@ CHANGELOG +3.6.5 (24 December 2018) +* NEW: Log, added timestamp to each line +* NEW: Auth, added support for custom compare method, [#116](https://github.com/bcosca/fatfree-core/issues/116) +* NEW: cache tag support for mongo & jig mapper, ref [#166](https://github.com/bcosca/fatfree-core/issues/116) +* NEW: Allow PHP functions as template token filters +* Web: Fix double redirect bug when running cURL with open_basedir disabled +* Web: Cope with responses from HTTP/2 servers +* Web->filler: remove very first space, when $std is false +* Web\OAuth2: Cope with HTTP/2 responses +* Web\OAuth2: take Content-Type header into account for json decoding, [#250](https://github.com/bcosca/fatfree-core/issues/250) [#251](https://github.com/bcosca/fatfree-core/issues/251) +* Web\OAuth2: fixed empty results on some endpoints [#250](https://github.com/bcosca/fatfree-core/issues/250) +* DB\SQL\Mapper: optimize mapper->count memory usage +* DB\SQL\Mapper: New table alias operator +* DB\SQL\Mapper: fix count() performance on non-grouped result sets, [bcosca/fatfree#1114](https://github.com/bcosca/fatfree/issues/1114) +* DB\SQL: Support for CTE in postgreSQL, [bcosca/fatfree#1107](https://github.com/bcosca/fatfree/issues/1107), [bcosca/fatfree#1116](https://github.com/bcosca/fatfree/issues/1116), [bcosca/fatfree#1021](https://github.com/bcosca/fatfree/issues/1021) +* DB\SQL->log: Remove extraneous whitespace +* DB\SQL: Added ability to add inline comments per SQL query +* CLI\WS, Refactoring: Streamline socket server +* CLI\WS: Add option for dropping query in OAuth2 URI +* CLI\WS: Add URL-safe base64 encoding +* CLI\WS: Detect errors in returned JSON values +* CLI\WS: Added support for Sec-WebSocket-Protocol header +* Matrix->calendar: Allow unix timestamp as date argument +* Basket: Access basket item by _id [#260](https://github.com/bcosca/fatfree-core/issues/260) +* SMTP: Added TLS 1.2 support [bcosca/fatfree#1115](https://github.com/bcosca/fatfree/issues/1115) +* SMTP->send: Respect $log argument +* Base->cast: recognize binary and octal numbers in config +* Base->cast: add awareness of hexadecimal literals +* Base->abort: Remove unnecessary Content-Encoding header +* Base->abort: Ensure headers have not been flushed +* Base->format: Differentiate between long- and full-date (with localized weekday) formats +* Base->format: Conform with intl extension's number output +* Enable route handler to override Access-Control headers in response to OPTIONS request, [#257](https://github.com/bcosca/fatfree-core/issues/257) +* Augment filters with a var_export function +* Bug fix php7.3: Fix template parse regex to be compatible with strict PCRE2 rules for hyphen placement in a character class +* Bug fix, Cache->set: update creation time when updating existing cache entries +* Bug fix: incorrect ICU date/time formatting +* Bug fix, Jig: lazy write on empty data +* Bug fix: Method uppercase to avoid route failure [#252](https://github.com/bcosca/fatfree-core/issues/252) +* Fixed error description when (PSR-11) `CONTAINER` fails to resolve a class [#253](https://github.com/bcosca/fatfree-core/issues/253) +* Mitigate CSRF predictability/vulnerability +* Expose Mapper->factory() method + 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) diff --git a/lib/auth.php b/lib/auth.php index c1e8a570b..d043cf224 100644 --- a/lib/auth.php +++ b/lib/auth.php @@ -2,7 +2,7 @@ /* - Copyright (c) 2009-2017 F3::Factory/Bong Cosca, All rights reserved. + Copyright (c) 2009-2018 F3::Factory/Bong Cosca, All rights reserved. This file is part of the Fat-Free Framework (http://fatfreeframework.com). @@ -35,7 +35,9 @@ class Auth { //! Mapper object $mapper, //! Storage options - $args; + $args, + //! Custom compare function + $func; /** * Jig storage handler @@ -45,22 +47,26 @@ class Auth { * @param $realm string **/ protected function _jig($id,$pw,$realm) { - return (bool) + $success = (bool) call_user_func_array( [$this->mapper,'load'], [ array_merge( [ - '@'.$this->args['id'].'==? AND '. - '@'.$this->args['pw'].'==?'. + '@'.$this->args['id'].'==?'. + ($this->func?'':' AND @'.$this->args['pw'].'==?'). (isset($this->args['realm'])? (' AND @'.$this->args['realm'].'==?'):''), - $id,$pw + $id ], + ($this->func?[]:[$pw]), (isset($this->args['realm'])?[$realm]:[]) ) ] ); + if ($success && $this->func) + $success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw'])); + return $success; } /** @@ -71,15 +77,16 @@ protected function _jig($id,$pw,$realm) { * @param $realm string **/ protected function _mongo($id,$pw,$realm) { - return (bool) + $success = (bool) $this->mapper->load( - [ - $this->args['id']=>$id, - $this->args['pw']=>$pw - ]+ + [$this->args['id']=>$id]+ + ($this->func?[]:[$this->args['pw']=>$pw])+ (isset($this->args['realm'])? [$this->args['realm']=>$realm]:[]) ); + if ($success && $this->func) + $success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw'])); + return $success; } /** @@ -90,22 +97,26 @@ protected function _mongo($id,$pw,$realm) { * @param $realm string **/ protected function _sql($id,$pw,$realm) { - return (bool) + $success = (bool) call_user_func_array( [$this->mapper,'load'], [ array_merge( [ - $this->args['id'].'=? AND '. - $this->args['pw'].'=?'. + $this->args['id'].'=?'. + ($this->func?'':' AND '.$this->args['pw'].'=?'). (isset($this->args['realm'])? (' AND '.$this->args['realm'].'=?'):''), - $id,$pw + $id ], + ($this->func?[]:[$pw]), (isset($this->args['realm'])?[$realm]:[]) ) ] ); + if ($success && $this->func) + $success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw'])); + return $success; } /** @@ -234,8 +245,9 @@ function basic($func=NULL) { * @return object * @param $storage string|object * @param $args array + * @param $func callback **/ - function __construct($storage,array $args=NULL) { + function __construct($storage,array $args=NULL,$func=NULL) { if (is_object($storage) && is_a($storage,'DB\Cursor')) { $this->storage=$storage->dbtype(); $this->mapper=$storage; @@ -244,6 +256,7 @@ function __construct($storage,array $args=NULL) { else $this->storage=$storage; $this->args=$args; + $this->func=$func; } } diff --git a/lib/base.php b/lib/base.php index e04abf40c..8990c2995 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.4-Release'; + VERSION='3.6.5-Release'; //@} //@{ HTTP status codes (RFC 2616) @@ -207,11 +207,13 @@ function parse($str) { } /** - * cast string variable to php type or constant + * Cast string variable to PHP type or constant * @param $val * @return mixed */ function cast($val) { + if (preg_match('/^(?:0x[0-9a-f]+|0[0-7]+|0b[01]+)$/i',$val)) + return intval($val,0); if (is_numeric($val)) return $val+0; $val=trim($val); @@ -241,13 +243,13 @@ function($sub) { $out='['. (isset($sub[3])? $this->compile($sub[3]): - var_export($sub[1],TRUE)). + $this->export($sub[1])). ']'; } else $out=function_exists($sub[1])? $sub[0]: - ('['.var_export($sub[1],TRUE).']'.$sub[2]); + ('['.$this->export($sub[1]).']'.$sub[2]); return $out; }, $expr[2] @@ -680,7 +682,7 @@ function stringify($arg,array $stack=NULL) { $str=''; foreach (get_object_vars($arg) as $key=>$val) $str.=($str?',':''). - var_export($key,TRUE).'=>'. + $this->export($key).'=>'. $this->stringify($val, array_merge($stack,[$arg])); return get_class($arg).'::__set_state(['.$str.'])'; @@ -690,11 +692,11 @@ function stringify($arg,array $stack=NULL) { ctype_digit(implode('',array_keys($arg))); foreach ($arg as $key=>$val) $str.=($str?',':''). - ($num?'':(var_export($key,TRUE).'=>')). + ($num?'':($this->export($key).'=>')). $this->stringify($val,array_merge($stack,[$arg])); return '['.$str.']'; default: - return var_export($arg,TRUE); + return $this->export($arg); } } @@ -890,8 +892,14 @@ function($expr) use($args,$conv) { return $expr[0]; if (isset($type)) { if (isset($this->hive['FORMATS'][$type])) - return $this->call($this->hive['FORMATS'][$type], - [$args[$pos],isset($mod)?$mod:null,isset($prop)?$prop:null]); + return $this->call( + $this->hive['FORMATS'][$type], + [ + $args[$pos], + isset($mod)?$mod:null, + isset($prop)?$prop:null + ] + ); switch ($type) { case 'plural': preg_match_all('/(?\w+)'. @@ -964,16 +972,22 @@ function_exists('money_format')) $args[$pos]*100,0,$decimal_point, $thousands_sep).'%'; } + $frac=$args[$pos]-(int)$args[$pos]; return number_format( - $args[$pos],isset($prop)?$prop:2, + $args[$pos], + isset($prop)? + $prop: + $frac?strlen($frac)-2:0, $decimal_point,$thousands_sep); case 'date': + $prop='%d %B %Y'; if (empty($mod) || $mod=='short') $prop='%x'; - elseif ($mod=='long') - $prop='%A, %d %B %Y'; + elseif ($mod=='full') + $prop='%A, '.$prop; return strftime($prop,$args[$pos]); case 'time': + $prop='%r'; if (empty($mod) || $mod=='short') $prop='%X'; return strftime($prop,$args[$pos]); @@ -987,6 +1001,15 @@ function_exists('money_format')) ); } + /** + * Return string representation of expression + * @return string + * @param $expr mixed + **/ + function export($expr) { + return var_export($expr,TRUE); + } + /** * Assign/auto-detect language * @return string @@ -1244,7 +1267,8 @@ function error($code,$text='',array $trace=NULL,$level=0) { if (!is_array($loggable)) $loggable=$this->split($loggable); foreach ($loggable as $status) - if ($status=='*' || preg_match('/^'.preg_replace('/\D/','\d',$status).'$/',$code)) { + if ($status=='*' || + preg_match('/^'.preg_replace('/\D/','\d',$status).'$/',$code)) { error_log($text); foreach (explode("\n",$trace) as $nexus) if ($nexus) @@ -1270,7 +1294,14 @@ function error($code,$text='',array $trace=NULL,$level=0) { 'beforeroute,afterroute')===FALSE) && !$prior && !$this->hive['CLI'] && !$this->hive['QUIET']) echo $this->hive['AJAX']? - json_encode(array_diff_key($this->hive['ERROR'],$this->hive['DEBUG']?[]:['trace'=>1])): + json_encode( + array_diff_key( + $this->hive['ERROR'], + $this->hive['DEBUG']? + []: + ['trace'=>1] + ) + ): (''.$eol. ''.$eol. ''. @@ -1557,7 +1588,7 @@ function run() { $cors=$this->hive['CORS']; header('Access-Control-Allow-Origin: '.$cors['origin']); header('Access-Control-Allow-Credentials: '. - var_export($cors['credentials'],TRUE)); + $this->export($cors['credentials'])); $preflight= isset($this->hive['HEADERS']['Access-Control-Request-Method']); } @@ -1674,12 +1705,15 @@ function($id) use($args) { // URL doesn't match any route $this->error(404); elseif (!$this->hive['CLI']) { - // Unhandled HTTP method - header('Allow: '.implode(',',array_unique($allowed))); + if (!preg_grep('/Allow:/',$headers_send=headers_list())) + // Unhandled HTTP method + header('Allow: '.implode(',',array_unique($allowed))); if ($cors) { - header('Access-Control-Allow-Methods: OPTIONS,'. - implode(',',$allowed)); - if ($cors['headers']) + if (!preg_grep('/Access-Control-Allow-Methods:/',$headers_send)) + header('Access-Control-Allow-Methods: OPTIONS,'. + implode(',',$allowed)); + if ($cors['headers'] && + !preg_grep('/Access-Control-Allow-Headers:/',$headers_send)) header('Access-Control-Allow-Headers: '. (is_array($cors['headers'])? implode(',',$cors['headers']): @@ -1733,7 +1767,9 @@ function until($func,$args=NULL,$timeout=60) { } /** - * Disconnect HTTP client + * Disconnect HTTP client; + * Set FcgidOutputBufferSize to zero if server uses mod_fcgid; + * Disable mod_deflate when rendering text/html output **/ function abort() { if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) @@ -1741,9 +1777,10 @@ function abort() { $out=''; while (ob_get_level()) $out=ob_get_clean().$out; - header('Content-Encoding: none'); - header('Content-Length: '.strlen($out)); - header('Connection: close'); + if (!headers_sent()) { + header('Content-Length: '.strlen($out)); + header('Connection: close'); + } session_commit(); echo $out; flush(); @@ -1771,11 +1808,13 @@ function grab($func,$args=NULL) { $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]); + 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)), + $this->stringify($parts[1])), E_USER_ERROR); } else { @@ -1916,7 +1955,8 @@ function config($source,$allow=FALSE) { call_user_func_array( [$this,$cmd[1]], array_merge([$match['lval']], - str_getcsv($cmd[1]=='config'?$this->cast($match['rval']): + str_getcsv($cmd[1]=='config'? + $this->cast($match['rval']): $match['rval'])) ); } @@ -1931,9 +1971,11 @@ function config($source,$allow=FALSE) { $args=array_map( function($val) { $val=$this->cast($val); - return is_string($val) - ? preg_replace('/\\\\"/','"',$val) - : $val; + if (is_string($val)) + $val=strlen($val)? + preg_replace('/\\\\"/','"',$val): + NULL; + return $val; }, // Mark quoted strings with 0x00 whitespace str_getcsv(preg_replace( @@ -2221,6 +2263,7 @@ function($level,$text,$file,$line) { ); if (!isset($_SERVER['SERVER_NAME']) || $_SERVER['SERVER_NAME']==='') $_SERVER['SERVER_NAME']=gethostname(); + $headers=[]; if ($cli=PHP_SAPI=='cli') { // Emulate HTTP request $_SERVER['REQUEST_METHOD']='GET'; @@ -2251,33 +2294,30 @@ function($level,$text,$file,$line) { $_SERVER['REQUEST_URI']=$req; parse_str($query,$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]; + elseif (function_exists('getallheaders')) { + foreach (getallheaders() as $key=>$val) { + $tmp=strtoupper(strtr($key,'-','_')); + // TODO: use ucwords delimiters for php 5.4.32+ & 5.5.16+ + $key=strtr(ucwords(strtolower(strtr($key,'-',' '))),' ','-'); + $headers[$key]=$val; + if (isset($_SERVER['HTTP_'.$tmp])) + $headers[$key]=&$_SERVER['HTTP_'.$tmp]; } } + else { + if (isset($_SERVER['CONTENT_LENGTH'])) + $headers['Content-Length']=&$_SERVER['CONTENT_LENGTH']; + if (isset($_SERVER['CONTENT_TYPE'])) + $headers['Content-Type']=&$_SERVER['CONTENT_TYPE']; + foreach (array_keys($_SERVER) as $key) + if (substr($key,0,5)=='HTTP_') + $headers[strtr(ucwords(strtolower(strtr( + substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key]; + } if (isset($headers['X-HTTP-Method-Override'])) $_SERVER['REQUEST_METHOD']=$headers['X-HTTP-Method-Override']; elseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method'])) - $_SERVER['REQUEST_METHOD']=$_POST['_method']; + $_SERVER['REQUEST_METHOD']=strtoupper($_POST['_method']); $scheme=isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' || isset($headers['X-Forwarded-Proto']) && $headers['X-Forwarded-Proto']=='https'?'https':'http'; @@ -2493,10 +2533,9 @@ function set($key,$val,$ttl=0) { if (!$this->dsn) return TRUE; $ndx=$this->prefix.'.'.$key; - $time=microtime(TRUE); if ($cached=$this->exists($key)) - list($time,$ttl)=$cached; - $data=$fw->serialize([$val,$time,$ttl]); + $ttl=$cached[1]; + $data=$fw->serialize([$val,microtime(TRUE),$ttl]); $parts=explode('=',$this->dsn,2); switch ($parts[0]) { case 'apc': @@ -2513,7 +2552,8 @@ function set($key,$val,$ttl=0) { case 'xcache': return xcache_set($ndx,$data,$ttl); case 'folder': - return $fw->write($parts[1].str_replace(['/','\\'],'',$ndx),$data); + return $fw->write($parts[1]. + str_replace(['/','\\'],'',$ndx),$data); } return FALSE; } @@ -2824,6 +2864,7 @@ class Preview extends View { 'c'=>'$this->c', 'esc'=>'$this->esc', 'raw'=>'$this->raw', + 'export'=>'Base::instance()->export', 'alias'=>'Base::instance()->alias', 'format'=>'Base::instance()->format' ]; @@ -2833,7 +2874,7 @@ class Preview extends View { $interpolation=true; /** - * enable/disable markup parsing interpolation + * Enable/disable markup parsing interpolation * mainly used for adding appropriate newlines * @param $bool bool */ @@ -2867,16 +2908,18 @@ function token($str) { $str,$parts)) { $str=trim($parts[1]); foreach ($fw->split(trim($parts[2],"\xC2\xA0")) as $func) - $str=is_string($cmd=$this->filter($func))? + $str=((empty($this->filter[$cmd=$func]) && + function_exists($cmd)) || + is_string($cmd=$this->filter($func)))? $cmd.'('.$str.')': 'Base::instance()->'. - 'call($this->filter(\''.$func.'\'),['.$str.'])'; + 'call($this->filter(\''.$func.'\'),['.$str.'])'; } return $str; } /** - * Register or get (a specific one or all) token filters + * Register or get (one specific or all) token filters * @param string $key * @param string|closure $func * @return array|closure|string diff --git a/lib/basket.php b/lib/basket.php index f939c7cde..08515ee25 100644 --- a/lib/basket.php +++ b/lib/basket.php @@ -90,7 +90,8 @@ function find($key=NULL,$val=NULL) { if (isset($_SESSION[$this->key])) { foreach ($_SESSION[$this->key] as $id=>$item) if (!isset($key) || - array_key_exists($key,$item) && $item[$key]==$val) { + array_key_exists($key,$item) && $item[$key]==$val || + $key=='_id' && $id==$val) { $obj=clone($this); $obj->id=$id; $obj->item=$item; diff --git a/lib/cli/ws.php b/lib/cli/ws.php index c11e8add9..b9c575bb8 100644 --- a/lib/cli/ws.php +++ b/lib/cli/ws.php @@ -52,6 +52,7 @@ class WS { $ctx, $wait, $sockets, + $protocol, $agents=[], $events=[]; @@ -61,16 +62,14 @@ class WS { * @param $socket resource **/ function alloc($socket) { - if (is_bool($str=$this->read($socket))) { - $this->close($socket); + if (is_bool($buf=$this->read($socket))) return; - } // Get WebSocket headers $hdrs=[]; - $CRLF="\r\n"; + $EOL="\r\n"; $verb=NULL; $uri=NULL; - foreach (explode($CRLF,trim($str)) as $line) + foreach (explode($EOL,trim($buf)) as $line) if (preg_match('/^(\w+)\s(.+)\sHTTP\/1\.\d$/', trim($line),$match)) { $verb=$match[1]; @@ -98,35 +97,29 @@ function alloc($socket) { if ($verb && $uri) $this->write( $socket, - $str='HTTP/1.1 400 Bad Request'.$CRLF. - 'Connection: close'.$CRLF.$CRLF + 'HTTP/1.1 400 Bad Request'.$EOL. + 'Connection: close'.$EOL.$EOL ); $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) { + $buf='HTTP/1.1 101 Switching Protocols'.$EOL. + 'Upgrade: websocket'.$EOL. + 'Connection: Upgrade'.$EOL; + if (isset($hdrs['Sec-Websocket-Protocol'])) + $buf.='Sec-WebSocket-Protocol: '. + $hdrs['Sec-Websocket-Protocol'].$EOL; + $buf.='Sec-WebSocket-Accept: '. + base64_encode( + sha1($hdrs['Sec-Websocket-Key'].WS::Magic,TRUE) + ).$EOL.$EOL; + if ($this->write($socket,$buf)) { // Connect agent to server - $this->sockets[]=$socket; + $this->sockets[(int)$socket]=$socket; $this->agents[(int)$socket]= new Agent($this,$socket,$verb,$uri,$hdrs); } - else - $this->close($socket); } /** @@ -135,34 +128,26 @@ function alloc($socket) { * @param $socket resource **/ function close($socket) { + if (isset($this->agents[(int)$socket])) + unset($this->sockets[(int)$socket],$this->agents[(int)$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); + $this->close($socket); return FALSE; } @@ -170,16 +155,17 @@ function read($socket) { * Write to stream socket * @return int|FALSE * @param $socket resource - * @param $str string + * @param $buf string **/ - function write($socket,$str) { - for ($i=0,$bytes=0;$ievents['error']) && is_callable($func=$this->events['error'])) $func($this); + $this->close($socket); return FALSE; } return $bytes; @@ -248,7 +234,7 @@ function run() { register_shutdown_function(function() use($listen) { foreach ($this->sockets as $socket) if ($socket!=$listen) - $this->free($socket); + $this->close($socket); $this->close($listen); if (isset($this->events['stop']) && is_callable($func=$this->events['stop'])) @@ -259,7 +245,7 @@ function run() { if (isset($this->events['start']) && is_callable($func=$this->events['start'])) $func($this); - $this->sockets=[$listen]; + $this->sockets=[(int)$listen=>$listen]; $empty=[]; $wait=$this->wait; while (TRUE) { @@ -289,26 +275,8 @@ function run() { } 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; - } - } + if (isset($this->agents[$id])) + $this->agents[$id]->fetch(); } } $wait-=microtime(TRUE)-$mark; @@ -319,10 +287,9 @@ function run() { } if (!$count) { $mark=microtime(TRUE); - foreach ($this->sockets as $socket) { + foreach ($this->sockets as $id=>$socket) { if (!is_resource($socket)) continue; - $id=(int)$socket; if ($socket!=$listen && isset($this->agents[$id]) && isset($this->events['idle']) && @@ -362,8 +329,7 @@ class Agent { $verb, $uri, $headers, - $events, - $buffer; + $events; /** * Return server instance @@ -381,6 +347,14 @@ function id() { return $this->id; } + /** + * Return socket + * @return object + **/ + function socket() { + return $this->socket; + } + /** * Return request method * @return string @@ -413,22 +387,20 @@ function headers() { * @param $payload string **/ function send($op,$data='') { + $server=$this->server; $mask=WS::Finale | $op & WS::OpCode; $len=strlen($data); - $str=''; + $buf=''; if ($len>0xffff) - $str=pack('CCNN',$mask,0x7f,$len); + $buf=pack('CCNN',$mask,0x7f,$len); else if ($len>0x7d) - $str=pack('CCn',$mask,0x7e,$len); + $buf=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(); + $buf=pack('CC',$mask,$len); + $buf.=$data; + if (is_bool($server->write($this->socket,$buf))) return FALSE; - } if (!in_array($op,[WS::Pong,WS::Close]) && isset($this->events['send']) && is_callable($func=$this->events['send'])) @@ -442,12 +414,9 @@ function send($op,$data='') { **/ function fetch() { // Unmask payload - $server=$this->server(); - if (is_bool($buf=$server->read($this->socket))) { - $this->free(); + $server=$this->server; + if (is_bool($buf=$server->read($this->socket))) return FALSE; - } - $buf=($this->buffer.=$buf); $op=ord($buf[0]) & WS::OpCode; $len=ord($buf[1]) & WS::Length; $pos=2; @@ -468,18 +437,25 @@ function fetch() { return FALSE; for ($i=0,$data='';$i<$len;$i++) $data.=chr(ord($buf[$pos+$i])^$mask[$i%4]); - $this->buffer=''; + // Dispatch + switch ($op & WS::OpCode) { + case WS::Ping: + $this->send(WS::Pong); + break; + case WS::Close: + $server->close($this->socket); + break; + case WS::Text: + $data=trim($data); + case WS::Binary: + if (isset($this->events['receive']) && + is_callable($func=$this->events['receive'])) + $func($this,$op,$data); + break; + } return [$op,$data]; } - /** - * Free stream socket - * @return NULL - **/ - function free() { - $this->server->free($this->socket); - } - /** * Destroy object * @return NULL @@ -507,7 +483,6 @@ function __construct($server,$socket,$verb,$uri,array $hdrs) { $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/lib/db/jig.php b/lib/db/jig.php index 79f33fb8e..fe3a302c6 100644 --- a/lib/db/jig.php +++ b/lib/db/jig.php @@ -167,7 +167,7 @@ function __construct($dir=NULL,$format=self::FORMAT_JSON,$lazy=FALSE) { function __destruct() { if ($this->lazy) { $this->lazy = FALSE; - foreach ($this->data as $file => $data) + 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 323f2c815..ef878f131 100644 --- a/lib/db/jig/mapper.php +++ b/lib/db/jig/mapper.php @@ -93,7 +93,7 @@ function clear($key) { * @param $id string * @param $row array **/ - protected function factory($id,$row) { + function factory($id,$row) { $mapper=clone($this); $mapper->reset(); $mapper->id=$id; @@ -153,7 +153,7 @@ function($expr) { * @return static[]|FALSE * @param $filter array * @param $options array - * @param $ttl int + * @param $ttl int|array * @param $log bool **/ function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) { @@ -170,9 +170,12 @@ function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) { $db=$this->db; $now=microtime(TRUE); $data=[]; + $tag=''; + if (is_array($ttl)) + list($ttl,$tag)=$ttl; if (!$fw->CACHE || !$ttl || !($cached=$cache->exists( $hash=$fw->hash($this->db->dir(). - $fw->stringify([$filter,$options])).'.jig',$data)) || + $fw->stringify([$filter,$options])).($tag?'.'.$tag:'').'.jig',$data)) || $cached[0]+$ttlread($this->file); if (is_null($data)) @@ -347,7 +350,7 @@ function reduce($key,$handler,$finalize=null){ * @return int * @param $filter array * @param $options array - * @param $ttl int + * @param $ttl int|array **/ function count($filter=NULL,array $options=NULL,$ttl=0) { $now=microtime(TRUE); diff --git a/lib/db/jig/session.php b/lib/db/jig/session.php index e7fecb3ad..03faa6ccf 100644 --- a/lib/db/jig/session.php +++ b/lib/db/jig/session.php @@ -180,7 +180,11 @@ function __construct(\DB\Jig $db,$file='sessions',$onsuspect=NULL,$key=NULL) { register_shutdown_function('session_commit'); $fw=\Base::instance(); $headers=$fw->HEADERS; - $this->_csrf=$fw->SEED.'.'.$fw->hash(mt_rand()); + $this->_csrf=$fw->hash($fw->SEED. + extension_loaded('openssl')? + implode(unpack('L',openssl_random_pseudo_bytes(4))): + mt_rand() + ); if ($key) $fw->$key=$this->_csrf; $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; diff --git a/lib/db/mongo/mapper.php b/lib/db/mongo/mapper.php index f038b6c04..4ad4cdfe7 100644 --- a/lib/db/mongo/mapper.php +++ b/lib/db/mongo/mapper.php @@ -91,7 +91,7 @@ function clear($key) { * @return static * @param $row array **/ - protected function factory($row) { + function factory($row) { $mapper=clone($this); $mapper->reset(); foreach ($row as $key=>$val) @@ -119,7 +119,7 @@ function cast($obj=NULL) { * @param $fields string * @param $filter array * @param $options array - * @param $ttl int + * @param $ttl int|array **/ function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) { if (!$options) @@ -130,10 +130,13 @@ function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) { 'limit'=>0, 'offset'=>0 ]; + $tag=''; + if (is_array($ttl)) + list($ttl,$tag)=$ttl; $fw=\Base::instance(); $cache=\Cache::instance(); if (!($cached=$cache->exists($hash=$fw->hash($this->db->dsn(). - $fw->stringify([$fields,$filter,$options])).'.mongo', + $fw->stringify([$fields,$filter,$options])).($tag?'.'.$tag:'').'.mongo', $result)) || !$ttl || $cached[0]+$ttlcollection->group( @@ -194,7 +197,7 @@ function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) { * @return static[] * @param $filter array * @param $options array - * @param $ttl int + * @param $ttl int|array **/ function find($filter=NULL,array $options=NULL,$ttl=0) { if (!$options) @@ -213,13 +216,16 @@ function find($filter=NULL,array $options=NULL,$ttl=0) { * @return int * @param $filter array * @param $options array - * @param $ttl int + * @param $ttl int|array **/ function count($filter=NULL,array $options=NULL,$ttl=0) { $fw=\Base::instance(); $cache=\Cache::instance(); + $tag=''; + if (is_array($ttl)) + list($ttl,$tag)=$ttl; if (!($cached=$cache->exists($hash=$fw->hash($fw->stringify( - [$filter])).'.mongo',$result)) || !$ttl || + [$filter])).($tag?'.'.$tag:'').'.mongo',$result)) || !$ttl || $cached[0]+$ttlcollection->count($filter?:[]); if ($fw->CACHE && $ttl) diff --git a/lib/db/mongo/session.php b/lib/db/mongo/session.php index 98bafd632..e0194771e 100644 --- a/lib/db/mongo/session.php +++ b/lib/db/mongo/session.php @@ -180,7 +180,11 @@ function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL,$key=NULL) register_shutdown_function('session_commit'); $fw=\Base::instance(); $headers=$fw->HEADERS; - $this->_csrf=$fw->SEED.'.'.$fw->hash(mt_rand()); + $this->_csrf=$fw->hash($fw->SEED. + extension_loaded('openssl')? + implode(unpack('L',openssl_random_pseudo_bytes(4))): + mt_rand() + ); if ($key) $fw->$key=$this->_csrf; $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; diff --git a/lib/db/sql.php b/lib/db/sql.php index 39e77f0cf..923f3d873 100644 --- a/lib/db/sql.php +++ b/lib/db/sql.php @@ -220,7 +220,7 @@ function exec($cmds,$args=NULL,$ttl=0,$log=TRUE,$stamp=FALSE) { '/'; } if ($log) - $this->log.=($stamp?(date('r').' '):'').' (-0ms) '. + $this->log.=($stamp?(date('r').' '):'').'(-0ms) '. preg_replace($keys,$vals, str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; $query->execute(); @@ -235,7 +235,7 @@ function exec($cmds,$args=NULL,$ttl=0,$log=TRUE,$stamp=FALSE) { user_error('PDOStatement: '.$error[2],E_USER_ERROR); } if (preg_match('/(?:^[\s\(]*'. - '(?:EXPLAIN|SELECT|PRAGMA|SHOW)|RETURNING)\b/is',$cmd) || + '(?:WITH|EXPLAIN|SELECT|PRAGMA|SHOW)|RETURNING)\b/is',$cmd) || (preg_match('/^\s*(?:CALL|EXEC)\b/is',$cmd) && $query->columnCount())) { $result=$query->fetchall(\PDO::FETCH_ASSOC); diff --git a/lib/db/sql/mapper.php b/lib/db/sql/mapper.php index afdc5c81a..ed7ef5334 100644 --- a/lib/db/sql/mapper.php +++ b/lib/db/sql/mapper.php @@ -34,6 +34,8 @@ class Mapper extends \DB\Cursor { $source, //! SQL table (quoted) $table, + //! Alias for SQL table + $as, //! Last insert ID $_id, //! Defined fields @@ -156,7 +158,7 @@ function __call($func,$args) { * @return static * @param $row array **/ - protected function factory($row) { + function factory($row) { $mapper=clone($this); $mapper->reset(); foreach ($row as $key=>$val) { @@ -207,10 +209,13 @@ function stringify($fields,$filter=NULL,array $options=NULL) { 'group'=>NULL, 'order'=>NULL, 'limit'=>0, - 'offset'=>0 + 'offset'=>0, + 'comment'=>NULL ]; $db=$this->db; $sql='SELECT '.$fields.' FROM '.$this->table; + if (isset($this->as)) + $sql.=' AS '.$this->db->quotekey($this->as); $args=[]; if (is_array($filter)) { $args=isset($filter[1]) && is_array($filter[1])? @@ -237,9 +242,10 @@ function($parts) use($db) { } if ($options['order']) { $char=substr($db->quotekey(''),0,1);// quoting char - $order=' ORDER BY '.(FALSE===strpos($options['order'],$char)? + $order=' ORDER BY '.(is_bool(strpos($options['order'],$char))? implode(',',array_map(function($str) use($db) { - return preg_match('/^\h*(\w+[._\-\w]*)(?:\h+((?:ASC|DESC)[\w\h]*))?\h*$/i', + 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; @@ -281,6 +287,8 @@ function($parts) use($db) { if ($options['offset']) $sql.=' OFFSET '.(int)$options['offset']; } + if ($options['comment']) + $sql.="\n".' /* '.$options['comment'].' */'; return [$sql,$args]; } @@ -345,19 +353,30 @@ function find($filter=NULL,array $options=NULL,$ttl=0) { * @param $ttl int|array **/ function count($filter=NULL,array $options=NULL,$ttl=0) { - $adhoc=''; + if (!($subquery_mode=($options && !empty($options['group'])))) + $this->adhoc['_rows']=['expr'=>'COUNT(*)','value'=>NULL]; + $adhoc=[]; foreach ($this->adhoc as $key=>$field) - $adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key); - $fields='*'.$adhoc; - if (preg_match('/mssql|dblib|sqlsrv/',$this->engine)) - $fields='TOP 100 PERCENT '.$fields; + // Add all adhoc fields + // (make them available for grouping, sorting, having) + $adhoc[]=$field['expr'].' AS '.$this->db->quotekey($key); + $fields=implode(',',$adhoc); + if ($subquery_mode) { + if (empty($fields)) + // Select at least one field, ideally the grouping fields + // or sqlsrv fails + $fields=preg_replace('/HAVING.+$/i','',$options['group']); + if (preg_match('/mssql|dblib|sqlsrv/',$this->engine)) + $fields='TOP 100 PERCENT '.$fields; + } list($sql,$args)=$this->stringify($fields,$filter,$options); - $sql='SELECT COUNT(*) AS '.$this->db->quotekey('_rows').' '. - 'FROM ('.$sql.') AS '.$this->db->quotekey('_temp'); + if ($subquery_mode) + $sql='SELECT COUNT(*) AS '.$this->db->quotekey('_rows').' '. + 'FROM ('.$sql.') AS '.$this->db->quotekey('_temp'); $result=$this->db->exec($sql,$args,$ttl); + unset($this->adhoc['_rows']); return (int)$result[0]['_rows']; } - /** * Return record at specified offset using same criteria as * previous load() call and make it active @@ -651,6 +670,15 @@ function getiterator() { return new \ArrayIterator($this->cast()); } + /** + * Assign alias for table + * @param $alias string + **/ + function alias($alias) { + $this->as=$alias; + return $this; + } + /** * Instantiate class * @param $db \DB\SQL diff --git a/lib/db/sql/session.php b/lib/db/sql/session.php index 799cd0ffb..efabd6005 100644 --- a/lib/db/sql/session.php +++ b/lib/db/sql/session.php @@ -204,7 +204,11 @@ function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL,$ register_shutdown_function('session_commit'); $fw=\Base::instance(); $headers=$fw->HEADERS; - $this->_csrf=$fw->SEED.'.'.$fw->hash(mt_rand()); + $this->_csrf=$fw->hash($fw->SEED. + extension_loaded('openssl')? + implode(unpack('L',openssl_random_pseudo_bytes(4))): + mt_rand() + ); if ($key) $fw->$key=$this->_csrf; $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; diff --git a/lib/log.php b/lib/log.php index 422baf256..a80ea6952 100644 --- a/lib/log.php +++ b/lib/log.php @@ -35,14 +35,15 @@ class Log { **/ function write($text,$format='r') { $fw=Base::instance(); - $fw->write( - $this->file, - date($format). - (isset($_SERVER['REMOTE_ADDR'])? - (' ['.$_SERVER['REMOTE_ADDR'].']'):'').' '. - trim($text).PHP_EOL, - TRUE - ); + foreach (preg_split('/\r?\n|\r/',trim($text)) as $line) + $fw->write( + $this->file, + date($format). + (isset($_SERVER['REMOTE_ADDR'])? + (' ['.$_SERVER['REMOTE_ADDR'].']'):'').' '. + trim($line).PHP_EOL, + TRUE + ); } /** diff --git a/lib/matrix.php b/lib/matrix.php index f3d71821f..d643f4926 100644 --- a/lib/matrix.php +++ b/lib/matrix.php @@ -92,13 +92,15 @@ function changekey(array &$var,$old,$new) { * Return month calendar of specified date, with optional setting for * first day of week (0 for Sunday) * @return array - * @param $date string + * @param $date string|int * @param $first int **/ function calendar($date='now',$first=0) { $out=FALSE; if (extension_loaded('calendar')) { - $parts=getdate(strtotime($date)); + if (is_string($date)) + $date=strtotime($date); + $parts=getdate($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=[]; diff --git a/lib/session.php b/lib/session.php index 3434f8d21..5ab889480 100644 --- a/lib/session.php +++ b/lib/session.php @@ -182,7 +182,11 @@ function __construct($onsuspect=NULL,$key=NULL,$cache=null) { register_shutdown_function('session_commit'); $fw=\Base::instance(); $headers=$fw->HEADERS; - $this->_csrf=$fw->SEED.'.'.$fw->hash(mt_rand()); + $this->_csrf=$fw->hash($fw->SEED. + extension_loaded('openssl')? + implode(unpack('L',openssl_random_pseudo_bytes(4))): + mt_rand() + ); if ($key) $fw->$key=$this->_csrf; $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; diff --git a/lib/smtp.php b/lib/smtp.php index 6a8e53338..caaebd569 100644 --- a/lib/smtp.php +++ b/lib/smtp.php @@ -204,14 +204,19 @@ function send($message,$log=TRUE,$mock=FALSE) { stream_set_blocking($socket,TRUE); } // Get server's initial response - $this->dialog(NULL,TRUE,$mock); + $this->dialog(NULL,$log,$mock); // Announce presence $reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock); if (strtolower($this->scheme)=='tls') { $this->dialog('STARTTLS',$log,$mock); - if (!$mock) - stream_socket_enable_crypto( - $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); + if (!$mock) { + $method=STREAM_CRYPTO_METHOD_TLS_CLIENT; + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { + $method|=STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + $method|=STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + } + stream_socket_enable_crypto($socket,TRUE,$method); + } $reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock); } $message=wordwrap($message,998); diff --git a/lib/template.php b/lib/template.php index 919d1c6de..0e1303445 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)) { diff --git a/lib/web.php b/lib/web.php index 36812ee15..9c1b546e0 100644 --- a/lib/web.php +++ b/lib/web.php @@ -269,7 +269,7 @@ function progress($id) { **/ protected function _curl($url,$options) { $curl=curl_init($url); - if (!ini_get('open_basedir')) + if (!$open_basedir=ini_get('open_basedir')) curl_setopt($curl,CURLOPT_FOLLOWLOCATION, $options['follow_location']); curl_setopt($curl,CURLOPT_MAXREDIRS, @@ -306,7 +306,7 @@ function($curl,$line) use(&$headers) { curl_close($curl); $body=ob_get_clean(); if (!$err && - $options['follow_location'] && + $options['follow_location'] && $open_basedir && preg_grep('/HTTP\/1\.\d 3\d{2}/',$headers) && preg_match('/^Location: (.+)$/m',implode(PHP_EOL,$headers),$loc)) { $options['max_redirects']--; @@ -350,7 +350,7 @@ protected function _stream($url,$options) { if (is_string($body)) { $match=NULL; foreach ($headers as $header) - if (preg_match('/Content-Encoding: (.+)/',$header,$match)) + if (preg_match('/Content-Encoding: (.+)/i',$header,$match)) break; if ($match) switch ($match[1]) { @@ -442,7 +442,7 @@ protected function _socket($url,$options) { $headers=array_merge($headers,$current=explode($eol,$html[0])); $match=NULL; foreach ($current as $header) - if (preg_match('/Content-Encoding: (.+)/',$header,$match)) + if (preg_match('/Content-Encoding: (.+)/i',$header,$match)) break; if ($match) switch ($match[1]) { @@ -550,7 +550,7 @@ function request($url,array $options=NULL) { ); if (isset($options['content']) && is_string($options['content'])) { if ($options['method']=='POST' && - !preg_grep('/^Content-Type:/',$options['header'])) + !preg_grep('/^Content-Type:/i',$options['header'])) $this->subst($options['header'], 'Content-Type: application/x-www-form-urlencoded'); $this->subst($options['header'], @@ -588,7 +588,7 @@ function request($url,array $options=NULL) { $result['cached']=TRUE; } elseif (preg_match('/Cache-Control:(?:.*)max-age=(\d+)(?:,?.*'. - preg_quote($eol).')/',implode($eol,$result['headers']),$exp)) + preg_quote($eol).')/i',implode($eol,$result['headers']),$exp)) $cache->set($hash,$result,$exp[1]); } $req=[$options['method'].' '.$url]; @@ -903,7 +903,7 @@ function filler($count=1,$max=20,$std=TRUE) { for ($i=0,$add=$count-(int)$std;$i<$add;$i++) { shuffle($rnd); $words=array_slice($rnd,0,mt_rand(3,$max)); - $out.=' '.ucfirst(implode(' ',$words)).'.'; + $out.=(!$std&&$i==0?'':' ').ucfirst(implode(' ',$words)).'.'; } return $out; } diff --git a/lib/web/geo.php b/lib/web/geo.php index 9d680025e..80031cf39 100644 --- a/lib/web/geo.php +++ b/lib/web/geo.php @@ -64,8 +64,11 @@ function location($ip=NULL) { $out=@geoip_record_by_name($ip)) { $out['request']=$ip; $out['region_code']=$out['region']; - $out['region_name']=(!empty($out['country_code']) && !empty($out['region'])) - ? geoip_region_name_by_code($out['country_code'],$out['region']) : ''; + $out['region_name']=''; + if (!empty($out['country_code']) && !empty($out['region'])) + $out['region_name']=geoip_region_name_by_code( + $out['country_code'],$out['region'] + ); unset($out['country_code3'],$out['region'],$out['postal_code']); return $out; } diff --git a/lib/web/oauth2.php b/lib/web/oauth2.php index 916674f8e..fd9acf9fd 100644 --- a/lib/web/oauth2.php +++ b/lib/web/oauth2.php @@ -33,9 +33,10 @@ class OAuth2 extends \Magic { * Return OAuth2 authentication URI * @return string * @param $endpoint string + * @param $query bool **/ - function uri($endpoint) { - return $endpoint.'?'.http_build_query($this->args); + function uri($endpoint,$query=TRUE) { + return $endpoint.($query?('?'.http_build_query($this->args)):''); } /** @@ -54,7 +55,7 @@ function request($uri,$method,$token=NULL) { ]; if ($token) array_push($options['header'],'Authorization: Bearer '.$token); - elseif ($method=='POST') + elseif ($method=='POST' && isset($this->args['client_id'])) array_push($options['header'],'Authorization: Basic '. base64_encode( $this->args['client_id'].':'. @@ -64,10 +65,20 @@ function request($uri,$method,$token=NULL) { $response=$web->request($uri,$options); if ($response['error']) user_error($response['error'],E_USER_ERROR); - return $response['body'] && - preg_grep('/HTTP\/1\.\d 200/',$response['headers'])? - json_decode($response['body'],TRUE): - FALSE; + if (isset($response['body'])) { + if (preg_grep('/^Content-Type:.*application\/json/i', + $response['headers'])) { + $token=json_decode($response['body'],TRUE); + if (isset($token['error_description'])) + user_error($token['error_description'],E_USER_ERROR); + if (isset($token['error'])) + user_error($token['error'],E_USER_ERROR); + return $token; + } + else + return $response['body']; + } + return FALSE; } /** @@ -78,16 +89,21 @@ function request($uri,$method,$token=NULL) { function jwt($token) { return json_decode( base64_decode( - str_replace( - ['-','_'], - ['+','/'], - explode('.',$token)[1] - ) + str_replace(['-','_'],['+','/'],explode('.',$token)[1]) ), TRUE ); } + /** + * URL-safe base64 encoding + * @return array + * @param $data string + **/ + function b64url($data) { + return trim(strtr(base64_encode($data),'+/','-_'),'='); + } + /** * Return TRUE if scope/claim exists * @return bool