Browse Source

UPD: Bump F3 tro version 3.6.5

Giancarlo 7 years ago
parent
commit
a082424d77

+ 43 - 0
lib/f3/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)

+ 29 - 16
lib/f3/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 @@ class Auth {
 	*	@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 @@ class Auth {
 	*	@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 @@ class Auth {
 	*	@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 @@ class Auth {
 		else
 			$this->storage=$storage;
 		$this->args=$args;
+		$this->func=$func;
 	}
 
 }

+ 104 - 61
lib/f3/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 @@ final class Base extends Prefab implements ArrayAccess {
 	}
 
 	/**
-	 * 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 @@ final class Base extends Prefab implements ArrayAccess {
 								$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 @@ final class Base extends Prefab implements ArrayAccess {
 				$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 @@ final class Base extends Prefab implements ArrayAccess {
 					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 @@ final class Base extends Prefab implements ArrayAccess {
 					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('/(?<tag>\w+)'.
@@ -964,16 +972,22 @@ final class Base extends Prefab implements ArrayAccess {
 											$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 @@ final class Base extends Prefab implements ArrayAccess {
 		);
 	}
 
+	/**
+	*	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 @@ final class Base extends Prefab implements ArrayAccess {
 		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 @@ final class Base extends Prefab implements ArrayAccess {
 				'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]
+					)
+				):
 				('<!DOCTYPE html>'.$eol.
 				'<html>'.$eol.
 				'<head>'.
@@ -1557,7 +1588,7 @@ final class Base extends Prefab implements ArrayAccess {
 			$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 @@ final class Base extends Prefab implements ArrayAccess {
 			// 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 @@ final class Base extends Prefab implements ArrayAccess {
 	}
 
 	/**
-	*	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 @@ final class Base extends Prefab implements ArrayAccess {
 		$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 @@ final class Base extends Prefab implements ArrayAccess {
 						$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 @@ final class Base extends Prefab implements ArrayAccess {
 						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 @@ final class Base extends Prefab implements ArrayAccess {
 						$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 @@ final class Base extends Prefab implements ArrayAccess {
 		);
 		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 @@ final class Base extends Prefab implements ArrayAccess {
 			$_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 @@ class Cache extends Prefab {
 		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 @@ class Cache extends Prefab {
 			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 @@ class Preview extends View {
 			$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

+ 2 - 1
lib/f3/basket.php

@@ -90,7 +90,8 @@ class Basket extends Magic {
 		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;

+ 69 - 94
lib/f3/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 @@ class WS {
 			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 @@ class WS {
 	*	@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)<self::Packet)
-			return $str;
+		if (is_string($buf=@fread($socket,WS::Packet)) &&
+			strlen($buf) &&
+			strlen($buf)<WS::Packet)
+			return $buf;
 		if (isset($this->events['error']) &&
 			is_callable($func=$this->events['error']))
 			$func($this);
+		$this->close($socket);
 		return FALSE;
 	}
 
@@ -170,16 +155,17 @@ class WS {
 	*	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;$i<strlen($str);$i+=$bytes) {
-			if (($bytes=@fwrite($socket,substr($str,$i))) &&
+	function write($socket,$buf) {
+		for ($i=0,$bytes=0;$i<strlen($buf);$i+=$bytes) {
+			if (($bytes=@fwrite($socket,substr($buf,$i))) &&
 				@fflush($socket))
 				continue;
 			if (isset($this->events['error']) &&
 				is_callable($func=$this->events['error']))
 				$func($this);
+			$this->close($socket);
 			return FALSE;
 		}
 		return $bytes;
@@ -248,7 +234,7 @@ class WS {
 		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 @@ class WS {
 		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 @@ class WS {
 					}
 					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 @@ class WS {
 			}
 			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 @@ class Agent {
 		return $this->id;
 	}
 
+	/**
+	*	Return socket
+	*	@return object
+	**/
+	function socket() {
+		return $this->socket;
+	}
+
 	/**
 	*	Return request method
 	*	@return string
@@ -413,22 +387,20 @@ class Agent {
 	*	@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 @@ class Agent {
 	**/
 	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 @@ class Agent {
 			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 @@ class Agent {
 		$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);

+ 1 - 1
lib/f3/db/jig.php

@@ -167,7 +167,7 @@ class Jig {
 	function __destruct() {
 		if ($this->lazy) {
 			$this->lazy = FALSE;
-			foreach ($this->data as $file => $data)
+			foreach ($this->data?:[] as $file => $data)
 				$this->write($file,$data);
 		}
 	}

+ 7 - 4
lib/f3/db/jig/mapper.php

@@ -93,7 +93,7 @@ class Mapper extends \DB\Cursor {
 	*	@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 @@ class Mapper extends \DB\Cursor {
 	*	@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 @@ class Mapper extends \DB\Cursor {
 		$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]+$ttl<microtime(TRUE)) {
 			$data=$db->read($this->file);
 			if (is_null($data))
@@ -347,7 +350,7 @@ class Mapper extends \DB\Cursor {
 	*	@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);

+ 5 - 1
lib/f3/db/jig/session.php

@@ -180,7 +180,11 @@ class Session extends Mapper {
 		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']:'';

+ 12 - 6
lib/f3/db/mongo/mapper.php

@@ -91,7 +91,7 @@ class Mapper extends \DB\Cursor {
 	*	@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 @@ class Mapper extends \DB\Cursor {
 	*	@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 @@ class Mapper extends \DB\Cursor {
 			'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]+$ttl<microtime(TRUE)) {
 			if ($options['group']) {
 				$grp=$this->collection->group(
@@ -194,7 +197,7 @@ class Mapper extends \DB\Cursor {
 	*	@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 @@ class Mapper extends \DB\Cursor {
 	*	@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]+$ttl<microtime(TRUE)) {
 			$result=$this->collection->count($filter?:[]);
 			if ($fw->CACHE && $ttl)

+ 5 - 1
lib/f3/db/mongo/session.php

@@ -180,7 +180,11 @@ class Session extends Mapper {
 		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']:'';

+ 2 - 2
lib/f3/db/sql.php

@@ -220,7 +220,7 @@ class SQL {
 						'/';
 				}
 				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 @@ class SQL {
 					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);

+ 40 - 12
lib/f3/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 @@ class Mapper extends \DB\Cursor {
 	*	@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 @@ class Mapper extends \DB\Cursor {
 			'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 @@ class Mapper extends \DB\Cursor {
 		}
 		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 @@ class Mapper extends \DB\Cursor {
 			if ($options['offset'])
 				$sql.=' OFFSET '.(int)$options['offset'];
 		}
+		if ($options['comment'])
+			$sql.="\n".' /* '.$options['comment'].' */';
 		return [$sql,$args];
 	}
 
@@ -345,19 +353,30 @@ class Mapper extends \DB\Cursor {
 	*	@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 @@ class Mapper extends \DB\Cursor {
 		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

+ 5 - 1
lib/f3/db/sql/session.php

@@ -204,7 +204,11 @@ class Session extends Mapper {
 		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']:'';

+ 9 - 8
lib/f3/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
+			);
 	}
 
 	/**

+ 4 - 2
lib/f3/matrix.php

@@ -92,13 +92,15 @@ class Matrix extends Prefab {
 	*	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=[];

+ 5 - 1
lib/f3/session.php

@@ -182,7 +182,11 @@ class Session {
 		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']:'';

+ 9 - 4
lib/f3/smtp.php

@@ -204,14 +204,19 @@ class SMTP extends Magic {
 			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);

+ 1 - 1
lib/f3/template.php

@@ -273,7 +273,7 @@ class Template extends Preview {
 		// Build tree structure
 		for ($ptr=0,$w=5,$len=strlen($text),$tree=[],$tmp='';$ptr<$len;)
 			if (preg_match('/^(.{0,'.$w.'}?)<(\/?)(?:F3:)?'.
-				'('.$this->tags.')\b((?:\s+[\w-.:@!]+'.
+				'('.$this->tags.')\b((?:\s+[\w.:@!-]+'.
 				'(?:\h*=\h*(?:"(?:.*?)"|\'(?:.*?)\'))?|'.
 				'\h*\{\{.+?\}\})*)\h*(\/?)>/is',
 				substr($text,$ptr),$match)) {

+ 7 - 7
lib/f3/web.php

@@ -269,7 +269,7 @@ class Web extends Prefab {
 	**/
 	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 @@ class Web extends Prefab {
 		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 @@ class Web extends Prefab {
 		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 @@ class Web extends Prefab {
 			$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 @@ class Web extends Prefab {
 		);
 		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 @@ class Web extends Prefab {
 				$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 @@ class Web extends Prefab {
 		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;
 	}

+ 5 - 2
lib/f3/web/geo.php

@@ -64,8 +64,11 @@ class Geo extends \Prefab {
 			$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;
 		}

+ 28 - 12
lib/f3/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 @@ class OAuth2 extends \Magic {
 		];
 		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 @@ class OAuth2 extends \Magic {
 		$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 @@ class OAuth2 extends \Magic {
 	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