Browse Source

update libs, tests and config

ikkez 11 years ago
parent
commit
90efcbd873

+ 26 - 2
php-fatfree/benchmark_config

@@ -8,7 +8,19 @@
       "db_url": "/db-orm",
       "query_url": "/db-orm/",
       "port": 8080,
-      "sort": 101
+      "approach": "Realistic",
+      "classification": "Fullstack",
+      "database": "MySQL",
+      "framework": "Fat Free Framework",
+      "language": "PHP",
+      "orm": "Full",
+      "platform": "PHP-FPM",
+      "webserver": "nginx",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "fat-free",
+      "notes": "",
+      "versus": "php"
     },
     "raw": {
       "setup_file": "setup",
@@ -17,7 +29,19 @@
       "fortune_url": "/fortune",
       "update_url": "/updateraw",
       "port": 8080,
-      "sort": 102
+      "approach": "Realistic",
+      "classification": "Fullstack",
+      "database": "MySQL",
+      "framework": "Fat Free Framework",
+      "language": "PHP",
+      "orm": "Raw",
+      "platform": "PHP-FPM",
+      "webserver": "nginx",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "fat-free",
+      "notes": "",
+      "versus": "php"
     }
   }]
 }

BIN
php-fatfree/lib/api.chm


+ 3 - 3
php-fatfree/lib/audit.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -51,7 +51,7 @@ class Audit extends Prefab {
 	*	@param $addr string
 	**/
 	function ipv4($addr) {
-		return filter_var($addr,FILTER_VALIDATE_IP,FILTER_FLAG_IPV4);
+		return (bool)filter_var($addr,FILTER_VALIDATE_IP,FILTER_FLAG_IPV4);
 	}
 
 	/**
@@ -163,7 +163,7 @@ class Audit extends Prefab {
 
 	/**
 	*	Return entropy estimate of a password (NIST 800-63)
-	*	@return int
+	*	@return int|float
 	*	@param $str string
 	**/
 	function entropy($str) {

+ 14 - 13
php-fatfree/lib/auth.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -22,7 +22,7 @@ class Auth {
 		E_SMTP='SMTP connection failure';
 	//@}
 
-	private
+	protected
 		//! Auth storage
 		$storage,
 		//! Mapper object
@@ -40,7 +40,7 @@ class Auth {
 	protected function _jig($id,$pw,$realm) {
 		return (bool)
 			call_user_func_array(
-				array($this->mapper,'findone'),
+				array($this->mapper,'load'),
 				array(
 					array_merge(
 						array(
@@ -65,7 +65,7 @@ class Auth {
 	**/
 	protected function _mongo($id,$pw,$realm) {
 		return (bool)
-			$this->mapper->findone(
+			$this->mapper->load(
 				array(
 					$this->args['id']=>$id,
 					$this->args['pw']=>$pw
@@ -85,7 +85,7 @@ class Auth {
 	protected function _sql($id,$pw,$realm) {
 		return (bool)
 			call_user_func_array(
-				array($this->mapper,'findone'),
+				array($this->mapper,'load'),
 				array(
 					array_merge(
 						array(
@@ -186,15 +186,18 @@ class Auth {
 	*	HTTP basic auth mechanism
 	*	@return bool
 	*	@param $func callback
-	*	@param $halt bool
 	**/
-	function basic($func=NULL,$halt=TRUE) {
+	function basic($func=NULL) {
 		$fw=Base::instance();
 		$realm=$fw->get('REALM');
+		$hdr=NULL;
 		if (isset($_SERVER['HTTP_AUTHORIZATION']))
+			$hdr=$_SERVER['HTTP_AUTHORIZATION'];
+		elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']))
+			$hdr=$_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
+		if (!empty($hdr))
 			list($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW'])=
-				explode(':',base64_decode(
-					substr($_SERVER['HTTP_AUTHORIZATION'],6)));
+				explode(':',base64_decode(substr($hdr,6)));
 		if (isset($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']) &&
 			$this->login(
 				$_SERVER['PHP_AUTH_USER'],
@@ -206,8 +209,7 @@ class Auth {
 			return TRUE;
 		if (PHP_SAPI!='cli')
 			header('WWW-Authenticate: Basic realm="'.$realm.'"');
-		if ($halt)
-			$fw->error(401);
+		$fw->status(401);
 		return FALSE;
 	}
 
@@ -219,8 +221,7 @@ class Auth {
 	**/
 	function __construct($storage,array $args=NULL) {
 		if (is_object($storage) && is_a($storage,'DB\Cursor')) {
-			$ref=new ReflectionClass(get_class($storage));
-			$this->storage=basename(dirname($ref->getfilename()));
+			$this->storage=$storage->dbtype();
 			$this->mapper=$storage;
 			unset($ref);
 		}

File diff suppressed because it is too large
+ 380 - 206
php-fatfree/lib/base.php


+ 26 - 14
php-fatfree/lib/basket.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -72,23 +72,38 @@ class Basket {
 	}
 
 	/**
-	*	Return item that matches key/value pair
-	*	@return object|FALSE
+	*	Return items that match key/value pair;
+	*	If no key/value pair specified, return all items
+	*	@return array|FALSE
 	*	@param $key string
 	*	@param $val mixed
 	**/
-	function find($key,$val) {
-		if (isset($_SESSION[$this->key]))
+	function find($key=NULL,$val=NULL) {
+		if (isset($_SESSION[$this->key])) {
+			$out=array();
 			foreach ($_SESSION[$this->key] as $id=>$item)
-				if (array_key_exists($key,$item) && $item[$key]==$val) {
+				if (!isset($key) ||
+					array_key_exists($key,$item) && $item[$key]==$val) {
 					$obj=clone($this);
 					$obj->id=$id;
 					$obj->item=$item;
-					return $obj;
+					$out[]=$obj;
 				}
+			return $out;
+		}
 		return FALSE;
 	}
 
+	/**
+	*	Return first item that matches key/value pair
+	*	@return object|FALSE
+	*	@param $key string
+	*	@param $val mixed
+	**/
+	function findone($key,$val) {
+		return ($data=$this->find($key,$val))?$data[0]:FALSE;
+	}
+
 	/**
 	*	Map current item to matching key/value pair
 	*	@return array
@@ -97,8 +112,8 @@ class Basket {
 	**/
 	function load($key,$val) {
 		if ($found=$this->find($key,$val)) {
-			$this->id=$found->id;
-			return $this->item=$found->item;
+			$this->id=$found[0]->id;
+			return $this->item=$found[0]->item;
 		}
 		$this->reset();
 		return array();
@@ -126,9 +141,8 @@ class Basket {
 	**/
 	function save() {
 		if (!$this->id)
-			$this->id=uniqid();
+			$this->id=uniqid(NULL,TRUE);
 		$_SESSION[$this->key][$this->id]=$this->item;
-		session_commit();
 		return $this->item;
 	}
 
@@ -140,9 +154,8 @@ class Basket {
 	**/
 	function erase($key,$val) {
 		$found=$this->find($key,$val);
-		if ($found && $id=$found->id) {
+		if ($found && $id=$found[0]->id) {
 			unset($_SESSION[$this->key][$id]);
-			session_commit();
 			if ($id==$this->id)
 				$this->reset();
 			return TRUE;
@@ -165,7 +178,6 @@ class Basket {
 	**/
 	function drop() {
 		unset($_SESSION[$this->key]);
-		session_commit();
 	}
 
 	/**

+ 21 - 6
php-fatfree/lib/bcrypt.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -18,10 +18,14 @@ class Bcrypt extends Prefab {
 
 	//@{ Error messages
 	const
-		E_Cost='Invalid cost parameter',
-		E_Salt='Invalid salt (must be at least 22 alphanumeric characters)';
+		E_CostArg='Invalid cost parameter',
+		E_SaltArg='Salt must be at least 22 alphanumeric characters';
 	//@}
 
+	//! Default cost
+	const
+		COST=10;
+
 	/**
 	*	Generate bcrypt hash of string
 	*	@return string|FALSE
@@ -29,13 +33,13 @@ class Bcrypt extends Prefab {
 	*	@param $salt string
 	*	@param $cost int
 	**/
-	function hash($pw,$salt=NULL,$cost=10) {
+	function hash($pw,$salt=NULL,$cost=self::COST) {
 		if ($cost<4 || $cost>31)
-			trigger_error(self::E_Cost);
+			user_error(self::E_CostArg);
 		$len=22;
 		if ($salt) {
 			if (!preg_match('/^[[:alnum:]\.\/]{'.$len.',}$/',$salt))
-				trigger_error(self::E_Salt);
+				user_error(self::E_SaltArg);
 		}
 		else {
 			$raw=16;
@@ -54,6 +58,17 @@ class Bcrypt extends Prefab {
 		return strlen($hash)>13?$hash:FALSE;
 	}
 
+	/**
+	*	Check if password is still strong enough
+	*	@return bool
+	*	@param $hash string
+	*	@param $cost int
+	**/
+	function needs_rehash($hash,$cost=self::COST) {
+		list($pwcost)=sscanf($hash,"$2y$%d$");
+		return $pwcost<$cost;
+	}
+
 	/**
 	*	Verify password against hash using timing attack resistant approach
 	*	@return bool

+ 210 - 0
php-fatfree/lib/changelog.txt

@@ -1,5 +1,215 @@
 CHANGELOG
 
+3.2.2 (19 March 2014)
+*	NEW: Locales set automatically (Feature request #522)
+*	NEW: Mapper dbtype()
+*	NEW: before- and after- triggers for all mappers
+*	NEW: Decode HTML5 entities if PHP>5.3 detected (Feature request #552)
+*	NEW: Send credentials only if AUTH is present in the SMTP extension
+	response (Feature request #545)
+*	NEW: BITMASK variable to allow ENT_COMPAT override
+*	NEW: Redis support for caching
+*	Enable SMTP feature detection
+*	Enable extended ICU custom date format (Feature request #555)
+*	Enable custom time ICU format
+*	Add option to turn off session table creation (Feature request #557)
+*	Enhanced template token rendering and custom filters (Feature request
+	#550)
+*	Avert multiple loads in DB-managed sessions (Feature request #558)
+*	Add EXEC to associative fetch
+*	Bug fix: Building template tokens breaks on inline OR condition (Issue
+	#573)
+*	Bug fix: SMTP->send does not use the $log parameter (Issue #571)
+*	Bug fix: Allow setting sqlsrv primary keys on insert (Issue #570)
+*	Bug fix: Generated query for obtaining table schema in sqlsrv incorrect
+	(Bug #565)
+*	Bug fix: SQL mapper flag set even when value has not changed (Bug #562)
+*	Bug fix: Add XFRAME config option (Feature request #546)
+*	Bug fix: Incorrect parsing of comments (Issue #541)
+*	Bug fix: Multiple Set-Cookie headers (Issue #533)
+*	Bug fix: Mapper is dry after save()
+*	Bug fix: Prevent infinite loop when error handler is triggered
+	(Issue #361)
+*	Bug fix: Mapper tweaks not passing primary keys as arguments
+*	Bug fix: Zero indexes in dot-notated arrays fail to compile
+*	Bug fix: Prevent GROUP clause double-escaping
+*	Bug fix: Regression of zlib compression bug
+*	Bug fix: Method copyto() does not include ad hoc fields
+*	Check existence of OpenID mode (Issue #529)
+*	Generate a 404 when a tokenized class doesn't exist
+*	Fix SQLite quotes (Issue #521)
+*	Bug fix: BASE is incorrect on Windows
+
+3.2.1 (7 January 2014)
+*	NEW: EMOJI variable, UTF->translate(), UTF->emojify(), and UTF->strrev()
+*	Allow empty strings in config()
+*	Add support for turning off php://input buffering via RAW
+	(FALSE by default)
+*	Add Cursor->load() and Cursor->find() TTL support
+*	Support Web->receive() large file downloads via PUT
+*	ONERROR safety check
+*	Fix session CSRF cookie detection
+*	Framework object now passed to route handler contructors
+*	Allow override of DIACRITICS
+*	Various code optimizations
+*	Support log disabling (Issue #483)
+*	Implicit mapper load() on authentication
+*	Declare abstract methods for Cursor derivatives
+*	Support single-quoted HTML/XML attributes (Feature request #503)
+*	Relax property visibility of mappers and derivatives
+*	Deprecated: {{~ ~}} instructions and {{* *}} comments; Use {~ ~} and
+	{* *} instead
+*	Minor fix: Audit->ipv4() return value
+*	Bug fix: Backslashes in BASE not converted on Windows
+*	Bug fix: UTF->substr() with negative offset and specified length
+*	Bug fix: Replace named URL tokens on render()
+*	Bug fix: BASE is not empty when run from document root
+*	Bug fix: stringify() recursion
+
+3.2.0 (18 December 2013)
+*	NEW: Automatic CSRF protection (with IP and User-Agent checks) for
+	sessions mapped to SQL-, Jig-, Mongo- and Cache-based backends
+*	NEW: Named routes
+*	NEW: PATH variable; returns the URL relative to BASE
+*	NEW: Image->captcha() color parameters
+*	NEW: Ability to access MongoCuror thru the cursor() method
+*	NEW: Mapper->fields() method returns array of field names
+*	NEW: Mapper onload(), oninsert(), onupdate(), and onerase() event
+	listeners/triggers
+*	NEW: Preview class (a lightweight template engine)
+*	NEW: rel() method derives path from URL relative to BASE; useful for
+	rerouting
+*	NEW: PREFIX variable for prepending a string to a dictionary term;
+	Enable support for prefixed dictionary arrays and .ini files (Feature
+	request #440)
+*	NEW: Google static map plugin
+*	NEW: devoid() method
+*	Introduce clean(); similar to scrub(), except that arg is passed by
+	value
+*	Use $ttl for cookie expiration (Issue #457)
+*	Fix needs_rehash() cost comparison
+*	Add pass-by-reference argument to exists() so if method returns TRUE,
+	a subsequent get() is unnecessary
+*	Improve MySQL support
+*	Move esc(), raw(), and dupe() to View class where they more
+	appropriately belong
+*	Allow user-defined fields in SQL mapper constructor (Feature request
+	#450)
+*	Re-implement the pre-3.0 template resolve() feature
+*	Remove redundant instances of session_commit()
+*	Add support for input filtering in Mapper->copyfrom()
+*	Prevent intrusive behavior of Mapper->copyfrom()
+*	Support multiple SQL primary keys
+*	Support custom tag attributes/inline tokens defined at runtime
+	(Feature request #438)
+*	Broader support for HTTP basic auth
+*	Prohibit Jig _id clear()
+*	Add support for detailed stringify() output
+*	Add base directory to UI path as fallback
+*	Support Test->expect() chaining
+*	Support __tostring() in stringify()
+*	Trigger error on invalid CAPTCHA length (Issue #458)
+*	Bug fix: exists() pass-by-reference argument returns incorrect value
+*	Bug fix: DB Exec does not return affected row if query contains a
+	sub-SELECT (Issue #437)
+*	Improve seed generator and add code for detecting of acceptable
+	limits in Image->captcha() (Feature request #460)
+*	Add decimal format ICU extension
+*	Bug fix: 404-reported URI contains HTTP query
+*	Bug fix: Data type detection in DB->schema()
+*	Bug fix: TZ initialization
+*	Bug fix: paginate() passes incorrect argument to count()
+*	Bug fix: Incorrect query when reloading after insert()
+*	Bug fix: SQL preg_match error in pdo_type matching (Issue #447)
+*	Bug fix: Missing merge() function (Issue #444)
+*	Bug fix: BASE misdefined in command line mode
+*	Bug fix: Stringifying hive may run infinite (Issue #436)
+*	Bug fix: Incomplete stringify() when DEBUG<3 (Issue #432)
+*	Bug fix: Redirection of basic auth (Issue #430)
+*	Bug fix: Filter only PHP code (including short tags) in templates
+*	Bug fix: Markdown paragraph parser does not convert PHP code blocks
+	properly
+*	Bug fix: identicon() colors on same keys are randomized
+*	Bug fix: quotekey() fails on aliased keys
+*	Bug fix: Missing _id in Jig->find() return value
+*	Bug fix: LANGUAGE/LOCALES handling
+*	Bug fix: Loose comparison in stringify()
+
+3.1.2 (5 November 2013)
+*	Abandon .chm help format; Package API documentation in plain HTML;
+	(Launch lib/api/index.html in your browser)
+*	Deprecate BAIL in favor of HALT (default: TRUE)
+*	Revert to 3.1.0 autoload behavior; Add support for lowercase folder
+	names
+*	Allow Spring-style HTTP method overrides
+*	Add support for SQL Server-based sessions
+*	Capture full X-Forwarded-For header
+*	Add protection against malicious scripts; Extra check if file was really
+	uploaded
+*	Pass-thru page limit in return value of Cursor->paginate()
+*	Optimize code: Implement single-pass escaping
+*	Short circuit Jig->find() if source file is empty
+*	Bug fix: PHP globals passed by reference in hive() result (Issue #424)
+*	Bug fix: ZIP mime type incorrect behavior
+*	Bug fix: Jig->erase() filter malfunction
+*	Bug fix: Mongo->select() group
+*	Bug fix: Unknown bcrypt constant
+
+3.1.1 (13 October 2013)
+*	NEW: Support OpenID attribute exchange
+*	NEW: BAIL variable enables/disables continuance of execution on non-fatal
+	errors
+*	Deprecate BAIL in favor of HALT (default: FALSE)
+*	Add support for Oracle
+*	Mark cached queries in log (Feature Request #405)
+*	Implement Bcrypt->needs_reshash()
+*	Add entropy to SQL cache hash; Add uuid() method to DB backends
+*	Find real document root; Simplify debug paths
+*	Permit OpenID required fields to be declared as comma-separated string or
+	array
+*	Pass modified filename as argument to user-defined function in
+	Web->receive()
+*	Quote keys in optional SQL clauses (Issue #408)
+*	Allow UNLOAD to override fatal error detection (Issue #404)
+*	Mutex operator precedence error (Issue #406)
+*	Bug fix: exists() malfunction (Issue #401)
+*	Bug fix: Jig mapper triggers error when loading from CACHE (Issue #403)
+*	Bug fix: Array index check
+*	Bug fix: OpenID verified() return value
+*	Bug fix: Basket->find() should return a set of results (Issue #407);
+	Also implemented findone() for consistency with mappers
+*	Bug fix: PostgreSQL last insert ID (Issue #410)
+*	Bug fix: $port component URL overwritten by _socket()
+*	Bug fix: Calculation of elapsed time
+
+3.1.0 (20 August 2013)
+*	NEW: Web->filler() returns a chunk of text from the standard
+	Lorem Ipsum passage
+*	Change in behavior: Drop support for JSON serialization
+*	SQL->exec() now returns value of RETURNING clause
+*	Add support for $ttl argument in count() (Issue #393)
+*	Allow UI to be overridden by custom $path
+*	Return result of PDO primitives: begintransaction(), rollback(), and
+	commit()
+*	Full support for PHP 5.5
+*	Flush buffers only when DEBUG=0
+*	Support class->method, class::method, and lambda functions as
+	Web->basic() arguments
+*	Commit session on Basket->save()
+*	Optional enlargement in Image->resize()
+*	Support authentication on hosts running PHP-CGI
+*	Change visibility level of Cache properties
+*	Prevent ONERROR recursion
+*	Work around Apache pre-2.4 VirtualDocumentRoot bug
+*	Prioritize cURL in HTTP engine detection
+*	Bug fix: Minify tricky JS
+*	Bug fix: desktop() detection
+*	Bug fix: Double-slash on TEMP-relative path
+*	Bug fix: Cursor mapping of first() and last() records
+*	Bug fix: Premature end of Web->receive() on multiple files
+*	Bug fix: German umlaute to its corresponding grammatically-correct
+	equivalent
+
 3.0.9 (12 June 2013)
 *	NEW: Web->whois()
 *	NEW: Template <switch> <case> tags

+ 152 - 10
php-fatfree/lib/db/cursor.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -27,15 +27,39 @@ abstract class Cursor extends \Magic {
 		//! Query results
 		$query=array(),
 		//! Current position
-		$ptr=0;
+		$ptr=0,
+		//! Event listeners
+		$trigger=array();
+
+	/**
+	*	Return database type
+	*	@return string
+	**/
+	abstract function dbtype();
+
+	/**
+	*	Return fields of mapper object as an associative array
+	*	@return array
+	*	@param $obj object
+	**/
+	abstract function cast($obj=NULL);
 
 	/**
 	*	Return records (array of mapper objects) that match criteria
 	*	@return array
 	*	@param $filter string|array
 	*	@param $options array
+	*	@param $ttl int
 	**/
-	abstract function find($filter=NULL,array $options=NULL);
+	abstract function find($filter=NULL,array $options=NULL,$ttl=0);
+
+	/**
+	*	Count records that match criteria
+	*	@return int
+	*	@param $filter array
+	*	@param $ttl int
+	**/
+	abstract function count($filter=NULL,$ttl=0);
 
 	/**
 	*	Insert new record
@@ -49,6 +73,21 @@ abstract class Cursor extends \Magic {
 	**/
 	abstract function update();
 
+	/**
+	*	Hydrate mapper object using hive array variable
+	*	@return NULL
+	*	@param $key string
+	*	@param $func callback
+	**/
+	abstract function copyfrom($key,$func=NULL);
+
+	/**
+	*	Populate hive array variable with mapper fields
+	*	@return NULL
+	*	@param $key string
+	**/
+	abstract function copyto($key);
+
 	/**
 	*	Return TRUE if current cursor position is not mapped to any record
 	*	@return bool
@@ -70,16 +109,18 @@ abstract class Cursor extends \Magic {
 
 	/**
 	*	Return array containing subset of records matching criteria,
-	*	total number of records in superset, number of subsets available,
-	*	and actual subset position
+	*	total number of records in superset, specified limit, number of
+	*	subsets available, and actual subset position
 	*	@return array
 	*	@param $pos int
 	*	@param $size int
 	*	@param $filter string|array
 	*	@param $options array
+	*	@param $ttl int
 	**/
-	function paginate($pos=0,$size=10,$filter=NULL,array $options=NULL) {
-		$total=$this->count($filter,$options);
+	function paginate(
+		$pos=0,$size=10,$filter=NULL,array $options=NULL,$ttl=0) {
+		$total=$this->count($filter,$ttl);
 		$count=ceil($total/$size);
 		$pos=max(0,min($pos,$count-1));
 		return array(
@@ -87,9 +128,11 @@ abstract class Cursor extends \Magic {
 				array_merge(
 					$options?:array(),
 					array('limit'=>$size,'offset'=>$pos*$size)
-				)
+				),
+				$ttl
 			),
 			'total'=>$total,
+			'limit'=>$size,
 			'count'=>$count,
 			'pos'=>$pos<$count?$pos:0
 		);
@@ -100,12 +143,21 @@ abstract class Cursor extends \Magic {
 	*	@return array|FALSE
 	*	@param $filter string|array
 	*	@param $options array
+	*	@param $ttl int
 	**/
-	function load($filter=NULL,array $options=NULL) {
-		return ($this->query=$this->find($filter,$options)) &&
+	function load($filter=NULL,array $options=NULL,$ttl=0) {
+		return ($this->query=$this->find($filter,$options,$ttl)) &&
 			$this->skip(0)?$this->query[$this->ptr=0]:FALSE;
 	}
 
+	/**
+	*	Return the count of records loaded
+	*	@return int
+	**/
+	function loaded() {
+		return count($this->query);
+	}
+
 	/**
 	*	Map to first record in cursor
 	*	@return mixed
@@ -167,6 +219,96 @@ abstract class Cursor extends \Magic {
 		$this->ptr=0;
 	}
 
+	/**
+	*	Define onload trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function onload($func) {
+		return $this->trigger['load']=$func;
+	}
+
+	/**
+	*	Define beforeinsert trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function beforeinsert($func) {
+		return $this->trigger['beforeinsert']=$func;
+	}
+
+	/**
+	*	Define afterinsert trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function afterinsert($func) {
+		return $this->trigger['afterinsert']=$func;
+	}
+
+	/**
+	*	Define oninsert trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function oninsert($func) {
+		return $this->afterinsert($func);
+	}
+
+	/**
+	*	Define beforeupdate trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function beforeupdate($func) {
+		return $this->trigger['beforeupdate']=$func;
+	}
+
+	/**
+	*	Define afterupdate trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function afterupdate($func) {
+		return $this->trigger['afterupdate']=$func;
+	}
+
+	/**
+	*	Define onupdate trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function onupdate($func) {
+		return $this->afterupdate($func);
+	}
+
+	/**
+	*	Define beforeerase trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function beforeerase($func) {
+		return $this->trigger['beforeerase']=$func;
+	}
+
+	/**
+	*	Define aftererase trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function aftererase($func) {
+		return $this->trigger['aftererase']=$func;
+	}
+
+	/**
+	*	Define onerase trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function onerase($func) {
+		return $this->aftererase($func);
+	}
+
 	/**
 	*	Reset cursor
 	*	@return NULL

+ 21 - 4
php-fatfree/lib/db/jig.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -25,6 +25,8 @@ class Jig {
 	//@}
 
 	protected
+		//! UUID
+		$uuid,
 		//! Storage location
 		$dir,
 		//! Current storage format
@@ -69,8 +71,23 @@ class Jig {
 				$out=$fw->serialize($data);
 				break;
 		}
-		$out=$fw->write($this->dir.$file,$out);
-		return $out;
+		return $fw->write($this->dir.$file,$out);
+	}
+
+	/**
+	*	Return directory
+	*	@return string
+	**/
+	function dir() {
+		return $this->dir;
+	}
+
+	/**
+	*	Return UUID
+	*	@return string
+	**/
+	function uuid() {
+		return $this->uuid;
 	}
 
 	/**
@@ -109,7 +126,7 @@ class Jig {
 	function __construct($dir,$format=self::FORMAT_JSON) {
 		if (!is_dir($dir))
 			mkdir($dir,\Base::MODE,TRUE);
-		$this->dir=$dir;
+		$this->uuid=\Base::instance()->hash($this->dir=$dir);
 		$this->format=$format;
 	}
 

+ 65 - 14
php-fatfree/lib/db/jig/mapper.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -28,6 +28,14 @@ class Mapper extends \DB\Cursor {
 		//! Document contents
 		$document=array();
 
+	/**
+	*	Return database type
+	*	@return string
+	**/
+	function dbtype() {
+		return 'Jig';
+	}
+
 	/**
 	*	Return TRUE if field is defined
 	*	@return bool
@@ -67,7 +75,8 @@ class Mapper extends \DB\Cursor {
 	*	@param $key string
 	**/
 	function clear($key) {
-		unset($this->document[$key]);
+		if ($key!='_id')
+			unset($this->document[$key]);
 	}
 
 	/**
@@ -83,6 +92,8 @@ class Mapper extends \DB\Cursor {
 		foreach ($row as $field=>$val)
 			$mapper->document[$field]=$val;
 		$mapper->query=array(clone($mapper));
+		if (isset($mapper->trigger['load']))
+			\Base::instance()->call($mapper->trigger['load'],$mapper);
 		return $mapper;
 	}
 
@@ -150,10 +161,14 @@ class Mapper extends \DB\Cursor {
 		$cache=\Cache::instance();
 		$db=$this->db;
 		$now=microtime(TRUE);
+		$data=array();
 		if (!$fw->get('CACHE') || !$ttl || !($cached=$cache->exists(
-			$hash=$fw->hash($fw->stringify(array($filter,$options))).'.jig',
-				$data)) || $cached[0]+$ttl<microtime(TRUE)) {
+			$hash=$fw->hash($this->db->dir().
+				$fw->stringify(array($filter,$options))).'.jig',$data)) ||
+			$cached[0]+$ttl<microtime(TRUE)) {
 			$data=$db->read($this->file);
+			if (is_null($data))
+				return FALSE;
 			foreach ($data as $id=>&$doc) {
 				$doc['_id']=$id;
 				unset($doc);
@@ -248,7 +263,7 @@ class Mapper extends \DB\Cursor {
 			$out[]=$this->factory($id,$doc);
 			unset($doc);
 		}
-		if ($log) {
+		if ($log && isset($args)) {
 			if ($filter)
 				foreach ($args as $key=>$val) {
 					$vals[]=$fw->stringify(is_array($val)?$val[0]:$val);
@@ -265,10 +280,11 @@ class Mapper extends \DB\Cursor {
 	*	Count records that match criteria
 	*	@return int
 	*	@param $filter array
+	*	@param $ttl int
 	**/
-	function count($filter=NULL) {
+	function count($filter=NULL,$ttl=0) {
 		$now=microtime(TRUE);
-		$out=count($this->find($filter,NULL,FALSE));
+		$out=count($this->find($filter,NULL,$ttl,FALSE));
 		$this->db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
 			$this->file.' [count] '.($filter?json_encode($filter):''));
 		return $out;
@@ -283,6 +299,8 @@ class Mapper extends \DB\Cursor {
 	function skip($ofs=1) {
 		$this->document=($out=parent::skip($ofs))?$out->document:array();
 		$this->id=$out?$out->id:NULL;
+		if ($this->document && isset($this->trigger['load']))
+			\Base::instance()->call($this->trigger['load'],$this);
 		return $out;
 	}
 
@@ -295,16 +313,23 @@ class Mapper extends \DB\Cursor {
 			return $this->update();
 		$db=$this->db;
 		$now=microtime(TRUE);
-		while (($id=uniqid()) &&
+		while (($id=uniqid(NULL,TRUE)) &&
 			($data=$db->read($this->file)) && isset($data[$id]) &&
 			!connection_aborted())
 			usleep(mt_rand(0,100));
 		$this->id=$id;
 		$data[$id]=$this->document;
+		$pkey=array('_id'=>$this->id);
+		if (isset($this->trigger['beforeinsert']))
+			\Base::instance()->call($this->trigger['beforeinsert'],
+				array($this,$pkey));
 		$db->write($this->file,$data);
-		parent::reset();
 		$db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
 			$this->file.' [insert] '.json_encode($this->document));
+		if (isset($this->trigger['afterinsert']))
+			\Base::instance()->call($this->trigger['afterinsert'],
+				array($this,$pkey));
+		$this->load(array('@_id=?',$this->id));
 		return $this->document;
 	}
 
@@ -317,9 +342,15 @@ class Mapper extends \DB\Cursor {
 		$now=microtime(TRUE);
 		$data=$db->read($this->file);
 		$data[$this->id]=$this->document;
+		if (isset($this->trigger['beforeupdate']))
+			\Base::instance()->call($this->trigger['beforeupdate'],
+				array($this,array('_id'=>$this->id)));
 		$db->write($this->file,$data);
 		$db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
 			$this->file.' [update] '.json_encode($this->document));
+		if (isset($this->trigger['afterupdate']))
+			\Base::instance()->call($this->trigger['afterupdate'],
+				array($this,array('_id'=>$this->id)));
 		return $this->document;
 	}
 
@@ -332,10 +363,12 @@ class Mapper extends \DB\Cursor {
 		$db=$this->db;
 		$now=microtime(TRUE);
 		$data=$db->read($this->file);
+		$pkey=array('_id'=>$this->id);
 		if ($filter) {
-			$data=$this->find($filter,NULL,FALSE);
-			foreach (array_keys(array_reverse($data)) as $id)
-				unset($data[$id]);
+			foreach ($this->find($filter,NULL,FALSE) as $mapper)
+				if (!$mapper->erase())
+					return FALSE;
+			return TRUE;
 		}
 		elseif (isset($this->id)) {
 			unset($data[$this->id]);
@@ -344,6 +377,9 @@ class Mapper extends \DB\Cursor {
 		}
 		else
 			return FALSE;
+		if (isset($this->trigger['beforeerase']))
+			\Base::instance()->call($this->trigger['beforeerase'],
+				array($this,$pkey));
 		$db->write($this->file,$data);
 		if ($filter) {
 			$args=isset($filter[1]) && is_array($filter[1])?
@@ -359,6 +395,9 @@ class Mapper extends \DB\Cursor {
 		$db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
 			$this->file.' [erase] '.
 			($filter?preg_replace($keys,$vals,$filter[0],1):''));
+		if (isset($this->trigger['aftererase']))
+			\Base::instance()->call($this->trigger['aftererase'],
+				array($this,$pkey));
 		return TRUE;
 	}
 
@@ -376,9 +415,13 @@ class Mapper extends \DB\Cursor {
 	*	Hydrate mapper object using hive array variable
 	*	@return NULL
 	*	@param $key string
+	*	@param $func callback
 	**/
-	function copyfrom($key) {
-		foreach (\Base::instance()->get($key) as $key=>$val)
+	function copyfrom($key,$func=NULL) {
+		$var=\Base::instance()->get($key);
+		if ($func)
+			$var=call_user_func($func,$var);
+		foreach ($var as $key=>$val)
 			$this->document[$key]=$val;
 	}
 
@@ -393,6 +436,14 @@ class Mapper extends \DB\Cursor {
 			$var[$key]=$field;
 	}
 
+	/**
+	*	Return field names
+	*	@return array
+	**/
+	function fields() {
+		return array_keys($this->document);
+	}
+
 	/**
 	*	Instantiate class
 	*	@return void

+ 47 - 16
php-fatfree/lib/db/jig/session.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -18,6 +18,10 @@ namespace DB\Jig;
 //! Jig-managed session handler
 class Session extends Mapper {
 
+	protected
+		//! Session ID
+		$sid;
+
 	/**
 	*	Open session
 	*	@return TRUE
@@ -42,7 +46,8 @@ class Session extends Mapper {
 	*	@param $id string
 	**/
 	function read($id) {
-		$this->load(array('@session_id==?',$id));
+		if ($id!=$this->sid)
+			$this->load(array('@session_id=?',$this->sid=$id));
 		return $this->dry()?FALSE:$this->get('data');
 	}
 
@@ -54,10 +59,15 @@ class Session extends Mapper {
 	**/
 	function write($id,$data) {
 		$fw=\Base::instance();
+		$sent=headers_sent();
 		$headers=$fw->get('HEADERS');
-		$this->load(array('@session_id==?',$id));
+		if ($id!=$this->sid)
+			$this->load(array('@session_id=?',$this->sid=$id));
+		$csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+			$fw->hash(mt_rand());
 		$this->set('session_id',$id);
 		$this->set('data',$data);
+		$this->set('csrf',$sent?$this->csrf():$csrf);
 		$this->set('ip',$fw->get('IP'));
 		$this->set('agent',
 			isset($headers['User-Agent'])?$headers['User-Agent']:'');
@@ -72,7 +82,10 @@ class Session extends Mapper {
 	*	@param $id string
 	**/
 	function destroy($id) {
-		$this->erase(array('@session_id==?',$id));
+		$this->erase(array('@session_id=?',$id));
+		setcookie(session_name(),'',strtotime('-1 year'));
+		unset($_COOKIE[session_name()]);
+		header_remove('Set-Cookie');
 		return TRUE;
 	}
 
@@ -87,32 +100,34 @@ class Session extends Mapper {
 	}
 
 	/**
-	*	Return IP address associated with specified session ID
+	*	Return anti-CSRF token
 	*	@return string|FALSE
-	*	@param $id string
 	**/
-	function ip($id=NULL) {
-		$this->load(array('@session_id==?',$id?:session_id()));
+	function csrf() {
+		return $this->dry()?FALSE:$this->get('csrf');
+	}
+
+	/**
+	*	Return IP address
+	*	@return string|FALSE
+	**/
+	function ip() {
 		return $this->dry()?FALSE:$this->get('ip');
 	}
 
 	/**
-	*	Return Unix timestamp associated with specified session ID
+	*	Return Unix timestamp
 	*	@return string|FALSE
-	*	@param $id string
 	**/
-	function stamp($id=NULL) {
-		$this->load(array('@session_id==?',$id?:session_id()));
+	function stamp() {
 		return $this->dry()?FALSE:$this->get('stamp');
 	}
 
 	/**
-	*	Return HTTP user agent associated with specified session ID
+	*	Return HTTP user agent
 	*	@return string|FALSE
-	*	@param $id string
 	**/
-	function agent($id=NULL) {
-		$this->load(array('@session_id==?',$id?:session_id()));
+	function agent() {
 		return $this->dry()?FALSE:$this->get('agent');
 	}
 
@@ -132,6 +147,22 @@ class Session extends Mapper {
 			array($this,'cleanup')
 		);
 		register_shutdown_function('session_commit');
+		@session_start();
+		$fw=\Base::instance();
+		$headers=$fw->get('HEADERS');
+		if (($ip=$this->ip()) && $ip!=$fw->get('IP') ||
+			($agent=$this->agent()) &&
+			(!isset($headers['User-Agent']) ||
+				$agent!=$headers['User-Agent'])) {
+			session_destroy();
+			$fw->error(403);
+		}
+		$csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+			$fw->hash(mt_rand());
+		if ($this->load(array('@session_id=?',$this->sid=session_id()))) {
+			$this->set('csrf',$csrf);
+			$this->save();
+		}
 	}
 
 }

+ 38 - 5
php-fatfree/lib/db/mongo.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -16,17 +16,39 @@
 namespace DB;
 
 //! MongoDB wrapper
-class Mongo extends \MongoDB {
+class Mongo {
 
 	//@{
 	const
 		E_Profiler='MongoDB profiler is disabled';
 	//@}
 
-	private
+	protected
+		//! UUID
+		$uuid,
+		//! Data source name
+		$dsn,
+		//! MongoDB object
+		$db,
 		//! MongoDB log
 		$log;
 
+	/**
+	*	Return data source name
+	*	@return string
+	**/
+	function dsn() {
+		return $this->dsn;
+	}
+
+	/**
+	*	Return UUID
+	*	@return string
+	**/
+	function uuid() {
+		return $this->uuid;
+	}
+
 	/**
 	*	Return MongoDB profiler results
 	*	@return string
@@ -51,11 +73,21 @@ class Mongo extends \MongoDB {
 	*	@return int
 	**/
 	function drop() {
-		$out=parent::drop();
+		$out=$this->db->drop();
 		$this->setprofilinglevel(2);
 		return $out;
 	}
 
+	/**
+	*	Redirect call to MongoDB object
+	*	@return mixed
+	*	@param $func string
+	*	@param $args array
+	**/
+	function __call($func,array $args) {
+		return call_user_func_array(array($this->db,$func),$args);
+	}
+
 	/**
 	*	Instantiate class
 	*	@param $dsn string
@@ -63,8 +95,9 @@ class Mongo extends \MongoDB {
 	*	@param $options array
 	**/
 	function __construct($dsn,$dbname,array $options=NULL) {
+		$this->uuid=\Base::instance()->hash($this->dsn=$dsn);
 		$class=class_exists('\MongoClient')?'\MongoClient':'\Mongo';
-		parent::__construct(new $class($dsn,$options?:array()),$dbname);
+		$this->db=new \MongoDB(new $class($dsn,$options?:array()),$dbname);
 		$this->setprofilinglevel(2);
 	}
 

+ 97 - 36
php-fatfree/lib/db/mongo/mapper.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -24,7 +24,17 @@ class Mapper extends \DB\Cursor {
 		//! Mongo collection
 		$collection,
 		//! Mongo document
-		$document=array();
+		$document=array(),
+		//! Mongo cursor
+		$cursor;
+
+	/**
+	*	Return database type
+	*	@return string
+	**/
+	function dbtype() {
+		return 'Mongo';
+	}
 
 	/**
 	*	Return TRUE if field is defined
@@ -77,6 +87,8 @@ class Mapper extends \DB\Cursor {
 		foreach ($row as $key=>$val)
 			$mapper->document[$key]=$val;
 		$mapper->query=array(clone($mapper));
+		if (isset($mapper->trigger['load']))
+			\Base::instance()->call($mapper->trigger['load'],$mapper);
 		return $mapper;
 	}
 
@@ -110,27 +122,24 @@ class Mapper extends \DB\Cursor {
 		);
 		$fw=\Base::instance();
 		$cache=\Cache::instance();
-		if (!$fw->get('CACHE') || !$ttl || !($cached=$cache->exists(
-			$hash=$fw->hash($fw->stringify(array($fields,$filter,$options))).
-				'.mongo',$result)) || $cached[0]+$ttl<microtime(TRUE)) {
+		if (!($cached=$cache->exists($hash=$fw->hash($this->db->dsn().
+			$fw->stringify(array($fields,$filter,$options))).'.mongo',
+			$result)) || !$ttl || $cached[0]+$ttl<microtime(TRUE)) {
 			if ($options['group']) {
-				$tmp=$this->db->selectcollection(
-					$fw->get('HOST').'.'.$fw->get('BASE').'.'.uniqid().'.tmp'
+				$grp=$this->collection->group(
+					$options['group']['keys'],
+					$options['group']['initial'],
+					$options['group']['reduce'],
+					array(
+						'condition'=>$filter,
+						'finalize'=>$options['group']['finalize']
+					)
 				);
-				$tmp->batchinsert(
-					$this->collection->group(
-						$options['group']['keys'],
-						$options['group']['initial'],
-						$options['group']['reduce'],
-						array(
-							'condition'=>array(
-								$filter,
-								$options['group']['finalize']
-							)
-						)
-					),
-					array('safe'=>TRUE)
+				$tmp=$this->db->selectcollection(
+					$fw->get('HOST').'.'.$fw->get('BASE').'.'.
+					uniqid(NULL,TRUE).'.tmp'
 				);
+				$tmp->batchinsert($grp['retval'],array('safe'=>TRUE));
 				$filter=array();
 				$collection=$tmp;
 			}
@@ -138,18 +147,18 @@ class Mapper extends \DB\Cursor {
 				$filter=$filter?:array();
 				$collection=$this->collection;
 			}
-			$cursor=$collection->find($filter,$fields?:array());
+			$this->cursor=$collection->find($filter,$fields?:array());
 			if ($options['order'])
-				$cursor=$cursor->sort($options['order']);
+				$this->cursor=$this->cursor->sort($options['order']);
 			if ($options['limit'])
-				$cursor=$cursor->limit($options['limit']);
+				$this->cursor=$this->cursor->limit($options['limit']);
 			if ($options['offset'])
-				$cursor=$cursor->skip($options['offset']);
+				$this->cursor=$this->cursor->skip($options['offset']);
+			$result=array();
+			while ($this->cursor->hasnext())
+				$result[]=$this->cursor->getnext();
 			if ($options['group'])
 				$tmp->drop();
-			$result=array();
-			while ($cursor->hasnext())
-				$result[]=$cursor->getnext();
 			if ($fw->get('CACHE') && $ttl)
 				// Save to cache backend
 				$cache->set($hash,$result,$ttl);
@@ -183,9 +192,20 @@ class Mapper extends \DB\Cursor {
 	*	Count records that match criteria
 	*	@return int
 	*	@param $filter array
+	*	@param $ttl int
 	**/
-	function count($filter=NULL) {
-		return $this->collection->count($filter);
+	function count($filter=NULL,$ttl=0) {
+		$fw=\Base::instance();
+		$cache=\Cache::instance();
+		if (!($cached=$cache->exists($hash=$fw->hash($fw->stringify(
+			array($filter))).'.mongo',$result)) || !$ttl ||
+			$cached[0]+$ttl<microtime(TRUE)) {
+			$result=$this->collection->count($filter);
+			if ($fw->get('CACHE') && $ttl)
+				// Save to cache backend
+				$cache->set($hash,$result,$ttl);
+		}
+		return $result;
 	}
 
 	/**
@@ -196,6 +216,8 @@ class Mapper extends \DB\Cursor {
 	**/
 	function skip($ofs=1) {
 		$this->document=($out=parent::skip($ofs))?$out->document:array();
+		if ($this->document && isset($this->trigger['load']))
+			\Base::instance()->call($this->trigger['load'],$this);
 		return $out;
 	}
 
@@ -206,7 +228,15 @@ class Mapper extends \DB\Cursor {
 	function insert() {
 		if (isset($this->document['_id']))
 			return $this->update();
+		if (isset($this->trigger['beforeinsert']))
+			\Base::instance()->call($this->trigger['beforeinsert'],
+				array($this,array('_id'=>$this->document['_id'])));
 		$this->collection->insert($this->document);
+		$pkey=array('_id'=>$this->document['_id']);
+		if (isset($this->trigger['afterinsert']))
+			\Base::instance()->call($this->trigger['afterinsert'],
+				array($this,$pkey));
+		$this->load($pkey);
 		return $this->document;
 	}
 
@@ -215,11 +245,15 @@ class Mapper extends \DB\Cursor {
 	*	@return array
 	**/
 	function update() {
+		$pkey=array('_id'=>$this->document['_id']);
+		if (isset($this->trigger['beforeupdate']))
+			\Base::instance()->call($this->trigger['beforeupdate'],
+				array($this,$pkey));
 		$this->collection->update(
-			array('_id'=>$this->document['_id']),
-			$this->document,
-			array('upsert'=>TRUE)
-		);
+			$pkey,$this->document,array('upsert'=>TRUE));
+		if (isset($this->trigger['afterupdate']))
+			\Base::instance()->call($this->trigger['afterupdate'],
+				array($this,$pkey));
 		return $this->document;
 	}
 
@@ -231,10 +265,17 @@ class Mapper extends \DB\Cursor {
 	function erase($filter=NULL) {
 		if ($filter)
 			return $this->collection->remove($filter);
+		$pkey=array('_id'=>$this->document['_id']);
+		if (isset($this->trigger['beforeerase']))
+			\Base::instance()->call($this->trigger['beforeerase'],
+				array($this,$pkey));
 		$result=$this->collection->
 			remove(array('_id'=>$this->document['_id']));
 		parent::erase();
 		$this->skip(0);
+		if (isset($this->trigger['aftererase']))
+			\Base::instance()->call($this->trigger['aftererase'],
+				array($this,$pkey));
 		return $result;
 	}
 
@@ -251,9 +292,13 @@ class Mapper extends \DB\Cursor {
 	*	Hydrate mapper object using hive array variable
 	*	@return NULL
 	*	@param $key string
+	*	@param $func callback
 	**/
-	function copyfrom($key) {
-		foreach (\Base::instance()->get($key) as $key=>$val)
+	function copyfrom($key,$func=NULL) {
+		$var=\Base::instance()->get($key);
+		if ($func)
+			$var=call_user_func($func,$var);
+		foreach ($var as $key=>$val)
 			$this->document[$key]=$val;
 	}
 
@@ -268,6 +313,22 @@ class Mapper extends \DB\Cursor {
 			$var[$key]=$field;
 	}
 
+	/**
+	*	Return field names
+	*	@return array
+	**/
+	function fields() {
+		return array_keys($this->document);
+	}
+
+	/**
+	*	Return the cursor from last query
+	*	@return object|NULL
+	**/
+	function cursor() {
+		return $this->cursor;
+	}
+
 	/**
 	*	Instantiate class
 	*	@return void
@@ -276,7 +337,7 @@ class Mapper extends \DB\Cursor {
 	**/
 	function __construct(\DB\Mongo $db,$collection) {
 		$this->db=$db;
-		$this->collection=$db->{$collection};
+		$this->collection=$db->selectcollection($collection);
 		$this->reset();
 	}
 

+ 52 - 15
php-fatfree/lib/db/mongo/session.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -18,6 +18,10 @@ namespace DB\Mongo;
 //! MongoDB-managed session handler
 class Session extends Mapper {
 
+	protected
+		//! Session ID
+		$sid;
+
 	/**
 	*	Open session
 	*	@return TRUE
@@ -42,7 +46,8 @@ class Session extends Mapper {
 	*	@param $id string
 	**/
 	function read($id) {
-		$this->load(array('session_id'=>$id));
+		if ($id!=$this->sid)
+			$this->load(array('session_id'=>$this->sid=$id));
 		return $this->dry()?FALSE:$this->get('data');
 	}
 
@@ -54,15 +59,26 @@ class Session extends Mapper {
 	**/
 	function write($id,$data) {
 		$fw=\Base::instance();
+		$sent=headers_sent();
 		$headers=$fw->get('HEADERS');
-		$this->load(array('session_id'=>$id));
+		if ($id!=$this->sid)
+			$this->load(array('session_id'=>$this->sid=$id));
+		$csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+			$fw->hash(mt_rand());
 		$this->set('session_id',$id);
 		$this->set('data',$data);
+		$this->set('csrf',$sent?$this->csrf():$csrf);
 		$this->set('ip',$fw->get('IP'));
 		$this->set('agent',
 			isset($headers['User-Agent'])?$headers['User-Agent']:'');
 		$this->set('stamp',time());
 		$this->save();
+		if (!$sent) {
+			if (isset($_COOKIE['_']))
+				setcookie('_','',strtotime('-1 year'));
+			call_user_func_array('setcookie',
+				array('_',$csrf)+$fw->get('JAR'));
+		}
 		return TRUE;
 	}
 
@@ -73,6 +89,9 @@ class Session extends Mapper {
 	**/
 	function destroy($id) {
 		$this->erase(array('session_id'=>$id));
+		setcookie(session_name(),'',strtotime('-1 year'));
+		unset($_COOKIE[session_name()]);
+		header_remove('Set-Cookie');
 		return TRUE;
 	}
 
@@ -87,32 +106,34 @@ class Session extends Mapper {
 	}
 
 	/**
-	*	Return IP address associated with specified session ID
+	*	Return anti-CSRF token
 	*	@return string|FALSE
-	*	@param $id string
 	**/
-	function ip($id=NULL) {
-		$this->load(array('session_id'=>$id?:session_id()));
+	function csrf() {
+		return $this->dry()?FALSE:$this->get('csrf');
+	}
+
+	/**
+	*	Return IP address
+	*	@return string|FALSE
+	**/
+	function ip() {
 		return $this->dry()?FALSE:$this->get('ip');
 	}
 
 	/**
-	*	Return Unix timestamp associated with specified session ID
+	*	Return Unix timestamp
 	*	@return string|FALSE
-	*	@param $id string
 	**/
-	function stamp($id=NULL) {
-		$this->load(array('session_id'=>$id?:session_id()));
+	function stamp() {
 		return $this->dry()?FALSE:$this->get('stamp');
 	}
 
 	/**
-	*	Return HTTP user agent associated with specified session ID
+	*	Return HTTP user agent
 	*	@return string|FALSE
-	*	@param $id string
 	**/
-	function agent($id=NULL) {
-		$this->load(array('session_id'=>$id?:session_id()));
+	function agent() {
 		return $this->dry()?FALSE:$this->get('agent');
 	}
 
@@ -132,6 +153,22 @@ class Session extends Mapper {
 			array($this,'cleanup')
 		);
 		register_shutdown_function('session_commit');
+		@session_start();
+		$fw=\Base::instance();
+		$headers=$fw->get('HEADERS');
+		if (($ip=$this->ip()) && $ip!=$fw->get('IP') ||
+			($agent=$this->agent()) &&
+			(!isset($headers['User-Agent']) ||
+				$agent!=$headers['User-Agent'])) {
+			session_destroy();
+			$fw->error(403);
+		}
+		$csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+			$fw->hash(mt_rand());
+		if ($this->load(array('session_id'=>$this->sid=session_id()))) {
+			$this->set('csrf',$csrf);
+			$this->save();
+		}
 	}
 
 }

+ 121 - 56
php-fatfree/lib/db/sql.php

@@ -1,9 +1,7 @@
 <?php
 
-namespace DB;
-
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -15,10 +13,16 @@ namespace DB;
 	Please see the license.txt file for more information.
 */
 
+namespace DB;
+
 //! PDO wrapper
 class SQL extends \PDO {
 
-	private
+	protected
+		//! UUID
+		$uuid,
+		//! Data source name
+		$dsn,
 		//! Database engine
 		$engine,
 		//! Database name
@@ -32,29 +36,32 @@ class SQL extends \PDO {
 
 	/**
 	*	Begin SQL transaction
-	*	@return NULL
+	*	@return bool
 	**/
 	function begin() {
-		parent::begintransaction();
+		$out=parent::begintransaction();
 		$this->trans=TRUE;
+		return $out;
 	}
 
 	/**
 	*	Rollback SQL transaction
-	*	@return NULL
+	*	@return bool
 	**/
 	function rollback() {
-		parent::rollback();
+		$out=parent::rollback();
 		$this->trans=FALSE;
+		return $out;
 	}
 
 	/**
 	*	Commit SQL transaction
-	*	@return NULL
+	*	@return bool
 	**/
 	function commit() {
-		parent::commit();
+		$out=parent::commit();
 		$this->trans=FALSE;
+		return $out;
 	}
 
 	/**
@@ -75,6 +82,24 @@ class SQL extends \PDO {
 		}
 	}
 
+	/**
+	*	Cast value to PHP type
+	*	@return scalar
+	*	@param $type string
+	*	@param $val scalar
+	**/
+	function value($type,$val) {
+		switch ($type) {
+			case \PDO::PARAM_NULL:
+				return (unset)$val;
+			case \PDO::PARAM_INT:
+				return (int)$val;
+			case \PDO::PARAM_BOOL:
+				return (bool)$val;
+			case \PDO::PARAM_STR:
+				return (string)$val;
+		}
+	}
 
 	/**
 	*	Execute SQL statement(s)
@@ -105,12 +130,16 @@ class SQL extends \PDO {
 		}
 		$fw=\Base::instance();
 		$cache=\Cache::instance();
+		$result=FALSE;
 		foreach (array_combine($cmds,$args) as $cmd=>$arg) {
+			if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd))
+				continue;
 			$now=microtime(TRUE);
 			$keys=$vals=array();
 			if ($fw->get('CACHE') && $ttl && ($cached=$cache->exists(
-				$hash=$fw->hash($cmd.$fw->stringify($arg)).'.sql',
-				$result)) && $cached[0]+$ttl>microtime(TRUE)) {
+				$hash=$fw->hash($this->dsn.$cmd.
+				$fw->stringify($arg)).'.sql',$result)) &&
+				$cached[0]+$ttl>microtime(TRUE)) {
 				foreach ($arg as $key=>$val) {
 					$vals[]=$fw->stringify(is_array($val)?$val[0]:$val);
 					$keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/';
@@ -121,12 +150,13 @@ class SQL extends \PDO {
 					if (is_array($val)) {
 						// User-specified data type
 						$query->bindvalue($key,$val[0],$val[1]);
-						$vals[]=$fw->stringify($val[0]);
+						$vals[]=$fw->stringify($this->value($val[1],$val[0]));
 					}
 					else {
 						// Convert to PDO data type
-						$query->bindvalue($key,$val,$this->type($val));
-						$vals[]=$fw->stringify($val);
+						$query->bindvalue($key,$val,
+							$type=$this->type($val));
+						$vals[]=$fw->stringify($this->value($type,$val));
 					}
 					$keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/';
 				}
@@ -138,9 +168,18 @@ class SQL extends \PDO {
 						$this->rollback();
 					user_error('PDOStatement: '.$error[2]);
 				}
-				if (preg_match(
-					'/\b(?:CALL|EXPLAIN|SELECT|PRAGMA|SHOW)\b/i',$cmd)) {
+				if (preg_match('/^\s*'.
+					'(?:CALL|EXPLAIN|SELECT|PRAGMA|SHOW|RETURNING|EXEC)\b/is',
+					$cmd)) {
 					$result=$query->fetchall(\PDO::FETCH_ASSOC);
+					// Work around SQLite quote bug
+					if (preg_match('/sqlite2?/',$this->engine))
+						foreach ($result as $pos=>$rec) {
+							unset($result[$pos]);
+							$result[$pos]=array();
+							foreach ($rec as $key=>$val)
+								$result[$pos][trim($key,'\'"[]`')]=$val;
+						}
 					$this->rows=count($result);
 					if ($fw->get('CACHE') && $ttl)
 						// Save to cache backend
@@ -163,6 +202,7 @@ class SQL extends \PDO {
 			if ($log)
 				$this->log.=date('r').' ('.
 					sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
+					(empty($cached)?'':'[CACHED] ').
 					preg_replace($keys,$vals,$cmd,1).PHP_EOL;
 		}
 		if ($this->trans && $auto)
@@ -190,14 +230,15 @@ class SQL extends \PDO {
 	*	Retrieve schema of SQL table
 	*	@return array|FALSE
 	*	@param $table string
+	*	@param $fields array|string
 	*	@param $ttl int
 	**/
-	function schema($table,$ttl=0) {
+	function schema($table,$fields=NULL,$ttl=0) {
 		// Supported engines
 		$cmd=array(
 			'sqlite2?'=>array(
 				'PRAGMA table_info("'.$table.'");',
-				'name','type','dflt_value','notnull',0,'pk',1),
+				'name','type','dflt_value','notnull',0,'pk',TRUE),
 			'mysql'=>array(
 				'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`;',
 				'Field','Type','Default','Null','YES','Key','PRI'),
@@ -213,45 +254,63 @@ class SQL extends \PDO {
 					'information_schema.key_column_usage AS k '.
 					'ON '.
 						'c.table_name=k.table_name AND '.
-						'c.column_name=k.column_name '.
+						'c.column_name=k.column_name AND '.
+						'c.table_schema=k.table_schema '.
 						($this->dbname?
-							('AND '.
-							($this->engine=='pgsql'?
-								'c.table_catalog=k.table_catalog':
-								'c.table_schema=k.table_schema').' '):'').
+							('AND c.table_catalog=k.table_catalog '):'').
 				'LEFT OUTER JOIN '.
 					'information_schema.table_constraints AS t ON '.
 						'k.table_name=t.table_name AND '.
-						'k.constraint_name=t.constraint_name '.
+						'k.constraint_name=t.constraint_name AND '.
+						'k.table_schema=t.table_schema '.
 						($this->dbname?
-							('AND '.
-							($this->engine=='pgsql'?
-								'k.table_catalog=t.table_catalog':
-								'k.table_schema=t.table_schema').' '):'').
+							('AND k.table_catalog=t.table_catalog '):'').
 				'WHERE '.
-					'c.table_name='.$this->quote($table).' '.
+					'c.table_name='.$this->quote($table).
 					($this->dbname?
-						('AND '.
-							($this->engine=='pgsql'?
-							'c.table_catalog':'c.table_schema').
-							'='.$this->quote($this->dbname)):'').
+						(' AND c.table_catalog='.
+							$this->quote($this->dbname)):'').
 				';',
-				'field','type','defval','nullable','YES','pkey','PRIMARY KEY')
+				'field','type','defval','nullable','YES','pkey','PRIMARY KEY'),
+			'oci'=>array(
+				'SELECT c.column_name AS field, '.
+					'c.data_type AS type, '.
+					'c.data_default AS defval, '.
+					'c.nullable AS nullable, '.
+					'(SELECT t.constraint_type '.
+						'FROM all_cons_columns acc '.
+						'LEFT OUTER JOIN all_constraints t '.
+						'ON acc.constraint_name=t.constraint_name '.
+						'WHERE acc.table_name='.$this->quote($table).' '.
+						'AND acc.column_name=c.column_name '.
+						'AND constraint_type='.$this->quote('P').') AS pkey '.
+				'FROM all_tab_cols c '.
+				'WHERE c.table_name='.$this->quote($table),
+				'FIELD','TYPE','DEFVAL','NULLABLE','Y','PKEY','P')
 		);
+		if (is_string($fields))
+			$fields=\Base::instance()->split($fields);
 		foreach ($cmd as $key=>$val)
 			if (preg_match('/'.$key.'/',$this->engine)) {
+				// Improve InnoDB performance on MySQL with
+				// SET GLOBAL innodb_stats_on_metadata=0;
+				// This requires SUPER privilege!
 				$rows=array();
-				foreach ($this->exec($val[0],NULL,$ttl) as $row)
-					$rows[$row[$val[1]]]=array(
-						'type'=>$row[$val[2]],
-						'pdo_type'=>
-							preg_match('/int|bool/i',$row[$val[2]],$parts)?
-							constant('\PDO::PARAM_'.strtoupper($parts[0])):
-							\PDO::PARAM_STR,
-						'default'=>$row[$val[3]],
-						'nullable'=>$row[$val[4]]==$val[5],
-						'pkey'=>$row[$val[6]]==$val[7]
-					);
+				foreach ($this->exec($val[0],NULL,$ttl) as $row) {
+					if (!$fields || in_array($row[$val[1]],$fields))
+						$rows[$row[$val[1]]]=array(
+							'type'=>$row[$val[2]],
+							'pdo_type'=>
+								preg_match('/int\b|int(?=eger)|bool/i',
+									$row[$val[2]],$parts)?
+								constant('\PDO::PARAM_'.
+									strtoupper($parts[0])):
+								\PDO::PARAM_STR,
+							'default'=>$row[$val[3]],
+							'nullable'=>$row[$val[4]]==$val[5],
+							'pkey'=>$row[$val[6]]==$val[7]
+						);
+				}
 				return $rows;
 			}
 		return FALSE;
@@ -271,6 +330,14 @@ class SQL extends \PDO {
 			parent::quote($val,$type);
 	}
 
+	/**
+	*	Return UUID
+	*	@return string
+	**/
+	function uuid() {
+		return $this->uuid;
+	}
+
 	/**
 	*	Return database engine
 	*	@return string
@@ -302,15 +369,13 @@ class SQL extends \PDO {
 	**/
 	function quotekey($key) {
 		if ($this->engine=='mysql')
-			$key="`".$key."`";
+			$key="`".implode('`.`',explode('.',$key))."`";
 		elseif (preg_match('/sybase|dblib/',$this->engine))
-			$key="'".$key."'";
-		elseif (preg_match('/sqlite2?|pgsql/',$this->engine))
-			$key='"'.$key.'"';
+			$key="'".implode("'.'",explode('.',$key))."'";
+		elseif (preg_match('/sqlite2?|pgsql|oci/',$this->engine))
+			$key='"'.implode('"."',explode('.',$key)).'"';
 		elseif (preg_match('/mssql|sqlsrv|odbc/',$this->engine))
-			$key="[".$key."]";
-		elseif ($this->engine=='oci')
-			$key='"'.strtoupper($key).'"';
+			$key="[".implode('].[',explode('.',$key))."]";
 		return $key;
 	}
 
@@ -322,15 +387,15 @@ class SQL extends \PDO {
 	*	@param $options array
 	**/
 	function __construct($dsn,$user=NULL,$pw=NULL,array $options=NULL) {
+		$fw=\Base::instance();
+		$this->uuid=$fw->hash($this->dsn=$dsn);
 		if (preg_match('/^.+?(?:dbname|database)=(.+?)(?=;|$)/i',$dsn,$parts))
 			$this->dbname=$parts[1];
 		if (!$options)
 			$options=array();
-		$options+=array(\PDO::ATTR_EMULATE_PREPARES=>FALSE);
 		if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql')
 			$options+=array(\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '.
-				strtolower(str_replace('-','',
-					\Base::instance()->get('ENCODING'))).';');
+				strtolower(str_replace('-','',$fw->get('ENCODING'))).';');
 		parent::__construct($dsn,$user,$pw,$options);
 		$this->engine=parent::getattribute(parent::ATTR_DRIVER_NAME);
 	}

+ 144 - 74
php-fatfree/lib/db/sql/mapper.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -29,6 +29,8 @@ class Mapper extends \DB\Cursor {
 		//! Database engine
 		$engine,
 		//! SQL table
+		$source,
+		//! SQL table (quoted)
 		$table,
 		//! Last insert ID
 		$_id,
@@ -37,6 +39,14 @@ class Mapper extends \DB\Cursor {
 		//! Adhoc fields
 		$adhoc=array();
 
+	/**
+	*	Return database type
+	*	@return string
+	**/
+	function dbtype() {
+		return 'SQL';
+	}
+
 	/**
 	*	Return TRUE if field is defined
 	*	@return bool
@@ -55,9 +65,9 @@ class Mapper extends \DB\Cursor {
 	function set($key,$val) {
 		if (array_key_exists($key,$this->fields)) {
 			$val=is_null($val) && $this->fields[$key]['nullable']?
-				NULL:$this->value($this->fields[$key]['pdo_type'],$val);
+				NULL:$this->db->value($this->fields[$key]['pdo_type'],$val);
 			if ($this->fields[$key]['value']!==$val ||
-				$this->fields[$key]['default']!==$val)
+				$this->fields[$key]['default']!==$val && is_null($val))
 				$this->fields[$key]['changed']=TRUE;
 			return $this->fields[$key]['value']=$val;
 		}
@@ -109,25 +119,6 @@ class Mapper extends \DB\Cursor {
 		}
 	}
 
-	/**
-	*	Cast value to PHP type
-	*	@return scalar
-	*	@param $type string
-	*	@param $val scalar
-	**/
-	function value($type,$val) {
-		switch ($type) {
-			case \PDO::PARAM_NULL:
-				return (unset)$val;
-			case \PDO::PARAM_INT:
-				return (int)$val;
-			case \PDO::PARAM_BOOL:
-				return (bool)$val;
-			case \PDO::PARAM_STR:
-				return (string)$val;
-		}
-	}
-
 	/**
 	*	Convert array to mapper object
 	*	@return object
@@ -137,12 +128,19 @@ class Mapper extends \DB\Cursor {
 		$mapper=clone($this);
 		$mapper->reset();
 		foreach ($row as $key=>$val) {
-			$var=array_key_exists($key,$this->fields)?'fields':'adhoc';
+			if (array_key_exists($key,$this->fields))
+				$var='fields';
+			elseif (array_key_exists($key,$this->adhoc))
+				$var='adhoc';
+			else
+				continue;
 			$mapper->{$var}[$key]['value']=$val;
 			if ($var=='fields' && $mapper->{$var}[$key]['pkey'])
 				$mapper->{$var}[$key]['previous']=$val;
 		}
 		$mapper->query=array(clone($mapper));
+		if (isset($mapper->trigger['load']))
+			\Base::instance()->call($mapper->trigger['load'],$mapper);
 		return $mapper;
 	}
 
@@ -191,21 +189,37 @@ class Mapper extends \DB\Cursor {
 			}
 			$sql.=' WHERE '.$filter;
 		}
+		$db=$this->db;
 		if ($options['group'])
-			$sql.=' GROUP BY '.$options['group'];
-		if ($options['order'])
-			$sql.=' ORDER BY '.$options['order'];
+			$sql.=' GROUP BY '.implode(',',array_map(
+				function($str) use($db) {
+					return preg_match('/^(\w+)(?:\h+HAVING|\h*(?:,|$))/i',
+						$str,$parts)?
+						($db->quotekey($parts[1]).
+						(isset($parts[2])?(' '.$parts[2]):'')):$str;
+				},
+				explode(',',$options['group'])));
+		if ($options['order']) {
+			$sql.=' ORDER BY '.implode(',',array_map(
+				function($str) use($db) {
+					return preg_match('/^(\w+)(?:\h+(ASC|DESC))?\h*(?:,|$)/i',
+						$str,$parts)?
+						($db->quotekey($parts[1]).
+						(isset($parts[2])?(' '.$parts[2]):'')):$str;
+				},
+				explode(',',$options['order'])));
+		}
 		if ($options['limit'])
-			$sql.=' LIMIT '.$options['limit'];
+			$sql.=' LIMIT '.(int)$options['limit'];
 		if ($options['offset'])
-			$sql.=' OFFSET '.$options['offset'];
-		$result=$this->db->exec($sql.';',$args,$ttl);
+			$sql.=' OFFSET '.(int)$options['offset'];
+		$result=$this->db->exec($sql,$args,$ttl);
 		$out=array();
 		foreach ($result as &$row) {
 			foreach ($row as $field=>&$val) {
 				if (array_key_exists($field,$this->fields)) {
 					if (!is_null($val) || !$this->fields[$field]['nullable'])
-						$val=$this->value(
+						$val=$this->db->value(
 							$this->fields[$field]['pdo_type'],$val);
 				}
 				elseif (array_key_exists($field,$this->adhoc))
@@ -237,16 +251,20 @@ class Mapper extends \DB\Cursor {
 		$adhoc='';
 		foreach ($this->adhoc as $key=>$field)
 			$adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key);
-		return $this->select('*'.$adhoc,$filter,$options,$ttl);
+		return $this->select(($options['group']?:implode(',',
+			array_map(array($this->db,'quotekey'),array_keys($this->fields)))).
+			$adhoc,$filter,$options,$ttl);
 	}
 
 	/**
 	*	Count records that match criteria
 	*	@return int
 	*	@param $filter string|array
+	*	@param $ttl int
 	**/
-	function count($filter=NULL) {
-		$sql='SELECT COUNT(*) AS rows FROM '.$this->table;
+	function count($filter=NULL,$ttl=0) {
+		$sql='SELECT COUNT(*) AS '.
+			$this->db->quotekey('rows').' FROM '.$this->table;
 		$args=array();
 		if ($filter) {
 			if (is_array($filter)) {
@@ -258,7 +276,7 @@ class Mapper extends \DB\Cursor {
 			}
 			$sql.=' WHERE '.$filter;
 		}
-		$result=$this->db->exec($sql.';',$args);
+		$result=$this->db->exec($sql,$args,$ttl);
 		return $result[0]['rows'];
 	}
 
@@ -269,84 +287,106 @@ class Mapper extends \DB\Cursor {
 	*	@param $ofs int
 	**/
 	function skip($ofs=1) {
-		if ($out=parent::skip($ofs)) {
-			foreach ($this->fields as $key=>&$field) {
-				$field['value']=$out->fields[$key]['value'];
-				$field['changed']=FALSE;
-				if ($field['pkey'])
-					$field['previous']=$out->fields[$key]['value'];
-				unset($field);
-			}
-			foreach ($this->adhoc as $key=>&$field) {
-				$field['value']=$out->adhoc[$key]['value'];
-				unset($field);
-			}
+		$out=parent::skip($ofs);
+		$dry=$this->dry();
+		foreach ($this->fields as $key=>&$field) {
+			$field['value']=$dry?NULL:$out->fields[$key]['value'];
+			$field['changed']=FALSE;
+			if ($field['pkey'])
+				$field['previous']=$dry?NULL:$out->fields[$key]['value'];
+			unset($field);
+		}
+		foreach ($this->adhoc as $key=>&$field) {
+			$field['value']=$dry?NULL:$out->adhoc[$key]['value'];
+			unset($field);
 		}
+		if (isset($this->trigger['load']))
+			\Base::instance()->call($this->trigger['load'],$this);
 		return $out;
 	}
 
 	/**
 	*	Insert new record
-	*	@return array
+	*	@return object
 	**/
 	function insert() {
 		$args=array();
 		$ctr=0;
 		$fields='';
 		$values='';
+		$filter='';
 		$pkeys=array();
+		$nkeys=array();
+		$ckeys=array();
 		$inc=NULL;
+		foreach ($this->fields as $key=>$field)
+			if ($field['pkey'])
+				$pkeys[$key]=$field['previous'];
+		if (isset($this->trigger['beforeinsert']))
+			\Base::instance()->call($this->trigger['beforeinsert'],
+				array($this,$pkeys));
 		foreach ($this->fields as $key=>&$field) {
 			if ($field['pkey']) {
-				$pkeys[]=$key;
 				$field['previous']=$field['value'];
 				if (!$inc && $field['pdo_type']==\PDO::PARAM_INT &&
 					empty($field['value']) && !$field['nullable'])
 					$inc=$key;
+				$filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?';
+				$nkeys[$ctr+1]=array($field['value'],$field['pdo_type']);
 			}
 			if ($field['changed'] && $key!=$inc) {
 				$fields.=($ctr?',':'').$this->db->quotekey($key);
 				$values.=($ctr?',':'').'?';
 				$args[$ctr+1]=array($field['value'],$field['pdo_type']);
 				$ctr++;
+				$ckeys[]=$key;
 			}
 			$field['changed']=FALSE;
 			unset($field);
 		}
-		if ($fields)
+		if ($fields) {
 			$this->db->exec(
+				(preg_match('/mssql|dblib|sqlsrv/',$this->engine) &&
+				array_intersect(array_keys($pkeys),$ckeys)?
+					'SET IDENTITY_INSERT '.$this->table.' ON;':'').
 				'INSERT INTO '.$this->table.' ('.$fields.') '.
-				'VALUES ('.$values.');',$args
+				'VALUES ('.$values.')',$args
 			);
-		$seq=NULL;
-		if ($this->engine=='pgsql')
-			$seq=$this->table.'_'.end($pkeys).'_seq';
-		$this->_id=$this->db->lastinsertid($seq);
-		if (!$inc) {
-			$ctr=0;
-			$query='';
-			$args='';
-			foreach ($pkeys as $pkey) {
-				$query.=($query?' AND ':'').$this->db->quotekey($pkey).'=?';
-				$args[$ctr+1]=$this->fields[$pkey]['value'];
-				$ctr++;
+			$seq=NULL;
+			if ($this->engine=='pgsql') {
+				$names=array_keys($pkeys);
+				$seq=$this->source.'_'.end($names).'_seq';
 			}
-			return $this->load(array($query,$args));
+			if ($this->engine!='oci')
+				$this->_id=$this->db->lastinsertid($seq);
+			// Reload to obtain default and auto-increment field values
+			$this->load($inc?
+				array($inc.'=?',$this->db->value(
+					$this->fields[$inc]['pdo_type'],$this->_id)):
+				array($filter,$nkeys));
+			if (isset($this->trigger['afterinsert']))
+				\Base::instance()->call($this->trigger['afterinsert'],
+					array($this,$pkeys));
 		}
-		// Reload to obtain default and auto-increment field values
-		return $this->load(array($inc.'=?',
-			$this->value($this->fields[$inc]['pdo_type'],$this->_id)));
+		return $this;
 	}
 
 	/**
 	*	Update current record
-	*	@return array
+	*	@return object
 	**/
 	function update() {
 		$args=array();
 		$ctr=0;
 		$pairs='';
 		$filter='';
+		$pkeys=array();
+		foreach ($this->fields as $key=>$field)
+			if ($field['pkey'])
+				$pkeys[$key]=$field['previous'];
+		if (isset($this->trigger['beforeupdate']))
+			\Base::instance()->call($this->trigger['beforeupdate'],
+				array($this,$pkeys));
 		foreach ($this->fields as $key=>$field)
 			if ($field['changed']) {
 				$pairs.=($pairs?',':'').$this->db->quotekey($key).'=?';
@@ -363,8 +403,12 @@ class Mapper extends \DB\Cursor {
 			$sql='UPDATE '.$this->table.' SET '.$pairs;
 			if ($filter)
 				$sql.=' WHERE '.$filter;
-			return $this->db->exec($sql.';',$args);
+			$this->db->exec($sql,$args);
+			if (isset($this->trigger['afterupdate']))
+				\Base::instance()->call($this->trigger['afterupdate'],
+					array($this,$pkeys));
 		}
+		return $this;
 	}
 
 	/**
@@ -388,10 +432,12 @@ class Mapper extends \DB\Cursor {
 		$args=array();
 		$ctr=0;
 		$filter='';
+		$pkeys=array();
 		foreach ($this->fields as $key=>&$field) {
 			if ($field['pkey']) {
 				$filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?';
 				$args[$ctr+1]=array($field['previous'],$field['pdo_type']);
+				$pkeys[$key]=$field['previous'];
 				$ctr++;
 			}
 			$field['value']=NULL;
@@ -406,8 +452,15 @@ class Mapper extends \DB\Cursor {
 		}
 		parent::erase();
 		$this->skip(0);
-		return $this->db->
+		if (isset($this->trigger['beforeerase']))
+			\Base::instance()->call($this->trigger['beforeerase'],
+				array($this,$pkeys));
+		$out=$this->db->
 			exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args);
+		if (isset($this->trigger['aftererase']))
+			\Base::instance()->call($this->trigger['aftererase'],
+				array($this,$pkeys));
+		return $out;
 	}
 
 	/**
@@ -433,9 +486,13 @@ class Mapper extends \DB\Cursor {
 	*	Hydrate mapper object using hive array variable
 	*	@return NULL
 	*	@param $key string
+	*	@param $func callback
 	**/
-	function copyfrom($key) {
-		foreach (\Base::instance()->get($key) as $key=>$val)
+	function copyfrom($key,$func=NULL) {
+		$var=\Base::instance()->get($key);
+		if ($func)
+			$var=call_user_func($func,$var);
+		foreach ($var as $key=>$val)
 			if (in_array($key,array_keys($this->fields))) {
 				$field=&$this->fields[$key];
 				if ($field['value']!==$val) {
@@ -453,7 +510,7 @@ class Mapper extends \DB\Cursor {
 	**/
 	function copyto($key) {
 		$var=&\Base::instance()->ref($key);
-		foreach ($this->fields as $key=>$field)
+		foreach ($this->fields+$this->adhoc as $key=>$field)
 			$var[$key]=$field['value'];
 	}
 
@@ -465,17 +522,30 @@ class Mapper extends \DB\Cursor {
 		return $this->fields;
 	}
 
+	/**
+	*	Return field names
+	*	@return array
+	*	@param $adhoc bool
+	**/
+	function fields($adhoc=TRUE) {
+		return array_keys($this->fields+($adhoc?$this->adhoc:array()));
+	}
+
 	/**
 	*	Instantiate class
 	*	@param $db object
 	*	@param $table string
+	*	@param $fields array|string
 	*	@param $ttl int
 	**/
-	function __construct(\DB\SQL $db,$table,$ttl=60) {
+	function __construct(\DB\SQL $db,$table,$fields=NULL,$ttl=60) {
 		$this->db=$db;
 		$this->engine=$db->driver();
+		if ($this->engine=='oci')
+			$table=strtoupper($table);
+		$this->source=$table;
 		$this->table=$this->db->quotekey($table);
-		$this->fields=$db->schema($table,$ttl);
+		$this->fields=$db->schema($table,$fields,$ttl);
 		$this->reset();
 	}
 

+ 66 - 27
php-fatfree/lib/db/sql/session.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -18,6 +18,10 @@ namespace DB\SQL;
 //! SQL-managed session handler
 class Session extends Mapper {
 
+	protected
+		//! Session ID
+		$sid;
+
 	/**
 	*	Open session
 	*	@return TRUE
@@ -42,7 +46,8 @@ class Session extends Mapper {
 	*	@param $id string
 	**/
 	function read($id) {
-		$this->load(array('session_id=?',$id));
+		if ($id!=$this->sid)
+			$this->load(array('session_id=?',$this->sid=$id));
 		return $this->dry()?FALSE:$this->get('data');
 	}
 
@@ -54,10 +59,15 @@ class Session extends Mapper {
 	**/
 	function write($id,$data) {
 		$fw=\Base::instance();
+		$sent=headers_sent();
 		$headers=$fw->get('HEADERS');
-		$this->load(array('session_id=?',$id));
+		if ($id!=$this->sid)
+			$this->load(array('session_id=?',$this->sid=$id));
+		$csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+			$fw->hash(mt_rand());
 		$this->set('session_id',$id);
 		$this->set('data',$data);
+		$this->set('csrf',$sent?$this->csrf():$csrf);
 		$this->set('ip',$fw->get('IP'));
 		$this->set('agent',
 			isset($headers['User-Agent'])?$headers['User-Agent']:'');
@@ -73,6 +83,9 @@ class Session extends Mapper {
 	**/
 	function destroy($id) {
 		$this->erase(array('session_id=?',$id));
+		setcookie(session_name(),'',strtotime('-1 year'));
+		unset($_COOKIE[session_name()]);
+		header_remove('Set-Cookie');
 		return TRUE;
 	}
 
@@ -87,32 +100,34 @@ class Session extends Mapper {
 	}
 
 	/**
-	*	Return IP address associated with specified session ID
+	*	Return anti-CSRF token
+	*	@return string|FALSE
+	**/
+	function csrf() {
+		return $this->dry()?FALSE:$this->get('csrf');
+	}
+
+	/**
+	*	Return IP address
 	*	@return string|FALSE
-	*	@param $id string
 	**/
-	function ip($id=NULL) {
-		$this->load(array('session_id=?',$id?:session_id()));
+	function ip() {
 		return $this->dry()?FALSE:$this->get('ip');
 	}
 
 	/**
-	*	Return Unix timestamp associated with specified session ID
+	*	Return Unix timestamp
 	*	@return string|FALSE
-	*	@param $id string
 	**/
-	function stamp($id=NULL) {
-		$this->load(array('session_id=?',$id?:session_id()));
+	function stamp() {
 		return $this->dry()?FALSE:$this->get('stamp');
 	}
 
 	/**
-	*	Return HTTP user agent associated with specified session ID
+	*	Return HTTP user agent
 	*	@return string|FALSE
-	*	@param $id string
 	**/
-	function agent($id=NULL) {
-		$this->load(array('session_id=?',$id?:session_id()));
+	function agent() {
 		return $this->dry()?FALSE:$this->get('agent');
 	}
 
@@ -120,19 +135,27 @@ class Session extends Mapper {
 	*	Instantiate class
 	*	@param $db object
 	*	@param $table string
+	*	@param $force bool
 	**/
-	function __construct(\DB\SQL $db,$table='sessions') {
-		$db->exec(
-			'CREATE TABLE IF NOT EXISTS '.
-				(($name=$db->name())?($name.'.'):'').$table.' ('.
-				'session_id VARCHAR(40),'.
-				'data TEXT,'.
-				'ip VARCHAR(40),'.
-				'agent VARCHAR(255),'.
-				'stamp INTEGER,'.
-				'PRIMARY KEY(session_id)'.
-			');'
-		);
+	function __construct(\DB\SQL $db,$table='sessions',$force=TRUE) {
+		if ($force)
+			$db->exec(
+				(preg_match('/mssql|sqlsrv|sybase/',$db->driver())?
+					('IF NOT EXISTS (SELECT * FROM sysobjects WHERE '.
+						'name='.$db->quote($table).' AND xtype=\'U\') '.
+						'CREATE TABLE dbo.'):
+					('CREATE TABLE IF NOT EXISTS '.
+						(($name=$db->name())?($name.'.'):''))).
+				$table.' ('.
+					'session_id VARCHAR(40),'.
+					'data TEXT,'.
+					'csrf TEXT,'.
+					'ip VARCHAR(40),'.
+					'agent VARCHAR(255),'.
+					'stamp INTEGER,'.
+					'PRIMARY KEY(session_id)'.
+				');'
+			);
 		parent::__construct($db,$table);
 		session_set_save_handler(
 			array($this,'open'),
@@ -143,6 +166,22 @@ class Session extends Mapper {
 			array($this,'cleanup')
 		);
 		register_shutdown_function('session_commit');
+		@session_start();
+		$fw=\Base::instance();
+		$headers=$fw->get('HEADERS');
+		if (($ip=$this->ip()) && $ip!=$fw->get('IP') ||
+			($agent=$this->agent()) &&
+			(!isset($headers['User-Agent']) ||
+				$agent!=$headers['User-Agent'])) {
+			session_destroy();
+			$fw->error(403);
+		}
+		$csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+			$fw->hash(mt_rand());
+		if ($this->load(array('session_id=?',$this->sid=session_id()))) {
+			$this->set('csrf',$csrf);
+			$this->save();
+		}
 	}
 
 }

+ 1 - 1
php-fatfree/lib/f3.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 

+ 50 - 19
php-fatfree/lib/image.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -19,7 +19,8 @@ class Image {
 	//@{ Messages
 	const
 		E_Color='Invalid color specified: %s',
-		E_Font='CAPTCHA font not found';
+		E_Font='CAPTCHA font not found',
+		E_Length='Invalid CAPTCHA length: %s';
 	//@}
 
 	//@{ Positional cues
@@ -32,7 +33,7 @@ class Image {
 		POS_Bottom=32;
 	//@}
 
-	private
+	protected
 		//! Source filename
 		$file,
 		//! Image resource
@@ -269,11 +270,16 @@ class Image {
 	*	Apply an image overlay
 	*	@return object
 	*	@param $img object
-	*	@param $align int
+	*	@param $align int|array
+	*	@param $alpha int
 	**/
-	function overlay(Image $img,$align=NULL) {
+	function overlay(Image $img,$align=NULL,$alpha=100) {
 		if (is_null($align))
 			$align=self::POS_Right|self::POS_Bottom;
+		if (is_array($align)) {
+			list($posx,$posy)=$align;
+			$align = 0;
+		}
 		$ovr=imagecreatefromstring($img->dump());
 		imagesavealpha($ovr,TRUE);
 		$imgw=$this->width();
@@ -296,7 +302,14 @@ class Image {
 			$posx=0;
 		if (empty($posy))
 			$posy=0;
-		imagecopy($this->data,$ovr,$posx,$posy,0,0,$ovrw,$ovrh);
+		if ($alpha==100)
+			imagecopy($this->data,$ovr,$posx,$posy,0,0,$ovrw,$ovrh);
+		else {
+			$cut=imagecreatetruecolor($ovrw,$ovrh);
+			imagecopy($cut,$this->data,0,0,$posx,$posy,$ovrw,$ovrh);
+			imagecopy($cut,$ovr,0,0,0,0,$ovrw,$ovrh);
+			imagecopymerge($this->data,$cut,$posx,$posy,0,0,$ovrw,$ovrh,$alpha);
+		}
 		return $this->save();
 	}
 
@@ -326,11 +339,11 @@ class Image {
 			array(0,.5,.5,.5,.5,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1),
 			array(0,0,1,0,.5,.5,.5,0,0,.5,1,.5,.5,1,.5,.5,0,1)
 		);
+		$hash=sha1($str);
 		$this->data=imagecreatetruecolor($size,$size);
-		list($r,$g,$b)=$this->rgb(mt_rand(0x333,0xCCC));
+		list($r,$g,$b)=$this->rgb(hexdec(substr($hash,-3)));
 		$fg=imagecolorallocate($this->data,$r,$g,$b);
 		imagefill($this->data,0,0,IMG_COLOR_TRANSPARENT);
-		$hash=sha1($str);
 		$ctr=count($sprites);
 		$dim=$blocks*floor($size/$blocks)*2/$blocks;
 		for ($j=0,$y=ceil($blocks/2);$j<$y;$j++)
@@ -366,12 +379,21 @@ class Image {
 	*	@param $len int
 	*	@param $key string
 	*	@param $path string
+	*	@param $fg int
+	*	@param $bg int
 	**/
-	function captcha($font,$size=24,$len=5,$key=NULL,$path='') {
+	function captcha($font,$size=24,$len=5,
+		$key=NULL,$path='',$fg=0xFFFFFF,$bg=0x000000) {
+		if ((!$ssl=extension_loaded('openssl')) && ($len<4 || $len>13)) {
+			user_error(sprintf(self::E_Length,$len));
+			return FALSE;
+		}
 		$fw=Base::instance();
-		foreach ($fw->split($path?:$fw->get('UI')) as $dir)
+		foreach ($fw->split($path?:$fw->get('UI').';./') as $dir)
 			if (is_file($path=$dir.$font)) {
-				$seed=strtoupper(substr(uniqid(),-$len));
+				$seed=strtoupper(substr(
+					$ssl?bin2hex(openssl_random_pseudo_bytes($len)):uniqid(),
+					-$len));
 				$block=$size*3;
 				$tmp=array();
 				for ($i=0,$width=0,$height=0;$i<$len;$i++) {
@@ -380,10 +402,10 @@ class Image {
 					$w=$box[2]-$box[0];
 					$h=$box[1]-$box[5];
 					$char=imagecreatetruecolor($block,$block);
-					imagefill($char,0,0,0);
+					imagefill($char,0,0,$bg);
 					imagettftext($char,$size*2,0,
 						($block-$w)/2,$block-($block-$h)/2,
-						0xFFFFFF,$path,$seed[$i]);
+						$fg,$path,$seed[$i]);
 					$char=imagerotate($char,mt_rand(-30,30),
 						imagecolorallocatealpha($char,0,0,0,127));
 					// Reduce to normal size
@@ -510,6 +532,18 @@ class Image {
 		return $this;
 	}
 
+	/**
+	*	Load string
+	*	@return object
+	*	@param $str string
+	**/
+	function load($str) {
+		$this->data=imagecreatefromstring($str);
+		imagesavealpha($this->data,TRUE);
+		$this->save();
+		return $this;
+	}
+
 	/**
 	*	Instantiate image
 	*	@param $file string
@@ -522,12 +556,9 @@ class Image {
 			$fw=Base::instance();
 			// Create image from file
 			$this->file=$file;
-			foreach ($fw->split($path?:$fw->get('UI')) as $dir)
-				if (is_file($dir.$file)) {
-					$this->data=imagecreatefromstring($fw->read($dir.$file));
-					imagesavealpha($this->data,TRUE);
-					$this->save();
-				}
+			foreach ($fw->split($path?:$fw->get('UI').';./') as $dir)
+				if (is_file($dir.$file))
+					return $this->load($fw->read($dir.$file));
 		}
 	}
 

+ 1 - 1
php-fatfree/lib/log.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 

+ 3 - 3
php-fatfree/lib/magic.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -46,14 +46,14 @@ abstract class Magic implements ArrayAccess {
 	abstract function clear($key);
 
 	/**
-	*	Return TRUE if property has public visibility
+	*	Return TRUE if property has public/protected visibility
 	*	@return bool
 	*	@param $key string
 	**/
 	private function visible($key) {
 		if (property_exists($this,$key)) {
 			$ref=new ReflectionProperty(get_class($this),$key);
-			$out=$ref->ispublic();
+			$out=!$ref->isprivate();
 			unset($ref);
 			return $out;
 		}

+ 7 - 6
php-fatfree/lib/markdown.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -279,11 +279,12 @@ class Markdown extends Prefab {
 	protected function _p($str) {
 		$str=trim($str);
 		if (strlen($str)) {
-			if (preg_match('/(.+?\n)([>#].+)/',$str,$parts))
+			if (preg_match('/^(.+?\n)([>#].+)$/s',$str,$parts))
 				return $this->_p($parts[1]).$this->build($parts[2]);
 			$self=$this;
 			$str=preg_replace_callback(
-				'/([^<>\[]+)?(<.+?>|\[.+?\]\s*\(.+?\))([^<>\]]+)?|(.+)/s',
+				'/([^<>\[]+)?(<[\?%].+?[\?%]>|<.+?>|\[.+?\]\s*\(.+?\))|'.
+				'(.+)/s',
 				function($expr) use($self) {
 					$tmp='';
 					if (isset($expr[4]))
@@ -470,14 +471,14 @@ class Markdown extends Prefab {
 				'setext'=>'/^\h*(.+?)\h*\n([=-])+\h*(?:\n+|$)/',
 				'li'=>'/^(?:(?:[*+-]|\d+\.)\h.+?(?:\n+|$)'.
 					'(?:(?: {4}|\t)+.+?(?:\n+|$))*)+/s',
-				'raw'=>'/^((?:<!--.+?-->|<\?.+?\?>|<%.+?%>|'.
+				'raw'=>'/^((?:<!--.+?-->|'.
 					'<(address|article|aside|audio|blockquote|canvas|dd|'.
 					'div|dl|fieldset|figcaption|figure|footer|form|h\d|'.
 					'header|hgroup|hr|noscript|object|ol|output|p|pre|'.
 					'section|table|tfoot|ul|video).*?'.
 					'(?:\/>|>(?:(?>[^><]+)|(?R))*<\/\2>))'.
-					'\h*(?:\n{2,}|\n?$))/s',
-				'p'=>'/^(.+?(?:\n{2,}|\n?$))/s'
+					'\h*(?:\n{2,}|\n*$)|<[\?%].+?[\?%]>\h*(?:\n?$|\n*))/s',
+				'p'=>'/^(.+?(?:\n{2,}|\n*$))/s'
 			);
 		}
 		$self=$this;

+ 1 - 1
php-fatfree/lib/matrix.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 

+ 63 - 18
php-fatfree/lib/session.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -16,6 +16,10 @@
 //! Cache-based session handler
 class Session {
 
+	protected
+		//! Session ID
+		$sid;
+
 	/**
 	*	Open session
 	*	@return TRUE
@@ -40,6 +44,8 @@ class Session {
 	*	@param $id string
 	**/
 	function read($id) {
+		if ($id!=$this->sid)
+			$this->sid=$id;
 		return Cache::instance()->exists($id.'.@',$data)?$data['data']:FALSE;
 	}
 
@@ -51,17 +57,23 @@ class Session {
 	**/
 	function write($id,$data) {
 		$fw=Base::instance();
+		$sent=headers_sent();
 		$headers=$fw->get('HEADERS');
-		$jar=session_get_cookie_params();
+		$csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+			$fw->hash(mt_rand());
+		$jar=$fw->get('JAR');
+		if ($id!=$this->sid)
+			$this->sid=$id;
 		Cache::instance()->set($id.'.@',
 			array(
 				'data'=>$data,
+				'csrf'=>$sent?$this->csrf():$csrf,
 				'ip'=>$fw->get('IP'),
 				'agent'=>isset($headers['User-Agent'])?
 					$headers['User-Agent']:'',
 				'stamp'=>time()
 			),
-			$jar['lifetime']
+			$jar['expire']?($jar['expire']-time()):0
 		);
 		return TRUE;
 	}
@@ -73,6 +85,9 @@ class Session {
 	**/
 	function destroy($id) {
 		Cache::instance()->clear($id.'.@');
+		setcookie(session_name(),'',strtotime('-1 year'));
+		unset($_COOKIE[session_name()]);
+		header_remove('Set-Cookie');
 		return TRUE;
 	}
 
@@ -87,33 +102,43 @@ class Session {
 	}
 
 	/**
-	*	Return IP address associated with specified session ID
+	*	Return anti-CSRF token
 	*	@return string|FALSE
-	*	@param $id string
 	**/
-	function ip($id=NULL) {
-		return Cache::instance()->exists(($id?:session_id()).'.@',$data)?
-			$data['ip']:FALSE;
+	function csrf() {
+		return Cache::instance()->
+			exists(($this->sid?:session_id()).'.@',$data)?
+				$data['csrf']:FALSE;
 	}
 
 	/**
-	*	Return Unix timestamp associated with specified session ID
+	*	Return IP address
 	*	@return string|FALSE
-	*	@param $id string
 	**/
-	function stamp($id=NULL) {
-		return Cache::instance()->exists(($id?:session_id()).'.@',$data)?
-			$data['stamp']:FALSE;
+	function ip() {
+		return Cache::instance()->
+			exists(($this->sid?:session_id()).'.@',$data)?
+				$data['ip']:FALSE;
 	}
 
 	/**
-	*	Return HTTP user agent associated with specified session ID
+	*	Return Unix timestamp
+	*	@return string|FALSE
+	**/
+	function stamp() {
+		return Cache::instance()->
+			exists(($this->sid?:session_id()).'.@',$data)?
+				$data['stamp']:FALSE;
+	}
+
+	/**
+	*	Return HTTP user agent
 	*	@return string|FALSE
-	*	@param $id string
 	**/
-	function agent($id=NULL) {
-		return Cache::instance()->exists(($id?:session_id()).'.@',$data)?
-			$data['agent']:FALSE;
+	function agent() {
+		return Cache::instance()->
+			exists(($this->sid?:session_id()).'.@',$data)?
+				$data['agent']:FALSE;
 	}
 
 	/**
@@ -130,6 +155,26 @@ class Session {
 			array($this,'cleanup')
 		);
 		register_shutdown_function('session_commit');
+		@session_start();
+		$fw=\Base::instance();
+		$headers=$fw->get('HEADERS');
+		if (($ip=$this->ip()) && $ip!=$fw->get('IP') ||
+			($agent=$this->agent()) &&
+			(!isset($headers['User-Agent']) ||
+				$agent!=$headers['User-Agent'])) {
+			session_destroy();
+			\Base::instance()->error(403);
+		}
+		$csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+			$fw->hash(mt_rand());
+		$jar=$fw->get('JAR');
+		if (Cache::instance()->exists(($this->sid=session_id()).'.@',$data)) {
+			$data['csrf']=$csrf;
+			Cache::instance()->set($this->sid.'.@',
+				$data,
+				$jar['expire']?($jar['expire']-time()):0
+			);
+		}
 	}
 
 }

+ 34 - 27
php-fatfree/lib/smtp.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -23,7 +23,7 @@ class SMTP extends Magic {
 		E_Attach='Attachment %s not found';
 	//@}
 
-	private
+	protected
 		//! Message properties
 		$headers,
 		//! E-mail attachments
@@ -104,11 +104,11 @@ class SMTP extends Magic {
 
 	/**
 	*	Send SMTP command and record server response
-	*	@return NULL
+	*	@return string
 	*	@param $cmd string
 	*	@param $log bool
 	**/
-	protected function dialog($cmd=NULL,$log=FALSE) {
+	protected function dialog($cmd=NULL,$log=TRUE) {
 		$socket=&$this->socket;
 		if (!is_null($cmd))
 			fputs($socket,$cmd."\r\n");
@@ -123,6 +123,7 @@ class SMTP extends Magic {
 			$this->log.=$cmd."\n";
 			$this->log.=str_replace("\r",'',$reply);
 		}
+		return $reply;
 	}
 
 	/**
@@ -140,11 +141,17 @@ class SMTP extends Magic {
 	*	Transmit message
 	*	@return bool
 	*	@param $message string
+	*	@param $log bool
 	**/
-	function send($message) {
+	function send($message,$log=TRUE) {
 		if ($this->scheme=='ssl' && !extension_loaded('openssl'))
 			return FALSE;
+		// Message should not be blank
+		if (!$message)
+			user_error(self::E_Blank);
 		$fw=Base::instance();
+		// Retrieve headers
+		$headers=$this->headers;
 		// Connect to the server
 		$socket=&$this->socket;
 		$socket=@fsockopen($this->host,$this->port);
@@ -152,31 +159,32 @@ class SMTP extends Magic {
 			return FALSE;
 		stream_set_blocking($socket,TRUE);
 		// Get server's initial response
-		$this->dialog();
+		$this->dialog(NULL,FALSE);
 		// Announce presence
-		$this->dialog('EHLO '.$fw->get('HOST'),TRUE);
+		$reply=$this->dialog('EHLO '.$fw->get('HOST'),$log);
 		if (strtolower($this->scheme)=='tls') {
-			$this->dialog('STARTTLS',TRUE);
+			$this->dialog('STARTTLS',$log);
 			stream_socket_enable_crypto(
 				$socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT);
-			$this->dialog('EHLO '.$fw->get('HOST'),TRUE);
+			$reply=$this->dialog('EHLO '.$fw->get('HOST'),$log);
+			if (preg_match('/8BITMIME/',$reply))
+				$headers['Content-Transfer-Encoding']='8bit';
+			else {
+				$headers['Content-Transfer-Encoding']='quoted-printable';
+				$message=quoted_printable_encode($message);
+			}
 		}
-		if ($this->user && $this->pw) {
+		if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) {
 			// Authenticate
-			$this->dialog('AUTH LOGIN',TRUE);
-			$this->dialog(base64_encode($this->user),TRUE);
-			$this->dialog(base64_encode($this->pw),TRUE);
+			$this->dialog('AUTH LOGIN',$log);
+			$this->dialog(base64_encode($this->user),$log);
+			$this->dialog(base64_encode($this->pw),$log);
 		}
 		// Required headers
 		$reqd=array('From','To','Subject');
-		// Retrieve headers
-		$headers=$this->headers;
 		foreach ($reqd as $id)
 			if (empty($headers[$id]))
 				user_error(sprintf(self::E_Header,$id));
-		// Message should not be blank
-		if (!$message)
-			user_error(self::E_Blank);
 		$eol="\r\n";
 		$str='';
 		// Stringify headers
@@ -184,15 +192,15 @@ class SMTP extends Magic {
 			if (!in_array($key,$reqd))
 				$str.=$key.': '.$val.$eol;
 		// Start message dialog
-		$this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),TRUE);
+		$this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),$log);
 		foreach ($fw->split($headers['To'].
 			(isset($headers['Cc'])?(';'.$headers['Cc']):'').
 			(isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst)
-			$this->dialog('RCPT TO: '.strstr($dst,'<'),TRUE);
-		$this->dialog('DATA',TRUE);
+			$this->dialog('RCPT TO: '.strstr($dst,'<'),$log);
+		$this->dialog('DATA',$log);
 		if ($this->attachments) {
 			// Replace Content-Type
-			$hash=uniqid();
+			$hash=uniqid(NULL,TRUE);
 			$type=$headers['Content-Type'];
 			$headers['Content-Type']='multipart/mixed; '.
 				'boundary="'.$hash.'"';
@@ -221,7 +229,7 @@ class SMTP extends Magic {
 			$out.=$eol;
 			$out.='--'.$hash.'--'.$eol;
 			$out.='.';
-			$this->dialog($out,TRUE);
+			$this->dialog($out,FALSE);
 		}
 		else {
 			// Send mail headers
@@ -233,9 +241,9 @@ class SMTP extends Magic {
 			$out.=$message.$eol;
 			$out.='.';
 			// Send message
-			$this->dialog($out,TRUE);
+			$this->dialog($out);
 		}
-		$this->dialog('QUIT',TRUE);
+		$this->dialog('QUIT',$log);
 		if ($socket)
 			fclose($socket);
 		return TRUE;
@@ -253,8 +261,7 @@ class SMTP extends Magic {
 		$this->headers=array(
 			'MIME-Version'=>'1.0',
 			'Content-Type'=>'text/plain; '.
-				'charset='.Base::instance()->get('ENCODING'),
-			'Content-Transfer-Encoding'=>'8bit'
+				'charset='.Base::instance()->get('ENCODING')
 		);
 		$this->host=$host;
 		if (strtolower($this->scheme=strtolower($scheme))=='ssl')

+ 88 - 147
php-fatfree/lib/template.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -13,8 +13,8 @@
 	Please see the license.txt file for more information.
 */
 
-//! Template engine
-class Template extends View {
+//! XML-style template engine
+class Template extends Preview {
 
 	//@{ Error messages
 	const
@@ -22,46 +22,11 @@ class Template extends View {
 	//@}
 
 	protected
-		//! MIME type
-		$mime,
 		//! Template tags
 		$tags,
 		//! Custom tag handlers
 		$custom=array();
 
-	/**
-	*	Convert token to variable
-	*	@return string
-	*	@param $str string
-	**/
-	function token($str) {
-		$self=$this;
-		$str=preg_replace_callback(
-			'/(?<!\w)@(\w(?:[\w\.\[\]]|\->|::)*)/',
-			function($var) use($self) {
-				// Convert from JS dot notation to PHP array notation
-				return '$'.preg_replace_callback(
-					'/(\.\w+)|\[((?:[^\[\]]*|(?R))*)\]/',
-					function($expr) use($self) {
-						$fw=Base::instance();
-						return
-							'['.
-							($expr[1]?
-								$fw->stringify(substr($expr[1],1)):
-								(preg_match('/^\w+/',
-									$mix=$self->token($expr[2]))?
-									$fw->stringify($mix):
-									$mix)).
-							']';
-					},
-					$var[1]
-				);
-			},
-			$str
-		);
-		return trim(preg_replace('/{{(.+?)}}/',trim('\1'),$str));
-	}
-
 	/**
 	*	Template -set- tag handler
 	*	@return string
@@ -71,7 +36,7 @@ class Template extends View {
 		$out='';
 		foreach ($node['@attrib'] as $key=>$val)
 			$out.='$'.$key.'='.
-				(preg_match('/{{(.+?)}}/',$val)?
+				(preg_match('/\{\{(.+?)\}\}/',$val)?
 					$this->token($val):
 					Base::instance()->stringify($val)).'; ';
 		return '<?php '.$out.'?>';
@@ -84,14 +49,19 @@ class Template extends View {
 	**/
 	protected function _include(array $node) {
 		$attrib=$node['@attrib'];
+		$hive=isset($attrib['with']) &&
+			($attrib['with']=preg_match('/\{\{(.+?)\}\}/',$attrib['with'])?$this->token($attrib['with']):Base::instance()->stringify($attrib['with'])) &&
+			preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/',$attrib['with'],$pairs,PREG_SET_ORDER)?
+				'array('.implode(',',array_map(function($pair){return "'$pair[1]'=>$pair[2]";},$pairs)).')+get_defined_vars()':
+				'get_defined_vars()';
 		return
 			'<?php '.(isset($attrib['if'])?
 				('if ('.$this->token($attrib['if']).') '):'').
 				('echo $this->render('.
-					(preg_match('/{{(.+?)}}/',$attrib['href'])?
+					(preg_match('/\{\{(.+?)\}\}/',$attrib['href'])?
 						$this->token($attrib['href']):
 						Base::instance()->stringify($attrib['href'])).','.
-					'$this->mime,get_defined_vars()); ?>');
+					'$this->mime,'.$hive.'); ?>');
 	}
 
 	/**
@@ -217,7 +187,7 @@ class Template extends View {
 		$attrib=$node['@attrib'];
 		unset($node['@attrib']);
 		return
-			'<?php case '.(preg_match('/{{(.+?)}}/',$attrib['value'])?
+			'<?php case '.(preg_match('/\{\{(.+?)\}\}/',$attrib['value'])?
 				$this->token($attrib['value']):
 				Base::instance()->stringify($attrib['value'])).': ?>'.
 				$this->build($node).
@@ -244,20 +214,8 @@ class Template extends View {
 	*	@param $node array|string
 	**/
 	protected function build($node) {
-		if (is_string($node)) {
-			$self=$this;
-			return preg_replace_callback(
-				'/{{(.+?)}}/s',
-				function($expr) use($self) {
-					$str=trim($self->token($expr[1]));
-					if (preg_match('/^(.+?)\h*\|\h*(raw|esc|format)$/',
-						$str,$parts))
-						$str='Base::instance()->'.$parts[2].'('.$parts[1].')';
-					return '<?php echo '.$str.'; ?>';
-				},
-				$node
-			);
-		}
+		if (is_string($node))
+			return parent::build($node);
 		$out='';
 		foreach ($node as $key=>$val)
 			$out.=is_int($key)?$this->build($val):$this->{'_'.$key}($val);
@@ -290,103 +248,86 @@ class Template extends View {
 	}
 
 	/**
-	*	Render template
-	*	@return string
-	*	@param $file string
-	*	@param $mime string
-	*	@param $hive array
+	*	Parse string for template directives and tokens
+	*	@return string|array
+	*	@param $text string
 	**/
-	function render($file,$mime='text/html',array $hive=NULL) {
-		$fw=Base::instance();
-		if (!is_dir($tmp=$fw->get('TEMP')))
-			mkdir($tmp,Base::MODE,TRUE);
-		foreach ($fw->split($fw->get('UI')) as $dir)
-			if (is_file($view=$fw->fixslashes($dir.$file))) {
-				if (!is_file($this->view=($tmp.
-					$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
-					$fw->hash($view).'.php')) ||
-					filemtime($this->view)<filemtime($view)) {
-					// Remove PHP code and comments
-					$text=preg_replace('/<\?(?:php)?.+?\?>|{{\*.+?\*}}/is','',
-						$fw->read($view));
-					// Build tree structure
-					for ($ptr=0,$len=strlen($text),$tree=array(),$node=&$tree,
-						$stack=array(),$depth=0,$tmp='';$ptr<$len;)
-						if (preg_match('/^<(\/?)(?:F3:)?('.$this->tags.')\b'.
-							'((?:\h+\w+\h*=\h*(?:"(?:.+?)"|\'(?:.+?)\'))*)'.
-							'\h*(\/?)>/is',substr($text,$ptr),$match)) {
-							if (strlen($tmp))
-								$node[]=$tmp;
-							// Element node
-							if ($match[1]) {
-								// Find matching start tag
-								$save=$depth;
-								$found=FALSE;
-								while ($depth>0) {
-									$depth--;
-									foreach ($stack[$depth] as $item)
-										if (is_array($item) &&
-											isset($item[$match[2]])) {
-											// Start tag found
-											$found=TRUE;
-											break 2;
-										}
-								}
-								if (!$found)
-									// Unbalanced tag
-									$depth=$save;
-								$node=&$stack[$depth];
+	function parse($text) {
+		// Build tree structure
+		for ($ptr=0,$len=strlen($text),$tree=array(),$node=&$tree,
+			$stack=array(),$depth=0,$tmp='';$ptr<$len;)
+			if (preg_match('/^<(\/?)(?:F3:)?'.
+				'('.$this->tags.')\b((?:\h+[\w-]+'.
+				'(?:\h*=\h*(?:"(?:.+?)"|\'(?:.+?)\'))?|'.
+				'\h*\{\{.+?\}\})*)\h*(\/?)>/is',
+				substr($text,$ptr),$match)) {
+				if (strlen($tmp))
+					$node[]=$tmp;
+				// Element node
+				if ($match[1]) {
+					// Find matching start tag
+					$save=$depth;
+					$found=FALSE;
+					while ($depth>0) {
+						$depth--;
+						foreach ($stack[$depth] as $item)
+							if (is_array($item) && isset($item[$match[2]])) {
+								// Start tag found
+								$found=TRUE;
+								break 2;
 							}
-							else {
-								// Start tag
-								$stack[$depth]=&$node;
-								$node=&$node[][$match[2]];
-								if ($match[3]) {
-									// Process attributes
-									preg_match_all(
-										'/\b([\w-]+)\h*=\h*'.
-										'(?:"(.+?)"|\'(.+?)\')/s',
-										$match[3],$attr,PREG_SET_ORDER);
-									foreach ($attr as $kv)
-										$node['@attrib'][$kv[1]]=
-											$kv[2]?:$kv[3];
-								}
-								if ($match[4])
-									// Empty tag
-									$node=&$stack[$depth];
-								else
-									$depth++;
-							}
-							$tmp='';
-							$ptr+=strlen($match[0]);
-						}
-						else {
-							// Text node
-							$tmp.=substr($text,$ptr,1);
-							$ptr++;
-						}
-					if (strlen($tmp))
-						// Append trailing text
-						$node[]=$tmp;
-					// Break references
-					unset($node);
-					unset($stack);
-					$fw->write($this->view,$this->build($tree));
+					}
+					if (!$found)
+						// Unbalanced tag
+						$depth=$save;
+					$node=&$stack[$depth];
+				}
+				else {
+					// Start tag
+					$stack[$depth]=&$node;
+					$node=&$node[][$match[2]];
+					if ($match[3]) {
+						// Process attributes
+						preg_match_all(
+							'/(?:\b([\w-]+)\h*'.
+							'(?:=\h*(?:"(.+?)"|\'(.+?)\'))?|'.
+							'(\{\{.+?\}\}))/s',
+							$match[3],$attr,PREG_SET_ORDER);
+						foreach ($attr as $kv)
+							if (isset($kv[4]))
+								$node['@attrib'][]=$kv[4];
+							else
+								$node['@attrib'][$kv[1]]=
+									(empty($kv[2])?
+										(empty($kv[3])?NULL:$kv[3]):$kv[2]);
+					}
+					if ($match[4])
+						// Empty tag
+						$node=&$stack[$depth];
+					else
+						$depth++;
 				}
-				if (isset($_COOKIE[session_name()]))
-					@session_start();
-				$fw->sync('SESSION');
-				if (!$hive)
-					$hive=$fw->hive();
-				$this->hive=$fw->get('ESCAPE')?$fw->esc($hive):$hive;
-				if (PHP_SAPI!='cli')
-					header('Content-Type: '.($this->mime=$mime).'; '.
-						'charset='.$fw->get('ENCODING'));
-				return $this->sandbox();
+				$tmp='';
+				$ptr+=strlen($match[0]);
+			}
+			else {
+				// Text node
+				$tmp.=substr($text,$ptr,1);
+				$ptr++;
 			}
-		user_error(sprintf(Base::E_Open,$file));
+		if (strlen($tmp))
+			// Append trailing text
+			$node[]=$tmp;
+		// Break references
+		unset($node);
+		unset($stack);
+		return $tree;
 	}
 
+	/**
+	*	Class constructor
+	*	return object
+	**/
 	function __construct() {
 		$ref=new ReflectionClass(__CLASS__);
 		$this->tags='';

+ 5 - 6
php-fatfree/lib/test.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -23,7 +23,7 @@ class Test {
 		FLAG_Both=2;
 	//@}
 
-	private
+	protected
 		//! Test results
 		$data=array();
 
@@ -37,15 +37,13 @@ class Test {
 
 	/**
 	*	Evaluate condition and save test result
-	*	@return NULL
+	*	@return object
 	*	@param $cond bool
 	*	@param $text string
 	**/
 	function expect($cond,$text=NULL) {
 		$out=(bool)$cond;
-		if ($this->level==self::FLAG_True && $out ||
-			$this->level==self::FLAG_False && !$out ||
-			$this->level==self::FLAG_Both) {
+		if ($this->level==$out || $this->level==self::FLAG_Both) {
 			$data=array('status'=>$out,'text'=>$text,'source'=>NULL);
 			foreach (debug_backtrace() as $frame)
 				if (isset($frame['file'])) {
@@ -55,6 +53,7 @@ class Test {
 				}
 			$this->data[]=$data;
 		}
+		return $this;
 	}
 
 	/**

+ 53 - 46
php-fatfree/lib/utf.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -16,72 +16,49 @@
 //! Unicode string manager
 class UTF extends Prefab {
 
-	/**
-	*	Find position of first occurrence of a string (case-insensitive)
-	*	@return int|FALSE
-	*	@param $stack string
-	*	@param $needle string
-	*	@param $ofs int
-	**/
-	function stripos($stack,$needle,$ofs=0) {
-		return $this->strpos($stack,$needle,$ofs,TRUE);
-	}
-
 	/**
 	*	Get string length
 	*	@return int
 	*	@param $str string
 	**/
 	function strlen($str) {
-		preg_match_all('/./u',$str,$parts);
+		preg_match_all('/./us',$str,$parts);
 		return count($parts[0]);
 	}
 
 	/**
-	*	Find position of first occurrence of a string
-	*	@return int|FALSE
-	*	@param $stack string
-	*	@param $needle string
-	*	@param $ofs int
-	*	@param $case bool
+	*	Reverse a string
+	*	@return string
+	*	@param $str string
 	**/
-	function strpos($stack,$needle,$ofs=0,$case=FALSE) {
-		preg_match('/^(.*?)'.preg_quote($needle,'/').'/u'.($case?'i':''),
-			$this->substr($stack,$ofs),$match);
-		return isset($match[1])?$this->strlen($match[1]):FALSE;
+	function strrev($str) {
+		preg_match_all('/./us',$str,$parts);
+		return implode('',array_reverse($parts[0]));
 	}
 
 	/**
-	*	Finds position of last occurrence of a string (case-insensitive)
+	*	Find position of first occurrence of a string (case-insensitive)
 	*	@return int|FALSE
 	*	@param $stack string
 	*	@param $needle string
 	*	@param $ofs int
 	**/
-	function strripos($stack,$needle,$ofs=0) {
-		return $this->strrpos($stack,$needle,$ofs,TRUE);
+	function stripos($stack,$needle,$ofs=0) {
+		return $this->strpos($stack,$needle,$ofs,TRUE);
 	}
 
 	/**
-	*	Find position of last occurrence of a string
+	*	Find position of first occurrence of a string
 	*	@return int|FALSE
 	*	@param $stack string
 	*	@param $needle string
 	*	@param $ofs int
 	*	@param $case bool
 	**/
-	function strrpos($stack,$needle,$ofs=0,$case=FALSE) {
-		if (!$needle)
-			return FALSE;
-		$len=$this->strlen($stack);
-		for ($ptr=$ofs;$ptr<$len;$ptr+=$this->strlen($match[0])) {
-			$sub=$this->substr($stack,$ptr);
-			if (!$sub || !preg_match('/^(.*?)'.
-				preg_quote($needle,'/').'/u'.($case?'i':''),$sub,$match))
-				break;
-			$ofs=$ptr+$this->strlen($match[1]);
-		}
-		return $sub?$ofs:FALSE;
+	function strpos($stack,$needle,$ofs=0,$case=FALSE) {
+		return preg_match('/^(.{'.$ofs.'}.*?)'.
+			preg_quote($needle,'/').'/us'.($case?'i':''),$stack,$match)?
+			$this->strlen($match[1]):FALSE;
 	}
 
 	/**
@@ -93,7 +70,7 @@ class UTF extends Prefab {
 	*	@param $before bool
 	**/
 	function stristr($stack,$needle,$before=FALSE) {
-		return strstr($stack,$needle,$before,TRUE);
+		return $this->strstr($stack,$needle,$before,TRUE);
 	}
 
 	/**
@@ -108,7 +85,7 @@ class UTF extends Prefab {
 	function strstr($stack,$needle,$before=FALSE,$case=FALSE) {
 		if (!$needle)
 			return FALSE;
-		preg_match('/^(.*?)'.preg_quote($needle,'/').'/u'.($case?'i':''),
+		preg_match('/^(.*?)'.preg_quote($needle,'/').'/us'.($case?'i':''),
 			$stack,$match);
 		return isset($match[1])?
 			($before?
@@ -125,13 +102,11 @@ class UTF extends Prefab {
 	*	@param $len int
 	**/
 	function substr($str,$start,$len=0) {
-		if ($start<0) {
-			$len=-$start;
+		if ($start<0)
 			$start=$this->strlen($str)+$start;
-		}
 		if (!$len)
 			$len=$this->strlen($str)-$start;
-		return preg_match('/^.{'.$start.'}(.{0,'.$len.'})/u',$str,$match)?
+		return preg_match('/^.{'.$start.'}(.{0,'.$len.'})/us',$str,$match)?
 			$match[1]:FALSE;
 	}
 
@@ -142,7 +117,7 @@ class UTF extends Prefab {
 	*	@param $needle string
 	**/
 	function substr_count($stack,$needle) {
-		preg_match_all('/'.preg_quote($needle,'/').'/u',$stack,
+		preg_match_all('/'.preg_quote($needle,'/').'/us',$stack,
 			$matches,PREG_SET_ORDER);
 		return count($matches);
 	}
@@ -182,4 +157,36 @@ class UTF extends Prefab {
 		return chr(0xef).chr(0xbb).chr(0xbf);
 	}
 
+	/**
+	*	Convert code points to Unicode symbols
+	*	@return string
+	*	@param $str string
+	**/
+	function translate($str) {
+		return html_entity_decode(
+			preg_replace('/\\\\u([[:xdigit:]]+)/i','&#x\1;',$str));
+	}
+
+	/**
+	*	Translate emoji tokens to Unicode font-supported symbols
+	*	@return string
+	*	@param $str string
+	**/
+	function emojify($str) {
+		$map=array(
+			':('=>'\u2639', // frown
+			':)'=>'\u263a', // smile
+			'<3'=>'\u2665', // heart
+			':D'=>'\u1f603', // grin
+			'XD'=>'\u1f606', // laugh
+			';)'=>'\u1f609', // wink
+			':P'=>'\u1f60b', // tongue
+			':,'=>'\u1f60f', // think
+			':/'=>'\u1f623', // skeptic
+			'8O'=>'\u1f632', // oops
+		)+Base::instance()->get('EMOJI');
+		return $this->translate(str_replace(array_keys($map),
+			array_values($map),$str));
+	}
+
 }

+ 58 - 23
php-fatfree/lib/web.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -21,7 +21,7 @@ class Web extends Prefab {
 		E_Request='No suitable HTTP request engine found';
 	//@}
 
-	private
+	protected
 		//! HTTP request engine
 		$wrapper;
 
@@ -69,7 +69,7 @@ class Web extends Prefab {
 				'wav'=>'audio/wav',
 				'xls'=>'application/vnd.ms-excel',
 				'xml'=>'application/xml',
-				'zip'=>'application/zip'
+				'zip'=>'application/x-zip-compressed'
 			);
 			foreach ($map as $key=>$val)
 				if (preg_match('/'.$key.'/',strtolower($ext[0])))
@@ -154,7 +154,7 @@ class Web extends Prefab {
 	*	@return array|bool
 	*	@param $func callback
 	*	@param $overwrite bool
-	*	@param $slug bool
+	*	@param $slug callback|bool
 	**/
 	function receive($func=NULL,$overwrite=FALSE,$slug=TRUE) {
 		$fw=Base::instance();
@@ -162,11 +162,42 @@ class Web extends Prefab {
 		if (!is_dir($dir))
 			mkdir($dir,Base::MODE,TRUE);
 		if ($fw->get('VERB')=='PUT') {
-			$fw->write($dir.basename($fw->get('URI')),$fw->get('BODY'));
-			return TRUE;
+			$tmp=$fw->get('TEMP').
+				$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+				$fw->hash(uniqid());
+			if (!$fw->get('RAW'))
+				$fw->write($tmp,$fw->get('BODY'));
+			else {
+				$src=@fopen('php://input','r');
+				$dst=@fopen($tmp,'w');
+				if (!$src || !$dst)
+					return FALSE;
+				while (!feof($src) &&
+					($info=stream_get_meta_data($src)) &&
+					!$info['timed_out'] && $str=fgets($src,4096))
+					fputs($dst,$str,strlen($str));
+				fclose($dst);
+				fclose($src);
+			}
+			$base=basename($fw->get('URI'));
+			$file=array(
+				'name'=>$dir.
+					($slug && preg_match('/(.+?)(\.\w+)?$/',$base,$parts)?
+						(is_callable($slug)?
+							$slug($base):
+							($this->slug($parts[1]).
+								(isset($parts[2])?$parts[2]:''))):
+						$base),
+				'tmp_name'=>$tmp,
+				'type'=>$this->mime($base),
+				'size'=>filesize($tmp)
+			);
+			return (!file_exists($file['name']) || $overwrite) &&
+				(!$func || $fw->call($func,array($file))!==FALSE) &&
+				rename($tmp,$file['name']);
 		}
 		$out=array();
-		foreach ($_FILES as $item) {
+		foreach ($_FILES as $name=>$item) {
 			if (is_array($item['name'])) {
 				// Transpose array
 				$tmp=array();
@@ -181,15 +212,18 @@ class Web extends Prefab {
 				if (empty($file['name']))
 					continue;
 				$base=basename($file['name']);
-				$dst=$dir.
+				$file['name']=$dir.
 					($slug && preg_match('/(.+?)(\.\w+)?$/',$base,$parts)?
-						($this->slug($parts[1]).
-						(isset($parts[2])?$parts[2]:'')):$base);
-				$out[$dst]=!$file['error'] &&
-					$file['type']==$this->mime($file['name']) &&
-					(!file_exists($dst) || $overwrite) &&
-					(!$func || $fw->call($func,array($file))) &&
-					move_uploaded_file($file['tmp_name'],$dst);
+						(is_callable($slug)?
+							$slug($base):
+							($this->slug($parts[1]).
+								(isset($parts[2])?$parts[2]:''))):
+						$base);
+				$out[$file['name']]=!$file['error'] &&
+					is_uploaded_file($file['tmp_name']) &&
+					(!file_exists($file['name']) || $overwrite) &&
+					(!$func || $fw->call($func,array($file,$name))!==FALSE) &&
+					move_uploaded_file($file['tmp_name'],$file['name']);
 			}
 		}
 		return $out;
@@ -299,11 +333,13 @@ class Web extends Prefab {
 		$headers=array();
 		$body='';
 		$parts=parse_url($url);
+		$empty=empty($parts['port']);
 		if ($parts['scheme']=='https') {
 			$parts['host']='ssl://'.$parts['host'];
-			$parts['port']=443;
+			if ($empty)
+				$parts['port']=443;
 		}
-		else
+		elseif ($empty)
 			$parts['port']=80;
 		if (empty($parts['path']))
 			$parts['path']='/';
@@ -362,7 +398,7 @@ class Web extends Prefab {
 	*	@return string
 	*	@param $arg string
 	**/
-	function engine($arg='socket') {
+	function engine($arg='curl') {
 		$arg=strtolower($arg);
 		$flags=array(
 			'curl'=>extension_loaded('curl'),
@@ -501,7 +537,7 @@ class Web extends Prefab {
 		preg_match('/\w+$/',$files[0],$ext);
 		$cache=Cache::instance();
 		$dst='';
-		foreach ($fw->split($path?:$fw->get('UI')) as $dir)
+		foreach ($fw->split($path?:$fw->get('UI').';./') as $dir)
 			foreach ($files as $file)
 				if (is_file($save=$fw->fixslashes($dir.$file))) {
 					if ($fw->get('CACHE') &&
@@ -687,7 +723,6 @@ class Web extends Prefab {
 	function slug($text) {
 		return trim(strtolower(preg_replace('/([^\pL\pN])+/u','-',
 			trim(strtr(str_replace('\'','',$text),
-			Base::instance()->get('DIACRITICS')+
 			array(
 				'Ǎ'=>'A','А'=>'A','Ā'=>'A','Ă'=>'A','Ą'=>'A','Å'=>'A',
 				'Ǻ'=>'A','Ä'=>'Ae','Á'=>'A','À'=>'A','Ã'=>'A','Â'=>'A',
@@ -736,7 +771,7 @@ class Web extends Prefab {
 				'ǜ'=>'u','ǔ'=>'u','ǖ'=>'u','ũ'=>'u','ü'=>'ue','в'=>'v',
 				'ŵ'=>'w','ы'=>'y','ÿ'=>'y','ý'=>'y','ŷ'=>'y','ź'=>'z',
 				'ž'=>'z','з'=>'z','ż'=>'z','ж'=>'zh'
-			))))),'-');
+			)+Base::instance()->get('DIACRITICS'))))),'-');
 	}
 
 	/**
@@ -784,7 +819,7 @@ if (!function_exists('gzdecode')) {
 
 	/**
 	*	Decode gzip-compressed string
-	*	@param $data string
+	*	@param $str string
 	**/
 	function gzdecode($str) {
 		$fw=Base::instance();
@@ -792,7 +827,7 @@ if (!function_exists('gzdecode')) {
 			mkdir($tmp,Base::MODE,TRUE);
 		file_put_contents($file=$tmp.'/'.
 			$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
-			$fw->hash(uniqid()).'.gz',$str,LOCK_EX);
+			$fw->hash(uniqid(NULL,TRUE)).'.gz',$str,LOCK_EX);
 		ob_start();
 		readgzfile($file);
 		$out=ob_get_clean();

+ 1 - 1
php-fatfree/lib/web/geo.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 

+ 58 - 0
php-fatfree/lib/web/google/staticmap.php

@@ -0,0 +1,58 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace Web\Google;
+
+//! Google Static Maps API v2 plug-in
+class StaticMap {
+
+	const
+		//! API URL
+		URL_Static='http://maps.googleapis.com/maps/api/staticmap';
+
+	protected
+		//! Query arguments
+		$query=array();
+
+	/**
+	*	Specify API key-value pair via magic call
+	*	@return object
+	*	@param $func string
+	*	@param $args array
+	**/
+	function __call($func,array $args) {
+		$this->query[]=array($func,$args[0]);
+		return $this;
+	}
+
+	/**
+	*	Generate map
+	*	@return string
+	**/
+	function dump() {
+		$fw=\Base::instance();
+		$web=\Web::instance();
+		$out='';
+		return ($req=$web->request(
+			self::URL_Static.'?'.array_reduce(
+				$this->query,
+				function($out,$item) {
+					return ($out.=($out?'&':'').
+						urlencode($item[0]).'='.urlencode($item[1]));
+				}
+			))) && $req['body']?$req['body']:FALSE;
+	}
+
+}

+ 32 - 13
php-fatfree/lib/web/openid.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -18,12 +18,9 @@ namespace Web;
 //! OpenID consumer
 class OpenID extends \Magic {
 
-	//@{ Error messages
-	const
-		E_EndPoint='Unable to find OpenID provider';
-	//@}
-
-	var
+	protected
+		//! OpenID provider endpoint URL
+		$url,
 		//! HTTP request parameters
 		$args=array();
 
@@ -111,6 +108,7 @@ class OpenID extends \Magic {
 		}
 		elseif (isset($this->args['server'])) {
 			// OpenID 1.1
+			$this->args['ns']='http://openid.net/signon/1.1';
 			if (isset($this->args['delegate']))
 				$this->args['identity']=$this->args['delegate'];
 		}
@@ -132,8 +130,10 @@ class OpenID extends \Magic {
 	*	or redirect to OpenID provider URL
 	*	@return bool
 	*	@param $proxy string
+	*	@param $attr array
+	*	@param $reqd string|array
 	**/
-	function auth($proxy=NULL) {
+	function auth($proxy=NULL,$attr=array(),array $reqd=NULL) {
 		$fw=\Base::instance();
 		$root=$fw->get('SCHEME').'://'.$fw->get('HOST');
 		if (empty($this->args['trust_root']))
@@ -141,11 +141,19 @@ class OpenID extends \Magic {
 		if (empty($this->args['return_to']))
 			$this->args['return_to']=$root.$_SERVER['REQUEST_URI'];
 		$this->args['mode']='checkid_setup';
-		if ($url=$this->discover($proxy)) {
+		if ($this->url=$this->discover($proxy)) {
+			if ($attr) {
+				$this->args['ns.ax']='http://openid.net/srv/ax/1.0';
+				$this->args['ax.mode']='fetch_request';
+				foreach ($attr as $key=>$val)
+					$this->args['ax.type.'.$key]=$val;
+				$this->args['ax.required']=is_string($reqd)?
+					$reqd:implode(',',$reqd);
+			}
 			$var=array();
 			foreach ($this->args as $key=>$val)
 				$var['openid.'.$key]=$val;
-			$fw->reroute($url.'?'.http_build_query($var));
+			$fw->reroute($this->url.'?'.http_build_query($var));
 		}
 		return FALSE;
 	}
@@ -160,24 +168,34 @@ class OpenID extends \Magic {
 			$_SERVER['QUERY_STRING'],$matches,PREG_SET_ORDER);
 		foreach ($matches as $match)
 			$this->args[$match[1]]=urldecode($match[2]);
-		if ($this->args['mode']!='error' && $url=$this->discover($proxy)) {
+		if (isset($this->args['mode']) &&
+			$this->args['mode']!='error' &&
+			$this->url=$this->discover($proxy)) {
 			$this->args['mode']='check_authentication';
 			$var=array();
 			foreach ($this->args as $key=>$val)
 				$var['openid.'.$key]=$val;
 			$req=\Web::instance()->request(
-				$url,
+				$this->url,
 				array(
 					'method'=>'POST',
 					'content'=>http_build_query($var),
 					'proxy'=>$proxy
 				)
 			);
-			return preg_match('/is_valid:true/i',$req['body']);
+			return (bool)preg_match('/is_valid:true/i',$req['body']);
 		}
 		return FALSE;
 	}
 
+	/**
+	*	Return OpenID response fields
+	*	@return array
+	**/
+	function response() {
+		return $this->args;
+	}
+
 	/**
 	*	Return TRUE if OpenID request parameter exists
 	*	@return bool
@@ -216,3 +234,4 @@ class OpenID extends \Magic {
 	}
 
 }
+

+ 2 - 2
php-fatfree/lib/web/pingback.php

@@ -1,7 +1,7 @@
 <?php
 
 /*
-	Copyright (c) 2009-2013 F3::Factory/Bong Cosca, All rights reserved.
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
 
 	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
 
@@ -18,7 +18,7 @@ namespace Web;
 //! Pingback 1.0 protocol (client and server) implementation
 class Pingback extends \Prefab {
 
-	private
+	protected
 		//! Transaction history
 		$log;
 

+ 12 - 10
php-fatfree/setup.py

@@ -1,3 +1,4 @@
+
 import subprocess
 import sys
 import setup_util
@@ -14,16 +15,17 @@ def start(args):
   
   try:
     if os.name == 'nt':
-      subprocess.check_call('appcmd add site /name:PHP /bindings:http/*:8080: /physicalPath:"C:\\FrameworkBenchmarks\\php-fatfree"', shell=True)
+      subprocess.check_call('appcmd add site /name:PHP /bindings:http/*:8080: /physicalPath:"C:\\FrameworkBenchmarks\\php-fatfree"', shell=True, stderr=errfile, stdout=logfile)
       return 0
     
     #subprocess.check_call("sudo cp php-fatfree/deploy/php /etc/apache2/sites-available/", shell=True)
     #subprocess.check_call("sudo a2ensite php", shell=True)
     #subprocess.check_call("sudo /etc/init.d/apache2 start", shell=True)
-    
-    subprocess.check_call("sudo chown -R www-data:www-data php-fatfree", shell=True)
-    subprocess.check_call("sudo php-fpm --fpm-config config/php-fpm.conf -g " + home + "/FrameworkBenchmarks/php-fatfree/deploy/php-fpm.pid", shell=True)
-    subprocess.check_call("sudo /usr/local/nginx/sbin/nginx -c " + home + "/FrameworkBenchmarks/php-fatfree/deploy/nginx.conf", shell=True)
+
+    subprocess.check_call("sudo chown -R www-data:www-data php-fatfree", shell=True, stderr=errfile, stdout=logfile)
+    subprocess.check_call("sudo chmod -R 775 " + home + "/FrameworkBenchmarks/php-fatfree/tmp/", shell=True, stderr=errfile, stdout=logfile)
+    subprocess.check_call("sudo php-fpm --fpm-config config/php-fpm.conf -g " + home + "/FrameworkBenchmarks/php-fatfree/deploy/php-fpm.pid", shell=True, stderr=errfile, stdout=logfile)
+    subprocess.check_call("sudo /usr/local/nginx/sbin/nginx -c " + home + "/FrameworkBenchmarks/php-fatfree/deploy/nginx.conf", shell=True, stderr=errfile, stdout=logfile)
     
     return 0
   except subprocess.CalledProcessError:
@@ -31,13 +33,13 @@ def start(args):
 def stop():
   try:
     if os.name == 'nt':
-      subprocess.check_call('appcmd delete site PHP', shell=True)
+      subprocess.check_call('appcmd delete site PHP', shell=True, stderr=errfile, stdout=logfile)
       return 0
     
-    subprocess.call("sudo /usr/local/nginx/sbin/nginx -s stop", shell=True)
-    subprocess.call("sudo kill -QUIT $( cat php-fatfree/deploy/php-fpm.pid )", shell=True)
-    subprocess.check_call("sudo chown -R $USER:$USER php-fatfree", shell=True)
-    
+    subprocess.call("sudo /usr/local/nginx/sbin/nginx -s stop", shell=True, stderr=errfile, stdout=logfile)
+    subprocess.call("sudo kill -QUIT $( cat php-fatfree/deploy/php-fpm.pid )", shell=True, stderr=errfile, stdout=logfile)
+    subprocess.check_call("sudo chown -R $USER:$USER php-fatfree", shell=True, stderr=errfile, stdout=logfile)
+
     return 0
   except subprocess.CalledProcessError:
     return 1

+ 0 - 0
php-fatfree/tmp/placeholder


Some files were not shown because too many files changed in this diff