Browse Source

update libs and test for TFB-Round-11, 2nd try

ikkez 10 years ago
parent
commit
bbb6fd1a5a

+ 9 - 0
frameworks/PHP/fat-free/deploy/fat-free

@@ -0,0 +1,9 @@
+<VirtualHost *:8080>
+  Alias /fat-free/ "/home/ubuntu/FrameworkBenchmarks/fat-free/"
+  <Directory /home/ubuntu/FrameworkBenchmarks/fat-free/>
+          Options Indexes FollowSymLinks MultiViews
+          #AllowOverride None
+          Order allow,deny
+          allow from all
+  </Directory>
+</VirtualHost>

+ 3 - 0
frameworks/PHP/fat-free/deploy/nginx.conf

@@ -4,6 +4,7 @@ worker_processes  8;
 #error_log  logs/error.log;
 #error_log  logs/error.log;
 #error_log  logs/error.log  notice;
 #error_log  logs/error.log  notice;
 #error_log  logs/error.log  info;
 #error_log  logs/error.log  info;
+error_log stderr error;
 
 
 #pid        logs/nginx.pid;
 #pid        logs/nginx.pid;
 
 
@@ -12,6 +13,7 @@ events {
     worker_connections  1024;
     worker_connections  1024;
 }
 }
 
 
+
 http {
 http {
     include       /usr/local/nginx/conf/mime.types;
     include       /usr/local/nginx/conf/mime.types;
     default_type  application/octet-stream;
     default_type  application/octet-stream;
@@ -21,6 +23,7 @@ http {
     #                  '"$http_user_agent" "$http_x_forwarded_for"';
     #                  '"$http_user_agent" "$http_x_forwarded_for"';
 
 
     #access_log  logs/access.log  main;
     #access_log  logs/access.log  main;
+    access_log off;
 
 
     sendfile        on;
     sendfile        on;
     #tcp_nopush     on;
     #tcp_nopush     on;

+ 0 - 9
frameworks/PHP/fat-free/deploy/php

@@ -1,9 +0,0 @@
-<VirtualHost *:8080>
-  Alias /php-fatfree/ "/home/tfb/FrameworkBenchmarks/php-fatfree/"
-  <Directory /home/tfb/FrameworkBenchmarks/php-fatfree/>
-          Options -Indexes FollowSymLinks MultiViews
-          #AllowOverride None
-          Order allow,deny
-          allow from all
-  </Directory>
-</VirtualHost>

+ 5 - 2
frameworks/PHP/fat-free/index.php

@@ -1,6 +1,6 @@
 <?php
 <?php
 /** @var Base $f3 */
 /** @var Base $f3 */
-$f3=require('lib/base.php');
+$f3=require('src/base.php');
 
 
 $f3->set('DEBUG',2);
 $f3->set('DEBUG',2);
 $f3->set('CACHE','folder=tmp/cache/');
 $f3->set('CACHE','folder=tmp/cache/');
@@ -39,7 +39,10 @@ $f3->route(
         for ($i = 0; $i < $queries; $i++) {
         for ($i = 0; $i < $queries; $i++) {
             $id = mt_rand(1, 10000);
             $id = mt_rand(1, 10000);
             $res = $db->exec('SELECT id, randomNumber FROM World WHERE id = ?',$id,0,false);
             $res = $db->exec('SELECT id, randomNumber FROM World WHERE id = ?',$id,0,false);
-            $result[] = $res[0];
+            $result[] = array(
+                'id' => (int) $res[0]['id'],
+                'randomNumber' => (int) $res[0]['randomNumber'],
+            );
         }
         }
         header("Content-type: application/json");
         header("Content-type: application/json");
         echo json_encode($single ? $result[0] : $result);
         echo json_encode($single ? $result[0] : $result);

+ 3 - 3
frameworks/PHP/fat-free/setup.sh

@@ -3,9 +3,9 @@
 fw_depends php nginx
 fw_depends php nginx
 
 
 sed -i 's|localhost|'"${DBHOST}"'|g' index.php
 sed -i 's|localhost|'"${DBHOST}"'|g' index.php
-sed -i 's|.*/FrameworkBenchmarks/php-fatfree|'"${TROOT}"'|g' deploy/php
-sed -i 's|Directory .*/FrameworkBenchmarks/php-fatfree|Directory '"${TROOT}"'|g' deploy/php
-sed -i 's|root .*/FrameworkBenchmarks/php-fatfree|root '"${TROOT}"'|g' deploy/nginx.conf
+sed -i 's|.*/FrameworkBenchmarks/fat-free|'"${TROOT}"'|g' deploy/fat-free
+sed -i 's|Directory .*/FrameworkBenchmarks/fat-free|Directory '"${TROOT}"'|g' deploy/fat-free
+sed -i 's|root .*/FrameworkBenchmarks/fat-free|root '"${TROOT}"'|g' deploy/nginx.conf
 sed -i 's|/usr/local/nginx/|'"${IROOT}"'/nginx/|g' deploy/nginx.conf
 sed -i 's|/usr/local/nginx/|'"${IROOT}"'/nginx/|g' deploy/nginx.conf
 
 
 php-fpm --fpm-config $FWROOT/config/php-fpm.conf -g $TROOT/deploy/php-fpm.pid
 php-fpm --fpm-config $FWROOT/config/php-fpm.conf -g $TROOT/deploy/php-fpm.pid

+ 577 - 0
frameworks/PHP/fat-free/src/CHANGELOG

@@ -0,0 +1,577 @@
+CHANGELOG
+
+3.5.0 (2 June 2015)
+*	NEW: until() method for long polling
+*	NEW: abort() to disconnect HTTP client (and continue execution)
+*	NEW: SQL Mapper->required() returns TRUE if field is not nullable
+*	NEW: PREMAP variable for allowing prefixes to handlers named after HTTP verbs
+*	NEW: [configs] section to allow config includes
+*	NEW: Test->passed() returns TRUE if no test failed
+*	NEW: SQL mapper changed() function
+*	NEW: fatfree-core composer support
+*	NEW: constants() method to expose constants
+*	NEW: Preview->filter() for configurable token filters
+*	NEW: CORS variable for Cross-Origin Resource Sharing support, #731
+*	Change in behavior: Switch to htmlspecialchars for escaping
+*	Change in behavior: No movement in cursor position after erase(), #797
+*	Change in behavior: ERROR.trace is a multiline string now
+*	Change in behavior: Strict token recognition in <include> href attribute
+*	Router fix: loose method search
+*	Better route precedence order, #12
+*	Preserve contents of ROUTES, #723
+*	Alias: allow array of parameters
+*	Improvements on reroute method
+*	Fix for custom Jig session files
+*	Audit: better mobile detection
+*	Audit: add argument to test string as browser agent
+*	DB mappers: abort insert/update/erase from hooks, #684
+*	DB mappers: Allow array inputs in copyfrom()
+*	Cache,SQL,Jig,Mongo Session: custom callback for suspect sessions
+*	Fix for unexpected HIVE values when defining an empty HIVE array
+*	SQL mapper: check for results from CALL and EXEC queries, #771
+*	SQL mapper: consider SQL schema prefix, #820
+*	SQL mapper: write to log before execution to
+	enable tracking of PDOStatement error
+*	Add SQL Mapper->table() to return table name
+*	Allow override of the schema in SQL Mapper->schema()
+*	Improvement: Keep JIG table as reference, #758
+*	Expand regex to include whitespaces in SQL DB dsn, #817
+*	View: Removed reserved variables $fw and $implicit
+*	Add missing newlines after template expansion
+*	Web->receive: fix for complex field names, #806
+*	Web: Improvements in socket engine
+*	Web: customizable user_agent for all engines, #822
+*	SMTP: Provision for Content-ID in attachments
+*	Image + minify: allow absolute paths
+*	Promote framework error to E_USER_ERROR
+*	Geo->weather switch to OpenWeather
+*	Expose mask() and grab() methods for routing
+*	Expose trace() method to expose the debug backtrace
+*	Implement recursion strategy using IteratorAggregate, #714
+*	Exempt whitespace between % and succeeding operator from being minified, #773
+*	Optimized error detection and ONERROR handler, fatfree-core#18
+*	Tweak error log output
+*	Optimized If-Modified-Since cache header usage
+*	Improved APCu compatibility, #724
+*	Bug fix: Web::send fails on filename with spaces, #810
+*	Bug fix: overwrite limit in findone()
+*	Bug fix: locale-specific edge cases affecting SQL schema, #772
+*	Bug fix: Newline stripping in config()
+*	Bug fix: bracket delimited identifier for sybase and dblib driver
+*	Bug fix: Mongo mapper collection->count driver compatibility
+*	Bug fix: SQL Mapper->set() forces adhoc value if already defined
+*	Bug fix: Mapper ignores HAVING clause
+*	Bug fix: Constructor invocation in call()
+*	Bug fix: Wrong element returned by ajax/sync request
+*	Bug fix: handling of non-consecutive compound key members
+*	Bug fix: Virtual fields not retrieved when group option is present, #757
+*	Bug fix: group option generates incorrect SQL query, #757
+*	Bug fix: ONERROR does not receive PARAMS on fatal error
+
+3.4.0 (1 January 2015)
+*	NEW: [redirects] section
+*	NEW: Custom config sections
+*	NEW: User-defined AUTOLOAD function
+*	NEW: ONREROUTE variable
+*	NEW: Provision for in-memory Jig database (#727)
+*	Return run() result (#687)
+*	Pass result of run() to mock() (#687)
+*	Add port suffix to REALM variable
+*	New attribute in <include> tag to extend hive
+*	Adjust unit tests and clean up templates
+*	Expose header-related methods
+*	Web->request: allow content array
+*	Preserve contents of ROUTES (#723)
+*	Smart detection of PHP functions in template expressions
+*	Add afterrender() hook to View class
+*	Implement ArrayAccess and magic properties on hive
+*	Improvement on mocking of superglobals and request body
+*	Fix table creation for pgsql handled sessions
+*	Add QUERY to hive
+*	Exempt E_NOTICE from default error_reporting()
+*	Add method to build alias routes from template, fixes #693
+*	Fix dangerous caching of cookie values
+*	Fix multiple encoding in nested templates
+*	Fix node attribute parsing for empty/zero values
+*	Apply URL encoding on BASE to emulate v2 behavior (#123)
+*	Improve Base->map performance (#595)
+*	Add simple backtrace for fatal errors
+*	Count Cursor->load() results (#581)
+*	Add form field name to Web->receive() callback arguments
+*	Fix missing newlines after template expansion
+*	Fix overwrite of ENCODING variable
+*	limit & offset workaround for SQL Server, fixes #671
+*	SQL Mapper->find: GROUP BY SQL compliant statement
+*	Bug fix: Missing abstract method fields()
+*	Bug fix: Auto escaping does not work with mapper objects (#710)
+*	Bug fix: 'with' attribute in <include> tag raise error when no token
+	inside
+*	View rendering: optional Content-Type header
+*	Bug fix: Undefined variable: cache (#705)
+*	Bug fix: Routing does not work if project base path includes valid
+	special URI character (#704)
+*	Bug fix: Template hash collision (#702)
+*	Bug fix: Property visibility is incorrect (#697)
+*	Bug fix: Missing Allow header on HTTP 405 response
+*	Bug fix: Double quotes in lexicon files (#681)
+*	Bug fix: Space should not be mandatory in ICU pluralization format string
+*	Bug fix: Incorrect log entry when SQL query contains a question mark
+*	Bug fix: Error stack trace
+*	Bug fix: Cookie expiration (#665)
+*	Bug fix: OR operator (||) parsed incorrectly
+*	Bug fix: Routing treatment of * wildcard character
+*	Bug fix:  Mapper copyfrom() method doesn't allow class/object callbacks
+	(#590)
+*	Bug fix: exists() creates elements/properties (#591)
+*	Bug fix: Wildcard in routing pattern consumes entire query string (#592)
+*	Bug fix: Workaround bug in latest MongoDB driver
+*	Bug fix: Default error handler silently fails for AJAX request with
+	DEBUG>0 (#599)
+*	Bug fix: Mocked BODY overwritten (#601)
+*	Bug fix: Undefined pkey (#607)
+
+3.3.0 (8 August 2014)
+*	NEW: Attribute in <include> tag to extend hive
+*	NEW: Image overlay with transparency and alignment control
+*	NEW: Allow redirection of specified route patterns to a URL
+*	Bug fix: Missing AND operator in SQL Server schema query (Issue #576)
+*	Count Cursor->load() results (Feature request #581)
+*	Mapper copyfrom() method doesn't allow class/object callbacks (Issue #590)
+*	Bug fix: exists() creates elements/properties (Issue #591)
+*	Bug fix: Wildcard in routing pattern consumes entire query string
+	(Issue #592)
+*	Tweak Base->map performance (Issue #595)
+*	Bug fix: Default error handler silently fails for AJAX request with
+	DEBUG>0 (Issue #599)
+*	Bug fix: Mocked BODY overwritten (Issue #601)
+*	Bug fix: Undefined pkey (Issue #607)
+*	Bug fix: beforeupdate() position (Issue #633)
+*	Bug fix: exists() return value for cached keys
+*	Bug fix: Missing error code in UNLOAD handler
+*	Bug fix: OR operator (||) parsed incorrectly
+*	Add input name parameter to custom slug function
+*	Apply URL encoding on BASE to emulate v2 behavior (Issue #123)
+*	Reduce mapper update() iterations
+*	Bug fix: Routing treatment of * wildcard character
+*	SQL Mapper->find: GROUP BY SQL compliant statement
+*	Work around bug in latest MongoDB driver
+*	Work around probable race condition and optimize cache access
+*	View rendering: Optional Content-Type header
+*	Fix missing newlines after template expansion
+*	Add form field name to Web->receive() callback arguments
+*	Quick reference: add RAW variable
+
+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
+*	Improve CACHE consistency
+*	Case-insensitive MIME type detection
+*	Support pre-PHP 5.3.4 in Prefab->instance()
+*	Refactor isdesktop() and ismobile(); Add isbot()
+*	Add support for Markdown strike-through
+*	Work around ODBC's lack of quote() support
+*	Remove useless Prefab destructor
+*	Support multiple cache instances
+*	Bug fix: Underscores in OpenId keys mangled
+*	Refactor format()
+*	Numerous tweaks
+*	Bug fix: MongoId object not preserved
+*	Bug fix: Double-quotes included in lexicon() string (Issue #341)
+*	Bug fix: UTF-8 formatting mangled on Windows (Issue #342)
+*	Bug fix: Cache->load() error when CACHE is FALSE (Issue #344)
+*	Bug fix: send() ternary expression
+*	Bug fix: Country code constants
+
+3.0.8 (17 May 2013)
+*	NEW: Bcrypt lightweight hashing library\
+*	Return total number of records in superset in Cursor->paginate()
+*	ONERROR short-circuit (Enhancement #334)
+*	Apply quotes/backticks on DB identifiers
+*	Allow enabling/disabling of SQL log
+*	Normalize glob() behavior (Issue #330)
+*	Bug fix: mbstring 2-byte text truncation (Issue #325)
+*	Bug fix: Unsupported operand types (Issue #324)
+
+3.0.7 (2 May 2013)
+*	NEW: route() now allows an array of routing patterns as first argument;
+	support array as first argument of map()
+*	NEW: entropy() for calculating password strength (NIST 800-63)
+*	NEW: AGENT variable containing auto-detected HTTP user agent string
+*	NEW: ismobile() and isdesktop() methods
+*	NEW: Prefab class and descendants now accept constructor arguments
+*	Change in behavior: Cache->exists() now returns timestamp and TTL of
+	cache entry or FALSE if not found (Feature request #315)
+*	Preserve timestamp and TTL when updating cache entry (Feature request
+	#316)
+*	Improved currency formatting with C99 compliance
+*	Suppress unnecessary program halt at startup caused by misconfigured
+	server
+*	Add support for dashes in custom attribute names in templates
+*	Bug fix: Routing precedene (Issue #313)
+*	Bug fix: Remove Jig _id element from document property
+*	Bug fix: Web->rss() error when not enough items in the feed (Issue #299)
+*	Bug fix: Web engine fallback (Issue #300)
+*	Bug fix: <strong> and <em> formatting
+*	Bug fix: Text rendering of text with trailing punctuation (Issue #303)
+*	Bug fix: Incorrect regex in SMTP
+
+3.0.6 (31 Mar 2013)
+*	NEW: Image->crop()
+*	Modify documentation blocks for PHPDoc interoperability
+*	Allow user to control whether Base->rerouet() uses a permanent or
+	temporary redirect
+*	Allow JAR elements to be set individually
+*	Refactor DB\SQL\Mapper->insert() to cope with autoincrement fields
+*	Trigger error when captcha() font is missing
+*	Remove unnecessary markdown regex recursion
+*	Check for scalars instead of DB\SQL strings
+*	Implement more comprehensive diacritics table
+*	Add option for disabling 401 errors when basic auth() fails
+*	Add markdown syntax highlighting for Apache configuration
+*	Markdown->render() deprecated to remove dependency on UI variable;
+	Feature replaced by Markdown->convert() to enable translation from
+	markdown string to HTML
+*	Optimize factory() code of all data mappers
+*	Apply backticks on MySQL table names
+*	Bug fix: Routing failure when directory path contains a tilde (Issue #291)
+*	Bug fix: Incorrect markdown parsing of strong/em sequences and inline HTML
+*	Bug fix: Cached page not echoed (Issue #278)
+*	Bug fix: Object properties not escaped when rendering
+*	Bug fix: OpenID error response ignored
+*	Bug fix: memcache_get_extended_stats() timeout
+*	Bug fix: Base->set() doesn't pass TTL to Cache->set()
+*	Bug fix: Base->scrub() ignores pass-thru * argument (Issue #274)
+
+3.0.5 (16 Feb 2013)
+*	NEW: Markdown class with PHP, HTML, and .ini syntax highlighting support
+*	NEW: Options for caching of select() and find() results
+*	NEW: Web->acceptable()
+*	Add send() argument for forcing downloads
+*	Provide read() option for applying Unix LF as standard line ending
+*	Bypass lexicon() call if LANGUAGE is undefined
+*	Load fallback language dictionary if LANGUAGE is undefined
+*	map() now checks existence of class/methods for non-tokenized URLs
+*	Improve error reporting of non-existent Template methods
+*	Address output buffer issues on some servers
+*	Bug fix: Setting DEBUG to 0 won't suppress the stack trace when the
+	content type is application/json (Issue #257)
+*	Bug fix: Image dump/render additional arguments shifted
+*	Bug fix: ob_clean() causes buffer issues with zlib compression
+*	Bug fix: minify() fails when commenting CSS @ rules (Issue #251)
+*	Bug fix: Handling of commas inside quoted strings
+*	Bug fix: Glitch in stringify() handling of closures
+*	Bug fix: dry() in mappers returns TRUE despite being hydrated by
+	factory() (Issue #265)
+*	Bug fix: expect() not handling flags correctly
+*	Bug fix: weather() fails when server is unreachable
+
+3.0.4 (29 Jan 2013)
+*	NEW: Support for ICU/CLDR pluralization
+*	NEW: User-defined FALLBACK language
+*	NEW: minify() now recognizes CSS @import directives
+*	NEW: UTF->bom() returns byte order mark for UTF-8 encoding
+*	Expose SQL\Mapper->schema()
+*	Change in behavior: Send error response as JSON string if AJAX request is
+	detected
+*	Deprecated: afind*() methods
+*	Discard output buffer in favor of debug output
+*	Make _id available to Jig queries
+*	Magic class now implements ArrayAccess
+*	Abort execution on startup errors
+*	Suppress stack trace on DEBUG level 0
+*	Allow single = as equality operator in Jig query expressions
+*	Abort OpenID discovery if Web->request() fails
+*	Mimic PHP *RECURSION* in stringify()
+*	Modify Jig parser to allow wildcard-search using preg_match()
+*	Abort execution after error() execution
+*	Concatenate cached/uncached minify() iterations; Prevent spillover
+	caching of previous minify() result
+*	Work around obscure PHP session id regeneration bug
+*	Revise algorithm for Jig filter involving undefined fields (Issue #230)
+*	Use checkdnsrr() instead of gethostbyname() in DNSBL check
+*	Auto-adjust pagination to cursor boundaries
+*	Add Romanian diacritics
+*	Bug fix: Root namespace reference and sorting with undefined Jig fields
+*	Bug fix: Greedy receive() regex
+*	Bug fix: Default LANGUAGE always 'en'
+*	Bug fix: minify() hammers cache backend
+*	Bug fix: Previous values of primary keys not saved during factory()
+	instantiation
+*	Bug fix: Jig find() fails when search key is not present in all records
+*	Bug fix: Jig SORT_DESC (Issue #233)
+*	Bug fix: Error reporting (Issue #225)
+*	Bug fix: language() return value
+
+3.0.3 (29 Dec 2013)
+*	NEW: [ajax] and [sync] routing pattern modifiers
+*	NEW: Basket class (session-based pseudo-mapper, shopping cart, etc.)
+*	NEW: Test->message() method
+*	NEW: DB profiling via DB->log()
+*	NEW: Matrix->calendar()
+*	NEW: Audit->card() and Audit->mod10() for credit card verification
+*	NEW: Geo->weather()
+*	NEW: Base->relay() accepts comma-separated callbacks; but unlike
+	Base->chain(), result of previous callback becomes argument of the next
+*	Numerous performance tweaks
+*	Interoperability with new MongoClient class
+*	Web->request() now recognizes gzip and deflate encoding
+*	Differences in behavior of Web->request() engines rectified
+*	mutex() now uses an ID as argument (instead of filename to make it clear
+	that specified file is not the target being locked, but a primitive
+	cross-platform semaphore)
+*	DB\SQL\Mapper field _id now returned even in the absence of any
+	auto-increment field
+*	Magic class spinned off as a separate file
+*	ISO 3166-1 alpha-2 table updated
+*	Apache redirect emulation for PHP 5.4 CLI server mode
+*	Framework instance now passed as argument to any user-defined shutdown
+	function
+*	Cache engine now used as storage for Web->minify() output
+*	Flag added for enabling/disabling Image class filter history
+*	Bug fix: Trailing routing token consumes HTTP query
+*	Bug fix: LANGUAGE spills over to LOCALES setting
+*	Bug fix: Inconsistent dry() return value
+*	Bug fix: URL-decoding
+
+3.0.2 (23 Dec 2013)
+*	NEW: Syntax-highlighted stack traces via Base->highlight(); boolean
+	HIGHLIGHT global variable can be used to enable/disable this feature
+*	NEW: Template engine <ignore> tag
+*	NEW: Image->captcha()
+*	NEW: DNSBL-based spammer detection (ported from 2.x)
+*	NEW: paginate(), first(), and last() methods for data mappers
+*	NEW: X-HTTP-Method-Override header now recognized
+*	NEW: Base->chain() method for executing callbacks in succession
+*	NEW: HOST global variable; derived from either $_SERVER['SERVER_NAME'] or
+	gethostname()
+*	NEW: REALM global variable representing full canonical URI
+*	NEW: Auth plug-in
+*	NEW: Pingback plug-in (implements both Pingback 1.0 protocol client and
+	server)
+*	NEW: DEBUG verbosity can now reach up to level 3; Base->stringify() drills
+	down to object properties at this setting
+*	NEW: HTTP PATCH method added to recognized HTTP ReST methods
+*	Web->slug() now trims trailing dashes
+*	Web->request() now allows relative local URLs as argument
+*	Use of PARAMS in route handlers now unnecessary; framework now passes two
+	arguments to route handlers: the framework object instance and an array
+	containing the captured values of tokens in route patterns
+*	Standardized timeout settings among Web->request() backends
+*	Session IDs regenerated for additional security
+*	Automatic HTTP 404 responses by Base->call() now restricted to route
+	handlers
+*	Empty comments in ini-style files now parsed properly
+*	Use file_get_contents() in methods that don't involve high concurrency
+
+3.0.1 (14 Dec 2013)
+*	Major rewrite of much of the framework's core features

+ 621 - 0
frameworks/PHP/fat-free/src/COPYING

@@ -0,0 +1,621 @@
+GNU GENERAL PUBLIC LICENSE
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+
+Preamble
+
+The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+TERMS AND CONDITIONS
+
+0. Definitions.
+
+"This License" refers to version 3 of the GNU General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+1. Source Code.
+
+The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+The Corresponding Source for a work in source code form is that
+same work.
+
+2. Basic Permissions.
+
+All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+a) The work must carry prominent notices stating that you modified
+it, and giving a relevant date.
+
+b) The work must carry prominent notices stating that it is
+released under this License and any conditions added under section
+7. This requirement modifies the requirement in section 4 to
+"keep intact all notices".
+
+c) You must license the entire work, as a whole, under this
+License to anyone who comes into possession of a copy. This
+License will therefore apply, along with any applicable section 7
+additional terms, to the whole of the work, and all its parts,
+regardless of how they are packaged. This License gives no
+permission to license the work in any other way, but it does not
+invalidate such permission if you have separately received it.
+
+d) If the work has interactive user interfaces, each must display
+Appropriate Legal Notices; however, if the Program has interactive
+interfaces that do not display Appropriate Legal Notices, your
+work need not make them do so.
+
+A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+a) Convey the object code in, or embodied in, a physical product
+(including a physical distribution medium), accompanied by the
+Corresponding Source fixed on a durable physical medium
+customarily used for software interchange.
+
+b) Convey the object code in, or embodied in, a physical product
+(including a physical distribution medium), accompanied by a
+written offer, valid for at least three years and valid for as
+long as you offer spare parts or customer support for that product
+model, to give anyone who possesses the object code either (1) a
+copy of the Corresponding Source for all the software in the
+product that is covered by this License, on a durable physical
+medium customarily used for software interchange, for a price no
+more than your reasonable cost of physically performing this
+conveying of source, or (2) access to copy the
+Corresponding Source from a network server at no charge.
+
+c) Convey individual copies of the object code with a copy of the
+written offer to provide the Corresponding Source. This
+alternative is allowed only occasionally and noncommercially, and
+only if you received the object code with such an offer, in accord
+with subsection 6b.
+
+d) Convey the object code by offering access from a designated
+place (gratis or for a charge), and offer equivalent access to the
+Corresponding Source in the same way through the same place at no
+further charge. You need not require recipients to copy the
+Corresponding Source along with the object code. If the place to
+copy the object code is a network server, the Corresponding Source
+may be on a different server (operated by you or a third party)
+that supports equivalent copying facilities, provided you maintain
+clear directions next to the object code saying where to find the
+Corresponding Source. Regardless of what server hosts the
+Corresponding Source, you remain obligated to ensure that it is
+available for as long as needed to satisfy these requirements.
+
+e) Convey the object code using peer-to-peer transmission, provided
+you inform other peers where the object code and Corresponding
+Source of the work are being offered to the general public at no
+charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+a) Disclaiming warranty or limiting liability differently from the
+terms of sections 15 and 16 of this License; or
+
+b) Requiring preservation of specified reasonable legal notices or
+author attributions in that material or in the Appropriate Legal
+Notices displayed by works containing it; or
+
+c) Prohibiting misrepresentation of the origin of that material, or
+requiring that modified versions of such material be marked in
+reasonable ways as different from the original version; or
+
+d) Limiting the use for publicity purposes of names of licensors or
+authors of the material; or
+
+e) Declining to grant rights under trademark law for use of some
+trade names, trademarks, or service marks; or
+
+f) Requiring indemnification of licensors and authors of that
+material by anyone who conveys the material (or modified versions of
+it) with contractual assumptions of liability to the recipient, for
+any liability that these contractual assumptions directly impose on
+those licensors and authors.
+
+All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+8. Termination.
+
+You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+13. Use with the GNU Affero General Public License.
+
+Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS

+ 3087 - 0
frameworks/PHP/fat-free/src/base.php

@@ -0,0 +1,3087 @@
+<?php
+
+/*
+
+	Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfreeframework.com).
+
+	This is free software: you can redistribute it and/or modify it under the
+	terms of the GNU General Public License as published by the Free Software
+	Foundation, either version 3 of the License, or later.
+
+	Fat-Free Framework is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+	General Public License for more details.
+
+	You should have received a copy of the GNU General Public License along
+	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+//! Factory class for single-instance objects
+abstract class Prefab {
+
+	/**
+	*	Return class instance
+	*	@return static
+	**/
+	static function instance() {
+		if (!Registry::exists($class=get_called_class())) {
+			$ref=new Reflectionclass($class);
+			$args=func_get_args();
+			Registry::set($class,
+				$args?$ref->newinstanceargs($args):new $class);
+		}
+		return Registry::get($class);
+	}
+
+}
+
+//! Base structure
+final class Base extends Prefab implements ArrayAccess {
+
+	//@{ Framework details
+	const
+		PACKAGE='Fat-Free Framework',
+		VERSION='3.5.0-Release';
+	//@}
+
+	//@{ HTTP status codes (RFC 2616)
+	const
+		HTTP_100='Continue',
+		HTTP_101='Switching Protocols',
+		HTTP_200='OK',
+		HTTP_201='Created',
+		HTTP_202='Accepted',
+		HTTP_203='Non-Authorative Information',
+		HTTP_204='No Content',
+		HTTP_205='Reset Content',
+		HTTP_206='Partial Content',
+		HTTP_300='Multiple Choices',
+		HTTP_301='Moved Permanently',
+		HTTP_302='Found',
+		HTTP_303='See Other',
+		HTTP_304='Not Modified',
+		HTTP_305='Use Proxy',
+		HTTP_307='Temporary Redirect',
+		HTTP_400='Bad Request',
+		HTTP_401='Unauthorized',
+		HTTP_402='Payment Required',
+		HTTP_403='Forbidden',
+		HTTP_404='Not Found',
+		HTTP_405='Method Not Allowed',
+		HTTP_406='Not Acceptable',
+		HTTP_407='Proxy Authentication Required',
+		HTTP_408='Request Timeout',
+		HTTP_409='Conflict',
+		HTTP_410='Gone',
+		HTTP_411='Length Required',
+		HTTP_412='Precondition Failed',
+		HTTP_413='Request Entity Too Large',
+		HTTP_414='Request-URI Too Long',
+		HTTP_415='Unsupported Media Type',
+		HTTP_416='Requested Range Not Satisfiable',
+		HTTP_417='Expectation Failed',
+		HTTP_500='Internal Server Error',
+		HTTP_501='Not Implemented',
+		HTTP_502='Bad Gateway',
+		HTTP_503='Service Unavailable',
+		HTTP_504='Gateway Timeout',
+		HTTP_505='HTTP Version Not Supported';
+	//@}
+
+	const
+		//! Mapped PHP globals
+		GLOBALS='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV',
+		//! HTTP verbs
+		VERBS='GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT',
+		//! Default directory permissions
+		MODE=0755,
+		//! Syntax highlighting stylesheet
+		CSS='code.css';
+
+	//@{ HTTP request types
+	const
+		REQ_SYNC=1,
+		REQ_AJAX=2;
+	//@}
+
+	//@{ Error messages
+	const
+		E_Pattern='Invalid routing pattern: %s',
+		E_Named='Named route does not exist: %s',
+		E_Fatal='Fatal error: %s',
+		E_Open='Unable to open %s',
+		E_Routes='No routes specified',
+		E_Class='Invalid class %s',
+		E_Method='Invalid method %s',
+		E_Hive='Invalid hive key %s';
+	//@}
+
+	private
+		//! Globals
+		$hive,
+		//! Initial settings
+		$init,
+		//! Language lookup sequence
+		$languages,
+		//! Default fallback language
+		$fallback='en';
+
+	/**
+	*	Sync PHP global with corresponding hive key
+	*	@return array
+	*	@param $key string
+	**/
+	function sync($key) {
+		return $this->hive[$key]=&$GLOBALS['_'.$key];
+	}
+
+	/**
+	*	Return the parts of specified hive key
+	*	@return array
+	*	@param $key string
+	**/
+	private function cut($key) {
+		return preg_split('/\[\h*[\'"]?(.+?)[\'"]?\h*\]|(->)|\./',
+			$key,NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
+	}
+
+	/**
+	*	Replace tokenized URL with available token values
+	*	@return string
+	*	@param $url array|string
+	*	@param $params array
+	**/
+	function build($url,$params=array()) {
+		$params+=$this->hive['PARAMS'];
+		if (is_array($url))
+			foreach ($url as &$var) {
+				$var=$this->build($var,$params);
+				unset($var);
+			}
+		else {
+			$i=0;
+			$url=preg_replace_callback('/@(\w+)|\*/',
+				function($match) use(&$i,$params) {
+					$i++;
+					if (isset($match[1]) &&
+						array_key_exists($match[1],$params))
+						return $params[$match[1]];
+					return array_key_exists($i,$params)?
+						$params[$i]:
+						$match[0];
+				},$url);
+		}
+		return $url;
+	}
+
+	/**
+	*	assemble url from alias name
+	*	@return NULL
+	*	@param $name string
+	*	@param $params array|string
+	**/
+	function alias($name,$params=array()) {
+		if (!is_array($params))
+			$params=$this->parse($params);
+		if (empty($this->hive['ALIASES'][$name]))
+			user_error(sprintf(self::E_Named,$name),E_USER_ERROR);
+		$url=$this->build($this->hive['ALIASES'][$name],$params);
+		return $url;
+	}
+
+	/**
+	*	Parse string containing key-value pairs
+	*	@return array
+	*	@param $str string
+	**/
+	function parse($str) {
+		preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/',
+			$str,$pairs,PREG_SET_ORDER);
+		$out=array();
+		foreach ($pairs as $pair)
+			$out[$pair[1]]=trim($pair[2]);
+		return $out;
+	}
+
+	/**
+	*	Convert JS-style token to PHP expression
+	*	@return string
+	*	@param $str string
+	**/
+	function compile($str) {
+		$fw=$this;
+		return preg_replace_callback(
+			'/(?<!\w)@(\w(?:[\w\.\[\]\(]|\->|::)*)/',
+			function($var) use($fw) {
+				return '$'.preg_replace_callback(
+					'/\.(\w+)\(|\.(\w+)|\[((?:[^\[\]]*|(?R))*)\]/',
+					function($expr) use($fw) {
+						return $expr[1]?
+							((function_exists($expr[1])?
+								('.'.$expr[1]):
+								('['.var_export($expr[1],TRUE).']')).'('):
+							('['.var_export(
+								isset($expr[3])?
+									$fw->compile($expr[3]):
+									(ctype_digit($expr[2])?
+										(int)$expr[2]:
+										$expr[2]),TRUE).']');
+					},
+					$var[1]
+				);
+			},
+			$str
+		);
+	}
+
+	/**
+	*	Get hive key reference/contents; Add non-existent hive keys,
+	*	array elements, and object properties by default
+	*	@return mixed
+	*	@param $key string
+	*	@param $add bool
+	**/
+	function &ref($key,$add=TRUE) {
+		$null=NULL;
+		$parts=$this->cut($key);
+		if ($parts[0]=='SESSION') {
+			@session_start();
+			$this->sync('SESSION');
+		}
+		elseif (!preg_match('/^\w+$/',$parts[0]))
+			user_error(sprintf(self::E_Hive,$this->stringify($key)),
+				E_USER_ERROR);
+		if ($add)
+			$var=&$this->hive;
+		else
+			$var=$this->hive;
+		$obj=FALSE;
+		foreach ($parts as $part)
+			if ($part=='->')
+				$obj=TRUE;
+			elseif ($obj) {
+				$obj=FALSE;
+				if (!is_object($var))
+					$var=new stdclass;
+				if ($add || property_exists($var,$part))
+					$var=&$var->$part;
+				else {
+					$var=&$null;
+					break;
+				}
+			}
+			else {
+				if (!is_array($var))
+					$var=array();
+				if ($add || array_key_exists($part,$var))
+					$var=&$var[$part];
+				else {
+					$var=&$null;
+					break;
+				}
+			}
+		if ($parts[0]=='ALIASES')
+			$var=$this->build($var);
+		return $var;
+	}
+
+	/**
+	*	Return TRUE if hive key is set
+	*	(or return timestamp and TTL if cached)
+	*	@return bool
+	*	@param $key string
+	*	@param $val mixed
+	**/
+	function exists($key,&$val=NULL) {
+		$val=$this->ref($key,FALSE);
+		return isset($val)?
+			TRUE:
+			(Cache::instance()->exists($this->hash($key).'.var',$val)?:FALSE);
+	}
+
+	/**
+	*	Return TRUE if hive key is empty and not cached
+	*	@return bool
+	*	@param $key string
+	**/
+	function devoid($key) {
+		$val=$this->ref($key,FALSE);
+		return empty($val) &&
+			(!Cache::instance()->exists($this->hash($key).'.var',$val) ||
+				!$val);
+	}
+
+	/**
+	*	Bind value to hive key
+	*	@return mixed
+	*	@param $key string
+	*	@param $val mixed
+	*	@param $ttl int
+	**/
+	function set($key,$val,$ttl=0) {
+		$time=time();
+		if (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) {
+			$this->set('REQUEST'.$expr[2],$val);
+			if ($expr[1]=='COOKIE') {
+				$parts=$this->cut($key);
+				$jar=$this->unserialize($this->serialize($this->hive['JAR']));
+				if ($ttl)
+					$jar['expire']=$time+$ttl;
+				call_user_func_array('setcookie',array($parts[1],$val)+$jar);
+				return $val;
+			}
+		}
+		else switch ($key) {
+		case 'CACHE':
+			$val=Cache::instance()->load($val,TRUE);
+			break;
+		case 'ENCODING':
+			ini_set('default_charset',$val);
+			if (extension_loaded('mbstring'))
+				mb_internal_encoding($val);
+			break;
+		case 'FALLBACK':
+			$this->fallback=$val;
+			$lang=$this->language($this->hive['LANGUAGE']);
+		case 'LANGUAGE':
+			if (!isset($lang))
+				$val=$this->language($val);
+			$lex=$this->lexicon($this->hive['LOCALES']);
+		case 'LOCALES':
+			if (isset($lex) || $lex=$this->lexicon($val))
+				$this->mset($lex,$this->hive['PREFIX'],$ttl);
+			break;
+		case 'TZ':
+			date_default_timezone_set($val);
+			break;
+		}
+		$ref=&$this->ref($key);
+		$ref=$val;
+		if (preg_match('/^JAR\b/',$key)) {
+			$jar=$this->unserialize($this->serialize($this->hive['JAR']));
+			$jar['expire']-=$time;
+			call_user_func_array('session_set_cookie_params',$jar);
+		}
+		$cache=Cache::instance();
+		if ($cache->exists($hash=$this->hash($key).'.var') || $ttl)
+			// Persist the key-value pair
+			$cache->set($hash,$val,$ttl);
+		return $ref;
+	}
+
+	/**
+	*	Retrieve contents of hive key
+	*	@return mixed
+	*	@param $key string
+	*	@param $args string|array
+	**/
+	function get($key,$args=NULL) {
+		if (is_string($val=$this->ref($key,FALSE)) && !is_null($args))
+			return call_user_func_array(
+				array($this,'format'),
+				array_merge(array($val),is_array($args)?$args:array($args))
+			);
+		if (is_null($val)) {
+			// Attempt to retrieve from cache
+			if (Cache::instance()->exists($this->hash($key).'.var',$data))
+				return $data;
+		}
+		return $val;
+	}
+
+	/**
+	*	Unset hive key
+	*	@return NULL
+	*	@param $key string
+	**/
+	function clear($key) {
+		// Normalize array literal
+		$cache=Cache::instance();
+		$parts=$this->cut($key);
+		if ($key=='CACHE')
+			// Clear cache contents
+			$cache->reset();
+		elseif (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) {
+			$this->clear('REQUEST'.$expr[2]);
+			if ($expr[1]=='COOKIE') {
+				$parts=$this->cut($key);
+				$jar=$this->hive['JAR'];
+				$jar['expire']=strtotime('-1 year');
+				call_user_func_array('setcookie',
+					array_merge(array($parts[1],''),$jar));
+				unset($_COOKIE[$parts[1]]);
+			}
+		}
+		elseif ($parts[0]=='SESSION') {
+			@session_start();
+			if (empty($parts[1])) {
+				// End session
+				session_unset();
+				session_destroy();
+				unset($_COOKIE[session_name()]);
+				header_remove('Set-Cookie');
+			}
+			$this->sync('SESSION');
+		}
+		if (!isset($parts[1]) && array_key_exists($parts[0],$this->init))
+			// Reset global to default value
+			$this->hive[$parts[0]]=$this->init[$parts[0]];
+		else {
+			eval('unset('.$this->compile('@this->hive.'.$key).');');
+			if ($parts[0]=='SESSION') {
+				session_commit();
+				session_start();
+			}
+			if ($cache->exists($hash=$this->hash($key).'.var'))
+				// Remove from cache
+				$cache->clear($hash);
+		}
+	}
+
+	/**
+	*	Return TRUE if hive variable is 'on'
+	*	@return bool
+	*	@param $key string
+	**/
+	function checked($key) {
+		$ref=&$this->ref($key);
+		return $ref=='on';
+	}
+
+	/**
+	*	Return TRUE if property has public visibility
+	*	@return bool
+	*	@param $obj object
+	*	@param $key string
+	**/
+	function visible($obj,$key) {
+		if (property_exists($obj,$key)) {
+			$ref=new ReflectionProperty(get_class($obj),$key);
+			$out=$ref->ispublic();
+			unset($ref);
+			return $out;
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Multi-variable assignment using associative array
+	*	@return NULL
+	*	@param $vars array
+	*	@param $prefix string
+	*	@param $ttl int
+	**/
+	function mset(array $vars,$prefix='',$ttl=0) {
+		foreach ($vars as $key=>$val)
+			$this->set($prefix.$key,$val,$ttl);
+	}
+
+	/**
+	*	Publish hive contents
+	*	@return array
+	**/
+	function hive() {
+		return $this->hive;
+	}
+
+	/**
+	*	Copy contents of hive variable to another
+	*	@return mixed
+	*	@param $src string
+	*	@param $dst string
+	**/
+	function copy($src,$dst) {
+		$ref=&$this->ref($dst);
+		return $ref=$this->ref($src,FALSE);
+	}
+
+	/**
+	*	Concatenate string to hive string variable
+	*	@return string
+	*	@param $key string
+	*	@param $val string
+	**/
+	function concat($key,$val) {
+		$ref=&$this->ref($key);
+		$ref.=$val;
+		return $ref;
+	}
+
+	/**
+	*	Swap keys and values of hive array variable
+	*	@return array
+	*	@param $key string
+	*	@public
+	**/
+	function flip($key) {
+		$ref=&$this->ref($key);
+		return $ref=array_combine(array_values($ref),array_keys($ref));
+	}
+
+	/**
+	*	Add element to the end of hive array variable
+	*	@return mixed
+	*	@param $key string
+	*	@param $val mixed
+	**/
+	function push($key,$val) {
+		$ref=&$this->ref($key);
+		$ref[] = $val;
+		return $val;
+	}
+
+	/**
+	*	Remove last element of hive array variable
+	*	@return mixed
+	*	@param $key string
+	**/
+	function pop($key) {
+		$ref=&$this->ref($key);
+		return array_pop($ref);
+	}
+
+	/**
+	*	Add element to the beginning of hive array variable
+	*	@return mixed
+	*	@param $key string
+	*	@param $val mixed
+	**/
+	function unshift($key,$val) {
+		$ref=&$this->ref($key);
+		array_unshift($ref,$val);
+		return $val;
+	}
+
+	/**
+	*	Remove first element of hive array variable
+	*	@return mixed
+	*	@param $key string
+	**/
+	function shift($key) {
+		$ref=&$this->ref($key);
+		return array_shift($ref);
+	}
+
+	/**
+	*	Merge array with hive array variable
+	*	@return array
+	*	@param $key string
+	*	@param $src string|array
+	**/
+	function merge($key,$src) {
+		$ref=&$this->ref($key);
+		return array_merge($ref,is_string($src)?$this->hive[$src]:$src);
+	}
+
+	/**
+	*	Convert backslashes to slashes
+	*	@return string
+	*	@param $str string
+	**/
+	function fixslashes($str) {
+		return $str?strtr($str,'\\','/'):$str;
+	}
+
+	/**
+	*	Split comma-, semi-colon, or pipe-separated string
+	*	@return array
+	*	@param $str string
+	*	@param $noempty bool
+	**/
+	function split($str,$noempty=TRUE) {
+		return array_map('trim',
+			preg_split('/[,;|]/',$str,0,$noempty?PREG_SPLIT_NO_EMPTY:0));
+	}
+
+	/**
+	*	Convert PHP expression/value to compressed exportable string
+	*	@return string
+	*	@param $arg mixed
+	*	@param $stack array
+	**/
+	function stringify($arg,array $stack=NULL) {
+		if ($stack) {
+			foreach ($stack as $node)
+				if ($arg===$node)
+					return '*RECURSION*';
+		}
+		else
+			$stack=array();
+		switch (gettype($arg)) {
+			case 'object':
+				$str='';
+				foreach (get_object_vars($arg) as $key=>$val)
+					$str.=($str?',':'').
+						var_export($key,TRUE).'=>'.
+						$this->stringify($val,
+							array_merge($stack,array($arg)));
+				return get_class($arg).'::__set_state(array('.$str.'))';
+			case 'array':
+				$str='';
+				$num=isset($arg[0]) &&
+					ctype_digit(implode('',array_keys($arg)));
+				foreach ($arg as $key=>$val)
+					$str.=($str?',':'').
+						($num?'':(var_export($key,TRUE).'=>')).
+						$this->stringify($val,
+							array_merge($stack,array($arg)));
+				return 'array('.$str.')';
+			default:
+				return var_export($arg,TRUE);
+		}
+	}
+
+	/**
+	*	Flatten array values and return as CSV string
+	*	@return string
+	*	@param $args array
+	**/
+	function csv(array $args) {
+		return implode(',',array_map('stripcslashes',
+			array_map(array($this,'stringify'),$args)));
+	}
+
+	/**
+	*	Convert snakecase string to camelcase
+	*	@return string
+	*	@param $str string
+	**/
+	function camelcase($str) {
+		return preg_replace_callback(
+			'/_(\w)/',
+			function($match) {
+				return strtoupper($match[1]);
+			},
+			$str
+		);
+	}
+
+	/**
+	*	Convert camelcase string to snakecase
+	*	@return string
+	*	@param $str string
+	**/
+	function snakecase($str) {
+		return strtolower(preg_replace('/[[:upper:]]/','_\0',$str));
+	}
+
+	/**
+	*	Return -1 if specified number is negative, 0 if zero,
+	*	or 1 if the number is positive
+	*	@return int
+	*	@param $num mixed
+	**/
+	function sign($num) {
+		return $num?($num/abs($num)):0;
+	}
+
+	/**
+	*	Convert class constants to array
+	*	@return array
+	*	@param $class object|string
+	*	@param $prefix string
+	**/
+	function constants($class,$prefix='') {
+		$ref=new ReflectionClass($class);
+		$out=array();
+		foreach (preg_grep('/^'.$prefix.'/',array_keys($ref->getconstants()))
+			as $val) {
+			$out[$key=substr($val,strlen($prefix))]=
+				constant((is_object($class)?get_class($class):$class).'::'.$prefix.$key);
+		}
+		unset($ref);
+		return $out;
+	}
+
+	/**
+	*	Generate 64bit/base36 hash
+	*	@return string
+	*	@param $str
+	**/
+	function hash($str) {
+		return str_pad(base_convert(
+			substr(sha1($str),-16),16,36),11,'0',STR_PAD_LEFT);
+	}
+
+	/**
+	*	Return Base64-encoded equivalent
+	*	@return string
+	*	@param $data string
+	*	@param $mime string
+	**/
+	function base64($data,$mime) {
+		return 'data:'.$mime.';base64,'.base64_encode($data);
+	}
+
+	/**
+	*	Convert special characters to HTML entities
+	*	@return string
+	*	@param $str string
+	**/
+	function encode($str) {
+		return @htmlspecialchars($str,$this->hive['BITMASK'],
+			$this->hive['ENCODING'])?:$this->scrub($str);
+	}
+
+	/**
+	*	Convert HTML entities back to characters
+	*	@return string
+	*	@param $str string
+	**/
+	function decode($str) {
+		return htmlspecialchars_decode($str,$this->hive['BITMASK']);
+	}
+
+	/**
+	*	Invoke callback recursively for all data types
+	*	@return mixed
+	*	@param $arg mixed
+	*	@param $func callback
+	*	@param $stack array
+	**/
+	function recursive($arg,$func,$stack=NULL) {
+		if ($stack) {
+			foreach ($stack as $node)
+				if ($arg===$node)
+					return $arg;
+		}
+		else
+			$stack=array();
+		switch (gettype($arg)) {
+			case 'object':
+				if (method_exists('ReflectionClass','iscloneable')) {
+					$ref=new ReflectionClass($arg);
+					if ($ref->iscloneable()) {
+						$arg=clone($arg);
+						$cast=is_a($arg,'IteratorAggregate')?
+							iterator_to_array($arg):get_object_vars($arg);
+						foreach ($cast as $key=>$val)
+							$arg->$key=$this->recursive(
+								$val,$func,array_merge($stack,array($arg)));
+					}
+				}
+				return $arg;
+			case 'array':
+				$copy=array();
+				foreach ($arg as $key=>$val)
+					$copy[$key]=$this->recursive($val,$func,
+						array_merge($stack,array($arg)));
+				return $copy;
+		}
+		return $func($arg);
+	}
+
+	/**
+	*	Remove HTML tags (except those enumerated) and non-printable
+	*	characters to mitigate XSS/code injection attacks
+	*	@return mixed
+	*	@param $arg mixed
+	*	@param $tags string
+	**/
+	function clean($arg,$tags=NULL) {
+		$fw=$this;
+		return $this->recursive($arg,
+			function($val) use($fw,$tags) {
+				if ($tags!='*')
+					$val=trim(strip_tags($val,
+						'<'.implode('><',$fw->split($tags)).'>'));
+				return trim(preg_replace(
+					'/[\x00-\x08\x0B\x0C\x0E-\x1F]/','',$val));
+			}
+		);
+	}
+
+	/**
+	*	Similar to clean(), except that variable is passed by reference
+	*	@return mixed
+	*	@param $var mixed
+	*	@param $tags string
+	**/
+	function scrub(&$var,$tags=NULL) {
+		return $var=$this->clean($var,$tags);
+	}
+
+	/**
+	*	Return locale-aware formatted string
+	*	@return string
+	**/
+	function format() {
+		$args=func_get_args();
+		$val=array_shift($args);
+		// Get formatting rules
+		$conv=localeconv();
+		return preg_replace_callback(
+			'/\{(?P<pos>\d+)\s*(?:,\s*(?P<type>\w+)\s*'.
+			'(?:,\s*(?P<mod>(?:\w+(?:\s*\{.+?\}\s*,?)?)*)'.
+			'(?:,\s*(?P<prop>.+?))?)?)?\}/',
+			function($expr) use($args,$conv) {
+				extract($expr);
+				extract($conv);
+				if (!array_key_exists($pos,$args))
+					return $expr[0];
+				if (isset($type))
+					switch ($type) {
+						case 'plural':
+							preg_match_all('/(?<tag>\w+)'.
+								'(?:\s*\{\s*(?<data>.+?)\s*\})/',
+								$mod,$matches,PREG_SET_ORDER);
+							$ord=array('zero','one','two');
+							foreach ($matches as $match) {
+								extract($match);
+								if (isset($ord[$args[$pos]]) &&
+									$tag==$ord[$args[$pos]] || $tag=='other')
+									return str_replace('#',$args[$pos],$data);
+							}
+						case 'number':
+							if (isset($mod))
+								switch ($mod) {
+									case 'integer':
+										return number_format(
+											$args[$pos],0,'',$thousands_sep);
+									case 'currency':
+										if (function_exists('money_format'))
+											return money_format(
+												'%n',$args[$pos]);
+										$fmt=array(
+											0=>'(nc)',1=>'(n c)',
+											2=>'(nc)',10=>'+nc',
+											11=>'+n c',12=>'+ nc',
+											20=>'nc+',21=>'n c+',
+											22=>'nc +',30=>'n+c',
+											31=>'n +c',32=>'n+ c',
+											40=>'nc+',41=>'n c+',
+											42=>'nc +',100=>'(cn)',
+											101=>'(c n)',102=>'(cn)',
+											110=>'+cn',111=>'+c n',
+											112=>'+ cn',120=>'cn+',
+											121=>'c n+',122=>'cn +',
+											130=>'+cn',131=>'+c n',
+											132=>'+ cn',140=>'c+n',
+											141=>'c+ n',142=>'c +n'
+										);
+										if ($args[$pos]<0) {
+											$sgn=$negative_sign;
+											$pre='n';
+										}
+										else {
+											$sgn=$positive_sign;
+											$pre='p';
+										}
+										return str_replace(
+											array('+','n','c'),
+											array($sgn,number_format(
+												abs($args[$pos]),
+												$frac_digits,
+												$decimal_point,
+												$thousands_sep),
+												$currency_symbol),
+											$fmt[(int)(
+												(${$pre.'_cs_precedes'}%2).
+												(${$pre.'_sign_posn'}%5).
+												(${$pre.'_sep_by_space'}%3)
+											)]
+										);
+									case 'percent':
+										return number_format(
+											$args[$pos]*100,0,$decimal_point,
+											$thousands_sep).'%';
+									case 'decimal':
+										return number_format(
+											$args[$pos],$prop,$decimal_point,
+												$thousands_sep);
+								}
+							break;
+						case 'date':
+							if (empty($mod) || $mod=='short')
+								$prop='%x';
+							elseif ($mod=='long')
+								$prop='%A, %d %B %Y';
+							return strftime($prop,$args[$pos]);
+						case 'time':
+							if (empty($mod) || $mod=='short')
+								$prop='%X';
+							return strftime($prop,$args[$pos]);
+						default:
+							return $expr[0];
+					}
+				return $args[$pos];
+			},
+			$val
+		);
+	}
+
+	/**
+	*	Assign/auto-detect language
+	*	@return string
+	*	@param $code string
+	**/
+	function language($code) {
+		$code=preg_replace('/\h+|;q=[0-9.]+/','',$code);
+		$code.=($code?',':'').$this->fallback;
+		$this->languages=array();
+		foreach (array_reverse(explode(',',$code)) as $lang) {
+			if (preg_match('/^(\w{2})(?:-(\w{2}))?\b/i',$lang,$parts)) {
+				// Generic language
+				array_unshift($this->languages,$parts[1]);
+				if (isset($parts[2])) {
+					// Specific language
+					$parts[0]=$parts[1].'-'.($parts[2]=strtoupper($parts[2]));
+					array_unshift($this->languages,$parts[0]);
+				}
+			}
+		}
+		$this->languages=array_unique($this->languages);
+		$locales=array();
+		$windows=preg_match('/^win/i',PHP_OS);
+		foreach ($this->languages as $locale) {
+			if ($windows) {
+				$parts=explode('-',$locale);
+				$locale=@constant('ISO::LC_'.$parts[0]);
+				if (isset($parts[1]) &&
+					$country=@constant('ISO::CC_'.strtolower($parts[1])))
+					$locale.='-'.$country;
+			}
+			$locales[]=$locale;
+			$locales[]=$locale.'.'.ini_get('default_charset');
+		}
+		setlocale(LC_ALL,str_replace('-','_',$locales));
+		return implode(',',$this->languages);
+	}
+
+	/**
+	*	Return lexicon entries
+	*	@return array
+	*	@param $path string
+	**/
+	function lexicon($path) {
+		$lex=array();
+		foreach ($this->languages?:explode(',',$this->fallback) as $lang)
+			foreach ($this->split($path) as $dir)
+				if ((is_file($file=($base=$dir.$lang).'.php') ||
+					is_file($file=$base.'.php')) &&
+					is_array($dict=require($file)))
+					$lex+=$dict;
+				elseif (is_file($file=$base.'.ini')) {
+					preg_match_all(
+						'/(?<=^|\n)(?:'.
+							'\[(?<prefix>.+?)\]|'.
+							'(?<lval>[^\h\r\n;].*?)\h*=\h*'.
+							'(?<rval>(?:\\\\\h*\r?\n|.+?)*)'.
+						')(?=\r?\n|$)/',
+						$this->read($file),$matches,PREG_SET_ORDER);
+					if ($matches) {
+						$prefix='';
+						foreach ($matches as $match)
+							if ($match['prefix'])
+								$prefix=$match['prefix'].'.';
+							elseif (!array_key_exists(
+								$key=$prefix.$match['lval'],$lex))
+								$lex[$key]=trim(preg_replace(
+									'/\\\\\h*\r?\n/','',$match['rval']));
+					}
+				}
+		return $lex;
+	}
+
+	/**
+	*	Return string representation of PHP value
+	*	@return string
+	*	@param $arg mixed
+	**/
+	function serialize($arg) {
+		switch (strtolower($this->hive['SERIALIZER'])) {
+			case 'igbinary':
+				return igbinary_serialize($arg);
+			default:
+				return serialize($arg);
+		}
+	}
+
+	/**
+	*	Return PHP value derived from string
+	*	@return string
+	*	@param $arg mixed
+	**/
+	function unserialize($arg) {
+		switch (strtolower($this->hive['SERIALIZER'])) {
+			case 'igbinary':
+				return igbinary_unserialize($arg);
+			default:
+				return unserialize($arg);
+		}
+	}
+
+	/**
+	*	Send HTTP status header; Return text equivalent of status code
+	*	@return string
+	*	@param $code int
+	**/
+	function status($code) {
+		$reason=@constant('self::HTTP_'.$code);
+		if (PHP_SAPI!='cli')
+			header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason);
+		return $reason;
+	}
+
+	/**
+	*	Send cache metadata to HTTP client
+	*	@return NULL
+	*	@param $secs int
+	**/
+	function expire($secs=0) {
+		if (PHP_SAPI!='cli') {
+			header('X-Content-Type-Options: nosniff');
+			header('X-Frame-Options: '.$this->hive['XFRAME']);
+			header('X-Powered-By: '.$this->hive['PACKAGE']);
+			header('X-XSS-Protection: 1; mode=block');
+			if ($secs) {
+				$time=microtime(TRUE);
+				header_remove('Pragma');
+				header('Expires: '.gmdate('r',$time+$secs));
+				header('Cache-Control: max-age='.$secs);
+				header('Last-Modified: '.gmdate('r'));
+			}
+			else
+				header('Cache-Control: no-cache, no-store, must-revalidate');
+		}
+	}
+
+	/**
+	*	Return HTTP user agent
+	*	@return string
+	**/
+	function agent() {
+		$headers=$this->hive['HEADERS'];
+		return isset($headers['X-Operamini-Phone-UA'])?
+			$headers['X-Operamini-Phone-UA']:
+			(isset($headers['X-Skyfire-Phone'])?
+				$headers['X-Skyfire-Phone']:
+				(isset($headers['User-Agent'])?
+					$headers['User-Agent']:''));
+	}
+
+	/**
+	*	Return TRUE if XMLHttpRequest detected
+	*	@return bool
+	**/
+	function ajax() {
+		$headers=$this->hive['HEADERS'];
+		return isset($headers['X-Requested-With']) &&
+			$headers['X-Requested-With']=='XMLHttpRequest';
+	}
+
+	/**
+	*	Sniff IP address
+	*	@return string
+	**/
+	function ip() {
+		$headers=$this->hive['HEADERS'];
+		return isset($headers['Client-IP'])?
+			$headers['Client-IP']:
+			(isset($headers['X-Forwarded-For'])?
+				$headers['X-Forwarded-For']:
+				(isset($_SERVER['REMOTE_ADDR'])?
+					$_SERVER['REMOTE_ADDR']:''));
+	}
+
+	/**
+	*	Return formatted stack trace
+	*	@return string
+	*	@param $trace array|NULL
+	**/
+	function trace(array $trace=NULL) {
+		if (!$trace) {
+			$trace=debug_backtrace(FALSE);
+			$frame=$trace[0];
+			if (isset($frame['file']) && $frame['file']==__FILE__)
+				array_shift($trace);
+		}
+		$debug=$this->hive['DEBUG'];
+		$trace=array_filter(
+			$trace,
+			function($frame) use($debug) {
+				return $debug && isset($frame['file']) &&
+					($frame['file']!=__FILE__ || $debug>1) &&
+					(empty($frame['function']) ||
+					!preg_match('/^(?:(?:trigger|user)_error|'.
+						'__call|call_user_func)/',$frame['function']));
+			}
+		);
+		$out='';
+		$eol="\n";
+		// Analyze stack trace
+		foreach ($trace as $frame) {
+			$line='';
+			if (isset($frame['class']))
+				$line.=$frame['class'].$frame['type'];
+			if (isset($frame['function']))
+				$line.=$frame['function'].'('.
+					($debug>2 && isset($frame['args'])?
+						$this->csv($frame['args']):'').')';
+			$src=$this->fixslashes(str_replace($_SERVER['DOCUMENT_ROOT'].
+				'/','',$frame['file'])).':'.$frame['line'];
+			$out.='['.$src.'] '.$line.$eol;
+		}
+		return $out;
+	}
+
+	/**
+	*	Log error; Execute ONERROR handler if defined, else display
+	*	default error page (HTML for synchronous requests, JSON string
+	*	for AJAX requests)
+	*	@return NULL
+	*	@param $code int
+	*	@param $text string
+	*	@param $trace array
+	**/
+	function error($code,$text='',array $trace=NULL) {
+		$prior=$this->hive['ERROR'];
+		$header=$this->status($code);
+		$req=$this->hive['VERB'].' '.$this->hive['PATH'];
+		if (!$text)
+			$text='HTTP '.$code.' ('.$req.')';
+		error_log($text);
+		$trace=$this->trace($trace);
+		foreach (explode("\n",$trace) as $nexus)
+			if ($nexus)
+				error_log($nexus);
+		if ($highlight=PHP_SAPI!='cli' && !$this->hive['AJAX'] &&
+			$this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS))
+			$trace=$this->highlight($trace);
+		$this->hive['ERROR']=array(
+			'status'=>$header,
+			'code'=>$code,
+			'text'=>$text,
+			'trace'=>$trace
+		);
+		$handler=$this->hive['ONERROR'];
+		$this->hive['ONERROR']=NULL;
+		$eol="\n";
+		if ((!$handler ||
+			$this->call($handler,array($this,$this->hive['PARAMS']),
+				'beforeroute,afterroute')===FALSE) &&
+			!$prior && PHP_SAPI!='cli' && !$this->hive['QUIET'])
+			echo $this->hive['AJAX']?
+				json_encode($this->hive['ERROR']):
+				('<!DOCTYPE html>'.$eol.
+				'<html>'.$eol.
+				'<head>'.
+					'<title>'.$code.' '.$header.'</title>'.
+					($highlight?
+						('<style>'.$this->read($css).'</style>'):'').
+				'</head>'.$eol.
+				'<body>'.$eol.
+					'<h1>'.$header.'</h1>'.$eol.
+					'<p>'.$this->encode($text?:$req).'</p>'.$eol.
+					($this->hive['DEBUG']?('<pre>'.$trace.'</pre>'.$eol):'').
+				'</body>'.$eol.
+				'</html>');
+		if ($this->hive['HALT'])
+			die;
+	}
+
+	/**
+	*	Mock HTTP request
+	*	@return mixed
+	*	@param $pattern string
+	*	@param $args array
+	*	@param $headers array
+	*	@param $body string
+	**/
+	function mock($pattern,
+		array $args=NULL,array $headers=NULL,$body=NULL) {
+		if (!$args)
+			$args=array();
+		$types=array('sync','ajax');
+		preg_match('/([\|\w]+)\h+(?:@(\w+)(?:(\(.+?)\))*|([^\h]+))'.
+			'(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts);
+		$verb=strtoupper($parts[1]);
+		if ($parts[2]) {
+			if (empty($this->hive['ALIASES'][$parts[2]]))
+				user_error(sprintf(self::E_Named,$parts[2]),E_USER_ERROR);
+			$parts[4]=$this->hive['ALIASES'][$parts[2]];
+			$parts[4]=$this->build($parts[4],
+				isset($parts[3])?$this->parse($parts[3]):array());
+		}
+		if (empty($parts[4]))
+			user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR);
+		$url=parse_url($parts[4]);
+		parse_str(@$url['query'],$GLOBALS['_GET']);
+		if (preg_match('/GET|HEAD/',$verb))
+			$GLOBALS['_GET']=array_merge($GLOBALS['_GET'],$args);
+		$GLOBALS['_POST']=$verb=='POST'?$args:array();
+		$GLOBALS['_REQUEST']=array_merge($GLOBALS['_GET'],$GLOBALS['_POST']);
+		foreach ($headers?:array() as $key=>$val)
+			$_SERVER['HTTP_'.strtr(strtoupper($key),'-','_')]=$val;
+		$this->hive['VERB']=$verb;
+		$this->hive['URI']=$this->hive['BASE'].$url['path'];
+		if ($GLOBALS['_GET'])
+			$this->hive['URI'].='?'.http_build_query($GLOBALS['_GET']);
+		$this->hive['BODY']='';
+		if (!preg_match('/GET|HEAD/',$verb))
+			$this->hive['BODY']=$body?:http_build_query($args);
+		$this->hive['AJAX']=isset($parts[5]) &&
+			preg_match('/ajax/i',$parts[5]);
+		return $this->run();
+	}
+
+	/**
+	*	Bind handler to route pattern
+	*	@return NULL
+	*	@param $pattern string|array
+	*	@param $handler callback
+	*	@param $ttl int
+	*	@param $kbps int
+	**/
+	function route($pattern,$handler,$ttl=0,$kbps=0) {
+		$types=array('sync','ajax');
+		$alias=null;
+		if (is_array($pattern)) {
+			foreach ($pattern as $item)
+				$this->route($item,$handler,$ttl,$kbps);
+			return;
+		}
+		preg_match('/([\|\w]+)\h+(?:(?:@(\w+)\h*:\h*)?(@(\w+)|[^\h]+))'.
+			'(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts);
+		if (isset($parts[2]) && $parts[2])
+			$this->hive['ALIASES'][$alias=$parts[2]]=$parts[3];
+		elseif (!empty($parts[4])) {
+			if (empty($this->hive['ALIASES'][$parts[4]]))
+				user_error(sprintf(self::E_Named,$parts[4]),E_USER_ERROR);
+			$parts[3]=$this->hive['ALIASES'][$alias=$parts[4]];
+		}
+		if (empty($parts[3]))
+			user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR);
+		$type=empty($parts[5])?
+			self::REQ_SYNC|self::REQ_AJAX:
+			constant('self::REQ_'.strtoupper($parts[5]));
+		foreach ($this->split($parts[1]) as $verb) {
+			if (!preg_match('/'.self::VERBS.'/',$verb))
+				$this->error(501,$verb.' '.$this->hive['URI']);
+			$this->hive['ROUTES'][$parts[3]][$type][strtoupper($verb)]=
+				array($handler,$ttl,$kbps,$alias);
+		}
+	}
+
+	/**
+	*	Reroute to specified URI
+	*	@return NULL
+	*	@param $url string
+	*	@param $permanent bool
+	**/
+	function reroute($url=NULL,$permanent=FALSE) {
+		if (!$url)
+			$url=$this->hive['REALM'];
+		if (preg_match('/^(?:@(\w+)(?:(\(.+?)\))*)/',$url,$parts)) {
+			if (empty($this->hive['ALIASES'][$parts[1]]))
+				user_error(sprintf(self::E_Named,$parts[1]),E_USER_ERROR);
+			$url=$this->hive['ALIASES'][$parts[1]];
+		}
+		$url=$this->build($url,
+			isset($parts[2])?$this->parse($parts[2]):array());
+		if (($handler=$this->hive['ONREROUTE']) &&
+			$this->call($handler,array($url,$permanent))!==FALSE)
+			return;
+		if ($url[0]=='/')
+			$url=$this->hive['BASE'].$url;
+		if (PHP_SAPI!='cli') {
+			header('Location: '.$url);
+			$this->status($permanent?301:302);
+			die;
+		}
+		$this->mock('GET '.$url);
+	}
+
+	/**
+	*	Provide ReST interface by mapping HTTP verb to class method
+	*	@return NULL
+	*	@param $url string
+	*	@param $class string|object
+	*	@param $ttl int
+	*	@param $kbps int
+	**/
+	function map($url,$class,$ttl=0,$kbps=0) {
+		if (is_array($url)) {
+			foreach ($url as $item)
+				$this->map($item,$class,$ttl,$kbps);
+			return;
+		}
+		foreach (explode('|',self::VERBS) as $method)
+			$this->route($method.' '.$url,is_string($class)?
+				$class.'->'.$this->hive['PREMAP'].strtolower($method):
+				array($class,$this->hive['PREMAP'].strtolower($method)),
+				$ttl,$kbps);
+	}
+
+	/**
+	*	Redirect a route to another URL
+	*	@return NULL
+	*	@param $pattern string|array
+	*	@param $url string
+	*	@param $permanent bool
+	*/
+	function redirect($pattern,$url,$permanent=TRUE) {
+		if (is_array($pattern)) {
+			foreach ($pattern as $item)
+				$this->redirect($item,$url,$permanent);
+			return;
+		}
+		$this->route($pattern,function($fw) use($url,$permanent) {
+			$fw->reroute($url,$permanent);
+		});
+	}
+
+	/**
+	*	Return TRUE if IPv4 address exists in DNSBL
+	*	@return bool
+	*	@param $ip string
+	**/
+	function blacklisted($ip) {
+		if ($this->hive['DNSBL'] &&
+			!in_array($ip,
+				is_array($this->hive['EXEMPT'])?
+					$this->hive['EXEMPT']:
+					$this->split($this->hive['EXEMPT']))) {
+			// Reverse IPv4 dotted quad
+			$rev=implode('.',array_reverse(explode('.',$ip)));
+			foreach (is_array($this->hive['DNSBL'])?
+				$this->hive['DNSBL']:
+				$this->split($this->hive['DNSBL']) as $server)
+				// DNSBL lookup
+				if (checkdnsrr($rev.'.'.$server,'A'))
+					return TRUE;
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Applies the specified URL mask and returns parameterized matches
+	*	@return $args array
+	*	@param $pattern string
+	*	@param $url string|NULL
+	**/
+	function mask($pattern,$url=NULL) {
+		if (!$url)
+			$url=$this->rel($this->hive['URI']);
+		$case=$this->hive['CASELESS']?'i':'';
+		preg_match('/^'.
+			preg_replace('/@(\w+\b)/','(?P<\1>[^\/\?]+)',
+			str_replace('\*','([^\?]+)',preg_quote($pattern,'/'))).
+				'\/?(?:\?.*)?$/'.$case.'um',$url,$args);
+		return $args;
+	}
+
+	/**
+	*	Match routes against incoming URI
+	*	@return mixed
+	**/
+	function run() {
+		if ($this->blacklisted($this->hive['IP']))
+			// Spammer detected
+			$this->error(403);
+		if (!$this->hive['ROUTES'])
+			// No routes defined
+			user_error(self::E_Routes,E_USER_ERROR);
+		// Match specific routes first
+		$paths=array();
+		foreach ($keys=array_keys($this->hive['ROUTES']) as $key)
+			$paths[]=str_replace('@','*@',$key);
+		$vals=array_values($this->hive['ROUTES']);
+		array_multisort($paths,SORT_DESC,$keys,$vals);
+		$this->hive['ROUTES']=array_combine($keys,$vals);
+		// Convert to BASE-relative URL
+		$req=$this->rel($this->hive['URI']);
+		if ($cors=(isset($this->hive['HEADERS']['Origin']) &&
+			$this->hive['CORS']['origin'])) {
+			$cors=$this->hive['CORS'];
+			header('Access-Control-Allow-Origin: '.$cors['origin']);
+			header('Access-Control-Allow-Credentials: '.
+				($cors['credentials']?'true':'false'));
+		}
+		$allowed=array();
+		foreach ($this->hive['ROUTES'] as $pattern=>$routes) {
+			if (!$args=$this->mask($pattern,$req))
+				continue;
+			ksort($args);
+			$route=NULL;
+			if (isset(
+				$routes[$ptr=$this->hive['AJAX']+1][$this->hive['VERB']]))
+				$route=$routes[$ptr];
+			elseif (isset($routes[self::REQ_SYNC|self::REQ_AJAX]))
+				$route=$routes[self::REQ_SYNC|self::REQ_AJAX];
+			if (!$route)
+				continue;
+			if ($this->hive['VERB']!='OPTIONS' &&
+				isset($route[$this->hive['VERB']])) {
+				$parts=parse_url($req);
+				if ($this->hive['VERB']=='GET' &&
+					preg_match('/.+\/$/',$parts['path']))
+					$this->reroute(substr($parts['path'],0,-1).
+						(isset($parts['query'])?('?'.$parts['query']):''));
+				list($handler,$ttl,$kbps,$alias)=$route[$this->hive['VERB']];
+				if (is_bool(strpos($pattern,'/*')))
+					foreach (array_keys($args) as $key)
+						if (is_numeric($key) && $key)
+							unset($args[$key]);
+				// Capture values of route pattern tokens
+				$this->hive['PARAMS']=$args=array_map('urldecode',$args);
+				// Save matching route
+				$this->hive['ALIAS']=$alias;
+				$this->hive['PATTERN']=$pattern;
+				if ($cors && $cors['expose'])
+					header('Access-Control-Expose-Headers: '.(is_array($cors['expose'])?
+						implode(',',$cors['expose']):$cors['expose']));
+				if (is_string($handler)) {
+					// Replace route pattern tokens in handler if any
+					$handler=preg_replace_callback('/@(\w+\b)/',
+						function($id) use($args) {
+							return isset($args[$id[1]])?$args[$id[1]]:$id[0];
+						},
+						$handler
+					);
+					if (preg_match('/(.+)\h*(?:->|::)/',$handler,$match) &&
+						!class_exists($match[1]))
+						$this->error(404);
+				}
+				// Process request
+				$result=NULL;
+				$body='';
+				$now=microtime(TRUE);
+				if (preg_match('/GET|HEAD/',$this->hive['VERB']) && $ttl) {
+					// Only GET and HEAD requests are cacheable
+					$headers=$this->hive['HEADERS'];
+					$cache=Cache::instance();
+					$cached=$cache->exists(
+						$hash=$this->hash($this->hive['VERB'].' '.
+							$this->hive['URI']).'.url',$data);
+					if ($cached && $cached[0]+$ttl>$now) {
+						if (isset($headers['If-Modified-Since']) &&
+							strtotime($headers['If-Modified-Since'])+
+								$ttl>$now) {
+							$this->status(304);
+							die;
+						}
+						// Retrieve from cache backend
+						list($headers,$body,$result)=$data;
+						if (PHP_SAPI!='cli')
+							array_walk($headers,'header');
+						$this->expire($cached[0]+$ttl-$now);
+					}
+					else
+						// Expire HTTP client-cached page
+						$this->expire($ttl);
+				}
+				else
+					$this->expire(0);
+				if (!strlen($body)) {
+					if (!$this->hive['RAW'] && !$this->hive['BODY'])
+						$this->hive['BODY']=file_get_contents('php://input');
+					ob_start();
+					// Call route handler
+					$result=$this->call($handler,array($this,$args),
+						'beforeroute,afterroute');
+					$body=ob_get_clean();
+					if (isset($cache) && !error_get_last()) {
+						// Save to cache backend
+						$cache->set($hash,array(
+							// Remove cookies
+							preg_grep('/Set-Cookie\:/',headers_list(),
+								PREG_GREP_INVERT),$body,$result),$ttl);
+					}
+				}
+				$this->hive['RESPONSE']=$body;
+				if (!$this->hive['QUIET']) {
+					if ($kbps) {
+						$ctr=0;
+						foreach (str_split($body,1024) as $part) {
+							// Throttle output
+							$ctr++;
+							if ($ctr/$kbps>($elapsed=microtime(TRUE)-$now) &&
+								!connection_aborted())
+								usleep(1e6*($ctr/$kbps-$elapsed));
+							echo $part;
+						}
+					}
+					else
+						echo $body;
+				}
+				return $result;
+			}
+			$allowed=array_merge($allowed,array_keys($route));
+		}
+		if (!$allowed)
+			// URL doesn't match any route
+			$this->error(404);
+		elseif (PHP_SAPI!='cli') {
+			// Unhandled HTTP method
+			header('Allow: '.implode(',',array_unique($allowed)));
+			if ($cors) {
+				header('Access-Control-Allow-Methods: OPTIONS,'.implode(',',$allowed));
+				if ($cors['headers'])
+					header('Access-Control-Allow-Headers: '.(is_array($cors['headers'])?
+						implode(',',$cors['headers']):$cors['headers']));
+				if ($cors['ttl']>0)
+					header('Access-Control-Max-Age: '.$cors['ttl']);
+			}
+			if ($this->hive['VERB']!='OPTIONS')
+				$this->error(405);
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Loop until callback returns TRUE (for long polling)
+	*	@return mixed
+	*	@param $func callback
+	*	@param $args array
+	*	@param $timeout int
+	**/
+	function until($func,$args=NULL,$timeout=60) {
+		if (!$args)
+			$args=array();
+		$time=time();
+		$limit=max(0,min($timeout,$max=ini_get('max_execution_time')-1));
+		$out='';
+		$flag=FALSE;
+		// Not for the weak of heart
+		while (
+			// Still alive?
+			!connection_aborted() &&
+			// Got time left?
+			(time()-$time+1<$limit) &&
+			// Restart session
+			$flag=@session_start() &&
+			// CAUTION: Callback will kill host if it never becomes truthy!
+			!($out=$this->call($func,$args))) {
+			session_commit();
+			ob_flush();
+			flush();
+			// Hush down
+			sleep(1);
+		}
+		if ($flag) {
+			session_commit();
+			ob_flush();
+			flush();
+		}
+		return $out;
+	}
+
+	/**
+	*	Disconnect HTTP client
+	**/
+	function abort() {
+		@session_start();
+		session_commit();
+		header('Content-Length: 0');
+		while (ob_get_level())
+			ob_end_clean();
+		flush();
+		if (function_exists('fastcgi_finish_request'))
+			fastcgi_finish_request();
+	}
+
+	/**
+	*	Grab the real route handler behind the string expression
+	*	@return string|array
+	*	@param $func string
+	*	@param $args array
+	**/
+	function grab($func,$args=NULL) {
+		if (preg_match('/(.+)\h*(->|::)\h*(.+)/s',$func,$parts)) {
+			// Convert string to executable PHP callback
+			if (!class_exists($parts[1]))
+				user_error(sprintf(self::E_Class,$parts[1]),E_USER_ERROR);
+			if ($parts[2]=='->') {
+				if (is_subclass_of($parts[1],'Prefab'))
+					$parts[1]=call_user_func($parts[1].'::instance');
+				else {
+					$ref=new ReflectionClass($parts[1]);
+					$parts[1]=method_exists($parts[1],'__construct')?
+						$ref->newinstanceargs($args):
+						$ref->newinstance();
+				}
+			}
+			$func=array($parts[1],$parts[3]);
+		}
+		return $func;
+	}
+
+	/**
+	*	Execute callback/hooks (supports 'class->method' format)
+	*	@return mixed|FALSE
+	*	@param $func callback
+	*	@param $args mixed
+	*	@param $hooks string
+	**/
+	function call($func,$args=NULL,$hooks='') {
+		if (!is_array($args))
+			$args=array($args);
+		// Grab the real handler behind the string representation
+		if (is_string($func))
+			$func=$this->grab($func,$args);
+		// Execute function; abort if callback/hook returns FALSE
+		if (!is_callable($func))
+			// No route handler
+			if ($hooks=='beforeroute,afterroute') {
+				$allowed=array();
+				if (is_array($func))
+					$allowed=array_intersect(
+						array_map('strtoupper',get_class_methods($func[0])),
+						explode('|',self::VERBS)
+					);
+				header('Allow: '.implode(',',$allowed));
+				$this->error(405);
+			}
+			else
+				user_error(sprintf(self::E_Method,
+					is_string($func)?$func:$this->stringify($func)),
+					E_USER_ERROR);
+		$obj=FALSE;
+		if (is_array($func)) {
+			$hooks=$this->split($hooks);
+			$obj=TRUE;
+		}
+		// Execute pre-route hook if any
+		if ($obj && $hooks && in_array($hook='beforeroute',$hooks) &&
+			method_exists($func[0],$hook) &&
+			call_user_func_array(array($func[0],$hook),$args)===FALSE)
+			return FALSE;
+		// Execute callback
+		$out=call_user_func_array($func,$args?:array());
+		if ($out===FALSE)
+			return FALSE;
+		// Execute post-route hook if any
+		if ($obj && $hooks && in_array($hook='afterroute',$hooks) &&
+			method_exists($func[0],$hook) &&
+			call_user_func_array(array($func[0],$hook),$args)===FALSE)
+			return FALSE;
+		return $out;
+	}
+
+	/**
+	*	Execute specified callbacks in succession; Apply same arguments
+	*	to all callbacks
+	*	@return array
+	*	@param $funcs array|string
+	*	@param $args mixed
+	**/
+	function chain($funcs,$args=NULL) {
+		$out=array();
+		foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func)
+			$out[]=$this->call($func,$args);
+		return $out;
+	}
+
+	/**
+	*	Execute specified callbacks in succession; Relay result of
+	*	previous callback as argument to the next callback
+	*	@return array
+	*	@param $funcs array|string
+	*	@param $args mixed
+	**/
+	function relay($funcs,$args=NULL) {
+		foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func)
+			$args=array($this->call($func,$args));
+		return array_shift($args);
+	}
+
+	/**
+	*	Configure framework according to .ini-style file settings;
+	*	If optional 2nd arg is provided, template strings are interpreted
+	*	@return object
+	*	@param $file string
+	*	@param $allow bool
+	**/
+	function config($file,$allow=FALSE) {
+		preg_match_all(
+			'/(?<=^|\n)(?:'.
+				'\[(?<section>.+?)\]|'.
+				'(?<lval>[^\h\r\n;].*?)\h*=\h*'.
+				'(?<rval>(?:\\\\\h*\r?\n|.+?)*)'.
+			')(?=\r?\n|$)/',
+			$this->read($file),
+			$matches,PREG_SET_ORDER);
+		if ($matches) {
+			$sec='globals';
+			foreach ($matches as $match) {
+				if ($match['section']) {
+					$sec=$match['section'];
+					if (preg_match('/^(?!(?:global|config|route|map|redirect)s\b)'.
+						'((?:\.?\w)+)/i',$sec,$msec) && !$this->exists($msec[0]))
+						$this->set($msec[0],NULL);
+				}
+				else {
+					if ($allow) {
+						$match['lval']=Preview::instance()->
+							resolve($match['lval']);
+						$match['rval']=Preview::instance()->
+							resolve($match['rval']);
+					}
+					if (preg_match('/^(config|route|map|redirect)s\b/i',
+						$sec,$cmd)) {
+						call_user_func_array(
+							array($this,$cmd[1]),
+							array_merge(array($match['lval']),
+								str_getcsv($match['rval'])));
+					}
+					else {
+						$args=array_map(
+							function($val) {
+								if (is_numeric($val))
+									return $val+0;
+								$val=ltrim($val);
+								if (preg_match('/^\w+$/i',$val) &&
+									defined($val))
+									return constant($val);
+								return trim(preg_replace(
+									array('/\\\\"/','/\\\\\h*(\r?\n)/'),
+									array('"','\1'),$val));
+							},
+							// Mark quoted strings with 0x00 whitespace
+							str_getcsv(preg_replace('/(?<!\\\\)(")(.*?)\1/',
+								"\\1\x00\\2\\1",$match['rval']))
+						);
+						preg_match('/^(?<section>[^:]+)(?:\:(?<func>.+))?/',
+							$sec,$parts);
+						$func=isset($parts['func'])?$parts['func']:NULL;
+						$custom=(strtolower($parts['section'])!='globals');
+						if ($func)
+							$args=array($this->call($func,
+								count($args)>1?array($args):$args));
+						call_user_func_array(
+							array($this,'set'),
+							array_merge(
+								array(
+									($custom?($parts['section'].'.'):'').
+									$match['lval']
+								),
+								count($args)>1?array($args):$args
+							)
+						);
+					}
+				}
+			}
+		}
+		return $this;
+	}
+
+	/**
+	*	Create mutex, invoke callback then drop ownership when done
+	*	@return mixed
+	*	@param $id string
+	*	@param $func callback
+	*	@param $args mixed
+	**/
+	function mutex($id,$func,$args=NULL) {
+		if (!is_dir($tmp=$this->hive['TEMP']))
+			mkdir($tmp,self::MODE,TRUE);
+		// Use filesystem lock
+		if (is_file($lock=$tmp.
+			$this->hash($this->hive['ROOT'].$this->hive['BASE']).'.'.
+			$this->hash($id).'.lock') &&
+			filemtime($lock)+ini_get('max_execution_time')<microtime(TRUE))
+			// Stale lock
+			@unlink($lock);
+		while (!($handle=@fopen($lock,'x')) && !connection_aborted())
+			usleep(mt_rand(0,100));
+		$out=$this->call($func,$args);
+		fclose($handle);
+		@unlink($lock);
+		return $out;
+	}
+
+	/**
+	*	Read file (with option to apply Unix LF as standard line ending)
+	*	@return string
+	*	@param $file string
+	*	@param $lf bool
+	**/
+	function read($file,$lf=FALSE) {
+		$out=@file_get_contents($file);
+		return $lf?preg_replace('/\r\n|\r/',"\n",$out):$out;
+	}
+
+	/**
+	*	Exclusive file write
+	*	@return int|FALSE
+	*	@param $file string
+	*	@param $data mixed
+	*	@param $append bool
+	**/
+	function write($file,$data,$append=FALSE) {
+		return file_put_contents($file,$data,LOCK_EX|($append?FILE_APPEND:0));
+	}
+
+	/**
+	*	Apply syntax highlighting
+	*	@return string
+	*	@param $text string
+	**/
+	function highlight($text) {
+		$out='';
+		$pre=FALSE;
+		$text=trim($text);
+		if (!preg_match('/^<\?php/',$text)) {
+			$text='<?php '.$text;
+			$pre=TRUE;
+		}
+		foreach (token_get_all($text) as $token)
+			if ($pre)
+				$pre=FALSE;
+			else
+				$out.='<span'.
+					(is_array($token)?
+						(' class="'.
+							substr(strtolower(token_name($token[0])),2).'">'.
+							$this->encode($token[1]).''):
+						('>'.$this->encode($token))).
+					'</span>';
+		return $out?('<code>'.$out.'</code>'):$text;
+	}
+
+	/**
+	*	Dump expression with syntax highlighting
+	*	@return NULL
+	*	@param $expr mixed
+	**/
+	function dump($expr) {
+		echo $this->highlight($this->stringify($expr));
+	}
+
+	/**
+	*	Return path (and query parameters) relative to the base directory
+	*	@return string
+	*	@param $url string
+	**/
+	function rel($url) {
+		return preg_replace('/^(?:https?:\/\/)?'.
+			preg_quote($this->hive['BASE'],'/').'(\/.*|$)/','\1',$url);
+	}
+
+	/**
+	*	Namespace-aware class autoloader
+	*	@return mixed
+	*	@param $class string
+	**/
+	protected function autoload($class) {
+		$class=$this->fixslashes(ltrim($class,'\\'));
+		$func=NULL;
+		if (is_array($path=$this->hive['AUTOLOAD']) &&
+			isset($path[1]) && is_callable($path[1]))
+			list($path,$func)=$path;
+		foreach ($this->split($this->hive['PLUGINS'].';'.$path) as $auto)
+			if ($func && is_file($file=$func($auto.$class).'.php') ||
+				is_file($file=$auto.$class.'.php') ||
+				is_file($file=$auto.strtolower($class).'.php') ||
+				is_file($file=strtolower($auto.$class).'.php'))
+				return require($file);
+	}
+
+	/**
+	*	Execute framework/application shutdown sequence
+	*	@return NULL
+	*	@param $cwd string
+	**/
+	function unload($cwd) {
+		chdir($cwd);
+		if (!$error=error_get_last())
+			@session_commit();
+		$handler=$this->hive['UNLOAD'];
+		if ((!$handler || $this->call($handler,$this)===FALSE) &&
+			$error && in_array($error['type'],
+			array(E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR)))
+			// Fatal error detected
+			$this->error(500,sprintf(self::E_Fatal,$error['message']),
+				array($error));
+	}
+
+	/**
+	*	Convenience method for checking hive key
+	*	@return mixed
+	*	@param $key string
+	**/
+	function offsetexists($key) {
+		return $this->exists($key);
+	}
+
+	/**
+	*	Convenience method for assigning hive value
+	*	@return mixed
+	*	@param $key string
+	*	@param $val scalar
+	**/
+	function offsetset($key,$val) {
+		return $this->set($key,$val);
+	}
+
+	/**
+	*	Convenience method for retrieving hive value
+	*	@return mixed
+	*	@param $key string
+	**/
+	function &offsetget($key) {
+		$val=&$this->ref($key);
+		return $val;
+	}
+
+	/**
+	*	Convenience method for removing hive key
+	*	@return NULL
+	*	@param $key string
+	**/
+	function offsetunset($key) {
+		$this->clear($key);
+	}
+
+	/**
+	*	Alias for offsetexists()
+	*	@return mixed
+	*	@param $key string
+	**/
+	function __isset($key) {
+		return $this->offsetexists($key);
+	}
+
+	/**
+	*	Alias for offsetset()
+	*	@return mixed
+	*	@param $key string
+	*	@param $val mixed
+	**/
+	function __set($key,$val) {
+		return $this->offsetset($key,$val);
+	}
+
+	/**
+	*	Alias for offsetget()
+	*	@return mixed
+	*	@param $key string
+	**/
+	function &__get($key) {
+		$val=&$this->offsetget($key);
+		return $val;
+	}
+
+	/**
+	*	Alias for offsetunset()
+	*	@return mixed
+	*	@param $key string
+	**/
+	function __unset($key) {
+		$this->offsetunset($key);
+	}
+
+	/**
+	*	Call function identified by hive key
+	*	@return mixed
+	*	@param $key string
+	*	@param $args array
+	**/
+	function __call($key,$args) {
+		return call_user_func_array($this->get($key),$args);
+	}
+
+	//! Prohibit cloning
+	private function __clone() {
+	}
+
+	//! Bootstrap
+	function __construct() {
+		// Managed directives
+		ini_set('default_charset',$charset='UTF-8');
+		if (extension_loaded('mbstring'))
+			mb_internal_encoding($charset);
+		ini_set('display_errors',0);
+		// Deprecated directives
+		@ini_set('magic_quotes_gpc',0);
+		@ini_set('register_globals',0);
+		// Intercept errors/exceptions; PHP5.3-compatible
+		error_reporting((E_ALL|E_STRICT)&~(E_NOTICE|E_USER_NOTICE));
+		$fw=$this;
+		set_exception_handler(
+			function($obj) use($fw) {
+				$fw->hive['EXCEPTION']=$obj;
+				$fw->error(500,$obj->getmessage(),$obj->gettrace());
+			}
+		);
+		set_error_handler(
+			function($code,$text) use($fw) {
+				if ($code & error_reporting())
+					$fw->error(500,$text);
+			}
+		);
+		if (!isset($_SERVER['SERVER_NAME']))
+			$_SERVER['SERVER_NAME']=gethostname();
+		if (PHP_SAPI=='cli') {
+			// Emulate HTTP request
+			if (isset($_SERVER['argc']) && $_SERVER['argc']<2) {
+				$_SERVER['argc']++;
+				$_SERVER['argv'][1]='/';
+			}
+			$_SERVER['REQUEST_METHOD']='GET';
+			$_SERVER['REQUEST_URI']=$_SERVER['argv'][1];
+		}
+		$headers=array();
+		if (PHP_SAPI!='cli')
+			foreach (array_keys($_SERVER) as $key)
+				if (substr($key,0,5)=='HTTP_')
+					$headers[strtr(ucwords(strtolower(strtr(
+						substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key];
+		if (isset($headers['X-HTTP-Method-Override']))
+			$_SERVER['REQUEST_METHOD']=$headers['X-HTTP-Method-Override'];
+		elseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method']))
+			$_SERVER['REQUEST_METHOD']=$_POST['_method'];
+		$scheme=isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' ||
+			isset($headers['X-Forwarded-Proto']) &&
+			$headers['X-Forwarded-Proto']=='https'?'https':'http';
+		// Create hive early on to expose header methods
+		$this->hive=array('HEADERS'=>$headers);
+		if (function_exists('apache_setenv')) {
+			// Work around Apache pre-2.4 VirtualDocumentRoot bug
+			$_SERVER['DOCUMENT_ROOT']=str_replace($_SERVER['SCRIPT_NAME'],'',
+				$_SERVER['SCRIPT_FILENAME']);
+			apache_setenv("DOCUMENT_ROOT",$_SERVER['DOCUMENT_ROOT']);
+		}
+		$_SERVER['DOCUMENT_ROOT']=realpath($_SERVER['DOCUMENT_ROOT']);
+		$base='';
+		if (PHP_SAPI!='cli')
+			$base=rtrim($this->fixslashes(
+				dirname($_SERVER['SCRIPT_NAME'])),'/');
+		$uri=parse_url($_SERVER['REQUEST_URI']);
+		$path=preg_replace('/^'.preg_quote($base,'/').'/','',$uri['path']);
+		call_user_func_array('session_set_cookie_params',
+			$jar=array(
+				'expire'=>0,
+				'path'=>$base?:'/',
+				'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) &&
+					!filter_var($_SERVER['SERVER_NAME'],FILTER_VALIDATE_IP)?
+					$_SERVER['SERVER_NAME']:'',
+				'secure'=>($scheme=='https'),
+				'httponly'=>TRUE
+			)
+		);
+		$port=0;
+		if (isset($_SERVER['SERVER_PORT']))
+			$port=$_SERVER['SERVER_PORT'];
+		// Default configuration
+		$this->hive+=array(
+			'AGENT'=>$this->agent(),
+			'AJAX'=>$this->ajax(),
+			'ALIAS'=>NULL,
+			'ALIASES'=>array(),
+			'AUTOLOAD'=>'./',
+			'BASE'=>$base,
+			'BITMASK'=>ENT_COMPAT,
+			'BODY'=>NULL,
+			'CACHE'=>FALSE,
+			'CASELESS'=>TRUE,
+			'CONFIG'=>NULL,
+			'CORS'=>array(
+				'headers'=>'',
+				'origin'=>false,
+				'credentials'=>false,
+				'expose'=>false,
+				'ttl'=>0),
+			'DEBUG'=>0,
+			'DIACRITICS'=>array(),
+			'DNSBL'=>'',
+			'EMOJI'=>array(),
+			'ENCODING'=>$charset,
+			'ERROR'=>NULL,
+			'ESCAPE'=>TRUE,
+			'EXCEPTION'=>NULL,
+			'EXEMPT'=>NULL,
+			'FALLBACK'=>$this->fallback,
+			'FRAGMENT'=>isset($uri['fragment'])?$uri['fragment']:'',
+			'HALT'=>TRUE,
+			'HIGHLIGHT'=>TRUE,
+			'HOST'=>$_SERVER['SERVER_NAME'],
+			'IP'=>$this->ip(),
+			'JAR'=>$jar,
+			'LANGUAGE'=>isset($headers['Accept-Language'])?
+				$this->language($headers['Accept-Language']):
+				$this->fallback,
+			'LOCALES'=>'./',
+			'LOGS'=>'./',
+			'ONERROR'=>NULL,
+			'ONREROUTE'=>NULL,
+			'PACKAGE'=>self::PACKAGE,
+			'PARAMS'=>array(),
+			'PATH'=>$path,
+			'PATTERN'=>NULL,
+			'PLUGINS'=>$this->fixslashes(__DIR__).'/',
+			'PORT'=>$port,
+			'PREFIX'=>NULL,
+			'PREMAP'=>'',
+			'QUERY'=>isset($uri['query'])?$uri['query']:'',
+			'QUIET'=>FALSE,
+			'RAW'=>FALSE,
+			'REALM'=>$scheme.'://'.$_SERVER['SERVER_NAME'].
+				($port && $port!=80 && $port!=443?
+					(':'.$port):'').$_SERVER['REQUEST_URI'],
+			'RESPONSE'=>'',
+			'ROOT'=>$_SERVER['DOCUMENT_ROOT'],
+			'ROUTES'=>array(),
+			'SCHEME'=>$scheme,
+			'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php',
+			'TEMP'=>'tmp/',
+			'TIME'=>microtime(TRUE),
+			'TZ'=>(@ini_get('date.timezone'))?:'UTC',
+			'UI'=>'./',
+			'UNLOAD'=>NULL,
+			'UPLOADS'=>'./',
+			'URI'=>&$_SERVER['REQUEST_URI'],
+			'VERB'=>&$_SERVER['REQUEST_METHOD'],
+			'VERSION'=>self::VERSION,
+			'XFRAME'=>'SAMEORIGIN'
+		);
+		if (PHP_SAPI=='cli-server' &&
+			preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI']))
+			$this->reroute('/');
+		if (ini_get('auto_globals_jit'))
+			// Override setting
+			$GLOBALS+=array('_ENV'=>$_ENV,'_REQUEST'=>$_REQUEST);
+		// Sync PHP globals with corresponding hive keys
+		$this->init=$this->hive;
+		foreach (explode('|',self::GLOBALS) as $global) {
+			$sync=$this->sync($global);
+			$this->init+=array(
+				$global=>preg_match('/SERVER|ENV/',$global)?$sync:array()
+			);
+		}
+		if ($error=error_get_last())
+			// Error detected
+			$this->error(500,sprintf(self::E_Fatal,$error['message']),
+				array($error));
+		date_default_timezone_set($this->hive['TZ']);
+		// Register framework autoloader
+		spl_autoload_register(array($this,'autoload'));
+		// Register shutdown handler
+		register_shutdown_function(array($this,'unload'),getcwd());
+	}
+
+}
+
+//! Cache engine
+class Cache extends Prefab {
+
+	protected
+		//! Cache DSN
+		$dsn,
+		//! Prefix for cache entries
+		$prefix,
+		//! MemCache or Redis object
+		$ref;
+
+	/**
+	*	Return timestamp and TTL of cache entry or FALSE if not found
+	*	@return array|FALSE
+	*	@param $key string
+	*	@param $val mixed
+	**/
+	function exists($key,&$val=NULL) {
+		$fw=Base::instance();
+		if (!$this->dsn)
+			return FALSE;
+		$ndx=$this->prefix.'.'.$key;
+		$parts=explode('=',$this->dsn,2);
+		switch ($parts[0]) {
+			case 'apc':
+			case 'apcu':
+				$raw=apc_fetch($ndx);
+				break;
+			case 'redis':
+				$raw=$this->ref->get($ndx);
+				break;
+			case 'memcache':
+				$raw=memcache_get($this->ref,$ndx);
+				break;
+			case 'wincache':
+				$raw=wincache_ucache_get($ndx);
+				break;
+			case 'xcache':
+				$raw=xcache_get($ndx);
+				break;
+			case 'folder':
+				$raw=$fw->read($parts[1].$ndx);
+				break;
+		}
+		if (!empty($raw)) {
+			list($val,$time,$ttl)=(array)$fw->unserialize($raw);
+			if ($ttl===0 || $time+$ttl>microtime(TRUE))
+				return array($time,$ttl);
+			$val=null;
+			$this->clear($key);
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Store value in cache
+	*	@return mixed|FALSE
+	*	@param $key string
+	*	@param $val mixed
+	*	@param $ttl int
+	**/
+	function set($key,$val,$ttl=0) {
+		$fw=Base::instance();
+		if (!$this->dsn)
+			return TRUE;
+		$ndx=$this->prefix.'.'.$key;
+		$time=microtime(TRUE);
+		if ($cached=$this->exists($key))
+			list($time,$ttl)=$cached;
+		$data=$fw->serialize(array($val,$time,$ttl));
+		$parts=explode('=',$this->dsn,2);
+		switch ($parts[0]) {
+			case 'apc':
+			case 'apcu':
+				return apc_store($ndx,$data,$ttl);
+			case 'redis':
+				return $this->ref->set($ndx,$data,array('ex'=>$ttl));
+			case 'memcache':
+				return memcache_set($this->ref,$ndx,$data,0,$ttl);
+			case 'wincache':
+				return wincache_ucache_set($ndx,$data,$ttl);
+			case 'xcache':
+				return xcache_set($ndx,$data,$ttl);
+			case 'folder':
+				return $fw->write($parts[1].$ndx,$data);
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Retrieve value of cache entry
+	*	@return mixed|FALSE
+	*	@param $key string
+	**/
+	function get($key) {
+		return $this->dsn && $this->exists($key,$data)?$data:FALSE;
+	}
+
+	/**
+	*	Delete cache entry
+	*	@return bool
+	*	@param $key string
+	**/
+	function clear($key) {
+		if (!$this->dsn)
+			return;
+		$ndx=$this->prefix.'.'.$key;
+		$parts=explode('=',$this->dsn,2);
+		switch ($parts[0]) {
+			case 'apc':
+			case 'apcu':
+				return apc_delete($ndx);
+			case 'redis':
+				return $this->ref->del($ndx);
+			case 'memcache':
+				return memcache_delete($this->ref,$ndx);
+			case 'wincache':
+				return wincache_ucache_delete($ndx);
+			case 'xcache':
+				return xcache_unset($ndx);
+			case 'folder':
+				return @unlink($parts[1].$ndx);
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Clear contents of cache backend
+	*	@return bool
+	*	@param $suffix string
+	*	@param $lifetime int
+	**/
+	function reset($suffix=NULL,$lifetime=0) {
+		if (!$this->dsn)
+			return TRUE;
+		$regex='/'.preg_quote($this->prefix.'.','/').'.+?'.
+			preg_quote($suffix,'/').'/';
+		$parts=explode('=',$this->dsn,2);
+		switch ($parts[0]) {
+			case 'apc':
+			case 'apcu':
+				$info=apc_cache_info('user');
+				if (!empty($info['cache_list'])) {
+					$key=array_key_exists('info',$info['cache_list'][0])?'info':'key';
+					$mtkey=array_key_exists('mtime',$info['cache_list'][0])?
+						'mtime':'modification_time';
+					foreach ($info['cache_list'] as $item)
+						if (preg_match($regex,$item[$key]) &&
+							$item[$mtkey]+$lifetime<time())
+							apc_delete($item[$key]);
+				}
+				return TRUE;
+			case 'redis':
+				$fw=Base::instance();
+				$keys=$this->ref->keys($this->prefix.'.*'.$suffix);
+				foreach($keys as $key) {
+					$val=$fw->unserialize($this->ref->get($key));
+					if ($val[1]+$lifetime<time())
+						$this->ref->del($key);
+				}
+				return TRUE;
+			case 'memcache':
+				foreach (memcache_get_extended_stats(
+					$this->ref,'slabs') as $slabs)
+					foreach (array_filter(array_keys($slabs),'is_numeric')
+						as $id)
+						foreach (memcache_get_extended_stats(
+							$this->ref,'cachedump',$id) as $data)
+							if (is_array($data))
+								foreach ($data as $key=>$val)
+									if (preg_match($regex,$key) &&
+										$val[1]+$lifetime<time())
+										memcache_delete($this->ref,$key);
+				return TRUE;
+			case 'wincache':
+				$info=wincache_ucache_info();
+				foreach ($info['ucache_entries'] as $item)
+					if (preg_match($regex,$item['key_name']) &&
+						$item['use_time']+$lifetime<time())
+					wincache_ucache_delete($item['key_name']);
+				return TRUE;
+			case 'xcache':
+				return TRUE; /* Not supported */
+			case 'folder':
+				if ($glob=@glob($parts[1].'*'))
+					foreach ($glob as $file)
+						if (preg_match($regex,basename($file)) &&
+							filemtime($file)+$lifetime<time())
+							@unlink($file);
+				return TRUE;
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Load/auto-detect cache backend
+	*	@return string
+	*	@param $dsn bool|string
+	**/
+	function load($dsn) {
+		$fw=Base::instance();
+		if ($dsn=trim($dsn)) {
+			if (preg_match('/^redis=(.+)/',$dsn,$parts) &&
+				extension_loaded('redis')) {
+				$port=6379;
+				$parts=explode(':',$parts[1],2);
+				if (count($parts)>1)
+					list($host,$port)=$parts;
+				else
+					$host=$parts[0];
+				$this->ref=new Redis;
+				if(!$this->ref->connect($host,$port,2))
+					$this->ref=NULL;
+			}
+			elseif (preg_match('/^memcache=(.+)/',$dsn,$parts) &&
+				extension_loaded('memcache'))
+				foreach ($fw->split($parts[1]) as $server) {
+					$port=11211;
+					$parts=explode(':',$server,2);
+					if (count($parts)>1)
+						list($host,$port)=$parts;
+					else
+						$host=$parts[0];
+					if (empty($this->ref))
+						$this->ref=@memcache_connect($host,$port)?:NULL;
+					else
+						memcache_add_server($this->ref,$host,$port);
+				}
+			if (empty($this->ref) && !preg_match('/^folder\h*=/',$dsn))
+				$dsn=($grep=preg_grep('/^(apc|wincache|xcache)/',
+					array_map('strtolower',get_loaded_extensions())))?
+						// Auto-detect
+						current($grep):
+						// Use filesystem as fallback
+						('folder='.$fw->get('TEMP').'cache/');
+			if (preg_match('/^folder\h*=\h*(.+)/',$dsn,$parts) &&
+				!is_dir($parts[1]))
+				mkdir($parts[1],Base::MODE,TRUE);
+		}
+		$this->prefix=$fw->hash($_SERVER['SERVER_NAME'].$fw->get('BASE'));
+		return $this->dsn=$dsn;
+	}
+
+	/**
+	*	Class constructor
+	*	@return object
+	*	@param $dsn bool|string
+	**/
+	function __construct($dsn=FALSE) {
+		if ($dsn)
+			$this->load($dsn);
+	}
+
+}
+
+//! View handler
+class View extends Prefab {
+
+	protected
+		//! Template file
+		$view,
+		//! post-rendering handler
+		$trigger,
+		//! Nesting level
+		$level=0;
+
+	/**
+	*	Encode characters to equivalent HTML entities
+	*	@return string
+	*	@param $arg mixed
+	**/
+	function esc($arg) {
+		$fw=Base::instance();
+		return $fw->recursive($arg,
+			function($val) use($fw) {
+				return is_string($val)?$fw->encode($val):$val;
+			}
+		);
+	}
+
+	/**
+	*	Decode HTML entities to equivalent characters
+	*	@return string
+	*	@param $arg mixed
+	**/
+	function raw($arg) {
+		$fw=Base::instance();
+		return $fw->recursive($arg,
+			function($val) use($fw) {
+				return is_string($val)?$fw->decode($val):$val;
+			}
+		);
+	}
+
+	/**
+	*	Create sandbox for template execution
+	*	@return string
+	*	@param $hive array
+	**/
+	protected function sandbox(array $hive=NULL) {
+		$this->level++;
+		$fw=Base::instance();
+		$implicit=false;
+		if ($hive === null) {
+			$implicit=true;
+			$hive=$fw->hive();
+		}
+		if ($this->level<2 || $implicit) {
+			if ($fw->get('ESCAPE'))
+				$hive=$this->esc($hive);
+			if (isset($hive['ALIASES']))
+				$hive['ALIASES']=$fw->build($hive['ALIASES']);
+		}
+		unset($fw, $implicit);
+		extract($hive);
+		unset($hive);
+		ob_start();
+		require($this->view);
+		$this->level--;
+		return ob_get_clean();
+	}
+
+	/**
+	*	Render template
+	*	@return string
+	*	@param $file string
+	*	@param $mime string
+	*	@param $hive array
+	*	@param $ttl int
+	**/
+	function render($file,$mime='text/html',array $hive=NULL,$ttl=0) {
+		$fw=Base::instance();
+		$cache=Cache::instance();
+		$cached=$cache->exists($hash=$fw->hash($file),$data);
+		if ($cached && $cached[0]+$ttl>microtime(TRUE))
+			return $data;
+		foreach ($fw->split($fw->get('UI').';./') as $dir)
+			if (is_file($this->view=$fw->fixslashes($dir.$file))) {
+				if (isset($_COOKIE[session_name()]))
+					@session_start();
+				$fw->sync('SESSION');
+				if ($mime && PHP_SAPI!='cli' && !headers_sent())
+					header('Content-Type: '.$mime.'; '.
+						'charset='.$fw->get('ENCODING'));
+				$data=$this->sandbox($hive);
+				if(isset($this->trigger['afterrender']))
+					foreach($this->trigger['afterrender'] as $func)
+						$data=$fw->call($func,$data);
+				if ($ttl)
+					$cache->set($hash,$data);
+				return $data;
+			}
+		user_error(sprintf(Base::E_Open,$file),E_USER_ERROR);
+	}
+
+	/**
+	*	post rendering handler
+	*	@param $func callback
+	*/
+	function afterrender($func) {
+		$this->trigger['afterrender'][]=$func;
+	}
+
+}
+
+//! Lightweight template engine
+class Preview extends View {
+
+	protected
+		//! MIME type
+		$mime,
+		//! token filter
+		$filter = array(
+			'esc'=>'$this->esc',
+			'raw'=>'$this->raw',
+			'alias'=>'\Base::instance()->alias',
+			'format'=>'\Base::instance()->format'
+		);
+
+	/**
+	*	Convert token to variable
+	*	@return string
+	*	@param $str string
+	**/
+	function token($str) {
+		return trim(preg_replace('/\{\{(.+?)\}\}/s',trim('\1'),
+			Base::instance()->compile($str)));
+	}
+
+	/**
+	*	register token filter
+	*	@param string $key
+	*	@param string $func
+	*	@return array
+	*/
+	function filter($key=NULL,$func=NULL) {
+		if (!$key)
+			return array_keys($this->filter);
+		if (!$func)
+			return $this->filter[$key];
+		$this->filter[$key]=$func;
+	}
+
+	/**
+	*	Assemble markup
+	*	@return string
+	*	@param $node string
+	**/
+	protected function build($node) {
+		$self=$this;
+		return preg_replace_callback(
+			'/\{\-(.+?)\-\}|\{\{(.+?)\}\}(\n+)?/s',
+			function($expr) use($self) {
+				if ($expr[1])
+					return $expr[1];
+				$str=trim($self->token($expr[2]));
+				if (preg_match('/^([^|]+?)\h*\|(\h*\w+(?:\h*[,;]\h*\w+)*)/',
+					$str,$parts)) {
+					$str=$parts[1];
+					foreach (Base::instance()->split($parts[2]) as $func)
+						$str=$self->filter($func).'('.$str.')';
+				}
+				return '<?php echo '.$str.'; ?>'.
+					(isset($expr[3])?$expr[3]."\n":'');
+			},
+			preg_replace_callback(
+				'/\{~(.+?)~\}/s',
+				function($expr) use($self) {
+					return '<?php '.$self->token($expr[1]).' ?>';
+				},
+				$node
+			)
+		);
+	}
+
+	/**
+	*	Render template string
+	*	@return string
+	*	@param $str string
+	*	@param $hive array
+	**/
+	function resolve($str,array $hive=NULL) {
+		if (!$hive)
+			$hive=\Base::instance()->hive();
+		extract($hive);
+		ob_start();
+		eval(' ?>'.$this->build($str).'<?php ');
+		return ob_get_clean();
+	}
+
+	/**
+	*	Render template
+	*	@return string
+	*	@param $file string
+	*	@param $mime string
+	*	@param $hive array
+	*	@param $ttl int
+	**/
+	function render($file,$mime='text/html',array $hive=NULL,$ttl=0) {
+		$fw=Base::instance();
+		$cache=Cache::instance();
+		if (!is_dir($tmp=$fw->get('TEMP')))
+			mkdir($tmp,Base::MODE,TRUE);
+		foreach ($fw->split($fw->get('UI')) as $dir) {
+			$cached=$cache->exists($hash=$fw->hash($dir.$file),$data);
+			if ($cached && $cached[0]+$ttl>microtime(TRUE))
+				return $data;
+			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(
+						'/(?<!["\'])\h*<\?(?:php|\s*=).+?\?>\h*'.
+						'(?!["\'])|\{\*.+?\*\}/is','',
+						$fw->read($view));
+					if (method_exists($this,'parse'))
+						$text=$this->parse($text);
+					$fw->write($this->view,$this->build($text));
+				}
+				if (isset($_COOKIE[session_name()]))
+					@session_start();
+				$fw->sync('SESSION');
+				if ($mime && PHP_SAPI!='cli' && !headers_sent())
+					header('Content-Type: '.($this->mime=$mime).'; '.
+						'charset='.$fw->get('ENCODING'));
+				$data=$this->sandbox($hive);
+				if(isset($this->trigger['afterrender']))
+					foreach ($this->trigger['afterrender'] as $func)
+						$data = $fw->call($func, $data);
+				if ($ttl)
+					$cache->set($hash,$data);
+				return $data;
+			}
+		}
+		user_error(sprintf(Base::E_Open,$file),E_USER_ERROR);
+	}
+
+}
+
+//! ISO language/country codes
+class ISO extends Prefab {
+
+	//@{ ISO 3166-1 country codes
+	const
+		CC_af='Afghanistan',
+		CC_ax='Åland Islands',
+		CC_al='Albania',
+		CC_dz='Algeria',
+		CC_as='American Samoa',
+		CC_ad='Andorra',
+		CC_ao='Angola',
+		CC_ai='Anguilla',
+		CC_aq='Antarctica',
+		CC_ag='Antigua and Barbuda',
+		CC_ar='Argentina',
+		CC_am='Armenia',
+		CC_aw='Aruba',
+		CC_au='Australia',
+		CC_at='Austria',
+		CC_az='Azerbaijan',
+		CC_bs='Bahamas',
+		CC_bh='Bahrain',
+		CC_bd='Bangladesh',
+		CC_bb='Barbados',
+		CC_by='Belarus',
+		CC_be='Belgium',
+		CC_bz='Belize',
+		CC_bj='Benin',
+		CC_bm='Bermuda',
+		CC_bt='Bhutan',
+		CC_bo='Bolivia',
+		CC_bq='Bonaire, Sint Eustatius and Saba',
+		CC_ba='Bosnia and Herzegovina',
+		CC_bw='Botswana',
+		CC_bv='Bouvet Island',
+		CC_br='Brazil',
+		CC_io='British Indian Ocean Territory',
+		CC_bn='Brunei Darussalam',
+		CC_bg='Bulgaria',
+		CC_bf='Burkina Faso',
+		CC_bi='Burundi',
+		CC_kh='Cambodia',
+		CC_cm='Cameroon',
+		CC_ca='Canada',
+		CC_cv='Cape Verde',
+		CC_ky='Cayman Islands',
+		CC_cf='Central African Republic',
+		CC_td='Chad',
+		CC_cl='Chile',
+		CC_cn='China',
+		CC_cx='Christmas Island',
+		CC_cc='Cocos (Keeling) Islands',
+		CC_co='Colombia',
+		CC_km='Comoros',
+		CC_cg='Congo',
+		CC_cd='Congo, The Democratic Republic of',
+		CC_ck='Cook Islands',
+		CC_cr='Costa Rica',
+		CC_ci='Côte d\'ivoire',
+		CC_hr='Croatia',
+		CC_cu='Cuba',
+		CC_cw='Curaçao',
+		CC_cy='Cyprus',
+		CC_cz='Czech Republic',
+		CC_dk='Denmark',
+		CC_dj='Djibouti',
+		CC_dm='Dominica',
+		CC_do='Dominican Republic',
+		CC_ec='Ecuador',
+		CC_eg='Egypt',
+		CC_sv='El Salvador',
+		CC_gq='Equatorial Guinea',
+		CC_er='Eritrea',
+		CC_ee='Estonia',
+		CC_et='Ethiopia',
+		CC_fk='Falkland Islands (Malvinas)',
+		CC_fo='Faroe Islands',
+		CC_fj='Fiji',
+		CC_fi='Finland',
+		CC_fr='France',
+		CC_gf='French Guiana',
+		CC_pf='French Polynesia',
+		CC_tf='French Southern Territories',
+		CC_ga='Gabon',
+		CC_gm='Gambia',
+		CC_ge='Georgia',
+		CC_de='Germany',
+		CC_gh='Ghana',
+		CC_gi='Gibraltar',
+		CC_gr='Greece',
+		CC_gl='Greenland',
+		CC_gd='Grenada',
+		CC_gp='Guadeloupe',
+		CC_gu='Guam',
+		CC_gt='Guatemala',
+		CC_gg='Guernsey',
+		CC_gn='Guinea',
+		CC_gw='Guinea-Bissau',
+		CC_gy='Guyana',
+		CC_ht='Haiti',
+		CC_hm='Heard Island and McDonald Islands',
+		CC_va='Holy See (Vatican City State)',
+		CC_hn='Honduras',
+		CC_hk='Hong Kong',
+		CC_hu='Hungary',
+		CC_is='Iceland',
+		CC_in='India',
+		CC_id='Indonesia',
+		CC_ir='Iran, Islamic Republic of',
+		CC_iq='Iraq',
+		CC_ie='Ireland',
+		CC_im='Isle of Man',
+		CC_il='Israel',
+		CC_it='Italy',
+		CC_jm='Jamaica',
+		CC_jp='Japan',
+		CC_je='Jersey',
+		CC_jo='Jordan',
+		CC_kz='Kazakhstan',
+		CC_ke='Kenya',
+		CC_ki='Kiribati',
+		CC_kp='Korea, Democratic People\'s Republic of',
+		CC_kr='Korea, Republic of',
+		CC_kw='Kuwait',
+		CC_kg='Kyrgyzstan',
+		CC_la='Lao People\'s Democratic Republic',
+		CC_lv='Latvia',
+		CC_lb='Lebanon',
+		CC_ls='Lesotho',
+		CC_lr='Liberia',
+		CC_ly='Libya',
+		CC_li='Liechtenstein',
+		CC_lt='Lithuania',
+		CC_lu='Luxembourg',
+		CC_mo='Macao',
+		CC_mk='Macedonia, The Former Yugoslav Republic of',
+		CC_mg='Madagascar',
+		CC_mw='Malawi',
+		CC_my='Malaysia',
+		CC_mv='Maldives',
+		CC_ml='Mali',
+		CC_mt='Malta',
+		CC_mh='Marshall Islands',
+		CC_mq='Martinique',
+		CC_mr='Mauritania',
+		CC_mu='Mauritius',
+		CC_yt='Mayotte',
+		CC_mx='Mexico',
+		CC_fm='Micronesia, Federated States of',
+		CC_md='Moldova, Republic of',
+		CC_mc='Monaco',
+		CC_mn='Mongolia',
+		CC_me='Montenegro',
+		CC_ms='Montserrat',
+		CC_ma='Morocco',
+		CC_mz='Mozambique',
+		CC_mm='Myanmar',
+		CC_na='Namibia',
+		CC_nr='Nauru',
+		CC_np='Nepal',
+		CC_nl='Netherlands',
+		CC_nc='New Caledonia',
+		CC_nz='New Zealand',
+		CC_ni='Nicaragua',
+		CC_ne='Niger',
+		CC_ng='Nigeria',
+		CC_nu='Niue',
+		CC_nf='Norfolk Island',
+		CC_mp='Northern Mariana Islands',
+		CC_no='Norway',
+		CC_om='Oman',
+		CC_pk='Pakistan',
+		CC_pw='Palau',
+		CC_ps='Palestinian Territory, Occupied',
+		CC_pa='Panama',
+		CC_pg='Papua New Guinea',
+		CC_py='Paraguay',
+		CC_pe='Peru',
+		CC_ph='Philippines',
+		CC_pn='Pitcairn',
+		CC_pl='Poland',
+		CC_pt='Portugal',
+		CC_pr='Puerto Rico',
+		CC_qa='Qatar',
+		CC_re='Réunion',
+		CC_ro='Romania',
+		CC_ru='Russian Federation',
+		CC_rw='Rwanda',
+		CC_bl='Saint Barthélemy',
+		CC_sh='Saint Helena, Ascension and Tristan da Cunha',
+		CC_kn='Saint Kitts and Nevis',
+		CC_lc='Saint Lucia',
+		CC_mf='Saint Martin (French Part)',
+		CC_pm='Saint Pierre and Miquelon',
+		CC_vc='Saint Vincent and The Grenadines',
+		CC_ws='Samoa',
+		CC_sm='San Marino',
+		CC_st='Sao Tome and Principe',
+		CC_sa='Saudi Arabia',
+		CC_sn='Senegal',
+		CC_rs='Serbia',
+		CC_sc='Seychelles',
+		CC_sl='Sierra Leone',
+		CC_sg='Singapore',
+		CC_sk='Slovakia',
+		CC_sx='Sint Maarten (Dutch Part)',
+		CC_si='Slovenia',
+		CC_sb='Solomon Islands',
+		CC_so='Somalia',
+		CC_za='South Africa',
+		CC_gs='South Georgia and The South Sandwich Islands',
+		CC_ss='South Sudan',
+		CC_es='Spain',
+		CC_lk='Sri Lanka',
+		CC_sd='Sudan',
+		CC_sr='Suriname',
+		CC_sj='Svalbard and Jan Mayen',
+		CC_sz='Swaziland',
+		CC_se='Sweden',
+		CC_ch='Switzerland',
+		CC_sy='Syrian Arab Republic',
+		CC_tw='Taiwan, Province of China',
+		CC_tj='Tajikistan',
+		CC_tz='Tanzania, United Republic of',
+		CC_th='Thailand',
+		CC_tl='Timor-Leste',
+		CC_tg='Togo',
+		CC_tk='Tokelau',
+		CC_to='Tonga',
+		CC_tt='Trinidad and Tobago',
+		CC_tn='Tunisia',
+		CC_tr='Turkey',
+		CC_tm='Turkmenistan',
+		CC_tc='Turks and Caicos Islands',
+		CC_tv='Tuvalu',
+		CC_ug='Uganda',
+		CC_ua='Ukraine',
+		CC_ae='United Arab Emirates',
+		CC_gb='United Kingdom',
+		CC_us='United States',
+		CC_um='United States Minor Outlying Islands',
+		CC_uy='Uruguay',
+		CC_uz='Uzbekistan',
+		CC_vu='Vanuatu',
+		CC_ve='Venezuela',
+		CC_vn='Viet Nam',
+		CC_vg='Virgin Islands, British',
+		CC_vi='Virgin Islands, U.S.',
+		CC_wf='Wallis and Futuna',
+		CC_eh='Western Sahara',
+		CC_ye='Yemen',
+		CC_zm='Zambia',
+		CC_zw='Zimbabwe';
+	//@}
+
+	//@{ ISO 639-1 language codes (Windows-compatibility subset)
+	const
+		LC_af='Afrikaans',
+		LC_am='Amharic',
+		LC_ar='Arabic',
+		LC_as='Assamese',
+		LC_ba='Bashkir',
+		LC_be='Belarusian',
+		LC_bg='Bulgarian',
+		LC_bn='Bengali',
+		LC_bo='Tibetan',
+		LC_br='Breton',
+		LC_ca='Catalan',
+		LC_co='Corsican',
+		LC_cs='Czech',
+		LC_cy='Welsh',
+		LC_da='Danish',
+		LC_de='German',
+		LC_dv='Divehi',
+		LC_el='Greek',
+		LC_en='English',
+		LC_es='Spanish',
+		LC_et='Estonian',
+		LC_eu='Basque',
+		LC_fa='Persian',
+		LC_fi='Finnish',
+		LC_fo='Faroese',
+		LC_fr='French',
+		LC_gd='Scottish Gaelic',
+		LC_gl='Galician',
+		LC_gu='Gujarati',
+		LC_he='Hebrew',
+		LC_hi='Hindi',
+		LC_hr='Croatian',
+		LC_hu='Hungarian',
+		LC_hy='Armenian',
+		LC_id='Indonesian',
+		LC_ig='Igbo',
+		LC_is='Icelandic',
+		LC_it='Italian',
+		LC_ja='Japanese',
+		LC_ka='Georgian',
+		LC_kk='Kazakh',
+		LC_km='Khmer',
+		LC_kn='Kannada',
+		LC_ko='Korean',
+		LC_lb='Luxembourgish',
+		LC_lo='Lao',
+		LC_lt='Lithuanian',
+		LC_lv='Latvian',
+		LC_mi='Maori',
+		LC_ml='Malayalam',
+		LC_mr='Marathi',
+		LC_ms='Malay',
+		LC_mt='Maltese',
+		LC_ne='Nepali',
+		LC_nl='Dutch',
+		LC_no='Norwegian',
+		LC_oc='Occitan',
+		LC_or='Oriya',
+		LC_pl='Polish',
+		LC_ps='Pashto',
+		LC_pt='Portuguese',
+		LC_qu='Quechua',
+		LC_ro='Romanian',
+		LC_ru='Russian',
+		LC_rw='Kinyarwanda',
+		LC_sa='Sanskrit',
+		LC_si='Sinhala',
+		LC_sk='Slovak',
+		LC_sl='Slovenian',
+		LC_sq='Albanian',
+		LC_sv='Swedish',
+		LC_ta='Tamil',
+		LC_te='Telugu',
+		LC_th='Thai',
+		LC_tk='Turkmen',
+		LC_tr='Turkish',
+		LC_tt='Tatar',
+		LC_uk='Ukrainian',
+		LC_ur='Urdu',
+		LC_vi='Vietnamese',
+		LC_wo='Wolof',
+		LC_yo='Yoruba',
+		LC_zh='Chinese';
+	//@}
+
+	/**
+	*	Return list of languages indexed by ISO 639-1 language code
+	*	@return array
+	**/
+	function languages() {
+		return \Base::instance()->constants($this,'LC_');
+	}
+
+	/**
+	*	Return list of countries indexed by ISO 3166-1 country code
+	*	@return array
+	**/
+	function countries() {
+		return \Base::instance()->constants($this,'CC_');
+	}
+
+}
+
+//! Container for singular object instances
+final class Registry {
+
+	private static
+		//! Object catalog
+		$table;
+
+	/**
+	*	Return TRUE if object exists in catalog
+	*	@return bool
+	*	@param $key string
+	**/
+	static function exists($key) {
+		return isset(self::$table[$key]);
+	}
+
+	/**
+	*	Add object to catalog
+	*	@return object
+	*	@param $key string
+	*	@param $obj object
+	**/
+	static function set($key,$obj) {
+		return self::$table[$key]=$obj;
+	}
+
+	/**
+	*	Retrieve object from catalog
+	*	@return object
+	*	@param $key string
+	**/
+	static function get($key) {
+		return self::$table[$key];
+	}
+
+	/**
+	*	Delete object from catalog
+	*	@return NULL
+	*	@param $key string
+	**/
+	static function clear($key) {
+		self::$table[$key]=NULL;
+		unset(self::$table[$key]);
+	}
+
+	//! Prohibit cloning
+	private function __clone() {
+	}
+
+	//! Prohibit instantiation
+	private function __construct() {
+	}
+
+}
+
+return Base::instance();

+ 384 - 0
frameworks/PHP/fat-free/src/db/cursor.php

@@ -0,0 +1,384 @@
+<?php
+
+/*
+
+	Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfreeframework.com).
+
+	This is free software: you can redistribute it and/or modify it under the
+	terms of the GNU General Public License as published by the Free Software
+	Foundation, either version 3 of the License, or later.
+
+	Fat-Free Framework is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+	General Public License for more details.
+
+	You should have received a copy of the GNU General Public License along
+	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+namespace DB;
+
+//! Simple cursor implementation
+abstract class Cursor extends \Magic implements \IteratorAggregate {
+
+	//@{ Error messages
+	const
+		E_Field='Undefined field %s';
+	//@}
+
+	protected
+		//! Query results
+		$query=array(),
+		//! Current position
+		$ptr=0,
+		//! Event listeners
+		$trigger=array();
+
+	/**
+	*	Return database type
+	*	@return string
+	**/
+	abstract function dbtype();
+
+	/**
+	*	Return field names
+	*	@return array
+	**/
+	abstract function fields();
+
+	/**
+	*	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,$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
+	*	@return array
+	**/
+	abstract function insert();
+
+	/**
+	*	Update current record
+	*	@return array
+	**/
+	abstract function update();
+
+	/**
+	*	Hydrate mapper object using hive array variable
+	*	@return NULL
+	*	@param $var array|string
+	*	@param $func callback
+	**/
+	abstract function copyfrom($var,$func=NULL);
+
+	/**
+	*	Populate hive array variable with mapper fields
+	*	@return NULL
+	*	@param $key string
+	**/
+	abstract function copyto($key);
+
+	/**
+	*	Get cursor's equivalent external iterator
+	*	Causes a fatal error in PHP 5.3.5if uncommented
+	*	return ArrayIterator
+	**/
+	abstract function getiterator();
+
+
+	/**
+	*	Return TRUE if current cursor position is not mapped to any record
+	*	@return bool
+	**/
+	function dry() {
+		return empty($this->query[$this->ptr]);
+	}
+
+	/**
+	*	Return first record (mapper object) that matches criteria
+	*	@return \DB\Cursor|FALSE
+	*	@param $filter string|array
+	*	@param $options array
+	*	@param $ttl int
+	**/
+	function findone($filter=NULL,array $options=NULL,$ttl=0) {
+		if (!$options)
+			$options=array();
+		// Override limit
+		$options['limit']=1;
+		return ($data=$this->find($filter,$options,$ttl))?$data[0]:FALSE;
+	}
+
+	/**
+	*	Return array containing subset of records matching criteria,
+	*	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,$ttl=0) {
+		$total=$this->count($filter,$ttl);
+		$count=ceil($total/$size);
+		$pos=max(0,min($pos,$count-1));
+		return array(
+			'subset'=>$this->find($filter,
+				array_merge(
+					$options?:array(),
+					array('limit'=>$size,'offset'=>$pos*$size)
+				),
+				$ttl
+			),
+			'total'=>$total,
+			'limit'=>$size,
+			'count'=>$count,
+			'pos'=>$pos<$count?$pos:0
+		);
+	}
+
+	/**
+	*	Map to first record that matches criteria
+	*	@return array|FALSE
+	*	@param $filter string|array
+	*	@param $options array
+	*	@param $ttl int
+	**/
+	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
+	**/
+	function first() {
+		return $this->skip(-$this->ptr);
+	}
+
+	/**
+	*	Map to last record in cursor
+	*	@return mixed
+	**/
+	function last() {
+		return $this->skip(($ofs=count($this->query)-$this->ptr)?$ofs-1:0);
+	}
+
+	/**
+	*	Map to nth record relative to current cursor position
+	*	@return mixed
+	*	@param $ofs int
+	**/
+	function skip($ofs=1) {
+		$this->ptr+=$ofs;
+		return $this->ptr>-1 && $this->ptr<count($this->query)?
+			$this->query[$this->ptr]:FALSE;
+	}
+
+	/**
+	*	Map next record
+	*	@return mixed
+	**/
+	function next() {
+		return $this->skip();
+	}
+
+	/**
+	*	Map previous record
+	*	@return mixed
+	**/
+	function prev() {
+		return $this->skip(-1);
+	}
+
+	/**
+	 * Return whether current iterator position is valid.
+	 */
+	function valid() {
+		return !$this->dry();
+	}
+
+	/**
+	*	Save mapped record
+	*	@return mixed
+	**/
+	function save() {
+		return $this->query?$this->update():$this->insert();
+	}
+
+	/**
+	*	Delete current record
+	*	@return int|bool
+	**/
+	function erase() {
+		$this->query=array_slice($this->query,0,$this->ptr,TRUE)+
+			array_slice($this->query,$this->ptr,NULL,TRUE);
+		$this->skip(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 beforesave trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function beforesave($func) {
+		$this->trigger['beforeinsert']=$func;
+		$this->trigger['beforeupdate']=$func;
+		return $func;
+	}
+
+	/**
+	*	Define aftersave trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function aftersave($func) {
+		$this->trigger['afterinsert']=$func;
+		$this->trigger['afterupdate']=$func;
+		return $func;
+	}
+
+	/**
+	*	Define onsave trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function onsave($func) {
+		return $this->aftersave($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
+	**/
+	function reset() {
+		$this->query=array();
+		$this->ptr=0;
+	}
+
+}

+ 111 - 0
frameworks/PHP/fat-free/src/db/mongo.php

@@ -0,0 +1,111 @@
+<?php
+
+/*
+
+	Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfreeframework.com).
+
+	This is free software: you can redistribute it and/or modify it under the
+	terms of the GNU General Public License as published by the Free Software
+	Foundation, either version 3 of the License, or later.
+
+	Fat-Free Framework is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+	General Public License for more details.
+
+	You should have received a copy of the GNU General Public License along
+	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+namespace DB;
+
+//! MongoDB wrapper
+class Mongo {
+
+	//@{
+	const
+		E_Profiler='MongoDB profiler is disabled';
+	//@}
+
+	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
+	**/
+	function log() {
+		$cursor=$this->selectcollection('system.profile')->find();
+		foreach (iterator_to_array($cursor) as $frame)
+			if (!preg_match('/\.system\..+$/',$frame['ns']))
+				$this->log.=date('r',$frame['ts']->sec).' ('.
+					sprintf('%.1f',$frame['millis']).'ms) '.
+					$frame['ns'].' ['.$frame['op'].'] '.
+					(empty($frame['query'])?
+						'':json_encode($frame['query'])).
+					(empty($frame['command'])?
+						'':json_encode($frame['command'])).
+					PHP_EOL;
+		return $this->log;
+	}
+
+	/**
+	*	Intercept native call to re-enable profiler
+	*	@return int
+	**/
+	function 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
+	*	@param $dbname string
+	*	@param $options array
+	**/
+	function __construct($dsn,$dbname,array $options=NULL) {
+		$this->uuid=\Base::instance()->hash($this->dsn=$dsn);
+		$class=class_exists('\MongoClient')?'\MongoClient':'\Mongo';
+		$this->db=new \MongoDB(new $class($dsn,$options?:array()),$dbname);
+		$this->setprofilinglevel(2);
+	}
+
+}

+ 361 - 0
frameworks/PHP/fat-free/src/db/mongo/mapper.php

@@ -0,0 +1,361 @@
+<?php
+
+/*
+
+	Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfreeframework.com).
+
+	This is free software: you can redistribute it and/or modify it under the
+	terms of the GNU General Public License as published by the Free Software
+	Foundation, either version 3 of the License, or later.
+
+	Fat-Free Framework is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+	General Public License for more details.
+
+	You should have received a copy of the GNU General Public License along
+	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+namespace DB\Mongo;
+
+//! MongoDB mapper
+class Mapper extends \DB\Cursor {
+
+	protected
+		//! MongoDB wrapper
+		$db,
+		//! Mongo collection
+		$collection,
+		//! Mongo document
+		$document=array(),
+		//! Mongo cursor
+		$cursor;
+
+	/**
+	*	Return database type
+	*	@return string
+	**/
+	function dbtype() {
+		return 'Mongo';
+	}
+
+	/**
+	*	Return TRUE if field is defined
+	*	@return bool
+	*	@param $key string
+	**/
+	function exists($key) {
+		return array_key_exists($key,$this->document);
+	}
+
+	/**
+	*	Assign value to field
+	*	@return scalar|FALSE
+	*	@param $key string
+	*	@param $val scalar
+	**/
+	function set($key,$val) {
+		return $this->document[$key]=$val;
+	}
+
+	/**
+	*	Retrieve value of field
+	*	@return scalar|FALSE
+	*	@param $key string
+	**/
+	function &get($key) {
+		if ($this->exists($key))
+			return $this->document[$key];
+		user_error(sprintf(self::E_Field,$key),E_USER_ERROR);
+	}
+
+	/**
+	*	Delete field
+	*	@return NULL
+	*	@param $key string
+	**/
+	function clear($key) {
+		unset($this->document[$key]);
+	}
+
+	/**
+	*	Convert array to mapper object
+	*	@return \DB\Mongo\Mapper
+	*	@param $row array
+	**/
+	protected function factory($row) {
+		$mapper=clone($this);
+		$mapper->reset();
+		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;
+	}
+
+	/**
+	*	Return fields of mapper object as an associative array
+	*	@return array
+	*	@param $obj object
+	**/
+	function cast($obj=NULL) {
+		if (!$obj)
+			$obj=$this;
+		return $obj->document;
+	}
+
+	/**
+	*	Build query and execute
+	*	@return \DB\Mongo\Mapper[]
+	*	@param $fields string
+	*	@param $filter array
+	*	@param $options array
+	*	@param $ttl int
+	**/
+	function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) {
+		if (!$options)
+			$options=array();
+		$options+=array(
+			'group'=>NULL,
+			'order'=>NULL,
+			'limit'=>0,
+			'offset'=>0
+		);
+		$fw=\Base::instance();
+		$cache=\Cache::instance();
+		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']) {
+				$grp=$this->collection->group(
+					$options['group']['keys'],
+					$options['group']['initial'],
+					$options['group']['reduce'],
+					array(
+						'condition'=>$filter,
+						'finalize'=>$options['group']['finalize']
+					)
+				);
+				$tmp=$this->db->selectcollection(
+					$fw->get('HOST').'.'.$fw->get('BASE').'.'.
+					uniqid(NULL,TRUE).'.tmp'
+				);
+				$tmp->batchinsert($grp['retval'],array('w'=>1));
+				$filter=array();
+				$collection=$tmp;
+			}
+			else {
+				$filter=$filter?:array();
+				$collection=$this->collection;
+			}
+			$this->cursor=$collection->find($filter,$fields?:array());
+			if ($options['order'])
+				$this->cursor=$this->cursor->sort($options['order']);
+			if ($options['limit'])
+				$this->cursor=$this->cursor->limit($options['limit']);
+			if ($options['offset'])
+				$this->cursor=$this->cursor->skip($options['offset']);
+			$result=array();
+			while ($this->cursor->hasnext())
+				$result[]=$this->cursor->getnext();
+			if ($options['group'])
+				$tmp->drop();
+			if ($fw->get('CACHE') && $ttl)
+				// Save to cache backend
+				$cache->set($hash,$result,$ttl);
+		}
+		$out=array();
+		foreach ($result as $doc)
+			$out[]=$this->factory($doc);
+		return $out;
+	}
+
+	/**
+	*	Return records that match criteria
+	*	@return \DB\Mongo\Mapper[]
+	*	@param $filter array
+	*	@param $options array
+	*	@param $ttl int
+	**/
+	function find($filter=NULL,array $options=NULL,$ttl=0) {
+		if (!$options)
+			$options=array();
+		$options+=array(
+			'group'=>NULL,
+			'order'=>NULL,
+			'limit'=>0,
+			'offset'=>0
+		);
+		return $this->select(NULL,$filter,$options,$ttl);
+	}
+
+	/**
+	*	Count records that match criteria
+	*	@return int
+	*	@param $filter array
+	*	@param $ttl int
+	**/
+	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?:array());
+			if ($fw->get('CACHE') && $ttl)
+				// Save to cache backend
+				$cache->set($hash,$result,$ttl);
+		}
+		return $result;
+	}
+
+	/**
+	*	Return record at specified offset using criteria of previous
+	*	load() call and make it active
+	*	@return array
+	*	@param $ofs int
+	**/
+	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;
+	}
+
+	/**
+	*	Insert new record
+	*	@return array
+	**/
+	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'])))===FALSE)
+			return $this->document;
+		$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;
+	}
+
+	/**
+	*	Update current record
+	*	@return array
+	**/
+	function update() {
+		$pkey=array('_id'=>$this->document['_id']);
+		if (isset($this->trigger['beforeupdate']) &&
+			\Base::instance()->call($this->trigger['beforeupdate'],
+				array($this,$pkey))===FALSE)
+			return $this->document;
+		$this->collection->update(
+			$pkey,$this->document,array('upsert'=>TRUE));
+		if (isset($this->trigger['afterupdate']))
+			\Base::instance()->call($this->trigger['afterupdate'],
+				array($this,$pkey));
+		return $this->document;
+	}
+
+	/**
+	*	Delete current record
+	*	@return bool
+	*	@param $filter array
+	**/
+	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))===FALSE)
+			return FALSE;
+		$result=$this->collection->
+			remove(array('_id'=>$this->document['_id']));
+		parent::erase();
+		if (isset($this->trigger['aftererase']))
+			\Base::instance()->call($this->trigger['aftererase'],
+				array($this,$pkey));
+		return $result;
+	}
+
+	/**
+	*	Reset cursor
+	*	@return NULL
+	**/
+	function reset() {
+		$this->document=array();
+		parent::reset();
+	}
+
+	/**
+	*	Hydrate mapper object using hive array variable
+	*	@return NULL
+	*	@param $var array|string
+	*	@param $func callback
+	**/
+	function copyfrom($var,$func=NULL) {
+		if (is_string($var))
+			$var=\Base::instance()->get($var);
+		if ($func)
+			$var=call_user_func($func,$var);
+		foreach ($var as $key=>$val)
+			$this->document[$key]=$val;
+	}
+
+	/**
+	*	Populate hive array variable with mapper fields
+	*	@return NULL
+	*	@param $key string
+	**/
+	function copyto($key) {
+		$var=&\Base::instance()->ref($key);
+		foreach ($this->document as $key=>$field)
+			$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;
+	}
+
+	/**
+	*	Retrieve external iterator for fields
+	*	@return object
+	**/
+	function getiterator() {
+		return new \ArrayIterator($this->cast());
+	}
+
+	/**
+	*	Instantiate class
+	*	@return void
+	*	@param $db object
+	*	@param $collection string
+	**/
+	function __construct(\DB\Mongo $db,$collection) {
+		$this->db=$db;
+		$this->collection=$db->selectcollection($collection);
+		$this->reset();
+	}
+
+}

+ 180 - 0
frameworks/PHP/fat-free/src/db/mongo/session.php

@@ -0,0 +1,180 @@
+<?php
+
+/*
+
+	Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfreeframework.com).
+
+	This is free software: you can redistribute it and/or modify it under the
+	terms of the GNU General Public License as published by the Free Software
+	Foundation, either version 3 of the License, or later.
+
+	Fat-Free Framework is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+	General Public License for more details.
+
+	You should have received a copy of the GNU General Public License along
+	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+namespace DB\Mongo;
+
+//! MongoDB-managed session handler
+class Session extends Mapper {
+
+	protected
+		//! Session ID
+		$sid;
+
+	/**
+	*	Open session
+	*	@return TRUE
+	*	@param $path string
+	*	@param $name string
+	**/
+	function open($path,$name) {
+		return TRUE;
+	}
+
+	/**
+	*	Close session
+	*	@return TRUE
+	**/
+	function close() {
+		return TRUE;
+	}
+
+	/**
+	*	Return session data in serialized format
+	*	@return string|FALSE
+	*	@param $id string
+	**/
+	function read($id) {
+		if ($id!=$this->sid)
+			$this->load(array('session_id'=>$this->sid=$id));
+		return $this->dry()?FALSE:$this->get('data');
+	}
+
+	/**
+	*	Write session data
+	*	@return TRUE
+	*	@param $id string
+	*	@param $data string
+	**/
+	function write($id,$data) {
+		$fw=\Base::instance();
+		$sent=headers_sent();
+		$headers=$fw->get('HEADERS');
+		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();
+		return TRUE;
+	}
+
+	/**
+	*	Destroy session
+	*	@return TRUE
+	*	@param $id string
+	**/
+	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;
+	}
+
+	/**
+	*	Garbage collector
+	*	@return TRUE
+	*	@param $max int
+	**/
+	function cleanup($max) {
+		$this->erase(array('$where'=>'this.stamp+'.$max.'<'.time()));
+		return TRUE;
+	}
+
+	/**
+	*	Return anti-CSRF token
+	*	@return string|FALSE
+	**/
+	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
+	*	@return string|FALSE
+	**/
+	function stamp() {
+		return $this->dry()?FALSE:$this->get('stamp');
+	}
+
+	/**
+	*	Return HTTP user agent
+	*	@return string|FALSE
+	**/
+	function agent() {
+		return $this->dry()?FALSE:$this->get('agent');
+	}
+
+	/**
+	*	Instantiate class
+	*	@param $db object
+	*	@param $table string
+	*	@param $onsuspect callback
+	**/
+	function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL) {
+		parent::__construct($db,$table);
+		session_set_save_handler(
+			array($this,'open'),
+			array($this,'close'),
+			array($this,'read'),
+			array($this,'write'),
+			array($this,'destroy'),
+			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'])) {
+			if (isset($onsuspect))
+				$fw->call($onsuspect,array($this));
+			else {
+				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();
+		}
+	}
+
+}

+ 455 - 0
frameworks/PHP/fat-free/src/db/sql.php

@@ -0,0 +1,455 @@
+<?php
+
+/*
+
+	Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfreeframework.com).
+
+	This is free software: you can redistribute it and/or modify it under the
+	terms of the GNU General Public License as published by the Free Software
+	Foundation, either version 3 of the License, or later.
+
+	Fat-Free Framework is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+	General Public License for more details.
+
+	You should have received a copy of the GNU General Public License along
+	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+namespace DB;
+
+//! PDO wrapper
+class SQL {
+
+	//@{ Error messages
+	const
+		E_PKey='Table %s does not have a primary key';
+	//@}
+
+	protected
+		//! UUID
+		$uuid,
+		//! Raw PDO
+		$pdo,
+		//! Data source name
+		$dsn,
+		//! Database engine
+		$engine,
+		//! Database name
+		$dbname,
+		//! Transaction flag
+		$trans=FALSE,
+		//! Number of rows affected by query
+		$rows=0,
+		//! SQL log
+		$log;
+
+	/**
+	*	Begin SQL transaction
+	*	@return bool
+	**/
+	function begin() {
+		$out=$this->pdo->begintransaction();
+		$this->trans=TRUE;
+		return $out;
+	}
+
+	/**
+	*	Rollback SQL transaction
+	*	@return bool
+	**/
+	function rollback() {
+		$out=$this->pdo->rollback();
+		$this->trans=FALSE;
+		return $out;
+	}
+
+	/**
+	*	Commit SQL transaction
+	*	@return bool
+	**/
+	function commit() {
+		$out=$this->pdo->commit();
+		$this->trans=FALSE;
+		return $out;
+	}
+
+	/**
+	*	Map data type of argument to a PDO constant
+	*	@return int
+	*	@param $val scalar
+	**/
+	function type($val) {
+		switch (gettype($val)) {
+			case 'NULL':
+				return \PDO::PARAM_NULL;
+			case 'boolean':
+				return \PDO::PARAM_BOOL;
+			case 'integer':
+				return \PDO::PARAM_INT;
+			default:
+				return \PDO::PARAM_STR;
+		}
+	}
+
+	/**
+	*	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)
+	*	@return array|int|FALSE
+	*	@param $cmds string|array
+	*	@param $args string|array
+	*	@param $ttl int
+	*	@param $log bool
+	**/
+	function exec($cmds,$args=NULL,$ttl=0,$log=TRUE) {
+		$auto=FALSE;
+		if (is_null($args))
+			$args=array();
+		elseif (is_scalar($args))
+			$args=array(1=>$args);
+		if (is_array($cmds)) {
+			if (count($args)<($count=count($cmds)))
+				// Apply arguments to SQL commands
+				$args=array_fill(0,$count,$args);
+			if (!$this->trans) {
+				$this->begin();
+				$auto=TRUE;
+			}
+		}
+		else {
+			$count=1;
+			$cmds=array($cmds);
+			$args=array($args);
+		}
+		$fw=\Base::instance();
+		$cache=\Cache::instance();
+		$result=FALSE;
+		for ($i=0;$i<$count;$i++) {
+			$cmd=$cmds[$i];
+			$arg=$args[$i];
+			if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd))
+				continue;
+			$now=microtime(TRUE);
+			$keys=$vals=array();
+			if ($fw->get('CACHE') && $ttl && ($cached=$cache->exists(
+				$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[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key).
+						'/';
+				}
+				if ($log)
+					$this->log.=date('r').' ('.
+						sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
+						'[CACHED] '.
+						preg_replace($keys,$vals,
+							str_replace('?',chr(0).'?',$cmd),1).PHP_EOL;
+			}
+			elseif (is_object($query=$this->pdo->prepare($cmd))) {
+				foreach ($arg as $key=>$val) {
+					if (is_array($val)) {
+						// User-specified data type
+						$query->bindvalue($key,$val[0],$val[1]);
+						$vals[]=$fw->stringify($this->value($val[1],$val[0]));
+					}
+					else {
+						// Convert to PDO data type
+						$query->bindvalue($key,$val,
+							$type=$this->type($val));
+						$vals[]=$fw->stringify($this->value($type,$val));
+					}
+					$keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key).
+						'/';
+				}
+				if ($log)
+					$this->log.=date('r').' ('.
+						sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
+						preg_replace($keys,$vals,
+							str_replace('?',chr(0).'?',$cmd),1).PHP_EOL;
+				$query->execute();
+				$error=$query->errorinfo();
+				if ($error[0]!=\PDO::ERR_NONE) {
+					// Statement-level error occurred
+					if ($this->trans)
+						$this->rollback();
+					user_error('PDOStatement: '.$error[2],E_USER_ERROR);
+				}
+				if (preg_match('/^\s*'.
+					'(?:EXPLAIN|SELECT|PRAGMA|SHOW|RETURNING)\b/is',$cmd) ||
+					(preg_match('/^\s*(?:CALL|EXEC)\b/is',$cmd) &&
+						$query->columnCount())) {
+					$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
+						$cache->set($hash,$result,$ttl);
+				}
+				else
+					$this->rows=$result=$query->rowcount();
+				$query->closecursor();
+				unset($query);
+			}
+			else {
+				$error=$this->errorinfo();
+				if ($error[0]!=\PDO::ERR_NONE) {
+					// PDO-level error occurred
+					if ($this->trans)
+						$this->rollback();
+					user_error('PDO: '.$error[2],E_USER_ERROR);
+				}
+			}
+		}
+		if ($this->trans && $auto)
+			$this->commit();
+		return $result;
+	}
+
+	/**
+	*	Return number of rows affected by last query
+	*	@return int
+	**/
+	function count() {
+		return $this->rows;
+	}
+
+	/**
+	*	Return SQL profiler results
+	*	@return string
+	**/
+	function log() {
+		return $this->log;
+	}
+
+	/**
+	*	Retrieve schema of SQL table
+	*	@return array|FALSE
+	*	@param $table string
+	*	@param $fields array|string
+	*	@param $ttl int
+	**/
+	function schema($table,$fields=NULL,$ttl=0) {
+		if (strpos($table,'.'))
+			list($schema,$table)=explode('.',$table);
+		// Supported engines
+		$cmd=array(
+			'sqlite2?'=>array(
+				'PRAGMA table_info("'.$table.'");',
+				'name','type','dflt_value','notnull',0,'pk',TRUE),
+			'mysql'=>array(
+				'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`;',
+				'Field','Type','Default','Null','YES','Key','PRI'),
+			'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>array(
+				'SELECT '.
+					'c.column_name AS field,'.
+					'c.data_type AS type,'.
+					'c.column_default AS defval,'.
+					'c.is_nullable AS nullable,'.
+					't.constraint_type AS pkey '.
+				'FROM information_schema.columns AS c '.
+				'LEFT OUTER JOIN '.
+					'information_schema.key_column_usage AS k '.
+					'ON '.
+						'c.table_name=k.table_name AND '.
+						'c.column_name=k.column_name AND '.
+						'c.table_schema=k.table_schema '.
+						($this->dbname?
+							('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 AND '.
+						'k.table_schema=t.table_schema '.
+						($this->dbname?
+							('AND k.table_catalog=t.table_catalog '):'').
+				'WHERE '.
+					'c.table_name='.$this->quote($table).
+					($this->dbname?
+						(' AND c.table_catalog='.
+							$this->quote($this->dbname)):'').
+				';',
+				'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) {
+					if (!$fields || in_array($row[$val[1]],$fields))
+						$rows[$row[$val[1]]]=array(
+							'type'=>$row[$val[2]],
+							'pdo_type'=>
+								preg_match('/int\b|integer/i',$row[$val[2]])?
+									\PDO::PARAM_INT:
+									(preg_match('/bool/i',$row[$val[2]])?
+										\PDO::PARAM_BOOL:
+										\PDO::PARAM_STR),
+							'default'=>is_string($row[$val[3]])?
+								preg_replace('/^\s*([\'"])(.*)\1\s*/','\2',
+								$row[$val[3]]):$row[$val[3]],
+							'nullable'=>$row[$val[4]]==$val[5],
+							'pkey'=>$row[$val[6]]==$val[7]
+						);
+				}
+				return $rows;
+			}
+		user_error(sprintf(self::E_PKey,$table),E_USER_ERROR);
+		return FALSE;
+	}
+
+	/**
+	*	Quote string
+	*	@return string
+	*	@param $val mixed
+	*	@param $type int
+	**/
+	function quote($val,$type=\PDO::PARAM_STR) {
+		return $this->engine=='odbc'?
+			(is_string($val)?
+				\Base::instance()->stringify(str_replace('\'','\'\'',$val)):
+				$val):
+			$this->pdo->quote($val,$type);
+	}
+
+	/**
+	*	Return UUID
+	*	@return string
+	**/
+	function uuid() {
+		return $this->uuid;
+	}
+
+	/**
+	*	Return parent object
+	*	@return \PDO
+	**/
+	function pdo() {
+		return $this->pdo;
+	}
+
+	/**
+	*	Return database engine
+	*	@return string
+	**/
+	function driver() {
+		return $this->engine;
+	}
+
+	/**
+	*	Return server version
+	*	@return string
+	**/
+	function version() {
+		return $this->pdo->getattribute(\PDO::ATTR_SERVER_VERSION);
+	}
+
+	/**
+	*	Return database name
+	*	@return string
+	**/
+	function name() {
+		return $this->dbname;
+	}
+
+	/**
+	*	Return quoted identifier name
+	*	@return string
+	*	@param $key
+	**/
+	function quotekey($key) {
+		$delims=array(
+			'mysql'=>'``',
+			'sqlite2?|pgsql|oci'=>'""',
+			'mssql|sqlsrv|odbc|sybase|dblib'=>'[]'
+		);
+		$use='';
+		foreach ($delims as $engine=>$delim)
+			if (preg_match('/'.$engine.'/',$this->engine)) {
+				$use=$delim;
+				break;
+			}
+		return $use[0].implode($use[1].'.'.$use[0],explode('.',$key)).$use[1];
+	}
+
+	/**
+	*	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->pdo,$func),$args);
+	}
+
+	/**
+	*	Instantiate class
+	*	@param $dsn string
+	*	@param $user string
+	*	@param $pw string
+	*	@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)=(.+?)(?=;|$)/is',$dsn,$parts))
+			$this->dbname=$parts[1];
+		if (!$options)
+			$options=array();
+		if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql')
+			$options+=array(\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '.
+				strtolower(str_replace('-','',$fw->get('ENCODING'))).';');
+		$this->pdo=new \PDO($dsn,$user,$pw,$options);
+		$this->engine=$this->pdo->getattribute(\PDO::ATTR_DRIVER_NAME);
+	}
+
+}

+ 639 - 0
frameworks/PHP/fat-free/src/db/sql/mapper.php

@@ -0,0 +1,639 @@
+<?php
+
+/*
+
+	Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfreeframework.com).
+
+	This is free software: you can redistribute it and/or modify it under the
+	terms of the GNU General Public License as published by the Free Software
+	Foundation, either version 3 of the License, or later.
+
+	Fat-Free Framework is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+	General Public License for more details.
+
+	You should have received a copy of the GNU General Public License along
+	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+namespace DB\SQL;
+
+//! SQL data mapper
+class Mapper extends \DB\Cursor {
+
+	protected
+		//! PDO wrapper
+		$db,
+		//! Database engine
+		$engine,
+		//! SQL table
+		$source,
+		//! SQL table (quoted)
+		$table,
+		//! Last insert ID
+		$_id,
+		//! Defined fields
+		$fields,
+		//! Adhoc fields
+		$adhoc=array();
+
+	/**
+	*	Return database type
+	*	@return string
+	**/
+	function dbtype() {
+		return 'SQL';
+	}
+
+	/**
+	*	Return mapped table
+	*	@return string
+	**/
+	function table() {
+		return $this->source;
+	}
+
+	/**
+	*	Return TRUE if field is defined
+	*	@return bool
+	*	@param $key string
+	**/
+	function exists($key) {
+		return array_key_exists($key,$this->fields+$this->adhoc);
+	}
+
+	/**
+	*	Return TRUE if any/specified field value has changed
+	*	@return bool
+	*	@param $key string
+	**/
+	function changed($key=NULL) {
+		if (isset($key))
+			return $this->fields[$key]['changed'];
+		foreach($this->fields as $key=>$field)
+			if ($field['changed'])
+				return TRUE;
+		return FALSE;
+	}
+
+	/**
+	*	Assign value to field
+	*	@return scalar
+	*	@param $key string
+	*	@param $val scalar
+	**/
+	function set($key,$val) {
+		if (array_key_exists($key,$this->fields)) {
+			$val=is_null($val) && $this->fields[$key]['nullable']?
+				NULL:$this->db->value($this->fields[$key]['pdo_type'],$val);
+			if ($this->fields[$key]['value']!==$val ||
+				$this->fields[$key]['default']!==$val && is_null($val))
+				$this->fields[$key]['changed']=TRUE;
+			return $this->fields[$key]['value']=$val;
+		}
+		// adjust result on existing expressions
+		if (isset($this->adhoc[$key]))
+			$this->adhoc[$key]['value']=$val;
+		else
+			// Parenthesize expression in case it's a subquery
+			$this->adhoc[$key]=array('expr'=>'('.$val.')','value'=>NULL);
+		return $val;
+	}
+
+	/**
+	*	Retrieve value of field
+	*	@return scalar
+	*	@param $key string
+	**/
+	function &get($key) {
+		if ($key=='_id')
+			return $this->_id;
+		elseif (array_key_exists($key,$this->fields))
+			return $this->fields[$key]['value'];
+		elseif (array_key_exists($key,$this->adhoc))
+			return $this->adhoc[$key]['value'];
+		user_error(sprintf(self::E_Field,$key),E_USER_ERROR);
+	}
+
+	/**
+	*	Clear value of field
+	*	@return NULL
+	*	@param $key string
+	**/
+	function clear($key) {
+		if (array_key_exists($key,$this->adhoc))
+			unset($this->adhoc[$key]);
+	}
+
+	/**
+	*	Get PHP type equivalent of PDO constant
+	*	@return string
+	*	@param $pdo string
+	**/
+	function type($pdo) {
+		switch ($pdo) {
+			case \PDO::PARAM_NULL:
+				return 'unset';
+			case \PDO::PARAM_INT:
+				return 'int';
+			case \PDO::PARAM_BOOL:
+				return 'bool';
+			case \PDO::PARAM_STR:
+				return 'string';
+		}
+	}
+
+	/**
+	*	Convert array to mapper object
+	*	@return object
+	*	@param $row array
+	**/
+	protected function factory($row) {
+		$mapper=clone($this);
+		$mapper->reset();
+		foreach ($row as $key=>$val) {
+			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;
+	}
+
+	/**
+	*	Return fields of mapper object as an associative array
+	*	@return array
+	*	@param $obj object
+	**/
+	function cast($obj=NULL) {
+		if (!$obj)
+			$obj=$this;
+		return array_map(
+			function($row) {
+				return $row['value'];
+			},
+			$obj->fields+$obj->adhoc
+		);
+	}
+
+	/**
+	*	Build query string and execute
+	*	@return \DB\SQL\Mapper[]
+	*	@param $fields string
+	*	@param $filter string|array
+	*	@param $options array
+	*	@param $ttl int
+	**/
+	function select($fields,$filter=NULL,array $options=NULL,$ttl=0) {
+		if (!$options)
+			$options=array();
+		$options+=array(
+			'group'=>NULL,
+			'order'=>NULL,
+			'limit'=>0,
+			'offset'=>0
+		);
+		$db=$this->db;
+		$sql='SELECT '.$fields.' FROM '.$this->table;
+		$args=array();
+		if ($filter) {
+			if (is_array($filter)) {
+				$args=isset($filter[1]) && is_array($filter[1])?
+					$filter[1]:
+					array_slice($filter,1,NULL,TRUE);
+				$args=is_array($args)?$args:array(1=>$args);
+				list($filter)=$filter;
+			}
+			$sql.=' WHERE '.$filter;
+		}
+		if ($options['group']) {
+			$sql.=' GROUP BY '.implode(',',array_map(
+				function($str) use($db) {
+					return preg_replace_callback(
+						'/\b(\w+)\h*(HAVING.+|$)/i',
+						function($parts) use($db) {
+							return $db->quotekey($parts[1]);
+						},
+						$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 (preg_match('/mssql|sqlsrv|odbc/', $this->engine) &&
+			($options['limit'] || $options['offset'])) {
+			$pkeys=array();
+			foreach ($this->fields as $key=>$field)
+				if ($field['pkey'])
+					$pkeys[]=$key;
+			$ofs=$options['offset']?(int)$options['offset']:0;
+			$lmt=$options['limit']?(int)$options['limit']:0;
+			if (strncmp($db->version(),'11',2)>=0) {
+				// SQL Server 2012
+				if (!$options['order'])
+					$sql.=' ORDER BY '.$db->quotekey($pkeys[0]);
+				$sql.=' OFFSET '.$ofs.' ROWS';
+				if ($lmt)
+					$sql.=' FETCH NEXT '.$lmt.' ROWS ONLY';
+			}
+			else {
+				// SQL Server 2008
+				$sql=str_replace('SELECT',
+					'SELECT '.
+					($lmt>0?'TOP '.($ofs+$lmt):'').' ROW_NUMBER() '.
+					'OVER (ORDER BY '.
+						$db->quotekey($pkeys[0]).') AS rnum,',$sql);
+				$sql='SELECT * FROM ('.$sql.') x WHERE rnum > '.($ofs);
+			}
+		}
+		else {
+			if ($options['limit'])
+				$sql.=' LIMIT '.(int)$options['limit'];
+			if ($options['offset'])
+				$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->db->value(
+							$this->fields[$field]['pdo_type'],$val);
+				}
+				elseif (array_key_exists($field,$this->adhoc))
+					$this->adhoc[$field]['value']=$val;
+				unset($val);
+			}
+			$out[]=$this->factory($row);
+			unset($row);
+		}
+		return $out;
+	}
+
+	/**
+	*	Return records that match criteria
+	*	@return \DB\SQL\Mapper[]
+	*	@param $filter string|array
+	*	@param $options array
+	*	@param $ttl int
+	**/
+	function find($filter=NULL,array $options=NULL,$ttl=0) {
+		if (!$options)
+			$options=array();
+		$options+=array(
+			'group'=>NULL,
+			'order'=>NULL,
+			'limit'=>0,
+			'offset'=>0
+		);
+		$adhoc='';
+		foreach ($this->adhoc as $key=>$field)
+			$adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key);
+		return $this->select(
+			($options['group'] && !preg_match('/mysql|sqlite/',$this->engine)?
+				$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,$ttl=0) {
+		$sql='SELECT COUNT(*) AS '.
+			$this->db->quotekey('rows').' FROM '.$this->table;
+		$args=array();
+		if ($filter) {
+			if (is_array($filter)) {
+				$args=isset($filter[1]) && is_array($filter[1])?
+					$filter[1]:
+					array_slice($filter,1,NULL,TRUE);
+				$args=is_array($args)?$args:array(1=>$args);
+				list($filter)=$filter;
+			}
+			$sql.=' WHERE '.$filter;
+		}
+		$result=$this->db->exec($sql,$args,$ttl);
+		return $result[0]['rows'];
+	}
+
+	/**
+	*	Return record at specified offset using same criteria as
+	*	previous load() call and make it active
+	*	@return array
+	*	@param $ofs int
+	**/
+	function skip($ofs=1) {
+		$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 object
+	**/
+	function insert() {
+		$args=array();
+		$actr=0;
+		$nctr=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))===FALSE)
+			return $this;
+		foreach ($this->fields as $key=>&$field) {
+			if ($field['pkey']) {
+				$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[$nctr+1]=array($field['value'],$field['pdo_type']);
+				$nctr++;
+			}
+			if ($field['changed'] && $key!=$inc) {
+				$fields.=($actr?',':'').$this->db->quotekey($key);
+				$values.=($actr?',':'').'?';
+				$args[$actr+1]=array($field['value'],$field['pdo_type']);
+				$actr++;
+				$ckeys[]=$key;
+			}
+			$field['changed']=FALSE;
+			unset($field);
+		}
+		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
+			);
+			$seq=NULL;
+			if ($this->engine=='pgsql') {
+				$names=array_keys($pkeys);
+				$seq=$this->source.'_'.end($names).'_seq';
+			}
+			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));
+		}
+		return $this;
+	}
+
+	/**
+	*	Update current record
+	*	@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))===FALSE)
+			return $this;
+		foreach ($this->fields as $key=>$field)
+			if ($field['changed']) {
+				$pairs.=($pairs?',':'').$this->db->quotekey($key).'=?';
+				$args[$ctr+1]=array($field['value'],$field['pdo_type']);
+				$ctr++;
+			}
+		foreach ($this->fields as $key=>$field)
+			if ($field['pkey']) {
+				$filter.=($filter?' AND ':' WHERE ').
+					$this->db->quotekey($key).'=?';
+				$args[$ctr+1]=array($field['previous'],$field['pdo_type']);
+				$ctr++;
+			}
+		if ($pairs) {
+			$sql='UPDATE '.$this->table.' SET '.$pairs.$filter;
+			$this->db->exec($sql,$args);
+			if (isset($this->trigger['afterupdate']))
+				\Base::instance()->call($this->trigger['afterupdate'],
+					array($this,$pkeys));
+		}
+		return $this;
+	}
+
+	/**
+	*	Delete current record
+	*	@return int
+	*	@param $filter string|array
+	**/
+	function erase($filter=NULL) {
+		if ($filter) {
+			$args=array();
+			if (is_array($filter)) {
+				$args=isset($filter[1]) && is_array($filter[1])?
+					$filter[1]:
+					array_slice($filter,1,NULL,TRUE);
+				$args=is_array($args)?$args:array(1=>$args);
+				list($filter)=$filter;
+			}
+			return $this->db->
+				exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args);
+		}
+		$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;
+			$field['changed']=(bool)$field['default'];
+			if ($field['pkey'])
+				$field['previous']=NULL;
+			unset($field);
+		}
+		foreach ($this->adhoc as &$field) {
+			$field['value']=NULL;
+			unset($field);
+		}
+		parent::erase();
+		if (isset($this->trigger['beforeerase']) &&
+			\Base::instance()->call($this->trigger['beforeerase'],
+				array($this,$pkeys))===FALSE)
+			return 0;
+		$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;
+	}
+
+	/**
+	*	Reset cursor
+	*	@return NULL
+	**/
+	function reset() {
+		foreach ($this->fields as &$field) {
+			$field['value']=NULL;
+			$field['changed']=FALSE;
+			if ($field['pkey'])
+				$field['previous']=NULL;
+			unset($field);
+		}
+		foreach ($this->adhoc as &$field) {
+			$field['value']=NULL;
+			unset($field);
+		}
+		parent::reset();
+	}
+
+	/**
+	*	Hydrate mapper object using hive array variable
+	*	@return NULL
+	*	@param $var array|string
+	*	@param $func callback
+	**/
+	function copyfrom($var,$func=NULL) {
+		if (is_string($var))
+			$var=\Base::instance()->get($var);
+		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) {
+					$field['value']=$val;
+					$field['changed']=TRUE;
+				}
+				unset($field);
+			}
+	}
+
+	/**
+	*	Populate hive array variable with mapper fields
+	*	@return NULL
+	*	@param $key string
+	**/
+	function copyto($key) {
+		$var=&\Base::instance()->ref($key);
+		foreach ($this->fields+$this->adhoc as $key=>$field)
+			$var[$key]=$field['value'];
+	}
+
+	/**
+	*	Return schema and, if the first argument is provided, update it
+	*	@return array
+	*	@param $fields NULL|array
+	**/
+	function schema($fields=null) {
+		if ($fields)
+			$this->fields = $fields;
+		return $this->fields;
+	}
+
+	/**
+	*	Return field names
+	*	@return array
+	*	@param $adhoc bool
+	**/
+	function fields($adhoc=TRUE) {
+		return array_keys($this->fields+($adhoc?$this->adhoc:array()));
+	}
+
+	/**
+	*	Return TRUE if field is not nullable
+	*	@return bool
+	*	@param $field string
+	**/
+	function required($field) {
+		return isset($this->fields[$field]) &&
+			!$this->fields[$field]['nullable'];
+	}
+
+	/**
+	*	Retrieve external iterator for fields
+	*	@return object
+	**/
+	function getiterator() {
+		return new \ArrayIterator($this->cast());
+	}
+
+	/**
+	*	Instantiate class
+	*	@param $db object
+	*	@param $table string
+	*	@param $fields array|string
+	*	@param $ttl int
+	**/
+	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,$fields,$ttl);
+		$this->reset();
+	}
+
+}

+ 203 - 0
frameworks/PHP/fat-free/src/db/sql/session.php

@@ -0,0 +1,203 @@
+<?php
+
+/*
+
+	Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfreeframework.com).
+
+	This is free software: you can redistribute it and/or modify it under the
+	terms of the GNU General Public License as published by the Free Software
+	Foundation, either version 3 of the License, or later.
+
+	Fat-Free Framework is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+	General Public License for more details.
+
+	You should have received a copy of the GNU General Public License along
+	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+namespace DB\SQL;
+
+//! SQL-managed session handler
+class Session extends Mapper {
+
+	protected
+		//! Session ID
+		$sid;
+
+	/**
+	*	Open session
+	*	@return TRUE
+	*	@param $path string
+	*	@param $name string
+	**/
+	function open($path,$name) {
+		return TRUE;
+	}
+
+	/**
+	*	Close session
+	*	@return TRUE
+	**/
+	function close() {
+		return TRUE;
+	}
+
+	/**
+	*	Return session data in serialized format
+	*	@return string|FALSE
+	*	@param $id string
+	**/
+	function read($id) {
+		if ($id!=$this->sid)
+			$this->load(array('session_id=?',$this->sid=$id));
+		return $this->dry()?FALSE:$this->get('data');
+	}
+
+	/**
+	*	Write session data
+	*	@return TRUE
+	*	@param $id string
+	*	@param $data string
+	**/
+	function write($id,$data) {
+		$fw=\Base::instance();
+		$sent=headers_sent();
+		$headers=$fw->get('HEADERS');
+		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();
+		return TRUE;
+	}
+
+	/**
+	*	Destroy session
+	*	@return TRUE
+	*	@param $id string
+	**/
+	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;
+	}
+
+	/**
+	*	Garbage collector
+	*	@return TRUE
+	*	@param $max int
+	**/
+	function cleanup($max) {
+		$this->erase(array('stamp+?<?',$max,time()));
+		return TRUE;
+	}
+
+	/**
+	*	Return anti-CSRF token
+	*	@return string|FALSE
+	**/
+	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
+	*	@return string|FALSE
+	**/
+	function stamp() {
+		return $this->dry()?FALSE:$this->get('stamp');
+	}
+
+	/**
+	*	Return HTTP user agent
+	*	@return string|FALSE
+	**/
+	function agent() {
+		return $this->dry()?FALSE:$this->get('agent');
+	}
+
+	/**
+	*	Instantiate class
+	*	@param $db object
+	*	@param $table string
+	*	@param $force bool
+	*	@param $onsuspect callback
+	**/
+	function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL) {
+		if ($force) {
+			$eol="\n";
+			$tab="\t";
+			$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())&&$db->driver()!='pgsql')?
+							($name.'.'):''))).
+				$table.' ('.$eol.
+					$tab.$db->quotekey('session_id').' VARCHAR(40),'.$eol.
+					$tab.$db->quotekey('data').' TEXT,'.$eol.
+					$tab.$db->quotekey('csrf').' TEXT,'.$eol.
+					$tab.$db->quotekey('ip').' VARCHAR(40),'.$eol.
+					$tab.$db->quotekey('agent').' VARCHAR(255),'.$eol.
+					$tab.$db->quotekey('stamp').' INTEGER,'.$eol.
+					$tab.'PRIMARY KEY ('.$db->quotekey('session_id').')'.$eol.
+				');'
+			);
+		}
+		parent::__construct($db,$table);
+		session_set_save_handler(
+			array($this,'open'),
+			array($this,'close'),
+			array($this,'read'),
+			array($this,'write'),
+			array($this,'destroy'),
+			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'])) {
+			if (isset($onsuspect))
+				$fw->call($onsuspect,array($this));
+			else {
+				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();
+		}
+	}
+
+}

+ 139 - 0
frameworks/PHP/fat-free/src/magic.php

@@ -0,0 +1,139 @@
+<?php
+
+/*
+
+	Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfreeframework.com).
+
+	This is free software: you can redistribute it and/or modify it under the
+	terms of the GNU General Public License as published by the Free Software
+	Foundation, either version 3 of the License, or later.
+
+	Fat-Free Framework is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+	General Public License for more details.
+
+	You should have received a copy of the GNU General Public License along
+	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+//! PHP magic wrapper
+abstract class Magic implements ArrayAccess {
+
+	/**
+	*	Return TRUE if key is not empty
+	*	@return bool
+	*	@param $key string
+	**/
+	abstract function exists($key);
+
+	/**
+	*	Bind value to key
+	*	@return mixed
+	*	@param $key string
+	*	@param $val mixed
+	**/
+	abstract function set($key,$val);
+
+	/**
+	*	Retrieve contents of key
+	*	@return mixed
+	*	@param $key string
+	**/
+	abstract function &get($key);
+
+	/**
+	*	Unset key
+	*	@return NULL
+	*	@param $key string
+	**/
+	abstract function clear($key);
+
+	/**
+	*	Convenience method for checking property value
+	*	@return mixed
+	*	@param $key string
+	**/
+	function offsetexists($key) {
+		return Base::instance()->visible($this,$key)?
+			isset($this->$key):$this->exists($key);
+	}
+
+	/**
+	*	Convenience method for assigning property value
+	*	@return mixed
+	*	@param $key string
+	*	@param $val scalar
+	**/
+	function offsetset($key,$val) {
+		return Base::instance()->visible($this,$key)?
+			($this->key=$val):$this->set($key,$val);
+	}
+
+	/**
+	*	Convenience method for retrieving property value
+	*	@return mixed
+	*	@param $key string
+	**/
+	function &offsetget($key) {
+		if (Base::instance()->visible($this,$key))
+			$val=&$this->$key;
+		else
+			$val=&$this->get($key);
+		return $val;
+	}
+
+	/**
+	*	Convenience method for removing property value
+	*	@return NULL
+	*	@param $key string
+	**/
+	function offsetunset($key) {
+		if (Base::instance()->visible($this,$key))
+			unset($this->$key);
+		else
+			$this->clear($key);
+	}
+
+	/**
+	*	Alias for offsetexists()
+	*	@return mixed
+	*	@param $key string
+	**/
+	function __isset($key) {
+		return $this->offsetexists($key);
+	}
+
+	/**
+	*	Alias for offsetset()
+	*	@return mixed
+	*	@param $key string
+	*	@param $val scalar
+	**/
+	function __set($key,$val) {
+		return $this->offsetset($key,$val);
+	}
+
+	/**
+	*	Alias for offsetget()
+	*	@return mixed
+	*	@param $key string
+	**/
+	function &__get($key) {
+		$val=&$this->offsetget($key);
+		return $val;
+	}
+
+	/**
+	*	Alias for offsetunset()
+	*	@return NULL
+	*	@param $key string
+	**/
+	function __unset($key) {
+		$this->offsetunset($key);
+	}
+
+}

+ 108 - 0
frameworks/PHP/fat-free/src/matrix.php

@@ -0,0 +1,108 @@
+<?php
+
+/*
+
+	Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfreeframework.com).
+
+	This is free software: you can redistribute it and/or modify it under the
+	terms of the GNU General Public License as published by the Free Software
+	Foundation, either version 3 of the License, or later.
+
+	Fat-Free Framework is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+	General Public License for more details.
+
+	You should have received a copy of the GNU General Public License along
+	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+//! Generic array utilities
+class Matrix extends Prefab {
+
+	/**
+	*	Retrieve values from a specified column of a multi-dimensional
+	*	array variable
+	*	@return array
+	*	@param $var array
+	*	@param $col mixed
+	**/
+	function pick(array $var,$col) {
+		return array_map(
+			function($row) use($col) {
+				return $row[$col];
+			},
+			$var
+		);
+	}
+
+	/**
+	*	Rotate a two-dimensional array variable
+	*	@return NULL
+	*	@param $var array
+	**/
+	function transpose(array &$var) {
+		$out=array();
+		foreach ($var as $keyx=>$cols)
+			foreach ($cols as $keyy=>$valy)
+				$out[$keyy][$keyx]=$valy;
+		$var=$out;
+	}
+
+	/**
+	*	Sort a multi-dimensional array variable on a specified column
+	*	@return bool
+	*	@param $var array
+	*	@param $col mixed
+	*	@param $order int
+	**/
+	function sort(array &$var,$col,$order=SORT_ASC) {
+		uasort(
+			$var,
+			function($val1,$val2) use($col,$order) {
+				list($v1,$v2)=array($val1[$col],$val2[$col]);
+				$out=is_numeric($v1) && is_numeric($v2)?
+					Base::instance()->sign($v1-$v2):strcmp($v1,$v2);
+				if ($order==SORT_DESC)
+					$out=-$out;
+				return $out;
+			}
+		);
+		$var=array_values($var);
+	}
+
+	/**
+	*	Change the key of a two-dimensional array element
+	*	@return NULL
+	*	@param $var array
+	*	@param $old string
+	*	@param $new string
+	**/
+	function changekey(array &$var,$old,$new) {
+		$keys=array_keys($var);
+		$vals=array_values($var);
+		$keys[array_search($old,$keys)]=$new;
+		$var=array_combine($keys,$vals);
+	}
+
+	/**
+	*	Return month calendar of specified date, with optional setting for
+	*	first day of week (0 for Sunday)
+	*	@return array
+	*	@param $date string
+	*	@param $first int
+	**/
+	function calendar($date='now',$first=0) {
+		$parts=getdate(strtotime($date));
+		$days=cal_days_in_month(CAL_GREGORIAN,$parts['mon'],$parts['year']);
+		$ref=date('w',strtotime(date('Y-m',$parts[0]).'-01'))+(7-$first)%7;
+		$out=array();
+		for ($i=0;$i<$days;$i++)
+			$out[floor(($ref+$i)/7)][($ref+$i)%7]=$i+1;
+		return $out;
+	}
+
+}

+ 357 - 0
frameworks/PHP/fat-free/src/template.php

@@ -0,0 +1,357 @@
+<?php
+
+/*
+
+	Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfreeframework.com).
+
+	This is free software: you can redistribute it and/or modify it under the
+	terms of the GNU General Public License as published by the Free Software
+	Foundation, either version 3 of the License, or later.
+
+	Fat-Free Framework is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+	General Public License for more details.
+
+	You should have received a copy of the GNU General Public License along
+	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+//! XML-style template engine
+class Template extends Preview {
+
+	//@{ Error messages
+	const
+		E_Method='Call to undefined method %s()';
+	//@}
+
+	protected
+		//! Template tags
+		$tags,
+		//! Custom tag handlers
+		$custom=array();
+
+	/**
+	*	Template -set- tag handler
+	*	@return string
+	*	@param $node array
+	**/
+	protected function _set(array $node) {
+		$out='';
+		foreach ($node['@attrib'] as $key=>$val)
+			$out.='$'.$key.'='.
+				(preg_match('/\{\{(.+?)\}\}/',$val)?
+					$this->token($val):
+					Base::instance()->stringify($val)).'; ';
+		return '<?php '.$out.'?>';
+	}
+
+	/**
+	*	Template -include- tag handler
+	*	@return string
+	*	@param $node array
+	**/
+	protected function _include(array $node) {
+		$attrib=$node['@attrib'];
+		$hive=isset($attrib['with']) &&
+			($attrib['with']=$this->token($attrib['with'])) &&
+			preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/',
+				$attrib['with'],$pairs,PREG_SET_ORDER)?
+					'array('.implode(',',
+						array_map(function($pair) {
+							return '\''.$pair[1].'\'=>'.
+								(preg_match('/^\'.*\'$/',$pair[2]) ||
+									preg_match('/\$/',$pair[2])?
+									$pair[2]:
+									\Base::instance()->stringify($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'])?
+						$this->token($attrib['href']):
+						Base::instance()->stringify($attrib['href'])).','.
+					'$this->mime,'.$hive.'); ?>');
+	}
+
+	/**
+	*	Template -exclude- tag handler
+	*	@return string
+	**/
+	protected function _exclude() {
+		return '';
+	}
+
+	/**
+	*	Template -ignore- tag handler
+	*	@return string
+	*	@param $node array
+	**/
+	protected function _ignore(array $node) {
+		return $node[0];
+	}
+
+	/**
+	*	Template -loop- tag handler
+	*	@return string
+	*	@param $node array
+	**/
+	protected function _loop(array $node) {
+		$attrib=$node['@attrib'];
+		unset($node['@attrib']);
+		return
+			'<?php for ('.
+				$this->token($attrib['from']).';'.
+				$this->token($attrib['to']).';'.
+				$this->token($attrib['step']).'): ?>'.
+				$this->build($node).
+			'<?php endfor; ?>';
+	}
+
+	/**
+	*	Template -repeat- tag handler
+	*	@return string
+	*	@param $node array
+	**/
+	protected function _repeat(array $node) {
+		$attrib=$node['@attrib'];
+		unset($node['@attrib']);
+		return
+			'<?php '.
+				(isset($attrib['counter'])?
+					(($ctr=$this->token($attrib['counter'])).'=0; '):'').
+				'foreach (('.
+				$this->token($attrib['group']).'?:array()) as '.
+				(isset($attrib['key'])?
+					($this->token($attrib['key']).'=>'):'').
+				$this->token($attrib['value']).'):'.
+				(isset($ctr)?(' '.$ctr.'++;'):'').' ?>'.
+				$this->build($node).
+			'<?php endforeach; ?>';
+	}
+
+	/**
+	*	Template -check- tag handler
+	*	@return string
+	*	@param $node array
+	**/
+	protected function _check(array $node) {
+		$attrib=$node['@attrib'];
+		unset($node['@attrib']);
+		// Grab <true> and <false> blocks
+		foreach ($node as $pos=>$block)
+			if (isset($block['true']))
+				$true=array($pos,$block);
+			elseif (isset($block['false']))
+				$false=array($pos,$block);
+		if (isset($true,$false) && $true[0]>$false[0])
+			// Reverse <true> and <false> blocks
+			list($node[$true[0]],$node[$false[0]])=array($false[1],$true[1]);
+		return
+			'<?php if ('.$this->token($attrib['if']).'): ?>'.
+				$this->build($node).
+			'<?php endif; ?>';
+	}
+
+	/**
+	*	Template -true- tag handler
+	*	@return string
+	*	@param $node array
+	**/
+	protected function _true(array $node) {
+		return $this->build($node);
+	}
+
+	/**
+	*	Template -false- tag handler
+	*	@return string
+	*	@param $node array
+	**/
+	protected function _false(array $node) {
+		return '<?php else: ?>'.$this->build($node);
+	}
+
+	/**
+	*	Template -switch- tag handler
+	*	@return string
+	*	@param $node array
+	**/
+	protected function _switch(array $node) {
+		$attrib=$node['@attrib'];
+		unset($node['@attrib']);
+		foreach ($node as $pos=>$block)
+			if (is_string($block) && !preg_replace('/\s+/','',$block))
+				unset($node[$pos]);
+		return
+			'<?php switch ('.$this->token($attrib['expr']).'): ?>'.
+				$this->build($node).
+			'<?php endswitch; ?>';
+	}
+
+	/**
+	*	Template -case- tag handler
+	*	@return string
+	*	@param $node array
+	**/
+	protected function _case(array $node) {
+		$attrib=$node['@attrib'];
+		unset($node['@attrib']);
+		return
+			'<?php case '.(preg_match('/\{\{(.+?)\}\}/',$attrib['value'])?
+				$this->token($attrib['value']):
+				Base::instance()->stringify($attrib['value'])).': ?>'.
+				$this->build($node).
+			'<?php '.(isset($attrib['break'])?
+				'if ('.$this->token($attrib['break']).') ':'').
+				'break; ?>';
+	}
+
+	/**
+	*	Template -default- tag handler
+	*	@return string
+	*	@param $node array
+	**/
+	protected function _default(array $node) {
+		return
+			'<?php default: ?>'.
+				$this->build($node).
+			'<?php break; ?>';
+	}
+
+	/**
+	*	Assemble markup
+	*	@return string
+	*	@param $node array|string
+	**/
+	protected function build($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);
+		return $out;
+	}
+
+	/**
+	*	Extend template with custom tag
+	*	@return NULL
+	*	@param $tag string
+	*	@param $func callback
+	**/
+	function extend($tag,$func) {
+		$this->tags.='|'.$tag;
+		$this->custom['_'.$tag]=$func;
+	}
+
+	/**
+	*	Call custom tag handler
+	*	@return string|FALSE
+	*	@param $func callback
+	*	@param $args array
+	**/
+	function __call($func,array $args) {
+		if ($func[0]=='_')
+			return call_user_func_array($this->custom[$func],$args);
+		if (method_exists($this,$func))
+			return call_user_func_array(array($this,$func),$args);
+		user_error(sprintf(self::E_Method,$func),E_USER_ERROR);
+	}
+
+	/**
+	*	Parse string for template directives and tokens
+	*	@return string|array
+	*	@param $text string
+	**/
+	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;
+							}
+					}
+					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]]=
+									(isset($kv[2]) && $kv[2]!==''?
+										$kv[2]:
+										(isset($kv[3]) && $kv[3]!==''?
+											$kv[3]:NULL));
+					}
+					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);
+		return $tree;
+	}
+
+	/**
+	*	Class constructor
+	*	return object
+	**/
+	function __construct() {
+		$ref=new ReflectionClass(__CLASS__);
+		$this->tags='';
+		foreach ($ref->getmethods() as $method)
+			if (preg_match('/^_(?=[[:alpha:]])/',$method->name))
+				$this->tags.=(strlen($this->tags)?'|':'').
+					substr($method->name,1);
+	}
+
+}