Browse Source

Merge branch 'master' of http://github.com/F3Community/FrameworkBenchmarks into 928

James Yen 11 years ago
parent
commit
5569eb5b9a
44 changed files with 11658 additions and 0 deletions
  1. 19 0
      php-fatfree/.htaccess
  2. 25 0
      php-fatfree/README.md
  3. 0 0
      php-fatfree/__init__.py
  4. 48 0
      php-fatfree/benchmark_config
  5. 130 0
      php-fatfree/deploy/nginx.conf
  6. 9 0
      php-fatfree/deploy/php
  7. 144 0
      php-fatfree/index.php
  8. 177 0
      php-fatfree/lib/audit.php
  9. 233 0
      php-fatfree/lib/auth.php
  10. 2651 0
      php-fatfree/lib/base.php
  11. 229 0
      php-fatfree/lib/basket.php
  12. 89 0
      php-fatfree/lib/bcrypt.php
  13. 416 0
      php-fatfree/lib/changelog.txt
  14. 1 0
      php-fatfree/lib/code.css
  15. 321 0
      php-fatfree/lib/db/cursor.php
  16. 133 0
      php-fatfree/lib/db/jig.php
  17. 459 0
      php-fatfree/lib/db/jig/mapper.php
  18. 168 0
      php-fatfree/lib/db/jig/session.php
  19. 104 0
      php-fatfree/lib/db/mongo.php
  20. 344 0
      php-fatfree/lib/db/mongo/mapper.php
  21. 174 0
      php-fatfree/lib/db/mongo/session.php
  22. 403 0
      php-fatfree/lib/db/sql.php
  23. 552 0
      php-fatfree/lib/db/sql/mapper.php
  24. 187 0
      php-fatfree/lib/db/sql/session.php
  25. 35 0
      php-fatfree/lib/f3.php
  26. 583 0
      php-fatfree/lib/image.php
  27. 621 0
      php-fatfree/lib/license.txt
  28. 60 0
      php-fatfree/lib/log.php
  29. 140 0
      php-fatfree/lib/magic.php
  30. 570 0
      php-fatfree/lib/markdown.php
  31. 101 0
      php-fatfree/lib/matrix.php
  32. 180 0
      php-fatfree/lib/session.php
  33. 274 0
      php-fatfree/lib/smtp.php
  34. 340 0
      php-fatfree/lib/template.php
  35. 77 0
      php-fatfree/lib/test.php
  36. 192 0
      php-fatfree/lib/utf.php
  37. 838 0
      php-fatfree/lib/web.php
  38. 101 0
      php-fatfree/lib/web/geo.php
  39. 58 0
      php-fatfree/lib/web/google/staticmap.php
  40. 237 0
      php-fatfree/lib/web/openid.php
  41. 170 0
      php-fatfree/lib/web/pingback.php
  42. 45 0
      php-fatfree/setup.py
  43. 0 0
      php-fatfree/tmp/placeholder
  44. 20 0
      php-fatfree/ui/fortune.html

+ 19 - 0
php-fatfree/.htaccess

@@ -0,0 +1,19 @@
+# Enable rewrite engine and route requests to framework
+RewriteEngine On
+
+# Some servers require you to specify the `RewriteBase` directive
+# In such cases, it should be the path (relative to the document root)
+# containing this .htaccess file
+#
+RewriteBase /
+
+RewriteCond %{REQUEST_URI} \.ini$
+RewriteRule \.ini$ - [R=404]
+
+RewriteCond %{REQUEST_URI} \.html?$
+RewriteRule \.html?$ - [R=404]
+
+RewriteCond %{REQUEST_FILENAME} !-l
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule .* index.php [L,QSA,E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

+ 25 - 0
php-fatfree/README.md

@@ -0,0 +1,25 @@
+#PHP Fat-Free Benchmarking Test
+
+This is the PHP Fat-Free Framework portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
+
+## Test URLs
+### JSON Encoding Test
+
+http://localhost/json
+
+
+### Data-Store/Database Mapping Test
+
+Raw:
+http://localhost/db
+
+ORM:
+http://localhost/db-orm
+
+### Variable Query Test
+
+Raw:
+http://localhost/db/5
+
+ORM:
+http://localhost/db-orm/5

+ 0 - 0
php-fatfree/__init__.py


+ 48 - 0
php-fatfree/benchmark_config

@@ -0,0 +1,48 @@
+{
+  "framework": "fat-free",
+  "tests": [{
+    "default": {
+      "setup_file": "setup",
+      "json_url": "/json",
+      "plaintext_url": "/plaintext",
+      "db_url": "/db-orm",
+      "query_url": "/db-orm/",
+      "update_url": "/update-orm/",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Fullstack",
+      "database": "MySQL",
+      "framework": "Fat Free Framework",
+      "language": "PHP",
+      "orm": "Full",
+      "platform": "PHP-FPM",
+      "webserver": "nginx",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "fat-free",
+      "notes": "",
+      "versus": "php"
+    },
+    "raw": {
+      "setup_file": "setup",
+      "db_url": "/db",
+      "query_url": "/db/",
+      "fortune_url": "/fortune",
+      "update_url": "/update-raw/",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Fullstack",
+      "database": "MySQL",
+      "framework": "Fat Free Framework",
+      "language": "PHP",
+      "orm": "Raw",
+      "platform": "PHP-FPM",
+      "webserver": "nginx",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "fat-free",
+      "notes": "",
+      "versus": "php"
+    }
+  }]
+}

+ 130 - 0
php-fatfree/deploy/nginx.conf

@@ -0,0 +1,130 @@
+#user  nobody;
+worker_processes  8;
+
+#error_log  logs/error.log;
+#error_log  logs/error.log  notice;
+#error_log  logs/error.log  info;
+
+#pid        logs/nginx.pid;
+
+
+events {
+    worker_connections  1024;
+}
+
+http {
+    include       /usr/local/nginx/conf/mime.types;
+    default_type  application/octet-stream;
+
+    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
+    #                  '$status $body_bytes_sent "$http_referer" '
+    #                  '"$http_user_agent" "$http_x_forwarded_for"';
+
+    #access_log  logs/access.log  main;
+
+    sendfile        on;
+    #tcp_nopush     on;
+
+    #keepalive_timeout  0;
+    keepalive_timeout  65;
+
+    #gzip  on;
+
+    upstream fastcgi_backend {
+        server 127.0.0.1:9001;
+        keepalive 32;
+    }
+
+    server {
+        listen       8080;
+        server_name  localhost;
+
+        #charset koi8-r;
+
+        #access_log  logs/host.access.log  main;
+
+        #location / {
+        #    root   html;
+        #    index  index.html index.htm;
+        #}
+
+        #error_page  404              /404.html;
+
+        # redirect server error pages to the static page /50x.html
+        #
+        #error_page   500 502 503 504  /50x.html;
+        #location = /50x.html {
+        #    root   html;
+        #}
+
+        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
+        #
+        #location ~ \.php$ {
+        #    proxy_pass   http://127.0.0.1;
+        #}
+
+        root /home/ubuntu/FrameworkBenchmarks/php-fatfree/;
+        index  index.php;
+
+        location / {
+            try_files $uri $uri/ /index.php?$uri&$args;
+        }
+
+        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
+        #
+        location ~ \.php$ {
+            try_files $uri =404;
+            fastcgi_pass   fastcgi_backend;
+            fastcgi_keep_conn on;
+            fastcgi_index  index.php;
+#            fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
+            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
+            include        /usr/local/nginx/conf/fastcgi_params;
+        }
+
+        # deny access to .htaccess files, if Apache's document root
+        # concurs with nginx's one
+        #
+        #location ~ /\.ht {
+        #    deny  all;
+        #}
+    }
+
+
+    # another virtual host using mix of IP-, name-, and port-based configuration
+    #
+    #server {
+    #    listen       8000;
+    #    listen       somename:8080;
+    #    server_name  somename  alias  another.alias;
+
+    #    location / {
+    #        root   html;
+    #        index  index.html index.htm;
+    #    }
+    #}
+
+
+    # HTTPS server
+    #
+    #server {
+    #    listen       443;
+    #    server_name  localhost;
+
+    #    ssl                  on;
+    #    ssl_certificate      cert.pem;
+    #    ssl_certificate_key  cert.key;
+
+    #    ssl_session_timeout  5m;
+
+    #    ssl_protocols  SSLv2 SSLv3 TLSv1;
+    #    ssl_ciphers  HIGH:!aNULL:!MD5;
+    #    ssl_prefer_server_ciphers   on;
+
+    #    location / {
+    #        root   html;
+    #        index  index.html index.htm;
+    #    }
+    #}
+
+}

+ 9 - 0
php-fatfree/deploy/php

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

+ 144 - 0
php-fatfree/index.php

@@ -0,0 +1,144 @@
+<?php
+/** @var Base $f3 */
+$f3=require('lib/base.php');
+
+$f3->set('DEBUG',2);
+$f3->set('CACHE','folder=tmp/cache/');
+$f3->set('UI','ui/');
+
+$f3->set('DBS',array('mysql:host=localhost;port=3306;dbname=hello_world','benchmarkdbuser','benchmarkdbpass'));
+
+// http: //www.techempower.com/benchmarks/#section=code
+
+// JSON test
+$f3->route('GET /json',function($f3) {
+    /** @var Base $f3 */
+    header("Content-type: application/json");
+    echo json_encode(array('message' => 'Hello, World!'));
+});
+
+
+// DB RAW test
+$f3->route(
+    array(
+        'GET /db',                  // database-single-query
+        'GET /db/@queries',         // database-multiple-queries
+    ),
+    function ($f3,$params) {
+        /** @var Base $f3 */
+        $single = !isset($params['queries']);
+        if ($single)
+            $queries = 1;
+        else {
+            $queries = (int) $params['queries'];
+            $queries = ($queries < 1) ? 1 : (($queries > 500) ? 500 : $queries);
+        }
+        $dbc = $f3->get('DBS');
+        $db = new \DB\SQL($dbc[0],$dbc[1],$dbc[2],array( \PDO::ATTR_PERSISTENT => TRUE ));
+        $result = array();
+        for ($i = 0; $i < $queries; $i++) {
+            $id = mt_rand(1, 10000);
+            $result[] = $db->exec('SELECT randomNumber FROM World WHERE id = ?',$id,0,false);
+        }
+        header("Content-type: application/json");
+        echo json_encode($single ? $result[0] : $result);
+    }
+);
+
+// DB ORM test
+$f3->route(
+    array(
+         'GET /db-orm',             // database-single-query
+         'GET /db-orm/@queries',    // database-multiple-queries
+    ),
+    function ($f3, $params) {
+        /** @var Base $f3 */
+        $single = !isset($params['queries']);
+        if ($single)
+            $queries = 1;
+        else {
+            $queries = (int) $params['queries'];
+            $queries = ($queries < 1) ? 1 : (($queries > 500) ? 500 : $queries);
+        }
+        $dbc = $f3->get('DBS');
+        $db = new \DB\SQL($dbc[0],$dbc[1],$dbc[2],array( \PDO::ATTR_PERSISTENT => TRUE ));
+        $mapper = new \DB\SQL\Mapper($db,'World');
+        $result = array();
+        for ($i = 0; $i < $queries; $i++) {
+            $id = mt_rand(1, 10000);
+            $mapper->load(array('id = ?',$id));
+            $result[] = $mapper->cast();
+        }
+        header("Content-type: application/json");
+        echo json_encode($single ? $result[0] : $result);
+    }
+);
+
+
+$f3->route('GET /plaintext', function ($f3) {
+    echo "Hello, World!";
+});
+
+
+$f3->route('GET /fortune', function ($f3) {
+    /** @var Base $f3 */
+    $dbc = $f3->get('DBS');
+    $db = new \DB\SQL($dbc[0],$dbc[1],$dbc[2],array( \PDO::ATTR_PERSISTENT => TRUE ));
+    $result = $db->exec('SELECT id, message FROM Fortune');
+    $result[] = 'Additional fortune added at request time.';
+    asort($result);
+    $f3->set('result',$result);
+    echo \Template::instance()->render('fortune.html');
+});
+
+
+$f3->route('GET /update-raw/@queries', function($f3,$params) {
+    /** @var Base $f3 */
+    $queries = (int) $params['queries'];
+    $queries = ($queries < 1) ? 1 : (($queries > 500) ? 500 : $queries);
+
+    $dbc = $f3->get('DBS');
+    $db = new \DB\SQL($dbc[0],$dbc[1],$dbc[2],array( \PDO::ATTR_PERSISTENT => TRUE ));
+
+    $result = array();
+    for ($i = 0; $i < $queries; $i++) {
+        $id = mt_rand(1, 10000);
+        $row = array(
+            'id'=>$id,
+            'randomNumber'=>$db->exec('SELECT randomNumber FROM World WHERE id = ?',$id,0,false)
+        );
+        $rnu = mt_rand(1, 10000);
+        $row['randomNumber'] = $rnu;
+        $db->exec('UPDATE World SET randomNumber = :ranNum WHERE id = :id', array(':ranNum'=>$rnu,':id'=>$id),0,false);
+        $result[] = $row;
+    }
+
+    header("Content-type: application/json");
+    echo json_encode($result);
+
+});
+
+
+$f3->route('GET /update-orm/@queries', function($f3,$params) {
+    /** @var Base $f3 */
+    $queries = (int) $params['queries'];
+    $queries = ($queries < 1) ? 1 : (($queries > 500) ? 500 : $queries);
+
+    $dbc = $f3->get('DBS');
+    $db = new \DB\SQL($dbc[0],$dbc[1],$dbc[2],array( \PDO::ATTR_PERSISTENT => TRUE ));
+    $world = new \DB\SQL\Mapper($db,'World');
+
+    $result = array();
+    for ($i = 0; $i < $queries; $i++) {
+        $id = mt_rand(1, 10000);
+        $world->load(array('id = ?', $id));
+        $world->randomNumber = mt_rand(1, 10000);
+        $world->save();
+        $result[] = $world->cast();
+    }
+    header("Content-type: application/json");
+    echo json_encode($result);
+
+});
+
+$f3->run();

+ 177 - 0
php-fatfree/lib/audit.php

@@ -0,0 +1,177 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! Data validator
+class Audit extends Prefab {
+
+	//@{ User agents
+	const
+		UA_Mobile='android|blackberry|iphone|ipod|palm|windows\s+ce',
+		UA_Desktop='bsd|linux|os\s+[x9]|solaris|windows',
+		UA_Bot='bot|crawl|slurp|spider';
+	//@}
+
+	/**
+	*	Return TRUE if string is a valid URL
+	*	@return bool
+	*	@param $str string
+	**/
+	function url($str) {
+		return is_string(filter_var($str,FILTER_VALIDATE_URL));
+	}
+
+	/**
+	*	Return TRUE if string is a valid e-mail address;
+	*	Check DNS MX records if specified
+	*	@return bool
+	*	@param $str string
+	*	@param $mx boolean
+	**/
+	function email($str,$mx=TRUE) {
+		$hosts=array();
+		return is_string(filter_var($str,FILTER_VALIDATE_EMAIL)) &&
+			(!$mx || getmxrr(substr($str,strrpos($str,'@')+1),$hosts));
+	}
+
+	/**
+	*	Return TRUE if string is a valid IPV4 address
+	*	@return bool
+	*	@param $addr string
+	**/
+	function ipv4($addr) {
+		return (bool)filter_var($addr,FILTER_VALIDATE_IP,FILTER_FLAG_IPV4);
+	}
+
+	/**
+	*	Return TRUE if string is a valid IPV6 address
+	*	@return bool
+	*	@param $addr string
+	**/
+	function ipv6($addr) {
+		return (bool)filter_var($addr,FILTER_VALIDATE_IP,FILTER_FLAG_IPV6);
+	}
+
+	/**
+	*	Return TRUE if IP address is within private range
+	*	@return bool
+	*	@param $addr string
+	**/
+	function isprivate($addr) {
+		return !(bool)filter_var($addr,FILTER_VALIDATE_IP,
+			FILTER_FLAG_IPV4|FILTER_FLAG_IPV6|FILTER_FLAG_NO_PRIV_RANGE);
+	}
+
+	/**
+	*	Return TRUE if IP address is within reserved range
+	*	@return bool
+	*	@param $addr string
+	**/
+	function isreserved($addr) {
+		return !(bool)filter_var($addr,FILTER_VALIDATE_IP,
+			FILTER_FLAG_IPV4|FILTER_FLAG_IPV6|FILTER_FLAG_NO_RES_RANGE);
+	}
+
+	/**
+	*	Return TRUE if IP address is neither private nor reserved
+	*	@return bool
+	*	@param $addr string
+	**/
+	function ispublic($addr) {
+		return (bool)filter_var($addr,FILTER_VALIDATE_IP,
+			FILTER_FLAG_IPV4|FILTER_FLAG_IPV6|
+			FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE);
+	}
+
+	/**
+	*	Return TRUE if user agent is a desktop browser
+	*	@return bool
+	**/
+	function isdesktop() {
+		$agent=Base::instance()->get('AGENT');
+		return (bool)preg_match('/('.self::UA_Desktop.')/i',$agent) &&
+			!$this->ismobile();
+	}
+
+	/**
+	*	Return TRUE if user agent is a mobile device
+	*	@return bool
+	**/
+	function ismobile() {
+		$agent=Base::instance()->get('AGENT');
+		return (bool)preg_match('/('.self::UA_Mobile.')/i',$agent);
+	}
+
+	/**
+	*	Return TRUE if user agent is a Web bot
+	*	@return bool
+	**/
+	function isbot() {
+		$agent=Base::instance()->get('AGENT');
+		return (bool)preg_match('/('.self::UA_Bot.')/i',$agent);
+	}
+
+	/**
+	*	Return TRUE if specified ID has a valid (Luhn) Mod-10 check digit
+	*	@return bool
+	*	@param $id string
+	**/
+	function mod10($id) {
+		if (!ctype_digit($id))
+			return FALSE;
+		$id=strrev($id);
+		$sum=0;
+		for ($i=0,$l=strlen($id);$i<$l;$i++)
+			$sum+=$id[$i]+$i%2*(($id[$i]>4)*-4+$id[$i]%5);
+		return !($sum%10);
+	}
+
+	/**
+	*	Return credit card type if number is valid
+	*	@return string|FALSE
+	*	@param $id string
+	**/
+	function card($id) {
+		$id=preg_replace('/[^\d]/','',$id);
+		if ($this->mod10($id)) {
+			if (preg_match('/^3[47][0-9]{13}$/',$id))
+				return 'American Express';
+			if (preg_match('/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/',$id))
+				return 'Diners Club';
+			if (preg_match('/^6(?:011|5[0-9][0-9])[0-9]{12}$/',$id))
+				return 'Discover';
+			if (preg_match('/^(?:2131|1800|35\d{3})\d{11}$/',$id))
+				return 'JCB';
+			if (preg_match('/^5[1-5][0-9]{14}$/',$id))
+				return 'MasterCard';
+			if (preg_match('/^4[0-9]{12}(?:[0-9]{3})?$/',$id))
+				return 'Visa';
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Return entropy estimate of a password (NIST 800-63)
+	*	@return int|float
+	*	@param $str string
+	**/
+	function entropy($str) {
+		$len=strlen($str);
+		return 4*min($len,1)+($len>1?(2*(min($len,8)-1)):0)+
+			($len>8?(1.5*(min($len,20)-8)):0)+($len>20?($len-20):0)+
+			6*(bool)(preg_match(
+				'/[A-Z].*?[0-9[:punct:]]|[0-9[:punct:]].*?[A-Z]/',$str));
+	}
+
+}

+ 233 - 0
php-fatfree/lib/auth.php

@@ -0,0 +1,233 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! Authorization/authentication plug-in
+class Auth {
+
+	//@{ Error messages
+	const
+		E_LDAP='LDAP connection failure',
+		E_SMTP='SMTP connection failure';
+	//@}
+
+	protected
+		//! Auth storage
+		$storage,
+		//! Mapper object
+		$mapper,
+		//! Storage options
+		$args;
+
+	/**
+	*	Jig storage handler
+	*	@return bool
+	*	@param $id string
+	*	@param $pw string
+	*	@param $realm string
+	**/
+	protected function _jig($id,$pw,$realm) {
+		return (bool)
+			call_user_func_array(
+				array($this->mapper,'load'),
+				array(
+					array_merge(
+						array(
+							'@'.$this->args['id'].'==? AND '.
+							'@'.$this->args['pw'].'==?'.
+							(isset($this->args['realm'])?
+								(' AND @'.$this->args['realm'].'==?'):''),
+							$id,$pw
+						),
+						(isset($this->args['realm'])?array($realm):array())
+					)
+				)
+			);
+	}
+
+	/**
+	*	MongoDB storage handler
+	*	@return bool
+	*	@param $id string
+	*	@param $pw string
+	*	@param $realm string
+	**/
+	protected function _mongo($id,$pw,$realm) {
+		return (bool)
+			$this->mapper->load(
+				array(
+					$this->args['id']=>$id,
+					$this->args['pw']=>$pw
+				)+
+				(isset($this->args['realm'])?
+					array($this->args['realm']=>$realm):array())
+			);
+	}
+
+	/**
+	*	SQL storage handler
+	*	@return bool
+	*	@param $id string
+	*	@param $pw string
+	*	@param $realm string
+	**/
+	protected function _sql($id,$pw,$realm) {
+		return (bool)
+			call_user_func_array(
+				array($this->mapper,'load'),
+				array(
+					array_merge(
+						array(
+							$this->args['id'].'=? AND '.
+							$this->args['pw'].'=?'.
+							(isset($this->args['realm'])?
+								(' AND '.$this->args['realm'].'=?'):''),
+							$id,$pw
+						),
+						(isset($this->args['realm'])?array($realm):array())
+					)
+				)
+			);
+	}
+
+	/**
+	*	LDAP storage handler
+	*	@return bool
+	*	@param $id string
+	*	@param $pw string
+	**/
+	protected function _ldap($id,$pw) {
+		$dc=@ldap_connect($this->args['dc']);
+		if ($dc &&
+			ldap_set_option($dc,LDAP_OPT_PROTOCOL_VERSION,3) &&
+			ldap_set_option($dc,LDAP_OPT_REFERRALS,0) &&
+			ldap_bind($dc,$this->args['rdn'],$this->args['pw']) &&
+			($result=ldap_search($dc,$this->args['base_dn'],
+				'uid='.$id)) &&
+			ldap_count_entries($dc,$result) &&
+			($info=ldap_get_entries($dc,$result)) &&
+			@ldap_bind($dc,$info[0]['dn'],$pw) &&
+			@ldap_close($dc)) {
+			return $info[0]['uid'][0]==$id;
+		}
+		user_error(self::E_LDAP);
+	}
+
+	/**
+	*	SMTP storage handler
+	*	@return bool
+	*	@param $id string
+	*	@param $pw string
+	**/
+	protected function _smtp($id,$pw) {
+		$socket=@fsockopen(
+			(strtolower($this->args['scheme'])=='ssl'?
+				'ssl://':'').$this->args['host'],
+				$this->args['port']);
+		$dialog=function($cmd=NULL) use($socket) {
+			if (!is_null($cmd))
+				fputs($socket,$cmd."\r\n");
+			$reply='';
+			while (!feof($socket) &&
+				($info=stream_get_meta_data($socket)) &&
+				!$info['timed_out'] && $str=fgets($socket,4096)) {
+				$reply.=$str;
+				if (preg_match('/(?:^|\n)\d{3} .+\r\n/s',
+					$reply))
+					break;
+			}
+			return $reply;
+		};
+		if ($socket) {
+			stream_set_blocking($socket,TRUE);
+			$dialog();
+			$fw=Base::instance();
+			$dialog('EHLO '.$fw->get('HOST'));
+			if (strtolower($this->args['scheme'])=='tls') {
+				$dialog('STARTTLS');
+				stream_socket_enable_crypto(
+					$socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT);
+				$dialog('EHLO '.$fw->get('HOST'));
+			}
+			// Authenticate
+			$dialog('AUTH LOGIN');
+			$dialog(base64_encode($id));
+			$reply=$dialog(base64_encode($pw));
+			$dialog('QUIT');
+			fclose($socket);
+			return (bool)preg_match('/^235 /',$reply);
+		}
+		user_error(self::E_SMTP);
+	}
+
+	/**
+	*	Login auth mechanism
+	*	@return bool
+	*	@param $id string
+	*	@param $pw string
+	*	@param $realm string
+	**/
+	function login($id,$pw,$realm=NULL) {
+		return $this->{'_'.$this->storage}($id,$pw,$realm);
+	}
+
+	/**
+	*	HTTP basic auth mechanism
+	*	@return bool
+	*	@param $func callback
+	**/
+	function basic($func=NULL) {
+		$fw=Base::instance();
+		$realm=$fw->get('REALM');
+		$hdr=NULL;
+		if (isset($_SERVER['HTTP_AUTHORIZATION']))
+			$hdr=$_SERVER['HTTP_AUTHORIZATION'];
+		elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']))
+			$hdr=$_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
+		if (!empty($hdr))
+			list($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW'])=
+				explode(':',base64_decode(substr($hdr,6)));
+		if (isset($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']) &&
+			$this->login(
+				$_SERVER['PHP_AUTH_USER'],
+				$func?
+					$fw->call($func,$_SERVER['PHP_AUTH_PW']):
+					$_SERVER['PHP_AUTH_PW'],
+				$realm
+			))
+			return TRUE;
+		if (PHP_SAPI!='cli')
+			header('WWW-Authenticate: Basic realm="'.$realm.'"');
+		$fw->status(401);
+		return FALSE;
+	}
+
+	/**
+	*	Instantiate class
+	*	@return object
+	*	@param $storage string|object
+	*	@param $args array
+	**/
+	function __construct($storage,array $args=NULL) {
+		if (is_object($storage) && is_a($storage,'DB\Cursor')) {
+			$this->storage=$storage->dbtype();
+			$this->mapper=$storage;
+			unset($ref);
+		}
+		else
+			$this->storage=$storage;
+		$this->args=$args;
+	}
+
+}

+ 2651 - 0
php-fatfree/lib/base.php

@@ -0,0 +1,2651 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! 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
+class Base extends Prefab {
+
+	//@{ Framework details
+	const
+		PACKAGE='Fat-Free Framework',
+		VERSION='3.3.0-Dev';
+	//@}
+
+	//@{ 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 current route's token values
+	*	@return string
+	*	@param $url array|string
+	**/
+	function build($url) {
+		if (is_array($url))
+			foreach ($url as &$var) {
+				$var=$this->build($var);
+				unset($var);
+			}
+		elseif (preg_match_all('/@(\w+)/',$url,$matches,PREG_SET_ORDER))
+			foreach ($matches as $match)
+				if (array_key_exists($match[1],$this->hive['PARAMS']))
+					$url=str_replace($match[0],
+						$this->hive['PARAMS'][$match[1]],$url);
+		return $url;
+	}
+
+	/**
+	*	Parse string containing key-value pairs and use as routing tokens
+	*	@return NULL
+	*	@param $str string
+	**/
+	function parse($str) {
+		preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/',
+			$str,$pairs,PREG_SET_ORDER);
+		foreach ($pairs as $pair)
+			$this->hive['PARAMS'][$pair[1]]=trim($pair[2]);
+	}
+
+	/**
+	*	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+)|\[((?:[^\[\]]*|(?R))*)\]/',
+					function($expr) use($fw) {
+						return '['.var_export(
+							isset($expr[2])?
+								$fw->compile($expr[2]):
+								(ctype_digit($expr[1])?
+									(int)$expr[1]:
+									$expr[1]),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) {
+		$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)));
+		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=&$this->null;
+					break;
+				}
+			}
+			else {
+				if (!is_array($var))
+					$var=array();
+				if ($add || array_key_exists($part,$var))
+					$var=&$var[$part];
+				else {
+					$var=&$this->null;
+					break;
+				}
+			}
+		if ($parts[0]=='ALIASES')
+			$var=$this->build($var);
+		return $var;
+	}
+
+	/**
+	*	Return TRUE if hive key is not 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)?
+				$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) {
+		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->hive['JAR'];
+				if ($ttl)
+					$jar['expire']=time()+$ttl;
+				call_user_func_array('setcookie',array($parts[1],$val)+$jar);
+			}
+		}
+		else switch ($key) {
+			case 'CACHE':
+				$val=Cache::instance()->load($val,TRUE);
+				break;
+			case 'ENCODING':
+				$val=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) || $lang=$this->language($val))
+					$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))
+			call_user_func_array(
+				'session_set_cookie_params',$this->hive['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);
+		}
+	}
+
+	/**
+	*	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);
+		array_push($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
+	**/
+	function split($str) {
+		return array_map('trim',
+			preg_split('/[,;|]/',$str,0,PREG_SPLIT_NO_EMPTY));
+	}
+
+	/**
+	*	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;
+	}
+
+	/**
+	*	Generate 64bit/base36 hash
+	*	@return string
+	*	@param $str
+	**/
+	function hash($str) {
+		return str_pad(base_convert(
+			hexdec(substr(sha1($str),-16)),10,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 @htmlentities($str,$this->hive['BITMASK'],
+			$this->hive['ENCODING'],FALSE)?:$this->scrub($str);
+	}
+
+	/**
+	*	Convert HTML entities back to characters
+	*	@return string
+	*	@param $str string
+	**/
+	function decode($str) {
+		return html_entity_decode($str,$this->hive['BITMASK'],
+			$this->hive['ENCODING']);
+	}
+
+	/**
+	*	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);
+						foreach (get_object_vars($arg) as $key=>$val)
+							$arg->$key=$this->recursive($val,$func,
+								array_merge($stack,array($arg)));
+					}
+				}
+				return $arg;
+			case 'array':
+				$tmp=array();
+				foreach ($arg as $key=>$val)
+					$tmp[$key]=$this->recursive($val,$func,
+						array_merge($stack,array($arg)));
+				return $tmp;
+		}
+		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('/;q=.+?(?=,|$)/','',$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);
+	}
+
+	/**
+	*	Transfer lexicon entries to hive
+	*	@return array
+	*	@param $path string
+	**/
+	function lexicon($path) {
+		$lex=array();
+		foreach ($this->languages?:array($this->fallback) as $lang) {
+			if ((is_file($file=($base=$path.$lang).'.php') ||
+				is_file($file=$base.'.php')) &&
+				is_array($dict=require($file)))
+				$lex+=$dict;
+			elseif (is_file($file=$base.'.ini')) {
+				preg_match_all(
+					'/(?<=^|\n)(?:'.
+					'(.+?)\h*=\h*'.
+					'((?:\\\\\h*\r?\n|.+?)*)'.
+					')(?=\r?\n|$)/',
+					$this->read($file),$matches,PREG_SET_ORDER);
+				if ($matches)
+					foreach ($matches as $match)
+						if (isset($match[1]) &&
+							!array_key_exists($match[1],$lex))
+							$lex[$match[1]]=trim(preg_replace(
+								'/(?<!\\\\)"|\\\\\h*\r?\n/','',$match[2]));
+			}
+		}
+		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/1.1 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('HTTP/1.1 '.$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'));
+				$headers=$this->hive['HEADERS'];
+				if (isset($headers['If-Modified-Since']) &&
+					strtotime($headers['If-Modified-Since'])+$secs>$time) {
+					$this->status(304);
+					die;
+				}
+			}
+			else
+				header('Cache-Control: no-cache, no-store, must-revalidate');
+		}
+	}
+
+	/**
+	*	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);
+		if (!$trace)
+			$trace=array_slice(debug_backtrace(FALSE),1);
+		$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']));
+			}
+		);
+		$highlight=PHP_SAPI!='cli' &&
+			$this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS);
+		$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'].' ';
+			error_log('- '.$src.$line);
+			$out.='• '.($highlight?
+				($this->highlight($src).' '.$this->highlight($line)):
+				($src.$line)).$eol;
+		}
+		$this->hive['ERROR']=array(
+			'status'=>$header,
+			'code'=>$code,
+			'text'=>$text,
+			'trace'=>$trace
+		);
+		$handler=$this->hive['ONERROR'];
+		$this->hive['ONERROR']=NULL;
+		if ((!$handler ||
+			$this->call($handler,$this,'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.
+					($debug?('<pre>'.$out.'</pre>'.$eol):'').
+				'</body>'.$eol.
+				'</html>');
+		if ($this->hive['HALT'])
+			die;
+	}
+
+	/**
+	*	Mock HTTP request
+	*	@return NULL
+	*	@param $pattern string
+	*	@param $args array
+	*	@param $headers array
+	*	@param $body string
+	**/
+	function mock($pattern,array $args=NULL,array $headers=NULL,$body=NULL) {
+		$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]));
+			$parts[4]=$this->hive['ALIASES'][$parts[2]];
+			if (isset($parts[3]))
+				$this->parse($parts[3]);
+			$parts[4]=$this->build($parts[4]);
+		}
+		if (empty($parts[4]))
+			user_error(sprintf(self::E_Pattern,$pattern));
+		$url=parse_url($parts[4]);
+		$query='';
+		if ($args)
+			$query.=http_build_query($args);
+		$query.=isset($url['query'])?(($query?'&':'').$url['query']):'';
+		if ($query && preg_match('/GET|POST/',$verb)) {
+			parse_str($query,$GLOBALS['_'.$verb]);
+			parse_str($query,$GLOBALS['_REQUEST']);
+		}
+		foreach ($headers?:array() as $key=>$val)
+			$_SERVER['HTTP_'.strtr(strtoupper($key),'-','_')]=$val;
+		$this->hive['VERB']=$verb;
+		$this->hive['URI']=$this->hive['BASE'].$url['path'];
+		$this->hive['AJAX']=isset($parts[5]) &&
+			preg_match('/ajax/i',$parts[5]);
+		if (preg_match('/GET|HEAD/',$verb) && $query)
+			$this->hive['URI'].='?'.$query;
+		else
+			$this->hive['BODY']=$body?:$query;
+		$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');
+		if (is_array($pattern)) {
+			foreach ($pattern as $item)
+				$this->route($item,$handler,$ttl,$kbps);
+			return;
+		}
+		preg_match('/([\|\w]+)\h+(?:(?:@(\w+)\h*:\h*)?([^\h]+)|@(\w+))'.
+			'(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts);
+		if ($parts[2])
+			$this->hive['ALIASES'][$parts[2]]=$parts[3];
+		elseif (!empty($parts[4])) {
+			if (empty($this->hive['ALIASES'][$parts[4]]))
+				user_error(sprintf(self::E_Named,$parts[4]));
+			$parts[3]=$this->hive['ALIASES'][$parts[4]];
+		}
+		if (empty($parts[3]))
+			user_error(sprintf(self::E_Pattern,$pattern));
+		$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'][str_replace('@',"\x00".'@',$parts[3])]
+				[$type][strtoupper($verb)]=array($handler,$ttl,$kbps);
+		}
+	}
+
+	/**
+	*	Reroute to specified URI
+	*	@return NULL
+	*	@param $url string
+	*	@param $permanent bool
+	**/
+	function reroute($url,$permanent=FALSE) {
+		if (PHP_SAPI!='cli') {
+			if (preg_match('/^(?:@(\w+)(?:(\(.+?)\))*|https?:\/\/)/',
+				$url,$parts)) {
+				if (isset($parts[1])) {
+					if (empty($this->hive['ALIASES'][$parts[1]]))
+						user_error(sprintf(self::E_Named,$parts[1]));
+					$url=$this->hive['BASE'].
+						$this->hive['ALIASES'][$parts[1]];
+					if (isset($parts[2]))
+						$this->parse($parts[2]);
+					$url=$this->build($url);
+				}
+			}
+			else
+				$url=$this->hive['BASE'].$url;
+			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
+	*	@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,
+				$class.'->'.strtolower($method),$ttl,$kbps);
+	}
+
+	/**
+	*	Function to redirect a route to another url or route
+	*	@return NULL
+	*	@param $pattern string|array
+	*	@param $url string
+	*/
+	function redirect($pattern,$url) {
+		if (is_array($pattern)) {
+			foreach ($pattern as $item)
+				$this->redirect($item,$url);
+			return;
+		}
+		$this->route($pattern, function($this) use ($url) { $this->reroute($url); });
+	}
+
+	/**
+	*	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;
+	}
+
+	/**
+	*	Match routes against incoming URI
+	*	@return NULL
+	**/
+	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);
+		// Match specific routes first
+		krsort($this->hive['ROUTES']);
+		// Convert to BASE-relative URL
+		$req=preg_replace(
+			'/^'.preg_quote($this->hive['BASE'],'/').'(\/.*|$)/','\1',
+			$this->hive['URI']
+		);
+		$allowed=array();
+		$case=$this->hive['CASELESS']?'i':'';
+		foreach ($this->hive['ROUTES'] as $url=>$routes) {
+			$url=str_replace("\x00".'@','@',$url);
+			if (!preg_match('/^'.
+				preg_replace('/@(\w+\b)/','(?P<\1>[^\/\?]+)',
+				str_replace('\*','([^\?]*)',preg_quote($url,'/'))).
+				'(?:[\?\/].*)?$/'.$case.'um',$req,$args))
+				continue;
+			$route=NULL;
+			if (isset($routes[$this->hive['AJAX']+1]))
+				$route=$routes[$this->hive['AJAX']+1];
+			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)=$route[$this->hive['VERB']];
+				if (is_bool(strpos($url,'/*')))
+					foreach (array_keys($args) as $key)
+						if (is_numeric($key) && $key)
+							unset($args[$key]);
+				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);
+				}
+				// Capture values of route pattern tokens
+				$this->hive['PARAMS']=$args=array_map('urldecode',$args);
+				// Save matching route
+				$this->hive['PATTERN']=$url;
+				// Process request
+				$body='';
+				$now=microtime(TRUE);
+				if (preg_match('/GET|HEAD/',$this->hive['VERB']) &&
+					isset($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) {
+						// Retrieve from cache backend
+						list($headers,$body)=$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
+					$this->call($handler,array($this,$args),
+						'beforeroute,afterroute');
+					$body=ob_get_clean();
+					if ($ttl && !error_get_last())
+						// Save to cache backend
+						$cache->set($hash,array(headers_list(),$body),$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;
+			}
+			$allowed=array_keys($route);
+			break;
+		}
+		if (!$allowed)
+			// URL doesn't match any route
+			$this->error(404);
+		elseif (PHP_SAPI!='cli') {
+			// Unhandled HTTP method
+			header('Allow: '.implode(',',$allowed));
+			if ($this->hive['VERB']!='OPTIONS')
+				$this->error(405);
+		}
+	}
+
+	/**
+	*	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);
+		// Execute function; abort if callback/hook returns FALSE
+		if (is_string($func) &&
+			preg_match('/(.+)\h*(->|::)\h*(.+)/s',$func,$parts)) {
+			// Convert string to executable PHP callback
+			if (!class_exists($parts[1]))
+				user_error(sprintf(self::E_Class,
+					is_string($func)?$parts[1]:$this->stringify()));
+			if ($parts[2]=='->')
+				$parts[1]=is_subclass_of($parts[1],'Prefab')?
+					call_user_func($parts[1].'::instance'):
+					new $parts[1]($this);
+			$func=array($parts[1],$parts[3]);
+		}
+		if (!is_callable($func))
+			// No route handler
+			if ($hooks=='beforeroute,afterroute')
+				$this->error(405);
+			else
+				user_error(sprintf(self::E_Method,
+					is_string($func)?$func:$this->stringify($func)));
+		$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
+	*	@return NULL
+	*	@param $file string
+	**/
+	function config($file) {
+		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'];
+				elseif (in_array($sec,array('routes','maps','redirects'))) {
+					call_user_func_array(
+						array($this,rtrim($sec,'s')),
+						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 preg_replace('/\\\\\h*(\r?\n)/','\1',$val);
+						},
+						// Mark quoted strings with 0x00 whitespace
+						str_getcsv(preg_replace('/(?<!\\\\)(")(.*?)\1/',
+							"\\1\x00\\2\\1",$match['rval']))
+					);
+					call_user_func_array(array($this,'set'),
+						array_merge(
+							array($match['lval']),
+							count($args)>1?array($args):$args));
+				}
+			}
+		}
+	}
+
+	/**
+	*	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 relative to the base directory
+	*	@return string
+	*	@param $url string
+	**/
+	function rel($url) {
+		return preg_replace('/(?:https?:\/\/)?'.
+			preg_quote($this->hive['BASE'],'/').'/','',rtrim($url,'/'));
+	}
+
+	/**
+	*	Namespace-aware class autoloader
+	*	@return mixed
+	*	@param $class string
+	**/
+	protected function autoload($class) {
+		$class=$this->fixslashes(ltrim($class,'\\'));
+		foreach ($this->split($this->hive['PLUGINS'].';'.
+			$this->hive['AUTOLOAD']) as $auto)
+			if (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(sprintf(self::E_Fatal,$error['message']));
+	}
+
+	//! 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);
+		// Abort on startup error
+		// Intercept errors/exceptions; PHP5.3-compatible
+		error_reporting(E_ALL|E_STRICT);
+		$fw=$this;
+		set_exception_handler(
+			function($obj) use($fw) {
+				$fw->error(500,$obj->getmessage(),$obj->gettrace());
+			}
+		);
+		set_error_handler(
+			function($code,$text) use($fw) {
+				if (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';
+		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=implode('/',array_map('urlencode',
+				explode('/',rtrim($this->fixslashes(
+					dirname($_SERVER['SCRIPT_NAME'])),'/'))));
+		$path=preg_replace('/^'.preg_quote($base,'/').'/','',
+			parse_url($_SERVER['REQUEST_URI'],PHP_URL_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
+			)
+		);
+		// Default configuration
+		$this->hive=array(
+			'AGENT'=>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']:'')),
+			'AJAX'=>isset($headers['X-Requested-With']) &&
+				$headers['X-Requested-With']=='XMLHttpRequest',
+			'ALIASES'=>array(),
+			'AUTOLOAD'=>'./',
+			'BASE'=>$base,
+			'BITMASK'=>ENT_COMPAT,
+			'BODY'=>NULL,
+			'CACHE'=>FALSE,
+			'CASELESS'=>TRUE,
+			'DEBUG'=>0,
+			'DIACRITICS'=>array(),
+			'DNSBL'=>'',
+			'EMOJI'=>array(),
+			'ENCODING'=>$charset,
+			'ERROR'=>NULL,
+			'ESCAPE'=>TRUE,
+			'EXEMPT'=>NULL,
+			'FALLBACK'=>$this->fallback,
+			'HEADERS'=>$headers,
+			'HALT'=>TRUE,
+			'HIGHLIGHT'=>TRUE,
+			'HOST'=>$_SERVER['SERVER_NAME'],
+			'IP'=>isset($headers['Client-IP'])?
+				$headers['Client-IP']:
+				(isset($headers['X-Forwarded-For'])?
+					$headers['X-Forwarded-For']:
+					(isset($_SERVER['REMOTE_ADDR'])?
+						$_SERVER['REMOTE_ADDR']:'')),
+			'JAR'=>$jar,
+			'LANGUAGE'=>isset($headers['Accept-Language'])?
+				$this->language($headers['Accept-Language']):
+				$this->fallback,
+			'LOCALES'=>'./',
+			'LOGS'=>'./',
+			'ONERROR'=>NULL,
+			'PACKAGE'=>self::PACKAGE,
+			'PARAMS'=>array(),
+			'PATH'=>$path,
+			'PATTERN'=>NULL,
+			'PLUGINS'=>$this->fixslashes(__DIR__).'/',
+			'PORT'=>isset($_SERVER['SERVER_PORT'])?
+				$_SERVER['SERVER_PORT']:NULL,
+			'PREFIX'=>NULL,
+			'QUIET'=>FALSE,
+			'RAW'=>FALSE,
+			'REALM'=>$scheme.'://'.
+				$_SERVER['SERVER_NAME'].$_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);
+			$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':
+				$key='info';
+			case 'apcu':
+				if (empty($key))
+					$key='key';
+				$info=apc_cache_info('user');
+				foreach ($info['cache_list'] as $item)
+					if (preg_match($regex,$item[$key]) &&
+						$item['mtime']+$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;
+
+	/**
+	*	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) {
+		$fw=Base::instance();
+		if (!$hive)
+			$hive=$fw->hive();
+		if ($fw->get('ESCAPE'))
+			$hive=$this->esc($hive);
+		if (isset($hive['ALIASES']))
+			$hive['ALIASES']=$fw->build($hive['ALIASES']);
+		extract($hive);
+		unset($fw);
+		unset($hive);
+		ob_start();
+		require($this->view);
+		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')
+					header('Content-Type: '.$mime.'; '.
+						'charset='.$fw->get('ENCODING'));
+				$data=$this->sandbox($hive);
+				if ($ttl)
+					$cache->set($hash,$data);
+				return $data;
+			}
+		user_error(sprintf(Base::E_Open,$file));
+	}
+
+}
+
+//! Lightweight template engine
+class Preview extends View {
+
+	protected
+		//! MIME type
+		$mime;
+
+	/**
+	*	Convert token to variable
+	*	@return string
+	*	@param $str string
+	**/
+	function token($str) {
+		return trim(preg_replace('/\{\{(.+?)\}\}/s',trim('\1'),
+			Base::instance()->compile($str)));
+	}
+
+	/**
+	*	Assemble markup
+	*	@return string
+	*	@param $node string
+	**/
+	protected function build($node) {
+		$self=$this;
+		return preg_replace_callback(
+			'/\{\{(.+?)\}\}/s',
+			function($expr) use($self) {
+				$str=trim($self->token($expr[1]));
+				if (preg_match('/^(.+?)\h*\|(\h*\w+(?:\h*[,;]\h*\w+)*)/',
+					$str,$parts)) {
+					$str=$parts[1];
+					foreach (Base::instance()->split($parts[2]) as $func)
+						$str=(($func=='format')?'\Base::instance()':'$this').
+							'->'.$func.'('.$str.')';
+				}
+				return '<?php echo '.$str.'; ?>';
+			},
+			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();
+		$cached=$cache->exists($hash=$fw->hash($file),$data);
+		if ($cached && $cached[0]+$ttl>microtime(TRUE))
+			return $data;
+		if (!is_dir($tmp=$fw->get('TEMP')))
+			mkdir($tmp,Base::MODE,TRUE);
+		foreach ($fw->split($fw->get('UI')) as $dir)
+			if (is_file($view=$fw->fixslashes($dir.$file))) {
+				if (!is_file($this->view=($tmp.
+					$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+					$fw->hash($view).'.php')) ||
+					filemtime($this->view)<filemtime($view)) {
+					// Remove PHP code and comments
+					$text=preg_replace(
+						'/(?<!["\'])\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')
+					header('Content-Type: '.($this->mime=$mime).'; '.
+						'charset='.$fw->get('ENCODING'));
+				$data=$this->sandbox($hive);
+				if ($ttl)
+					$cache->set($hash,$data);
+				return $data;
+			}
+		user_error(sprintf(Base::E_Open,$file));
+	}
+
+}
+
+//! 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';
+	//@}
+
+	/**
+	*	Convert class constants to array
+	*	@return array
+	*	@param $prefix string
+	**/
+	protected function constants($prefix) {
+		$ref=new ReflectionClass($this);
+		$out=array();
+		foreach (preg_grep('/^'.$prefix.'/',array_keys($ref->getconstants()))
+			as $val) {
+			$out[$key=substr($val,strlen($prefix))]=
+				constant('self::'.$prefix.$key);
+		}
+		unset($ref);
+		return $out;
+	}
+
+	/**
+	*	Return list of languages indexed by ISO 639-1 language code
+	*	@return array
+	**/
+	function languages() {
+		return $this->constants('LC_');
+	}
+
+	/**
+	*	Return list of countries indexed by ISO 3166-1 country code
+	*	@return array
+	**/
+	function countries() {
+		return $this->constants('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();

+ 229 - 0
php-fatfree/lib/basket.php

@@ -0,0 +1,229 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! Session-based pseudo-mapper
+class Basket {
+
+	//@{ Error messages
+	const
+		E_Field='Undefined field %s';
+	//@}
+
+	protected
+		//! Session key
+		$key,
+		//! Current item identifier
+		$id,
+		//! Current item contents
+		$item=array();
+
+	/**
+	*	Return TRUE if field is defined
+	*	@return bool
+	*	@param $key string
+	**/
+	function exists($key) {
+		return array_key_exists($key,$this->item);
+	}
+
+	/**
+	*	Assign value to field
+	*	@return scalar|FALSE
+	*	@param $key string
+	*	@param $val scalar
+	**/
+	function set($key,$val) {
+		return ($key=='_id')?FALSE:($this->item[$key]=$val);
+	}
+
+	/**
+	*	Retrieve value of field
+	*	@return scalar|FALSE
+	*	@param $key string
+	**/
+	function get($key) {
+		if ($key=='_id')
+			return $this->id;
+		if (array_key_exists($key,$this->item))
+			return $this->item[$key];
+		user_error(sprintf(self::E_Field,$key));
+		return FALSE;
+	}
+
+	/**
+	*	Delete field
+	*	@return NULL
+	*	@param $key string
+	**/
+	function clear($key) {
+		unset($this->item[$key]);
+	}
+
+	/**
+	*	Return items that match key/value pair;
+	*	If no key/value pair specified, return all items
+	*	@return array|FALSE
+	*	@param $key string
+	*	@param $val mixed
+	**/
+	function find($key=NULL,$val=NULL) {
+		if (isset($_SESSION[$this->key])) {
+			$out=array();
+			foreach ($_SESSION[$this->key] as $id=>$item)
+				if (!isset($key) ||
+					array_key_exists($key,$item) && $item[$key]==$val) {
+					$obj=clone($this);
+					$obj->id=$id;
+					$obj->item=$item;
+					$out[]=$obj;
+				}
+			return $out;
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Return first item that matches key/value pair
+	*	@return object|FALSE
+	*	@param $key string
+	*	@param $val mixed
+	**/
+	function findone($key,$val) {
+		return ($data=$this->find($key,$val))?$data[0]:FALSE;
+	}
+
+	/**
+	*	Map current item to matching key/value pair
+	*	@return array
+	*	@param $key string
+	*	@param $val mixed
+	**/
+	function load($key,$val) {
+		if ($found=$this->find($key,$val)) {
+			$this->id=$found[0]->id;
+			return $this->item=$found[0]->item;
+		}
+		$this->reset();
+		return array();
+	}
+
+	/**
+	*	Return TRUE if current item is empty/undefined
+	*	@return bool
+	**/
+	function dry() {
+		return !$this->item;
+	}
+
+	/**
+	*	Return number of items in basket
+	*	@return int
+	**/
+	function count() {
+		return isset($_SESSION[$this->key])?count($_SESSION[$this->key]):0;
+	}
+
+	/**
+	*	Save current item
+	*	@return array
+	**/
+	function save() {
+		if (!$this->id)
+			$this->id=uniqid(NULL,TRUE);
+		$_SESSION[$this->key][$this->id]=$this->item;
+		return $this->item;
+	}
+
+	/**
+	*	Erase item matching key/value pair
+	*	@return bool
+	*	@param $key string
+	*	@param $val mixed
+	**/
+	function erase($key,$val) {
+		$found=$this->find($key,$val);
+		if ($found && $id=$found[0]->id) {
+			unset($_SESSION[$this->key][$id]);
+			if ($id==$this->id)
+				$this->reset();
+			return TRUE;
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Reset cursor
+	*	@return NULL
+	**/
+	function reset() {
+		$this->id=NULL;
+		$this->item=array();
+	}
+
+	/**
+	*	Empty basket
+	*	@return NULL
+	**/
+	function drop() {
+		unset($_SESSION[$this->key]);
+	}
+
+	/**
+	*	Hydrate item using hive array variable
+	*	@return NULL
+	*	@param $key string
+	**/
+	function copyfrom($key) {
+		foreach (\Base::instance()->get($key) as $key=>$val)
+			$this->item[$key]=$val;
+	}
+
+	/**
+	*	Populate hive array variable with item contents
+	*	@return NULL
+	*	@param $key string
+	**/
+	function copyto($key) {
+		$var=&\Base::instance()->ref($key);
+		foreach ($this->item as $key=>$field)
+			$var[$key]=$field;
+	}
+
+	/**
+	*	Check out basket contents
+	*	@return array
+	**/
+	function checkout() {
+		if (isset($_SESSION[$this->key])) {
+			$out=$_SESSION[$this->key];
+			unset($_SESSION[$this->key]);
+			return $out;
+		}
+		return array();
+	}
+
+	/**
+	*	Instantiate class
+	*	@return void
+	*	@param $key string
+	**/
+	function __construct($key='basket') {
+		$this->key=$key;
+		@session_start();
+		Base::instance()->sync('SESSION');
+		$this->reset();
+	}
+
+}

+ 89 - 0
php-fatfree/lib/bcrypt.php

@@ -0,0 +1,89 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! Lightweight password hashing library
+class Bcrypt extends Prefab {
+
+	//@{ Error messages
+	const
+		E_CostArg='Invalid cost parameter',
+		E_SaltArg='Salt must be at least 22 alphanumeric characters';
+	//@}
+
+	//! Default cost
+	const
+		COST=10;
+
+	/**
+	*	Generate bcrypt hash of string
+	*	@return string|FALSE
+	*	@param $pw string
+	*	@param $salt string
+	*	@param $cost int
+	**/
+	function hash($pw,$salt=NULL,$cost=self::COST) {
+		if ($cost<4 || $cost>31)
+			user_error(self::E_CostArg);
+		$len=22;
+		if ($salt) {
+			if (!preg_match('/^[[:alnum:]\.\/]{'.$len.',}$/',$salt))
+				user_error(self::E_SaltArg);
+		}
+		else {
+			$raw=16;
+			$iv='';
+			if (extension_loaded('mcrypt'))
+				$iv=mcrypt_create_iv($raw,MCRYPT_DEV_URANDOM);
+			if (!$iv && extension_loaded('openssl'))
+				$iv=openssl_random_pseudo_bytes($raw);
+			if (!$iv)
+				for ($i=0;$i<$raw;$i++)
+					$iv.=chr(mt_rand(0,255));
+			$salt=str_replace('+','.',base64_encode($iv));
+		}
+		$salt=substr($salt,0,$len);
+		$hash=crypt($pw,sprintf('$2y$%02d$',$cost).$salt);
+		return strlen($hash)>13?$hash:FALSE;
+	}
+
+	/**
+	*	Check if password is still strong enough
+	*	@return bool
+	*	@param $hash string
+	*	@param $cost int
+	**/
+	function needs_rehash($hash,$cost=self::COST) {
+		list($pwcost)=sscanf($hash,"$2y$%d$");
+		return $pwcost<$cost;
+	}
+
+	/**
+	*	Verify password against hash using timing attack resistant approach
+	*	@return bool
+	*	@param $pw string
+	*	@param $hash string
+	**/
+	function verify($pw,$hash) {
+		$val=crypt($pw,$hash);
+		$len=strlen($val);
+		if ($len!=strlen($hash) || $len<14)
+			return FALSE;
+		$out=0;
+		for ($i=0;$i<$len;$i++)
+			$out|=(ord($val[$i])^ord($hash[$i]));
+		return $out===0;
+	}
+
+}

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

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

+ 1 - 0
php-fatfree/lib/code.css

@@ -0,0 +1 @@
+code{word-wrap:break-word;color:black}.comment,.doc_comment,.ml_comment{color:dimgray;font-style:italic}.variable{color:blueviolet}.const,.constant_encapsed_string,.class_c,.dir,.file,.func_c,.halt_compiler,.line,.method_c,.lnumber,.dnumber{color:crimson}.string,.and_equal,.boolean_and,.boolean_or,.concat_equal,.dec,.div_equal,.inc,.is_equal,.is_greater_or_equal,.is_identical,.is_not_equal,.is_not_identical,.is_smaller_or_equal,.logical_and,.logical_or,.logical_xor,.minus_equal,.mod_equal,.mul_equal,.ns_c,.ns_separator,.or_equal,.plus_equal,.sl,.sl_equal,.sr,.sr_equal,.xor_equal,.start_heredoc,.end_heredoc,.object_operator,.paamayim_nekudotayim{color:black}.abstract,.array,.array_cast,.as,.break,.case,.catch,.class,.clone,.continue,.declare,.default,.do,.echo,.else,.elseif,.empty.enddeclare,.endfor,.endforach,.endif,.endswitch,.endwhile,.eval,.exit,.extends,.final,.for,.foreach,.function,.global,.goto,.if,.implements,.include,.include_once,.instanceof,.interface,.isset,.list,.namespace,.new,.print,.private,.public,.protected,.require,.require_once,.return,.static,.switch,.throw,.try,.unset,.use,.var,.while{color:royalblue}.open_tag,.open_tag_with_echo,.close_tag{color:orange}.ini_section{color:black}.ini_key{color:royalblue}.ini_value{color:crimson}.xml_tag{color:dodgerblue}.xml_attr{color:blueviolet}.xml_data{color:red}.section{color:black}.directive{color:blue}.data{color:dimgray}

+ 321 - 0
php-fatfree/lib/db/cursor.php

@@ -0,0 +1,321 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace DB;
+
+//! Simple cursor implementation
+abstract class Cursor extends \Magic {
+
+	//@{ 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 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 $key string
+	*	@param $func callback
+	**/
+	abstract function copyfrom($key,$func=NULL);
+
+	/**
+	*	Populate hive array variable with mapper fields
+	*	@return NULL
+	*	@param $key string
+	**/
+	abstract function copyto($key);
+
+	/**
+	*	Return TRUE if current cursor position is not mapped to any record
+	*	@return bool
+	**/
+	function dry() {
+		return empty($this->query[$this->ptr]);
+	}
+
+	/**
+	*	Return first record (mapper object) that matches criteria
+	*	@return object|FALSE
+	*	@param $filter string|array
+	*	@param $options array
+	*	@param $ttl int
+	**/
+	function findone($filter=NULL,array $options=NULL,$ttl=0) {
+		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);
+	}
+
+	/**
+	*	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->ptr=0;
+	}
+
+	/**
+	*	Define onload trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function onload($func) {
+		return $this->trigger['load']=$func;
+	}
+
+	/**
+	*	Define beforeinsert trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function beforeinsert($func) {
+		return $this->trigger['beforeinsert']=$func;
+	}
+
+	/**
+	*	Define afterinsert trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function afterinsert($func) {
+		return $this->trigger['afterinsert']=$func;
+	}
+
+	/**
+	*	Define oninsert trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function oninsert($func) {
+		return $this->afterinsert($func);
+	}
+
+	/**
+	*	Define beforeupdate trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function beforeupdate($func) {
+		return $this->trigger['beforeupdate']=$func;
+	}
+
+	/**
+	*	Define afterupdate trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function afterupdate($func) {
+		return $this->trigger['afterupdate']=$func;
+	}
+
+	/**
+	*	Define onupdate trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function onupdate($func) {
+		return $this->afterupdate($func);
+	}
+
+	/**
+	*	Define beforeerase trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function beforeerase($func) {
+		return $this->trigger['beforeerase']=$func;
+	}
+
+	/**
+	*	Define aftererase trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function aftererase($func) {
+		return $this->trigger['aftererase']=$func;
+	}
+
+	/**
+	*	Define onerase trigger
+	*	@return callback
+	*	@param $func callback
+	**/
+	function onerase($func) {
+		return $this->aftererase($func);
+	}
+
+	/**
+	*	Reset cursor
+	*	@return NULL
+	**/
+	function reset() {
+		$this->query=array();
+		$this->ptr=0;
+	}
+
+}

+ 133 - 0
php-fatfree/lib/db/jig.php

@@ -0,0 +1,133 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace DB;
+
+//! Flat-file DB wrapper
+class Jig {
+
+	//@{ Storage formats
+	const
+		FORMAT_JSON=0,
+		FORMAT_Serialized=1;
+	//@}
+
+	protected
+		//! UUID
+		$uuid,
+		//! Storage location
+		$dir,
+		//! Current storage format
+		$format,
+		//! Jig log
+		$log;
+
+	/**
+	*	Read data from file
+	*	@return array
+	*	@param $file string
+	**/
+	function read($file) {
+		$fw=\Base::instance();
+		if (!is_file($dst=$this->dir.$file))
+			return array();
+		$raw=$fw->read($dst);
+		switch ($this->format) {
+			case self::FORMAT_JSON:
+				$data=json_decode($raw,TRUE);
+				break;
+			case self::FORMAT_Serialized:
+				$data=$fw->unserialize($raw);
+				break;
+		}
+		return $data;
+	}
+
+	/**
+	*	Write data to file
+	*	@return int
+	*	@param $file string
+	*	@param $data array
+	**/
+	function write($file,array $data=NULL) {
+		$fw=\Base::instance();
+		switch ($this->format) {
+			case self::FORMAT_JSON:
+				$out=json_encode($data,@constant('JSON_PRETTY_PRINT'));
+				break;
+			case self::FORMAT_Serialized:
+				$out=$fw->serialize($data);
+				break;
+		}
+		return $fw->write($this->dir.$file,$out);
+	}
+
+	/**
+	*	Return directory
+	*	@return string
+	**/
+	function dir() {
+		return $this->dir;
+	}
+
+	/**
+	*	Return UUID
+	*	@return string
+	**/
+	function uuid() {
+		return $this->uuid;
+	}
+
+	/**
+	*	Return SQL profiler results
+	*	@return string
+	**/
+	function log() {
+		return $this->log;
+	}
+
+	/**
+	*	Jot down log entry
+	*	@return NULL
+	*	@param $frame string
+	**/
+	function jot($frame) {
+		if ($frame)
+			$this->log.=date('r').' '.$frame.PHP_EOL;
+	}
+
+	/**
+	*	Clean storage
+	*	@return NULL
+	**/
+	function drop() {
+		if ($glob=@glob($this->dir.'/*',GLOB_NOSORT))
+			foreach ($glob as $file)
+				@unlink($file);
+	}
+
+	/**
+	*	Instantiate class
+	*	@param $dir string
+	*	@param $format int
+	**/
+	function __construct($dir,$format=self::FORMAT_JSON) {
+		if (!is_dir($dir))
+			mkdir($dir,\Base::MODE,TRUE);
+		$this->uuid=\Base::instance()->hash($this->dir=$dir);
+		$this->format=$format;
+	}
+
+}

+ 459 - 0
php-fatfree/lib/db/jig/mapper.php

@@ -0,0 +1,459 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace DB\Jig;
+
+//! Flat-file DB mapper
+class Mapper extends \DB\Cursor {
+
+	protected
+		//! Flat-file DB wrapper
+		$db,
+		//! Data file
+		$file,
+		//! Document identifier
+		$id,
+		//! Document contents
+		$document=array();
+
+	/**
+	*	Return database type
+	*	@return string
+	**/
+	function dbtype() {
+		return 'Jig';
+	}
+
+	/**
+	*	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 ($key=='_id')?FALSE:($this->document[$key]=$val);
+	}
+
+	/**
+	*	Retrieve value of field
+	*	@return scalar|FALSE
+	*	@param $key string
+	**/
+	function get($key) {
+		if ($key=='_id')
+			return $this->id;
+		if (array_key_exists($key,$this->document))
+			return $this->document[$key];
+		user_error(sprintf(self::E_Field,$key));
+		return FALSE;
+	}
+
+	/**
+	*	Delete field
+	*	@return NULL
+	*	@param $key string
+	**/
+	function clear($key) {
+		if ($key!='_id')
+			unset($this->document[$key]);
+	}
+
+	/**
+	*	Convert array to mapper object
+	*	@return object
+	*	@param $id string
+	*	@param $row array
+	**/
+	protected function factory($id,$row) {
+		$mapper=clone($this);
+		$mapper->reset();
+		$mapper->id=$id;
+		foreach ($row as $field=>$val)
+			$mapper->document[$field]=$val;
+		$mapper->query=array(clone($mapper));
+		if (isset($mapper->trigger['load']))
+			\Base::instance()->call($mapper->trigger['load'],$mapper);
+		return $mapper;
+	}
+
+	/**
+	*	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+array('_id'=>$this->id);
+	}
+
+	/**
+	*	Convert tokens in string expression to variable names
+	*	@return string
+	*	@param $str string
+	**/
+	function token($str) {
+		$self=$this;
+		$str=preg_replace_callback(
+			'/(?<!\w)@(\w(?:[\w\.\[\]])*)/',
+			function($token) use($self) {
+				// Convert from JS dot notation to PHP array notation
+				return '$'.preg_replace_callback(
+					'/(\.\w+)|\[((?:[^\[\]]*|(?R))*)\]/',
+					function($expr) use($self) {
+						$fw=\Base::instance();
+						return
+							'['.
+							($expr[1]?
+								$fw->stringify(substr($expr[1],1)):
+								(preg_match('/^\w+/',
+									$mix=$self->token($expr[2]))?
+									$fw->stringify($mix):
+									$mix)).
+							']';
+					},
+					$token[1]
+				);
+			},
+			$str
+		);
+		return trim($str);
+	}
+
+	/**
+	*	Return records that match criteria
+	*	@return array|FALSE
+	*	@param $filter array
+	*	@param $options array
+	*	@param $ttl int
+	*	@param $log bool
+	**/
+	function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) {
+		if (!$options)
+			$options=array();
+		$options+=array(
+			'order'=>NULL,
+			'limit'=>0,
+			'offset'=>0
+		);
+		$fw=\Base::instance();
+		$cache=\Cache::instance();
+		$db=$this->db;
+		$now=microtime(TRUE);
+		$data=array();
+		if (!$fw->get('CACHE') || !$ttl || !($cached=$cache->exists(
+			$hash=$fw->hash($this->db->dir().
+				$fw->stringify(array($filter,$options))).'.jig',$data)) ||
+			$cached[0]+$ttl<microtime(TRUE)) {
+			$data=$db->read($this->file);
+			if (is_null($data))
+				return FALSE;
+			foreach ($data as $id=>&$doc) {
+				$doc['_id']=$id;
+				unset($doc);
+			}
+			if ($filter) {
+				if (!is_array($filter))
+					return FALSE;
+				// Normalize equality operator
+				$expr=preg_replace('/(?<=[^<>!=])=(?!=)/','==',$filter[0]);
+				// Prepare query arguments
+				$args=isset($filter[1]) && is_array($filter[1])?
+					$filter[1]:
+					array_slice($filter,1,NULL,TRUE);
+				$args=is_array($args)?$args:array(1=>$args);
+				$keys=$vals=array();
+				$tokens=array_slice(
+					token_get_all('<?php '.$this->token($expr)),1);
+				$data=array_filter($data,
+					function($_row) use($fw,$args,$tokens) {
+						$_expr='';
+						$ctr=0;
+						$named=FALSE;
+						foreach ($tokens as $token) {
+							if (is_string($token))
+								if ($token=='?') {
+									// Positional
+									$ctr++;
+									$key=$ctr;
+								}
+								else {
+									if ($token==':')
+										$named=TRUE;
+									else
+										$_expr.=$token;
+									continue;
+								}
+							elseif ($named &&
+								token_name($token[0])=='T_STRING') {
+								$key=':'.$token[1];
+								$named=FALSE;
+							}
+							else {
+								$_expr.=$token[1];
+								continue;
+							}
+							$_expr.=$fw->stringify(
+								is_string($args[$key])?
+									addcslashes($args[$key],'\''):
+									$args[$key]);
+						}
+						// Avoid conflict with user code
+						unset($fw,$tokens,$args,$ctr,$token,$key,$named);
+						extract($_row);
+						// Evaluate pseudo-SQL expression
+						return eval('return '.$_expr.';');
+					}
+				);
+			}
+			if (isset($options['order'])) {
+				$cols=$fw->split($options['order']);
+				uasort(
+					$data,
+					function($val1,$val2) use($cols) {
+						foreach ($cols as $col) {
+							$parts=explode(' ',$col,2);
+							$order=empty($parts[1])?
+								SORT_ASC:
+								constant($parts[1]);
+							$col=$parts[0];
+							if (!array_key_exists($col,$val1))
+								$val1[$col]=NULL;
+							if (!array_key_exists($col,$val2))
+								$val2[$col]=NULL;
+							list($v1,$v2)=array($val1[$col],$val2[$col]);
+							if ($out=strnatcmp($v1,$v2)*
+								(($order==SORT_ASC)*2-1))
+								return $out;
+						}
+						return 0;
+					}
+				);
+			}
+			$data=array_slice($data,
+				$options['offset'],$options['limit']?:NULL,TRUE);
+			if ($fw->get('CACHE') && $ttl)
+				// Save to cache backend
+				$cache->set($hash,$data,$ttl);
+		}
+		$out=array();
+		foreach ($data as $id=>&$doc) {
+			unset($doc['_id']);
+			$out[]=$this->factory($id,$doc);
+			unset($doc);
+		}
+		if ($log && isset($args)) {
+			if ($filter)
+				foreach ($args as $key=>$val) {
+					$vals[]=$fw->stringify(is_array($val)?$val[0]:$val);
+					$keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/';
+				}
+			$db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
+				$this->file.' [find] '.
+				($filter?preg_replace($keys,$vals,$filter[0],1):''));
+		}
+		return $out;
+	}
+
+	/**
+	*	Count records that match criteria
+	*	@return int
+	*	@param $filter array
+	*	@param $ttl int
+	**/
+	function count($filter=NULL,$ttl=0) {
+		$now=microtime(TRUE);
+		$out=count($this->find($filter,NULL,$ttl,FALSE));
+		$this->db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
+			$this->file.' [count] '.($filter?json_encode($filter):''));
+		return $out;
+	}
+
+	/**
+	*	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();
+		$this->id=$out?$out->id:NULL;
+		if ($this->document && isset($this->trigger['load']))
+			\Base::instance()->call($this->trigger['load'],$this);
+		return $out;
+	}
+
+	/**
+	*	Insert new record
+	*	@return array
+	**/
+	function insert() {
+		if ($this->id)
+			return $this->update();
+		$db=$this->db;
+		$now=microtime(TRUE);
+		while (($id=uniqid(NULL,TRUE)) &&
+			($data=$db->read($this->file)) && isset($data[$id]) &&
+			!connection_aborted())
+			usleep(mt_rand(0,100));
+		$this->id=$id;
+		$data[$id]=$this->document;
+		$pkey=array('_id'=>$this->id);
+		if (isset($this->trigger['beforeinsert']))
+			\Base::instance()->call($this->trigger['beforeinsert'],
+				array($this,$pkey));
+		$db->write($this->file,$data);
+		$db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
+			$this->file.' [insert] '.json_encode($this->document));
+		if (isset($this->trigger['afterinsert']))
+			\Base::instance()->call($this->trigger['afterinsert'],
+				array($this,$pkey));
+		$this->load(array('@_id=?',$this->id));
+		return $this->document;
+	}
+
+	/**
+	*	Update current record
+	*	@return array
+	**/
+	function update() {
+		$db=$this->db;
+		$now=microtime(TRUE);
+		$data=$db->read($this->file);
+		$data[$this->id]=$this->document;
+		if (isset($this->trigger['beforeupdate']))
+			\Base::instance()->call($this->trigger['beforeupdate'],
+				array($this,array('_id'=>$this->id)));
+		$db->write($this->file,$data);
+		$db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
+			$this->file.' [update] '.json_encode($this->document));
+		if (isset($this->trigger['afterupdate']))
+			\Base::instance()->call($this->trigger['afterupdate'],
+				array($this,array('_id'=>$this->id)));
+		return $this->document;
+	}
+
+	/**
+	*	Delete current record
+	*	@return bool
+	*	@param $filter array
+	**/
+	function erase($filter=NULL) {
+		$db=$this->db;
+		$now=microtime(TRUE);
+		$data=$db->read($this->file);
+		$pkey=array('_id'=>$this->id);
+		if ($filter) {
+			foreach ($this->find($filter,NULL,FALSE) as $mapper)
+				if (!$mapper->erase())
+					return FALSE;
+			return TRUE;
+		}
+		elseif (isset($this->id)) {
+			unset($data[$this->id]);
+			parent::erase();
+			$this->skip(0);
+		}
+		else
+			return FALSE;
+		if (isset($this->trigger['beforeerase']))
+			\Base::instance()->call($this->trigger['beforeerase'],
+				array($this,$pkey));
+		$db->write($this->file,$data);
+		if ($filter) {
+			$args=isset($filter[1]) && is_array($filter[1])?
+				$filter[1]:
+				array_slice($filter,1,NULL,TRUE);
+			$args=is_array($args)?$args:array(1=>$args);
+			foreach ($args as $key=>$val) {
+				$vals[]=\Base::instance()->
+					stringify(is_array($val)?$val[0]:$val);
+				$keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/';
+			}
+		}
+		$db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
+			$this->file.' [erase] '.
+			($filter?preg_replace($keys,$vals,$filter[0],1):''));
+		if (isset($this->trigger['aftererase']))
+			\Base::instance()->call($this->trigger['aftererase'],
+				array($this,$pkey));
+		return TRUE;
+	}
+
+	/**
+	*	Reset cursor
+	*	@return NULL
+	**/
+	function reset() {
+		$this->id=NULL;
+		$this->document=array();
+		parent::reset();
+	}
+
+	/**
+	*	Hydrate mapper object using hive array variable
+	*	@return NULL
+	*	@param $key string
+	*	@param $func callback
+	**/
+	function copyfrom($key,$func=NULL) {
+		$var=\Base::instance()->get($key);
+		if ($func)
+			$var=call_user_func($func,$var);
+		foreach ($var as $key=>$val)
+			$this->document[$key]=$val;
+	}
+
+	/**
+	*	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);
+	}
+
+	/**
+	*	Instantiate class
+	*	@return void
+	*	@param $db object
+	*	@param $file string
+	**/
+	function __construct(\DB\Jig $db,$file) {
+		$this->db=$db;
+		$this->file=$file;
+		$this->reset();
+	}
+
+}

+ 168 - 0
php-fatfree/lib/db/jig/session.php

@@ -0,0 +1,168 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace DB\Jig;
+
+//! Jig-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
+	**/
+	function __construct(\DB\Jig $db,$table='sessions') {
+		parent::__construct($db,'sessions');
+		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'])) {
+			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();
+		}
+	}
+
+}

+ 104 - 0
php-fatfree/lib/db/mongo.php

@@ -0,0 +1,104 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace 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);
+	}
+
+}

+ 344 - 0
php-fatfree/lib/db/mongo/mapper.php

@@ -0,0 +1,344 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace 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));
+		return FALSE;
+	}
+
+	/**
+	*	Delete field
+	*	@return NULL
+	*	@param $key string
+	**/
+	function clear($key) {
+		unset($this->document[$key]);
+	}
+
+	/**
+	*	Convert array to mapper object
+	*	@return object
+	*	@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 array
+	*	@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('safe'=>TRUE));
+				$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 array
+	*	@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);
+			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'])));
+		$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));
+		$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));
+		$result=$this->collection->
+			remove(array('_id'=>$this->document['_id']));
+		parent::erase();
+		$this->skip(0);
+		if (isset($this->trigger['aftererase']))
+			\Base::instance()->call($this->trigger['aftererase'],
+				array($this,$pkey));
+		return $result;
+	}
+
+	/**
+	*	Reset cursor
+	*	@return NULL
+	**/
+	function reset() {
+		$this->document=array();
+		parent::reset();
+	}
+
+	/**
+	*	Hydrate mapper object using hive array variable
+	*	@return NULL
+	*	@param $key string
+	*	@param $func callback
+	**/
+	function copyfrom($key,$func=NULL) {
+		$var=\Base::instance()->get($key);
+		if ($func)
+			$var=call_user_func($func,$var);
+		foreach ($var as $key=>$val)
+			$this->document[$key]=$val;
+	}
+
+	/**
+	*	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;
+	}
+
+	/**
+	*	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();
+	}
+
+}

+ 174 - 0
php-fatfree/lib/db/mongo/session.php

@@ -0,0 +1,174 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace 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();
+		if (!$sent) {
+			if (isset($_COOKIE['_']))
+				setcookie('_','',strtotime('-1 year'));
+			call_user_func_array('setcookie',
+				array('_',$csrf)+$fw->get('JAR'));
+		}
+		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
+	**/
+	function __construct(\DB\Mongo $db,$table='sessions') {
+		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'])) {
+			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();
+		}
+	}
+
+}

+ 403 - 0
php-fatfree/lib/db/sql.php

@@ -0,0 +1,403 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace DB;
+
+//! PDO wrapper
+class SQL extends \PDO {
+
+	protected
+		//! UUID
+		$uuid,
+		//! 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=parent::begintransaction();
+		$this->trans=TRUE;
+		return $out;
+	}
+
+	/**
+	*	Rollback SQL transaction
+	*	@return bool
+	**/
+	function rollback() {
+		$out=parent::rollback();
+		$this->trans=FALSE;
+		return $out;
+	}
+
+	/**
+	*	Commit SQL transaction
+	*	@return bool
+	**/
+	function commit() {
+		$out=parent::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 {
+			$cmds=array($cmds);
+			$args=array($args);
+		}
+		$fw=\Base::instance();
+		$cache=\Cache::instance();
+		$result=FALSE;
+		foreach (array_combine($cmds,$args) as $cmd=>$arg) {
+			if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd))
+				continue;
+			$now=microtime(TRUE);
+			$keys=$vals=array();
+			if ($fw->get('CACHE') && $ttl && ($cached=$cache->exists(
+				$hash=$fw->hash($this->dsn.$cmd.
+				$fw->stringify($arg)).'.sql',$result)) &&
+				$cached[0]+$ttl>microtime(TRUE)) {
+				foreach ($arg as $key=>$val) {
+					$vals[]=$fw->stringify(is_array($val)?$val[0]:$val);
+					$keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/';
+				}
+			}
+			elseif (is_object($query=$this->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[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/';
+				}
+				$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]);
+				}
+				if (preg_match('/^\s*'.
+					'(?:CALL|EXPLAIN|SELECT|PRAGMA|SHOW|RETURNING|EXEC)\b/is',
+					$cmd)) {
+					$result=$query->fetchall(\PDO::FETCH_ASSOC);
+					// Work around SQLite quote bug
+					if (preg_match('/sqlite2?/',$this->engine))
+						foreach ($result as $pos=>$rec) {
+							unset($result[$pos]);
+							$result[$pos]=array();
+							foreach ($rec as $key=>$val)
+								$result[$pos][trim($key,'\'"[]`')]=$val;
+						}
+					$this->rows=count($result);
+					if ($fw->get('CACHE') && $ttl)
+						// Save to cache backend
+						$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]);
+				}
+			}
+			if ($log)
+				$this->log.=date('r').' ('.
+					sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.
+					(empty($cached)?'':'[CACHED] ').
+					preg_replace($keys,$vals,$cmd,1).PHP_EOL;
+		}
+		if ($this->trans && $auto)
+			$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) {
+		// 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|int(?=eger)|bool/i',
+									$row[$val[2]],$parts)?
+								constant('\PDO::PARAM_'.
+									strtoupper($parts[0])):
+								\PDO::PARAM_STR,
+							'default'=>$row[$val[3]],
+							'nullable'=>$row[$val[4]]==$val[5],
+							'pkey'=>$row[$val[6]]==$val[7]
+						);
+				}
+				return $rows;
+			}
+		return FALSE;
+	}
+
+	/**
+	*	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):
+			parent::quote($val,$type);
+	}
+
+	/**
+	*	Return UUID
+	*	@return string
+	**/
+	function uuid() {
+		return $this->uuid;
+	}
+
+	/**
+	*	Return database engine
+	*	@return string
+	**/
+	function driver() {
+		return $this->engine;
+	}
+
+	/**
+	*	Return server version
+	*	@return string
+	**/
+	function version() {
+		return parent::getattribute(parent::ATTR_SERVER_VERSION);
+	}
+
+	/**
+	*	Return database name
+	*	@return string
+	**/
+	function name() {
+		return $this->dbname;
+	}
+
+	/**
+	*	Return quoted identifier name
+	*	@return string
+	*	@param $key
+	**/
+	function quotekey($key) {
+		if ($this->engine=='mysql')
+			$key="`".implode('`.`',explode('.',$key))."`";
+		elseif (preg_match('/sybase|dblib/',$this->engine))
+			$key="'".implode("'.'",explode('.',$key))."'";
+		elseif (preg_match('/sqlite2?|pgsql|oci/',$this->engine))
+			$key='"'.implode('"."',explode('.',$key)).'"';
+		elseif (preg_match('/mssql|sqlsrv|odbc/',$this->engine))
+			$key="[".implode('].[',explode('.',$key))."]";
+		return $key;
+	}
+
+	/**
+	*	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)=(.+?)(?=;|$)/i',$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'))).';');
+		parent::__construct($dsn,$user,$pw,$options);
+		$this->engine=parent::getattribute(parent::ATTR_DRIVER_NAME);
+	}
+
+}

+ 552 - 0
php-fatfree/lib/db/sql/mapper.php

@@ -0,0 +1,552 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace DB\SQL;
+
+//! SQL data mapper
+class Mapper extends \DB\Cursor {
+
+	//@{ Error messages
+	const
+		E_Adhoc='Unable to process ad hoc field %s';
+	//@}
+
+	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 TRUE if field is defined
+	*	@return bool
+	*	@param $key string
+	**/
+	function exists($key) {
+		return array_key_exists($key,$this->fields+$this->adhoc);
+	}
+
+	/**
+	*	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;
+		}
+		// 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));
+	}
+
+	/**
+	*	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 array
+	*	@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
+		);
+		$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;
+		}
+		$db=$this->db;
+		if ($options['group'])
+			$sql.=' GROUP BY '.implode(',',array_map(
+				function($str) use($db) {
+					return preg_match('/^(\w+)(?:\h+HAVING|\h*(?:,|$))/i',
+						$str,$parts)?
+						($db->quotekey($parts[1]).
+						(isset($parts[2])?(' '.$parts[2]):'')):$str;
+				},
+				explode(',',$options['group'])));
+		if ($options['order']) {
+			$sql.=' ORDER BY '.implode(',',array_map(
+				function($str) use($db) {
+					return preg_match('/^(\w+)(?:\h+(ASC|DESC))?\h*(?:,|$)/i',
+						$str,$parts)?
+						($db->quotekey($parts[1]).
+						(isset($parts[2])?(' '.$parts[2]):'')):$str;
+				},
+				explode(',',$options['order'])));
+		}
+		if ($options['limit'])
+			$sql.=' LIMIT '.(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 array
+	*	@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']?: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();
+		$ctr=0;
+		$fields='';
+		$values='';
+		$filter='';
+		$pkeys=array();
+		$nkeys=array();
+		$ckeys=array();
+		$inc=NULL;
+		foreach ($this->fields as $key=>$field)
+			if ($field['pkey'])
+				$pkeys[$key]=$field['previous'];
+		if (isset($this->trigger['beforeinsert']))
+			\Base::instance()->call($this->trigger['beforeinsert'],
+				array($this,$pkeys));
+		foreach ($this->fields as $key=>&$field) {
+			if ($field['pkey']) {
+				$field['previous']=$field['value'];
+				if (!$inc && $field['pdo_type']==\PDO::PARAM_INT &&
+					empty($field['value']) && !$field['nullable'])
+					$inc=$key;
+				$filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?';
+				$nkeys[$ctr+1]=array($field['value'],$field['pdo_type']);
+			}
+			if ($field['changed'] && $key!=$inc) {
+				$fields.=($ctr?',':'').$this->db->quotekey($key);
+				$values.=($ctr?',':'').'?';
+				$args[$ctr+1]=array($field['value'],$field['pdo_type']);
+				$ctr++;
+				$ckeys[]=$key;
+			}
+			$field['changed']=FALSE;
+			unset($field);
+		}
+		if ($fields) {
+			$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));
+		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 ':'').$this->db->quotekey($key).'=?';
+				$args[$ctr+1]=array($field['previous'],$field['pdo_type']);
+				$ctr++;
+			}
+		if ($pairs) {
+			$sql='UPDATE '.$this->table.' SET '.$pairs;
+			if ($filter)
+				$sql.=' WHERE '.$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();
+		$this->skip(0);
+		if (isset($this->trigger['beforeerase']))
+			\Base::instance()->call($this->trigger['beforeerase'],
+				array($this,$pkeys));
+		$out=$this->db->
+			exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args);
+		if (isset($this->trigger['aftererase']))
+			\Base::instance()->call($this->trigger['aftererase'],
+				array($this,$pkeys));
+		return $out;
+	}
+
+	/**
+	*	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 $key string
+	*	@param $func callback
+	**/
+	function copyfrom($key,$func=NULL) {
+		$var=\Base::instance()->get($key);
+		if ($func)
+			$var=call_user_func($func,$var);
+		foreach ($var as $key=>$val)
+			if (in_array($key,array_keys($this->fields))) {
+				$field=&$this->fields[$key];
+				if ($field['value']!==$val) {
+					$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
+	*	@return array
+	**/
+	function schema() {
+		return $this->fields;
+	}
+
+	/**
+	*	Return field names
+	*	@return array
+	*	@param $adhoc bool
+	**/
+	function fields($adhoc=TRUE) {
+		return array_keys($this->fields+($adhoc?$this->adhoc:array()));
+	}
+
+	/**
+	*	Instantiate class
+	*	@param $db object
+	*	@param $table string
+	*	@param $fields array|string
+	*	@param $ttl int
+	**/
+	function __construct(\DB\SQL $db,$table,$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();
+	}
+
+}

+ 187 - 0
php-fatfree/lib/db/sql/session.php

@@ -0,0 +1,187 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace 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
+	**/
+	function __construct(\DB\SQL $db,$table='sessions',$force=TRUE) {
+		if ($force)
+			$db->exec(
+				(preg_match('/mssql|sqlsrv|sybase/',$db->driver())?
+					('IF NOT EXISTS (SELECT * FROM sysobjects WHERE '.
+						'name='.$db->quote($table).' AND xtype=\'U\') '.
+						'CREATE TABLE dbo.'):
+					('CREATE TABLE IF NOT EXISTS '.
+						(($name=$db->name())?($name.'.'):''))).
+				$table.' ('.
+					'session_id VARCHAR(40),'.
+					'data TEXT,'.
+					'csrf TEXT,'.
+					'ip VARCHAR(40),'.
+					'agent VARCHAR(255),'.
+					'stamp INTEGER,'.
+					'PRIMARY KEY(session_id)'.
+				');'
+			);
+		parent::__construct($db,$table);
+		session_set_save_handler(
+			array($this,'open'),
+			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'])) {
+			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();
+		}
+	}
+
+}

+ 35 - 0
php-fatfree/lib/f3.php

@@ -0,0 +1,35 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! Legacy mode enabler
+class F3 {
+
+	static
+		//! Framework instance
+		$fw;
+
+	/**
+	*	Forward function calls to framework
+	*	@return mixed
+	*	@param $func callback
+	*	@param $args array
+	**/
+	static function __callstatic($func,array $args) {
+		if (!self::$fw)
+			self::$fw=Base::instance();
+		return call_user_func_array(array(self::$fw,$func),$args);
+	}
+
+}

+ 583 - 0
php-fatfree/lib/image.php

@@ -0,0 +1,583 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! Image manipulation tools
+class Image {
+
+	//@{ Messages
+	const
+		E_Color='Invalid color specified: %s',
+		E_Font='CAPTCHA font not found',
+		E_Length='Invalid CAPTCHA length: %s';
+	//@}
+
+	//@{ Positional cues
+	const
+		POS_Left=1,
+		POS_Center=2,
+		POS_Right=4,
+		POS_Top=8,
+		POS_Middle=16,
+		POS_Bottom=32;
+	//@}
+
+	protected
+		//! Source filename
+		$file,
+		//! Image resource
+		$data,
+		//! Enable/disable history
+		$flag=FALSE,
+		//! Filter count
+		$count=0;
+
+	/**
+	*	Convert RGB hex triad to array
+	*	@return array|FALSE
+	*	@param $color int
+	**/
+	function rgb($color) {
+		$hex=str_pad($hex=dechex($color),$color<4096?3:6,'0',STR_PAD_LEFT);
+		if (($len=strlen($hex))>6)
+			user_error(sprintf(self::E_Color,'0x'.$hex));
+		$color=str_split($hex,$len/3);
+		foreach ($color as &$hue) {
+			$hue=hexdec(str_repeat($hue,6/$len));
+			unset($hue);
+		}
+		return $color;
+	}
+
+	/**
+	*	Invert image
+	*	@return object
+	**/
+	function invert() {
+		imagefilter($this->data,IMG_FILTER_NEGATE);
+		return $this->save();
+	}
+
+	/**
+	*	Adjust brightness (range:-255 to 255)
+	*	@return object
+	*	@param $level int
+	**/
+	function brightness($level) {
+		imagefilter($this->data,IMG_FILTER_BRIGHTNESS,$level);
+		return $this->save();
+	}
+
+	/**
+	*	Adjust contrast (range:-100 to 100)
+	*	@return object
+	*	@param $level int
+	**/
+	function contrast($level) {
+		imagefilter($this->data,IMG_FILTER_CONTRAST,$level);
+		return $this->save();
+	}
+
+	/**
+	*	Convert to grayscale
+	*	@return object
+	**/
+	function grayscale() {
+		imagefilter($this->data,IMG_FILTER_GRAYSCALE);
+		return $this->save();
+	}
+
+	/**
+	*	Adjust smoothness
+	*	@return object
+	*	@param $level int
+	**/
+	function smooth($level) {
+		imagefilter($this->data,IMG_FILTER_SMOOTH,$level);
+		return $this->save();
+	}
+
+	/**
+	*	Emboss the image
+	*	@return object
+	**/
+	function emboss() {
+		imagefilter($this->data,IMG_FILTER_EMBOSS);
+		return $this->save();
+	}
+
+	/**
+	*	Apply sepia effect
+	*	@return object
+	**/
+	function sepia() {
+		imagefilter($this->data,IMG_FILTER_GRAYSCALE);
+		imagefilter($this->data,IMG_FILTER_COLORIZE,90,60,45);
+		return $this->save();
+	}
+
+	/**
+	*	Pixelate the image
+	*	@return object
+	*	@param $size int
+	**/
+	function pixelate($size) {
+		imagefilter($this->data,IMG_FILTER_PIXELATE,$size,TRUE);
+		return $this->save();
+	}
+
+	/**
+	*	Blur the image using Gaussian filter
+	*	@return object
+	*	@param $selective bool
+	**/
+	function blur($selective=FALSE) {
+		imagefilter($this->data,
+			$selective?IMG_FILTER_SELECTIVE_BLUR:IMG_FILTER_GAUSSIAN_BLUR);
+		return $this->save();
+	}
+
+	/**
+	*	Apply sketch effect
+	*	@return object
+	**/
+	function sketch() {
+		imagefilter($this->data,IMG_FILTER_MEAN_REMOVAL);
+		return $this->save();
+	}
+
+	/**
+	*	Flip on horizontal axis
+	*	@return object
+	**/
+	function hflip() {
+		$tmp=imagecreatetruecolor(
+			$width=$this->width(),$height=$this->height());
+		imagesavealpha($tmp,TRUE);
+		imagefill($tmp,0,0,IMG_COLOR_TRANSPARENT);
+		imagecopyresampled($tmp,$this->data,
+			0,0,$width-1,0,$width,$height,-$width,$height);
+		imagedestroy($this->data);
+		$this->data=$tmp;
+		return $this->save();
+	}
+
+	/**
+	*	Flip on vertical axis
+	*	@return object
+	**/
+	function vflip() {
+		$tmp=imagecreatetruecolor(
+			$width=$this->width(),$height=$this->height());
+		imagesavealpha($tmp,TRUE);
+		imagefill($tmp,0,0,IMG_COLOR_TRANSPARENT);
+		imagecopyresampled($tmp,$this->data,
+			0,0,0,$height-1,$width,$height,$width,-$height);
+		imagedestroy($this->data);
+		$this->data=$tmp;
+		return $this->save();
+	}
+
+	/**
+	*	Crop the image
+	*	@return object
+	*	@param $x1 int
+	*	@param $y1 int
+	*	@param $x2 int
+	*	@param $y2 int
+	**/
+	function crop($x1,$y1,$x2,$y2) {
+		$tmp=imagecreatetruecolor($width=$x2-$x1+1,$height=$y2-$y1+1);
+		imagesavealpha($tmp,TRUE);
+		imagefill($tmp,0,0,IMG_COLOR_TRANSPARENT);
+		imagecopyresampled($tmp,$this->data,
+			0,0,$x1,$y1,$width,$height,$width,$height);
+		imagedestroy($this->data);
+		$this->data=$tmp;
+		return $this->save();
+	}
+
+	/**
+	*	Resize image (Maintain aspect ratio); Crop relative to center
+	*	if flag is enabled; Enlargement allowed if flag is enabled
+	*	@return object
+	*	@param $width int
+	*	@param $height int
+	*	@param $crop bool
+	*	@param $enlarge bool
+	**/
+	function resize($width,$height,$crop=TRUE,$enlarge=TRUE) {
+		// Adjust dimensions; retain aspect ratio
+		$ratio=($origw=imagesx($this->data))/($origh=imagesy($this->data));
+		if (!$crop)
+			if ($width/$ratio<=$height)
+				$height=$width/$ratio;
+			else
+				$width=$height*$ratio;
+		if (!$enlarge) {
+			$width=min($origw,$width);
+			$height=min($origh,$height);
+		}
+		// Create blank image
+		$tmp=imagecreatetruecolor($width,$height);
+		imagesavealpha($tmp,TRUE);
+		imagefill($tmp,0,0,IMG_COLOR_TRANSPARENT);
+		// Resize
+		if ($crop) {
+			if ($width/$ratio<=$height) {
+				$cropw=$origh*$width/$height;
+				imagecopyresampled($tmp,$this->data,
+					0,0,($origw-$cropw)/2,0,$width,$height,$cropw,$origh);
+			}
+			else {
+				$croph=$origw*$height/$width;
+				imagecopyresampled($tmp,$this->data,
+					0,0,0,($origh-$croph)/2,$width,$height,$origw,$croph);
+			}
+		}
+		else
+			imagecopyresampled($tmp,$this->data,
+				0,0,0,0,$width,$height,$origw,$origh);
+		imagedestroy($this->data);
+		$this->data=$tmp;
+		return $this->save();
+	}
+
+	/**
+	*	Rotate image
+	*	@return object
+	*	@param $angle int
+	**/
+	function rotate($angle) {
+		$this->data=imagerotate($this->data,$angle,
+			imagecolorallocatealpha($this->data,0,0,0,127));
+		imagesavealpha($this->data,TRUE);
+		return $this->save();
+	}
+
+	/**
+	*	Apply an image overlay
+	*	@return object
+	*	@param $img object
+	*	@param $align int|array
+	*	@param $alpha int
+	**/
+	function overlay(Image $img,$align=NULL,$alpha=100) {
+		if (is_null($align))
+			$align=self::POS_Right|self::POS_Bottom;
+		if (is_array($align)) {
+			list($posx,$posy)=$align;
+			$align = 0;
+		}
+		$ovr=imagecreatefromstring($img->dump());
+		imagesavealpha($ovr,TRUE);
+		$imgw=$this->width();
+		$imgh=$this->height();
+		$ovrw=imagesx($ovr);
+		$ovrh=imagesy($ovr);
+		if ($align & self::POS_Left)
+			$posx=0;
+		if ($align & self::POS_Center)
+			$posx=($imgw-$ovrw)/2;
+		if ($align & self::POS_Right)
+			$posx=$imgw-$ovrw;
+		if ($align & self::POS_Top)
+			$posy=0;
+		if ($align & self::POS_Middle)
+			$posy=($imgh-$ovrh)/2;
+		if ($align & self::POS_Bottom)
+			$posy=$imgh-$ovrh;
+		if (empty($posx))
+			$posx=0;
+		if (empty($posy))
+			$posy=0;
+		if ($alpha==100)
+			imagecopy($this->data,$ovr,$posx,$posy,0,0,$ovrw,$ovrh);
+		else {
+			$cut=imagecreatetruecolor($ovrw,$ovrh);
+			imagecopy($cut,$this->data,0,0,$posx,$posy,$ovrw,$ovrh);
+			imagecopy($cut,$ovr,0,0,0,0,$ovrw,$ovrh);
+			imagecopymerge($this->data,$cut,$posx,$posy,0,0,$ovrw,$ovrh,$alpha);
+		}
+		return $this->save();
+	}
+
+	/**
+	*	Generate identicon
+	*	@return object
+	*	@param $str string
+	*	@param $size int
+	*	@param $blocks int
+	**/
+	function identicon($str,$size=64,$blocks=4) {
+		$sprites=array(
+			array(.5,1,1,0,1,1),
+			array(.5,0,1,0,.5,1,0,1),
+			array(.5,0,1,0,1,1,.5,1,1,.5),
+			array(0,.5,.5,0,1,.5,.5,1,.5,.5),
+			array(0,.5,1,0,1,1,0,1,1,.5),
+			array(1,0,1,1,.5,1,1,.5,.5,.5),
+			array(0,0,1,0,1,.5,0,0,.5,1,0,1),
+			array(0,0,.5,0,1,.5,.5,1,0,1,.5,.5),
+			array(.5,0,.5,.5,1,.5,1,1,.5,1,.5,.5,0,.5),
+			array(0,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1),
+			array(0,.5,.5,1,1,.5,.5,0,1,0,1,1,0,1),
+			array(.5,0,1,0,1,1,.5,1,1,.75,.5,.5,1,.25),
+			array(0,.5,.5,0,.5,.5,1,0,1,.5,.5,1,.5,.5,0,1),
+			array(0,0,1,0,1,1,0,1,1,.5,.5,.25,.5,.75,0,.5,.5,.25),
+			array(0,.5,.5,.5,.5,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1),
+			array(0,0,1,0,.5,.5,.5,0,0,.5,1,.5,.5,1,.5,.5,0,1)
+		);
+		$hash=sha1($str);
+		$this->data=imagecreatetruecolor($size,$size);
+		list($r,$g,$b)=$this->rgb(hexdec(substr($hash,-3)));
+		$fg=imagecolorallocate($this->data,$r,$g,$b);
+		imagefill($this->data,0,0,IMG_COLOR_TRANSPARENT);
+		$ctr=count($sprites);
+		$dim=$blocks*floor($size/$blocks)*2/$blocks;
+		for ($j=0,$y=ceil($blocks/2);$j<$y;$j++)
+			for ($i=$j,$x=$blocks-1-$j;$i<$x;$i++) {
+				$sprite=imagecreatetruecolor($dim,$dim);
+				imagefill($sprite,0,0,IMG_COLOR_TRANSPARENT);
+				if ($block=$sprites[
+					hexdec($hash[($j*$blocks+$i)*2])%$ctr]) {
+					for ($k=0,$pts=count($block);$k<$pts;$k++)
+						$block[$k]*=$dim;
+					imagefilledpolygon($sprite,$block,$pts/2,$fg);
+				}
+				$sprite=imagerotate($sprite,
+					90*(hexdec($hash[($j*$blocks+$i)*2+1])%4),
+					imagecolorallocatealpha($sprite,0,0,0,127));
+				for ($k=0;$k<4;$k++) {
+					imagecopyresampled($this->data,$sprite,
+						$i*$dim/2,$j*$dim/2,0,0,$dim/2,$dim/2,$dim,$dim);
+					$this->data=imagerotate($this->data,90,
+						imagecolorallocatealpha($this->data,0,0,0,127));
+				}
+				imagedestroy($sprite);
+			}
+		imagesavealpha($this->data,TRUE);
+		return $this->save();
+	}
+
+	/**
+	*	Generate CAPTCHA image
+	*	@return object|FALSE
+	*	@param $font string
+	*	@param $size int
+	*	@param $len int
+	*	@param $key string
+	*	@param $path string
+	*	@param $fg int
+	*	@param $bg int
+	**/
+	function captcha($font,$size=24,$len=5,
+		$key=NULL,$path='',$fg=0xFFFFFF,$bg=0x000000) {
+		if ((!$ssl=extension_loaded('openssl')) && ($len<4 || $len>13)) {
+			user_error(sprintf(self::E_Length,$len));
+			return FALSE;
+		}
+		$fw=Base::instance();
+		foreach ($fw->split($path?:$fw->get('UI').';./') as $dir)
+			if (is_file($path=$dir.$font)) {
+				$seed=strtoupper(substr(
+					$ssl?bin2hex(openssl_random_pseudo_bytes($len)):uniqid(),
+					-$len));
+				$block=$size*3;
+				$tmp=array();
+				for ($i=0,$width=0,$height=0;$i<$len;$i++) {
+					// Process at 2x magnification
+					$box=imagettfbbox($size*2,0,$path,$seed[$i]);
+					$w=$box[2]-$box[0];
+					$h=$box[1]-$box[5];
+					$char=imagecreatetruecolor($block,$block);
+					imagefill($char,0,0,$bg);
+					imagettftext($char,$size*2,0,
+						($block-$w)/2,$block-($block-$h)/2,
+						$fg,$path,$seed[$i]);
+					$char=imagerotate($char,mt_rand(-30,30),
+						imagecolorallocatealpha($char,0,0,0,127));
+					// Reduce to normal size
+					$tmp[$i]=imagecreatetruecolor(
+						($w=imagesx($char))/2,($h=imagesy($char))/2);
+					imagefill($tmp[$i],0,0,IMG_COLOR_TRANSPARENT);
+					imagecopyresampled($tmp[$i],$char,0,0,0,0,$w/2,$h/2,$w,$h);
+					imagedestroy($char);
+					$width+=$i+1<$len?$block/2:$w/2;
+					$height=max($height,$h/2);
+				}
+				$this->data=imagecreatetruecolor($width,$height);
+				imagefill($this->data,0,0,IMG_COLOR_TRANSPARENT);
+				for ($i=0;$i<$len;$i++) {
+					imagecopy($this->data,$tmp[$i],
+						$i*$block/2,($height-imagesy($tmp[$i]))/2,0,0,
+						imagesx($tmp[$i]),imagesy($tmp[$i]));
+					imagedestroy($tmp[$i]);
+				}
+				imagesavealpha($this->data,TRUE);
+				if ($key)
+					$fw->set($key,$seed);
+				return $this->save();
+			}
+		user_error(self::E_Font);
+		return FALSE;
+	}
+
+	/**
+	*	Return image width
+	*	@return int
+	**/
+	function width() {
+		return imagesx($this->data);
+	}
+
+	/**
+	*	Return image height
+	*	@return int
+	**/
+	function height() {
+		return imagesy($this->data);
+	}
+
+	/**
+	*	Send image to HTTP client
+	*	@return NULL
+	**/
+	function render() {
+		$args=func_get_args();
+		$format=$args?array_shift($args):'png';
+		if (PHP_SAPI!='cli') {
+			header('Content-Type: image/'.$format);
+			header('X-Powered-By: '.Base::instance()->get('PACKAGE'));
+		}
+		call_user_func_array('image'.$format,
+			array_merge(array($this->data),$args));
+	}
+
+	/**
+	*	Return image as a string
+	*	@return string
+	**/
+	function dump() {
+		$args=func_get_args();
+		$format=$args?array_shift($args):'png';
+		ob_start();
+		call_user_func_array('image'.$format,
+			array_merge(array($this->data),$args));
+		return ob_get_clean();
+	}
+
+	/**
+	*	Save current state
+	*	@return object
+	**/
+	function save() {
+		$fw=Base::instance();
+		if ($this->flag) {
+			if (!is_dir($dir=$fw->get('TEMP')))
+				mkdir($dir,Base::MODE,TRUE);
+			$this->count++;
+			$fw->write($dir.'/'.
+				$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+				$fw->hash($this->file).'-'.$this->count.'.png',
+				$this->dump());
+		}
+		return $this;
+	}
+
+	/**
+	*	Revert to specified state
+	*	@return object
+	*	@param $state int
+	**/
+	function restore($state=1) {
+		$fw=Base::instance();
+		if ($this->flag && is_file($file=($path=$fw->get('TEMP').
+			$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+			$fw->hash($this->file).'-').$state.'.png')) {
+			if (is_resource($this->data))
+				imagedestroy($this->data);
+			$this->data=imagecreatefromstring($fw->read($file));
+			imagesavealpha($this->data,TRUE);
+			foreach (glob($path.'*.png',GLOB_NOSORT) as $match)
+				if (preg_match('/-(\d+)\.png/',$match,$parts) &&
+					$parts[1]>$state)
+					@unlink($match);
+			$this->count=$state;
+		}
+		return $this;
+	}
+
+	/**
+	*	Undo most recently applied filter
+	*	@return object
+	**/
+	function undo() {
+		if ($this->flag) {
+			if ($this->count)
+				$this->count--;
+			return $this->restore($this->count);
+		}
+		return $this;
+	}
+
+	/**
+	*	Load string
+	*	@return object
+	*	@param $str string
+	**/
+	function load($str) {
+		$this->data=imagecreatefromstring($str);
+		imagesavealpha($this->data,TRUE);
+		$this->save();
+		return $this;
+	}
+
+	/**
+	*	Instantiate image
+	*	@param $file string
+	*	@param $flag bool
+	*	@param $path string
+	**/
+	function __construct($file=NULL,$flag=FALSE,$path='') {
+		$this->flag=$flag;
+		if ($file) {
+			$fw=Base::instance();
+			// Create image from file
+			$this->file=$file;
+			foreach ($fw->split($path?:$fw->get('UI').';./') as $dir)
+				if (is_file($dir.$file))
+					return $this->load($fw->read($dir.$file));
+		}
+	}
+
+	/**
+	*	Wrap-up
+	*	@return NULL
+	**/
+	function __destruct() {
+		if (is_resource($this->data)) {
+			imagedestroy($this->data);
+			$fw=Base::instance();
+			$path=$fw->get('TEMP').
+				$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+				$fw->hash($this->file);
+			if ($glob=@glob($path.'*.png',GLOB_NOSORT))
+				foreach ($glob as $match)
+					if (preg_match('/-(\d+)\.png/',$match))
+						@unlink($match);
+		}
+	}
+
+}

+ 621 - 0
php-fatfree/lib/license.txt

@@ -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

+ 60 - 0
php-fatfree/lib/log.php

@@ -0,0 +1,60 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! Custom logger
+class Log {
+
+	protected
+		//! File name
+		$file;
+
+	/**
+	*	Write specified text to log file
+	*	@return string
+	*	@param $text string
+	*	@param $format string
+	**/
+	function write($text,$format='r') {
+		$fw=Base::instance();
+		$fw->write(
+			$this->file,
+			date($format).
+				(isset($_SERVER['REMOTE_ADDR'])?
+					(' ['.$_SERVER['REMOTE_ADDR'].']'):'').' '.
+			trim($text).PHP_EOL,
+			TRUE
+		);
+	}
+
+	/**
+	*	Erase log
+	*	@return NULL
+	**/
+	function erase() {
+		@unlink($this->file);
+	}
+
+	/**
+	*	Instantiate class
+	*	@param $file string
+	**/
+	function __construct($file) {
+		$fw=Base::instance();
+		if (!is_dir($dir=$fw->get('LOGS')))
+			mkdir($dir,Base::MODE,TRUE);
+		$this->file=$dir.$file;
+	}
+
+}

+ 140 - 0
php-fatfree/lib/magic.php

@@ -0,0 +1,140 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! 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);
+
+	/**
+	*	Return TRUE if property has public/protected visibility
+	*	@return bool
+	*	@param $key string
+	**/
+	private function visible($key) {
+		if (property_exists($this,$key)) {
+			$ref=new ReflectionProperty(get_class($this),$key);
+			$out=!$ref->isprivate();
+			unset($ref);
+			return $out;
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Convenience method for checking property value
+	*	@return mixed
+	*	@param $key string
+	**/
+	function offsetexists($key) {
+		return $this->visible($key)?isset($this->$key):$this->exists($key);
+	}
+
+	/**
+	*	Alias for offsetexists()
+	*	@return mixed
+	*	@param $key string
+	**/
+	function __isset($key) {
+		return $this->offsetexists($key);
+	}
+
+	/**
+	*	Convenience method for assigning property value
+	*	@return mixed
+	*	@param $key string
+	*	@param $val scalar
+	**/
+	function offsetset($key,$val) {
+		return $this->visible($key)?($this->key=$val):$this->set($key,$val);
+	}
+
+	/**
+	*	Alias for offsetset()
+	*	@return mixed
+	*	@param $key string
+	*	@param $val scalar
+	**/
+	function __set($key,$val) {
+		return $this->offsetset($key,$val);
+	}
+
+	/**
+	*	Convenience method for retrieving property value
+	*	@return mixed
+	*	@param $key string
+	**/
+	function offsetget($key) {
+		return $this->visible($key)?$this->$key:$this->get($key);
+	}
+
+	/**
+	*	Alias for offsetget()
+	*	@return mixed
+	*	@param $key string
+	**/
+	function __get($key) {
+		return $this->offsetget($key);
+	}
+
+	/**
+	*	Convenience method for checking property value
+	*	@return NULL
+	*	@param $key string
+	**/
+	function offsetunset($key) {
+		if ($this->visible($key))
+			unset($this->$key);
+		else
+			$this->clear($key);
+	}
+
+	/**
+	*	Alias for offsetunset()
+	*	@return NULL
+	*	@param $key string
+	**/
+	function __unset($key) {
+		$this->offsetunset($key);
+	}
+
+}

+ 570 - 0
php-fatfree/lib/markdown.php

@@ -0,0 +1,570 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! Markdown-to-HTML converter
+class Markdown extends Prefab {
+
+	protected
+		//! Parsing rules
+		$blocks,
+		//! Special characters
+		$special;
+
+	/**
+	*	Process blockquote
+	*	@return string
+	*	@param $str string
+	**/
+	protected function _blockquote($str) {
+		$str=preg_replace('/(?<=^|\n)\h?>\h?(.*?(?:\n+|$))/','\1',$str);
+		return strlen($str)?
+			('<blockquote>'.$this->build($str).'</blockquote>'."\n\n"):'';
+	}
+
+	/**
+	*	Process whitespace-prefixed code block
+	*	@return string
+	*	@param $str string
+	**/
+	protected function _pre($str) {
+		$str=preg_replace('/(?<=^|\n)(?: {4}|\t)(.+?(?:\n+|$))/','\1',
+			$this->esc($str));
+		return strlen($str)?
+			('<pre><code>'.
+				$this->esc($this->snip($str)).
+			'</code></pre>'."\n\n"):
+			'';
+	}
+
+	/**
+	*	Process fenced code block
+	*	@return string
+	*	@param $hint string
+	*	@param $str string
+	**/
+	protected function _fence($hint,$str) {
+		$str=$this->snip($str);
+		$fw=Base::instance();
+		if ($fw->get('HIGHLIGHT')) {
+			switch (strtolower($hint)) {
+				case 'php':
+					$str=$fw->highlight($str);
+					break;
+				case 'apache':
+					preg_match_all('/(?<=^|\n)(\h*)'.
+						'(?:(<\/?)(\w+)((?:\h+[^>]+)*)(>)|'.
+						'(?:(\w+)(\h.+?)))(\h*(?:\n+|$))/',
+						$str,$matches,PREG_SET_ORDER);
+					$out='';
+					foreach ($matches as $match)
+						$out.=$match[1].
+							($match[3]?
+								('<span class="section">'.
+									$this->esc($match[2]).$match[3].
+								'</span>'.
+								($match[4]?
+									('<span class="data">'.
+										$this->esc($match[4]).
+									'</span>'):
+									'').
+								'<span class="section">'.
+									$this->esc($match[5]).
+								'</span>'):
+								('<span class="directive">'.
+									$match[6].
+								'</span>'.
+								'<span class="data">'.
+									$this->esc($match[7]).
+								'</span>')).
+							$match[8];
+					$str='<code>'.$out.'</code>';
+					break;
+				case 'html':
+					preg_match_all(
+						'/(?:(?:<(\/?)(\w+)'.
+						'((?:\h+(?:\w+\h*=\h*)?".+?"|[^>]+)*|'.
+						'\h+.+?)(\h*\/?)>)|(.+?))/s',
+						$str,$matches,PREG_SET_ORDER
+					);
+					$out='';
+					foreach ($matches as $match) {
+						if ($match[2]) {
+							$out.='<span class="xml_tag">&lt;'.
+								$match[1].$match[2].'</span>';
+							if ($match[3]) {
+								preg_match_all(
+									'/(?:\h+(?:(?:(\w+)\h*=\h*)?'.
+									'(".+?")|(.+)))/',
+									$match[3],$parts,PREG_SET_ORDER
+								);
+								foreach ($parts as $part)
+									$out.=' '.
+										(empty($part[3])?
+											((empty($part[1])?
+												'':
+												('<span class="xml_attr">'.
+													$part[1].'</span>=')).
+											'<span class="xml_data">'.
+												$part[2].'</span>'):
+											('<span class="xml_tag">'.
+												$part[3].'</span>'));
+							}
+							$out.='<span class="xml_tag">'.
+								$match[4].'&gt;</span>';
+						}
+						else
+							$out.=$this->esc($match[5]);
+					}
+					$str='<code>'.$out.'</code>';
+					break;
+				case 'ini':
+					preg_match_all(
+						'/(?<=^|\n)(?:'.
+						'(;[^\n]*)|(?:<\?php.+?\?>?)|'.
+						'(?:\[(.+?)\])|'.
+						'(.+?)\h*=\h*'.
+						'((?:\\\\\h*\r?\n|.+?)*)'.
+						')((?:\r?\n)+|$)/',
+						$str,$matches,PREG_SET_ORDER
+					);
+					$out='';
+					foreach ($matches as $match) {
+						if ($match[1])
+							$out.='<span class="comment">'.$match[1].
+								'</span>';
+						elseif ($match[2])
+							$out.='<span class="ini_section">['.$match[2].']'.
+								'</span>';
+						elseif ($match[3])
+							$out.='<span class="ini_key">'.$match[3].
+								'</span>='.
+								($match[4]?
+									('<span class="ini_value">'.
+										$match[4].'</span>'):'');
+						else
+							$out.=$match[0];
+						if (isset($match[5]))
+							$out.=$match[5];
+					}
+					$str='<code>'.$out.'</code>';
+					break;
+				default:
+					$str='<code>'.$this->esc($str).'</code>';
+					break;
+			}
+		}
+		else
+			$str='<code>'.$this->esc($str).'</code>';
+		return '<pre>'.$str.'</pre>'."\n\n";
+	}
+
+	/**
+	*	Process horizontal rule
+	*	@return string
+	**/
+	protected function _hr() {
+		return '<hr />'."\n\n";
+	}
+
+	/**
+	*	Process atx-style heading
+	*	@return string
+	*	@param $type string
+	*	@param $str string
+	**/
+	protected function _atx($type,$str) {
+		$level=strlen($type);
+		return '<h'.$level.' id="'.Web::instance()->slug($str).'">'.
+			$this->scan($str).'</h'.$level.'>'."\n\n";
+	}
+
+	/**
+	*	Process setext-style heading
+	*	@return string
+	*	@param $str string
+	*	@param $type string
+	**/
+	protected function _setext($str,$type) {
+		$level=strpos('=-',$type)+1;
+		return '<h'.$level.' id="'.Web::instance()->slug($str).'">'.
+			$this->scan($str).'</h'.$level.'>'."\n\n";
+	}
+
+	/**
+	*	Process ordered/unordered list
+	*	@return string
+	*	@param $str string
+	**/
+	protected function _li($str) {
+		// Initialize list parser
+		$len=strlen($str);
+		$ptr=0;
+		$dst='';
+		$first=TRUE;
+		$tight=TRUE;
+		$type='ul';
+		// Main loop
+		while ($ptr<$len) {
+			if (preg_match('/^\h*[*-](?:\h?[*-]){2,}(?:\n+|$)/',
+				substr($str,$ptr),$match)) {
+				$ptr+=strlen($match[0]);
+				// Embedded horizontal rule
+				return (strlen($dst)?
+					('<'.$type.'>'."\n".$dst.'</'.$type.'>'."\n\n"):'').
+					'<hr />'."\n\n".$this->build(substr($str,$ptr));
+			}
+			elseif (preg_match('/(?<=^|\n)([*+-]|\d+\.)\h'.
+				'(.+?(?:\n+|$))((?:(?: {4}|\t)+.+?(?:\n+|$))*)/s',
+				substr($str,$ptr),$match)) {
+				$match[3]=preg_replace('/(?<=^|\n)(?: {4}|\t)/','',$match[3]);
+				$found=FALSE;
+				foreach (array_slice($this->blocks,0,-1) as $regex)
+					if (preg_match($regex,$match[3])) {
+						$found=TRUE;
+						break;
+					}
+				// List
+				if ($first) {
+					// First pass
+					if (is_numeric($match[1]))
+						$type='ol';
+					if (preg_match('/\n{2,}$/',$match[2].
+						($found?'':$match[3])))
+						// Loose structure; Use paragraphs
+						$tight=FALSE;
+					$first=FALSE;
+				}
+				// Strip leading whitespaces
+				$ptr+=strlen($match[0]);
+				$tmp=$this->snip($match[2].$match[3]);
+				if ($tight) {
+					if ($found)
+						$tmp=$match[2].$this->build($this->snip($match[3]));
+				}
+				else
+					$tmp=$this->build($tmp);
+				$dst.='<li>'.$this->scan(trim($tmp)).'</li>'."\n";
+			}
+		}
+		return strlen($dst)?
+			('<'.$type.'>'."\n".$dst.'</'.$type.'>'."\n\n"):'';
+	}
+
+	/**
+	*	Ignore raw HTML
+	*	@return string
+	*	@param $str string
+	**/
+	protected function _raw($str) {
+		return $str;
+	}
+
+	/**
+	*	Process paragraph
+	*	@return string
+	*	@param $str string
+	**/
+	protected function _p($str) {
+		$str=trim($str);
+		if (strlen($str)) {
+			if (preg_match('/^(.+?\n)([>#].+)$/s',$str,$parts))
+				return $this->_p($parts[1]).$this->build($parts[2]);
+			$self=$this;
+			$str=preg_replace_callback(
+				'/([^<>\[]+)?(<[\?%].+?[\?%]>|<.+?>|\[.+?\]\s*\(.+?\))|'.
+				'(.+)/s',
+				function($expr) use($self) {
+					$tmp='';
+					if (isset($expr[4]))
+						$tmp.=$self->esc($expr[4]);
+					else {
+						if (isset($expr[1]))
+							$tmp.=$self->esc($expr[1]);
+						$tmp.=$expr[2];
+						if (isset($expr[3]))
+							$tmp.=$self->esc($expr[3]);
+					}
+					return $tmp;
+				},
+				$str
+			);
+			return '<p>'.$this->scan($str).'</p>'."\n\n";
+		}
+		return '';
+	}
+
+	/**
+	*	Process strong/em/strikethrough spans
+	*	@return string
+	*	@param $str string
+	**/
+	protected function _text($str) {
+		$tmp='';
+		while ($str!=$tmp)
+			$str=preg_replace_callback(
+				'/(?<!\\\\)([*_]{1,3})(.*?)(?!\\\\)\1(?=[\s[:punct:]]|$)/',
+				function($expr) {
+					switch (strlen($expr[1])) {
+						case 1:
+							return '<em>'.$expr[2].'</em>';
+						case 2:
+							return '<strong>'.$expr[2].'</strong>';
+						case 3:
+							return '<strong><em>'.$expr[2].'</em></strong>';
+					}
+				},
+				preg_replace(
+					'/(?<!\\\\)~~(.*?)(?!\\\\)~~(?=[\s[:punct:]]|$)/',
+					'<del>\1</del>',
+					$tmp=$str
+				)
+			);
+		return $str;
+	}
+
+	/**
+	*	Process image span
+	*	@return string
+	*	@param $str string
+	**/
+	protected function _img($str) {
+		$self=$this;
+		return preg_replace_callback(
+			'/!(?:\[(.+?)\])?\h*\(<?(.*?)>?(?:\h*"(.*?)"\h*)?\)/',
+			function($expr) use($self) {
+				return '<img src="'.$expr[2].'"'.
+					(empty($expr[1])?
+						'':
+						(' alt="'.$self->esc($expr[1]).'"')).
+					(empty($expr[3])?
+						'':
+						(' title="'.$self->esc($expr[3]).'"')).' />';
+			},
+			$str
+		);
+	}
+
+	/**
+	*	Process anchor span
+	*	@return string
+	*	@param $str string
+	**/
+	protected function _a($str) {
+		$self=$this;
+		return preg_replace_callback(
+			'/(?<!\\\\)\[(.+?)(?!\\\\)\]\h*\(<?(.*?)>?(?:\h*"(.*?)"\h*)?\)/',
+			function($expr) use($self) {
+				return '<a href="'.$self->esc($expr[2]).'"'.
+					(empty($expr[3])?
+						'':
+						(' title="'.$self->esc($expr[3]).'"')).
+					'>'.$self->scan($expr[1]).'</a>';
+			},
+			$str
+		);
+	}
+
+	/**
+	*	Auto-convert links
+	*	@return string
+	*	@param $str string
+	**/
+	protected function _auto($str) {
+		$self=$this;
+		return preg_replace_callback(
+			'/`.*?<(.+?)>.*?`|<(.+?)>/',
+			function($expr) use($self) {
+				if (empty($expr[1]) && parse_url($expr[2],PHP_URL_SCHEME)) {
+					$expr[2]=$self->esc($expr[2]);
+					return '<a href="'.$expr[2].'">'.$expr[2].'</a>';
+				}
+				return $expr[0];
+			},
+			$str
+		);
+	}
+
+	/**
+	*	Process code span
+	*	@return string
+	*	@param $str string
+	**/
+	protected function _code($str) {
+		$self=$this;
+		return preg_replace_callback(
+			'/`` (.+?) ``|(?<!\\\\)`(.+?)(?!\\\\)`/',
+			function($expr) use($self) {
+				return '<code>'.
+					$self->esc(empty($expr[1])?$expr[2]:$expr[1]).'</code>';
+			},
+			$str
+		);
+	}
+
+	/**
+	*	Convert characters to HTML entities
+	*	@return string
+	*	@param $str string
+	**/
+	function esc($str) {
+		if (!$this->special)
+			$this->special=array(
+				'...'=>'&hellip;',
+				'(tm)'=>'&trade;',
+				'(r)'=>'&reg;',
+				'(c)'=>'&copy;'
+			);
+		foreach ($this->special as $key=>$val)
+			$str=preg_replace('/'.preg_quote($key,'/').'/i',$val,$str);
+		return htmlspecialchars($str,ENT_COMPAT,
+			Base::instance()->get('ENCODING'),FALSE);
+	}
+
+	/**
+	*	Reduce multiple line feeds
+	*	@return string
+	*	@param $str string
+	**/
+	protected function snip($str) {
+		return preg_replace('/(?:(?<=\n)\n+)|\n+$/',"\n",$str);
+	}
+
+	/**
+	*	Scan line for convertible spans
+	*	@return string
+	*	@param $str string
+	**/
+	function scan($str) {
+		$inline=array('img','a','text','auto','code');
+		foreach ($inline as $func)
+			$str=$this->{'_'.$func}($str);
+		return $str;
+	}
+
+	/**
+	*	Assemble blocks
+	*	@return string
+	*	@param $str string
+	**/
+	protected function build($str) {
+		if (!$this->blocks) {
+			// Regexes for capturing entire blocks
+			$this->blocks=array(
+				'blockquote'=>'/^(?:\h?>\h?.*?(?:\n+|$))+/',
+				'pre'=>'/^(?:(?: {4}|\t).+?(?:\n+|$))+/',
+				'fence'=>'/^`{3}\h*(\w+)?.*?[^\n]*\n+(.+?)`{3}[^\n]*'.
+					'(?:\n+|$)/s',
+				'hr'=>'/^\h*[*_-](?:\h?[\*_-]){2,}\h*(?:\n+|$)/',
+				'atx'=>'/^\h*(#{1,6})\h?(.+?)\h*(?:#.*)?(?:\n+|$)/',
+				'setext'=>'/^\h*(.+?)\h*\n([=-])+\h*(?:\n+|$)/',
+				'li'=>'/^(?:(?:[*+-]|\d+\.)\h.+?(?:\n+|$)'.
+					'(?:(?: {4}|\t)+.+?(?:\n+|$))*)+/s',
+				'raw'=>'/^((?:<!--.+?-->|'.
+					'<(address|article|aside|audio|blockquote|canvas|dd|'.
+					'div|dl|fieldset|figcaption|figure|footer|form|h\d|'.
+					'header|hgroup|hr|noscript|object|ol|output|p|pre|'.
+					'section|table|tfoot|ul|video).*?'.
+					'(?:\/>|>(?:(?>[^><]+)|(?R))*<\/\2>))'.
+					'\h*(?:\n{2,}|\n*$)|<[\?%].+?[\?%]>\h*(?:\n?$|\n*))/s',
+				'p'=>'/^(.+?(?:\n{2,}|\n*$))/s'
+			);
+		}
+		$self=$this;
+		// Treat lines with nothing but whitespaces as empty lines
+		$str=preg_replace('/\n\h+(?=\n)/',"\n",$str);
+		// Initialize block parser
+		$len=strlen($str);
+		$ptr=0;
+		$dst='';
+		// Main loop
+		while ($ptr<$len) {
+			if (preg_match('/^ {0,3}\[([^\[\]]+)\]:\s*<?(.*?)>?\s*'.
+				'(?:"([^\n]*)")?(?:\n+|$)/s',substr($str,$ptr),$match)) {
+				// Reference-style link; Backtrack
+				$ptr+=strlen($match[0]);
+				$tmp='';
+				// Catch line breaks in title attribute
+				$ref=preg_replace('/\h/','\s',preg_quote($match[1],'/'));
+				while ($dst!=$tmp) {
+					$dst=preg_replace_callback(
+						'/(?<!\\\\)\[('.$ref.')(?!\\\\)\]\s*\[\]|'.
+						'(!?)(?:\[([^\[\]]+)\]\s*)?'.
+						'(?<!\\\\)\[('.$ref.')(?!\\\\)\]/',
+						function($expr) use($match,$self) {
+							return (empty($expr[2]))?
+								// Anchor
+								('<a href="'.$self->esc($match[2]).'"'.
+								(empty($match[3])?
+									'':
+									(' title="'.
+										$self->esc($match[3]).'"')).'>'.
+								// Link
+								$self->scan(
+									empty($expr[3])?
+										(empty($expr[1])?
+											$expr[4]:
+											$expr[1]):
+										$expr[3]
+								).'</a>'):
+								// Image
+								('<img src="'.$match[2].'"'.
+								(empty($expr[2])?
+									'':
+									(' alt="'.
+										$self->esc($expr[3]).'"')).
+								(empty($match[3])?
+									'':
+									(' title="'.
+										$self->esc($match[3]).'"')).
+								' />');
+						},
+						$tmp=$dst
+					);
+				}
+			}
+			else
+				foreach ($this->blocks as $func=>$regex)
+					if (preg_match($regex,substr($str,$ptr),$match)) {
+						$ptr+=strlen($match[0]);
+						$dst.=call_user_func_array(
+							array($this,'_'.$func),
+							count($match)>1?array_slice($match,1):$match
+						);
+						break;
+					}
+		}
+		return $dst;
+	}
+
+	/**
+	*	Render HTML equivalent of markdown
+	*	@return string
+	*	@param $txt string
+	**/
+	function convert($txt) {
+		$txt=preg_replace_callback(
+			'/(<code.*?>.+?<\/code>|'.
+			'<[^>\n]+>|\([^\n\)]+\)|"[^"\n]+")|'.
+			'\\\\(.)/s',
+			function($expr) {
+				// Process escaped characters
+				return empty($expr[1])?$expr[2]:$expr[1];
+			},
+			$this->build(preg_replace('/\r\n|\r/',"\n",$txt))
+		);
+		return $this->snip($txt);
+	}
+
+}

+ 101 - 0
php-fatfree/lib/matrix.php

@@ -0,0 +1,101 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! 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;
+	}
+
+}

+ 180 - 0
php-fatfree/lib/session.php

@@ -0,0 +1,180 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! Cache-based session handler
+class Session {
+
+	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->sid=$id;
+		return Cache::instance()->exists($id.'.@',$data)?$data['data']:FALSE;
+	}
+
+	/**
+	*	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');
+		$csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+			$fw->hash(mt_rand());
+		$jar=$fw->get('JAR');
+		if ($id!=$this->sid)
+			$this->sid=$id;
+		Cache::instance()->set($id.'.@',
+			array(
+				'data'=>$data,
+				'csrf'=>$sent?$this->csrf():$csrf,
+				'ip'=>$fw->get('IP'),
+				'agent'=>isset($headers['User-Agent'])?
+					$headers['User-Agent']:'',
+				'stamp'=>time()
+			),
+			$jar['expire']?($jar['expire']-time()):0
+		);
+		return TRUE;
+	}
+
+	/**
+	*	Destroy session
+	*	@return TRUE
+	*	@param $id string
+	**/
+	function destroy($id) {
+		Cache::instance()->clear($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) {
+		Cache::instance()->reset('.@',$max);
+		return TRUE;
+	}
+
+	/**
+	*	Return anti-CSRF token
+	*	@return string|FALSE
+	**/
+	function csrf() {
+		return Cache::instance()->
+			exists(($this->sid?:session_id()).'.@',$data)?
+				$data['csrf']:FALSE;
+	}
+
+	/**
+	*	Return IP address
+	*	@return string|FALSE
+	**/
+	function ip() {
+		return Cache::instance()->
+			exists(($this->sid?:session_id()).'.@',$data)?
+				$data['ip']:FALSE;
+	}
+
+	/**
+	*	Return Unix timestamp
+	*	@return string|FALSE
+	**/
+	function stamp() {
+		return Cache::instance()->
+			exists(($this->sid?:session_id()).'.@',$data)?
+				$data['stamp']:FALSE;
+	}
+
+	/**
+	*	Return HTTP user agent
+	*	@return string|FALSE
+	**/
+	function agent() {
+		return Cache::instance()->
+			exists(($this->sid?:session_id()).'.@',$data)?
+				$data['agent']:FALSE;
+	}
+
+	/**
+	*	Instantiate class
+	*	@return object
+	**/
+	function __construct() {
+		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'])) {
+			session_destroy();
+			\Base::instance()->error(403);
+		}
+		$csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+			$fw->hash(mt_rand());
+		$jar=$fw->get('JAR');
+		if (Cache::instance()->exists(($this->sid=session_id()).'.@',$data)) {
+			$data['csrf']=$csrf;
+			Cache::instance()->set($this->sid.'.@',
+				$data,
+				$jar['expire']?($jar['expire']-time()):0
+			);
+		}
+	}
+
+}

+ 274 - 0
php-fatfree/lib/smtp.php

@@ -0,0 +1,274 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! SMTP plug-in
+class SMTP extends Magic {
+
+	//@{ Locale-specific error/exception messages
+	const
+		E_Header='%s: header is required',
+		E_Blank='Message must not be blank',
+		E_Attach='Attachment %s not found';
+	//@}
+
+	protected
+		//! Message properties
+		$headers,
+		//! E-mail attachments
+		$attachments,
+		//! SMTP host
+		$host,
+		//! SMTP port
+		$port,
+		//! TLS/SSL
+		$scheme,
+		//! User ID
+		$user,
+		//! Password
+		$pw,
+		//! TCP/IP socket
+		$socket,
+		//! Server-client conversation
+		$log;
+
+	/**
+	*	Fix header
+	*	@return string
+	*	@param $key string
+	**/
+	protected function fixheader($key) {
+		return str_replace(' ','-',
+			ucwords(preg_replace('/[_-]/',' ',strtolower($key))));
+	}
+
+	/**
+	*	Return TRUE if header exists
+	*	@return bool
+	*	@param $key
+	**/
+	function exists($key) {
+		$key=$this->fixheader($key);
+		return isset($this->headers[$key]);
+	}
+
+	/**
+	*	Bind value to e-mail header
+	*	@return string
+	*	@param $key string
+	*	@param $val string
+	**/
+	function set($key,$val) {
+		$key=$this->fixheader($key);
+		return $this->headers[$key]=$val;
+	}
+
+	/**
+	*	Return value of e-mail header
+	*	@return string|NULL
+	*	@param $key string
+	**/
+	function get($key) {
+		$key=$this->fixheader($key);
+		return isset($this->headers[$key])?$this->headers[$key]:NULL;
+	}
+
+	/**
+	*	Remove header
+	*	@return NULL
+	*	@param $key string
+	**/
+	function clear($key) {
+		$key=$this->fixheader($key);
+		unset($this->headers[$key]);
+	}
+
+	/**
+	*	Return client-server conversation history
+	*	@return string
+	**/
+	function log() {
+		return str_replace("\n",PHP_EOL,$this->log);
+	}
+
+	/**
+	*	Send SMTP command and record server response
+	*	@return string
+	*	@param $cmd string
+	*	@param $log bool
+	**/
+	protected function dialog($cmd=NULL,$log=TRUE) {
+		$socket=&$this->socket;
+		if (!is_null($cmd))
+			fputs($socket,$cmd."\r\n");
+		$reply='';
+		while (!feof($socket) && ($info=stream_get_meta_data($socket)) &&
+			!$info['timed_out'] && $str=fgets($socket,4096)) {
+			$reply.=$str;
+			if (preg_match('/(?:^|\n)\d{3} .+?\r\n/s',$reply))
+				break;
+		}
+		if ($log) {
+			$this->log.=$cmd."\n";
+			$this->log.=str_replace("\r",'',$reply);
+		}
+		return $reply;
+	}
+
+	/**
+	*	Add e-mail attachment
+	*	@return NULL
+	*	@param $file
+	**/
+	function attach($file) {
+		if (!is_file($file))
+			user_error(sprintf(self::E_Attach,$file));
+		$this->attachments[]=$file;
+	}
+
+	/**
+	*	Transmit message
+	*	@return bool
+	*	@param $message string
+	*	@param $log bool
+	**/
+	function send($message,$log=TRUE) {
+		if ($this->scheme=='ssl' && !extension_loaded('openssl'))
+			return FALSE;
+		// Message should not be blank
+		if (!$message)
+			user_error(self::E_Blank);
+		$fw=Base::instance();
+		// Retrieve headers
+		$headers=$this->headers;
+		// Connect to the server
+		$socket=&$this->socket;
+		$socket=@fsockopen($this->host,$this->port);
+		if (!$socket)
+			return FALSE;
+		stream_set_blocking($socket,TRUE);
+		// Get server's initial response
+		$this->dialog(NULL,FALSE);
+		// Announce presence
+		$reply=$this->dialog('EHLO '.$fw->get('HOST'),$log);
+		if (strtolower($this->scheme)=='tls') {
+			$this->dialog('STARTTLS',$log);
+			stream_socket_enable_crypto(
+				$socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT);
+			$reply=$this->dialog('EHLO '.$fw->get('HOST'),$log);
+			if (preg_match('/8BITMIME/',$reply))
+				$headers['Content-Transfer-Encoding']='8bit';
+			else {
+				$headers['Content-Transfer-Encoding']='quoted-printable';
+				$message=quoted_printable_encode($message);
+			}
+		}
+		if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) {
+			// Authenticate
+			$this->dialog('AUTH LOGIN',$log);
+			$this->dialog(base64_encode($this->user),$log);
+			$this->dialog(base64_encode($this->pw),$log);
+		}
+		// Required headers
+		$reqd=array('From','To','Subject');
+		foreach ($reqd as $id)
+			if (empty($headers[$id]))
+				user_error(sprintf(self::E_Header,$id));
+		$eol="\r\n";
+		$str='';
+		// Stringify headers
+		foreach ($headers as $key=>$val)
+			if (!in_array($key,$reqd))
+				$str.=$key.': '.$val.$eol;
+		// Start message dialog
+		$this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),$log);
+		foreach ($fw->split($headers['To'].
+			(isset($headers['Cc'])?(';'.$headers['Cc']):'').
+			(isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst)
+			$this->dialog('RCPT TO: '.strstr($dst,'<'),$log);
+		$this->dialog('DATA',$log);
+		if ($this->attachments) {
+			// Replace Content-Type
+			$hash=uniqid(NULL,TRUE);
+			$type=$headers['Content-Type'];
+			$headers['Content-Type']='multipart/mixed; '.
+				'boundary="'.$hash.'"';
+			// Send mail headers
+			$out='';
+			foreach ($headers as $key=>$val)
+				if ($key!='Bcc')
+					$out.=$key.': '.$val.$eol;
+			$out.=$eol;
+			$out.='This is a multi-part message in MIME format'.$eol;
+			$out.=$eol;
+			$out.='--'.$hash.$eol;
+			$out.='Content-Type: '.$type.$eol;
+			$out.=$eol;
+			$out.=$message.$eol;
+			foreach ($this->attachments as $attachment) {
+				$out.='--'.$hash.$eol;
+				$out.='Content-Type: application/octet-stream'.$eol;
+				$out.='Content-Transfer-Encoding: base64'.$eol;
+				$out.='Content-Disposition: attachment; '.
+					'filename="'.basename($attachment).'"'.$eol;
+				$out.=$eol;
+				$out.=chunk_split(
+					base64_encode(file_get_contents($attachment))).$eol;
+			}
+			$out.=$eol;
+			$out.='--'.$hash.'--'.$eol;
+			$out.='.';
+			$this->dialog($out,FALSE);
+		}
+		else {
+			// Send mail headers
+			$out='';
+			foreach ($headers as $key=>$val)
+				if ($key!='Bcc')
+					$out.=$key.': '.$val.$eol;
+			$out.=$eol;
+			$out.=$message.$eol;
+			$out.='.';
+			// Send message
+			$this->dialog($out);
+		}
+		$this->dialog('QUIT',$log);
+		if ($socket)
+			fclose($socket);
+		return TRUE;
+	}
+
+	/**
+	*	Instantiate class
+	*	@param $host string
+	*	@param $port int
+	*	@param $scheme string
+	*	@param $user string
+	*	@param $pw string
+	**/
+	function __construct($host,$port,$scheme,$user,$pw) {
+		$this->headers=array(
+			'MIME-Version'=>'1.0',
+			'Content-Type'=>'text/plain; '.
+				'charset='.Base::instance()->get('ENCODING')
+		);
+		$this->host=$host;
+		if (strtolower($this->scheme=strtolower($scheme))=='ssl')
+			$this->host='ssl://'.$host;
+		$this->port=$port;
+		$this->user=$user;
+		$this->pw=$pw;
+	}
+
+}

+ 340 - 0
php-fatfree/lib/template.php

@@ -0,0 +1,340 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! 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']=preg_match('/\{\{(.+?)\}\}/',$attrib['with'])?$this->token($attrib['with']):Base::instance()->stringify($attrib['with'])) &&
+			preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/',$attrib['with'],$pairs,PREG_SET_ORDER)?
+				'array('.implode(',',array_map(function($pair){return "'$pair[1]'=>$pair[2]";},$pairs)).')+get_defined_vars()':
+				'get_defined_vars()';
+		return
+			'<?php '.(isset($attrib['if'])?
+				('if ('.$this->token($attrib['if']).') '):'').
+				('echo $this->render('.
+					(preg_match('/\{\{(.+?)\}\}/',$attrib['href'])?
+						$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));
+	}
+
+	/**
+	*	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]]=
+									(empty($kv[2])?
+										(empty($kv[3])?NULL:$kv[3]):$kv[2]);
+					}
+					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);
+	}
+
+}

+ 77 - 0
php-fatfree/lib/test.php

@@ -0,0 +1,77 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! Unit test kit
+class Test {
+
+	//@{ Reporting level
+	const
+		FLAG_False=0,
+		FLAG_True=1,
+		FLAG_Both=2;
+	//@}
+
+	protected
+		//! Test results
+		$data=array();
+
+	/**
+	*	Return test results
+	*	@return array
+	**/
+	function results() {
+		return $this->data;
+	}
+
+	/**
+	*	Evaluate condition and save test result
+	*	@return object
+	*	@param $cond bool
+	*	@param $text string
+	**/
+	function expect($cond,$text=NULL) {
+		$out=(bool)$cond;
+		if ($this->level==$out || $this->level==self::FLAG_Both) {
+			$data=array('status'=>$out,'text'=>$text,'source'=>NULL);
+			foreach (debug_backtrace() as $frame)
+				if (isset($frame['file'])) {
+					$data['source']=Base::instance()->
+						fixslashes($frame['file']).':'.$frame['line'];
+					break;
+				}
+			$this->data[]=$data;
+		}
+		return $this;
+	}
+
+	/**
+	*	Append message to test results
+	*	@return NULL
+	*	@param $text string
+	**/
+	function message($text) {
+		$this->expect(TRUE,$text);
+	}
+
+	/**
+	*	Class constructor
+	*	@return NULL
+	*	@param $level int
+	**/
+	function __construct($level=self::FLAG_Both) {
+		$this->level=$level;
+	}
+
+}

+ 192 - 0
php-fatfree/lib/utf.php

@@ -0,0 +1,192 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! Unicode string manager
+class UTF extends Prefab {
+
+	/**
+	*	Get string length
+	*	@return int
+	*	@param $str string
+	**/
+	function strlen($str) {
+		preg_match_all('/./us',$str,$parts);
+		return count($parts[0]);
+	}
+
+	/**
+	*	Reverse a string
+	*	@return string
+	*	@param $str string
+	**/
+	function strrev($str) {
+		preg_match_all('/./us',$str,$parts);
+		return implode('',array_reverse($parts[0]));
+	}
+
+	/**
+	*	Find position of first occurrence of a string (case-insensitive)
+	*	@return int|FALSE
+	*	@param $stack string
+	*	@param $needle string
+	*	@param $ofs int
+	**/
+	function stripos($stack,$needle,$ofs=0) {
+		return $this->strpos($stack,$needle,$ofs,TRUE);
+	}
+
+	/**
+	*	Find position of first occurrence of a string
+	*	@return int|FALSE
+	*	@param $stack string
+	*	@param $needle string
+	*	@param $ofs int
+	*	@param $case bool
+	**/
+	function strpos($stack,$needle,$ofs=0,$case=FALSE) {
+		return preg_match('/^(.{'.$ofs.'}.*?)'.
+			preg_quote($needle,'/').'/us'.($case?'i':''),$stack,$match)?
+			$this->strlen($match[1]):FALSE;
+	}
+
+	/**
+	*	Returns part of haystack string from the first occurrence of
+	*	needle to the end of haystack (case-insensitive)
+	*	@return string|FALSE
+	*	@param $stack string
+	*	@param $needle string
+	*	@param $before bool
+	**/
+	function stristr($stack,$needle,$before=FALSE) {
+		return $this->strstr($stack,$needle,$before,TRUE);
+	}
+
+	/**
+	*	Returns part of haystack string from the first occurrence of
+	*	needle to the end of haystack
+	*	@return string|FALSE
+	*	@param $stack string
+	*	@param $needle string
+	*	@param $before bool
+	*	@param $case bool
+	**/
+	function strstr($stack,$needle,$before=FALSE,$case=FALSE) {
+		if (!$needle)
+			return FALSE;
+		preg_match('/^(.*?)'.preg_quote($needle,'/').'/us'.($case?'i':''),
+			$stack,$match);
+		return isset($match[1])?
+			($before?
+				$match[1]:
+				$this->substr($stack,$this->strlen($match[1]))):
+			FALSE;
+	}
+
+	/**
+	*	Return part of a string
+	*	@return string|FALSE
+	*	@param $str string
+	*	@param $start int
+	*	@param $len int
+	**/
+	function substr($str,$start,$len=0) {
+		if ($start<0)
+			$start=$this->strlen($str)+$start;
+		if (!$len)
+			$len=$this->strlen($str)-$start;
+		return preg_match('/^.{'.$start.'}(.{0,'.$len.'})/us',$str,$match)?
+			$match[1]:FALSE;
+	}
+
+	/**
+	*	Count the number of substring occurrences
+	*	@return int
+	*	@param $stack string
+	*	@param $needle string
+	**/
+	function substr_count($stack,$needle) {
+		preg_match_all('/'.preg_quote($needle,'/').'/us',$stack,
+			$matches,PREG_SET_ORDER);
+		return count($matches);
+	}
+
+	/**
+	*	Strip whitespaces from the beginning of a string
+	*	@return string
+	*	@param $str string
+	**/
+	function ltrim($str) {
+		return preg_replace('/^[\pZ\pC]+/u','',$str);
+	}
+
+	/**
+	*	Strip whitespaces from the end of a string
+	*	@return string
+	*	@param $str string
+	**/
+	function rtrim($str) {
+		return preg_replace('/[\pZ\pC]+$/u','',$str);
+	}
+
+	/**
+	*	Strip whitespaces from the beginning and end of a string
+	*	@return string
+	*	@param $str string
+	**/
+	function trim($str) {
+		return preg_replace('/^[\pZ\pC]+|[\pZ\pC]+$/u','',$str);
+	}
+
+	/**
+	*	Return UTF-8 byte order mark
+	*	@return string
+	**/
+	function bom() {
+		return chr(0xef).chr(0xbb).chr(0xbf);
+	}
+
+	/**
+	*	Convert code points to Unicode symbols
+	*	@return string
+	*	@param $str string
+	**/
+	function translate($str) {
+		return html_entity_decode(
+			preg_replace('/\\\\u([[:xdigit:]]+)/i','&#x\1;',$str));
+	}
+
+	/**
+	*	Translate emoji tokens to Unicode font-supported symbols
+	*	@return string
+	*	@param $str string
+	**/
+	function emojify($str) {
+		$map=array(
+			':('=>'\u2639', // frown
+			':)'=>'\u263a', // smile
+			'<3'=>'\u2665', // heart
+			':D'=>'\u1f603', // grin
+			'XD'=>'\u1f606', // laugh
+			';)'=>'\u1f609', // wink
+			':P'=>'\u1f60b', // tongue
+			':,'=>'\u1f60f', // think
+			':/'=>'\u1f623', // skeptic
+			'8O'=>'\u1f632', // oops
+		)+Base::instance()->get('EMOJI');
+		return $this->translate(str_replace(array_keys($map),
+			array_values($map),$str));
+	}
+
+}

+ 838 - 0
php-fatfree/lib/web.php

@@ -0,0 +1,838 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+//! Wrapper for various HTTP utilities
+class Web extends Prefab {
+
+	//@{ Error messages
+	const
+		E_Request='No suitable HTTP request engine found';
+	//@}
+
+	protected
+		//! HTTP request engine
+		$wrapper;
+
+	/**
+	*	Detect MIME type using file extension
+	*	@return string
+	*	@param $file string
+	**/
+	function mime($file) {
+		if (preg_match('/\w+$/',$file,$ext)) {
+			$map=array(
+				'au'=>'audio/basic',
+				'avi'=>'video/avi',
+				'bmp'=>'image/bmp',
+				'bz2'=>'application/x-bzip2',
+				'css'=>'text/css',
+				'dtd'=>'application/xml-dtd',
+				'doc'=>'application/msword',
+				'gif'=>'image/gif',
+				'gz'=>'application/x-gzip',
+				'hqx'=>'application/mac-binhex40',
+				'html?'=>'text/html',
+				'jar'=>'application/java-archive',
+				'jpe?g'=>'image/jpeg',
+				'js'=>'application/x-javascript',
+				'midi'=>'audio/x-midi',
+				'mp3'=>'audio/mpeg',
+				'mpe?g'=>'video/mpeg',
+				'ogg'=>'audio/vorbis',
+				'pdf'=>'application/pdf',
+				'png'=>'image/png',
+				'ppt'=>'application/vnd.ms-powerpoint',
+				'ps'=>'application/postscript',
+				'qt'=>'video/quicktime',
+				'ram?'=>'audio/x-pn-realaudio',
+				'rdf'=>'application/rdf',
+				'rtf'=>'application/rtf',
+				'sgml?'=>'text/sgml',
+				'sit'=>'application/x-stuffit',
+				'svg'=>'image/svg+xml',
+				'swf'=>'application/x-shockwave-flash',
+				'tgz'=>'application/x-tar',
+				'tiff'=>'image/tiff',
+				'txt'=>'text/plain',
+				'wav'=>'audio/wav',
+				'xls'=>'application/vnd.ms-excel',
+				'xml'=>'application/xml',
+				'zip'=>'application/x-zip-compressed'
+			);
+			foreach ($map as $key=>$val)
+				if (preg_match('/'.$key.'/',strtolower($ext[0])))
+					return $val;
+		}
+		return 'application/octet-stream';
+	}
+
+	/**
+	*	Return the MIME types stated in the HTTP Accept header as an array;
+	*	If a list of MIME types is specified, return the best match; or
+	*	FALSE if none found
+	*	@return array|string|FALSE
+	*	@param $list string|array
+	**/
+	function acceptable($list=NULL) {
+		$accept=array();
+		foreach (explode(',',str_replace(' ','',$_SERVER['HTTP_ACCEPT']))
+			as $mime)
+			if (preg_match('/(.+?)(?:;q=([\d\.]+)|$)/',$mime,$parts))
+				$accept[$parts[1]]=isset($parts[2])?$parts[2]:1;
+		if (!$accept)
+			$accept['*/*']=1;
+		else {
+			krsort($accept);
+			arsort($accept);
+		}
+		if ($list) {
+			if (is_string($list))
+				$list=explode(',',$list);
+			foreach ($accept as $mime=>$q)
+				if ($q && $out=preg_grep('/'.
+					str_replace('\*','.*',preg_quote($mime,'/')).'/',$list))
+					return current($out);
+			return FALSE;
+		}
+		return $accept;
+	}
+
+	/**
+	*	Transmit file to HTTP client; Return file size if successful,
+	*	FALSE otherwise
+	*	@return int|FALSE
+	*	@param $file string
+	*	@param $mime string
+	*	@param $kbps int
+	*	@param $force bool
+	**/
+	function send($file,$mime=NULL,$kbps=0,$force=TRUE) {
+		if (!is_file($file))
+			return FALSE;
+		if (PHP_SAPI!='cli') {
+			header('Content-Type: '.($mime?:$this->mime($file)));
+			if ($force)
+				header('Content-Disposition: attachment; '.
+					'filename='.basename($file));
+			header('Accept-Ranges: bytes');
+			header('Content-Length: '.$size=filesize($file));
+			header('X-Powered-By: '.Base::instance()->get('PACKAGE'));
+		}
+		$ctr=0;
+		$handle=fopen($file,'rb');
+		$start=microtime(TRUE);
+		while (!feof($handle) &&
+			($info=stream_get_meta_data($handle)) &&
+			!$info['timed_out'] && !connection_aborted()) {
+			if ($kbps) {
+				// Throttle output
+				$ctr++;
+				if ($ctr/$kbps>$elapsed=microtime(TRUE)-$start)
+					usleep(1e6*($ctr/$kbps-$elapsed));
+			}
+			// Send 1KiB and reset timer
+			echo fread($handle,1024);
+		}
+		fclose($handle);
+		return $size;
+	}
+
+	/**
+	*	Receive file(s) from HTTP client
+	*	@return array|bool
+	*	@param $func callback
+	*	@param $overwrite bool
+	*	@param $slug callback|bool
+	**/
+	function receive($func=NULL,$overwrite=FALSE,$slug=TRUE) {
+		$fw=Base::instance();
+		$dir=$fw->get('UPLOADS');
+		if (!is_dir($dir))
+			mkdir($dir,Base::MODE,TRUE);
+		if ($fw->get('VERB')=='PUT') {
+			$tmp=$fw->get('TEMP').
+				$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+				$fw->hash(uniqid());
+			if (!$fw->get('RAW'))
+				$fw->write($tmp,$fw->get('BODY'));
+			else {
+				$src=@fopen('php://input','r');
+				$dst=@fopen($tmp,'w');
+				if (!$src || !$dst)
+					return FALSE;
+				while (!feof($src) &&
+					($info=stream_get_meta_data($src)) &&
+					!$info['timed_out'] && $str=fgets($src,4096))
+					fputs($dst,$str,strlen($str));
+				fclose($dst);
+				fclose($src);
+			}
+			$base=basename($fw->get('URI'));
+			$file=array(
+				'name'=>$dir.
+					($slug && preg_match('/(.+?)(\.\w+)?$/',$base,$parts)?
+						(is_callable($slug)?
+							$slug($base):
+							($this->slug($parts[1]).
+								(isset($parts[2])?$parts[2]:''))):
+						$base),
+				'tmp_name'=>$tmp,
+				'type'=>$this->mime($base),
+				'size'=>filesize($tmp)
+			);
+			return (!file_exists($file['name']) || $overwrite) &&
+				(!$func || $fw->call($func,array($file))!==FALSE) &&
+				rename($tmp,$file['name']);
+		}
+		$out=array();
+		foreach ($_FILES as $name=>$item) {
+			if (is_array($item['name'])) {
+				// Transpose array
+				$tmp=array();
+				foreach ($item as $keyx=>$cols)
+					foreach ($cols as $keyy=>$valy)
+						$tmp[$keyy][$keyx]=$valy;
+				$item=$tmp;
+			}
+			else
+				$item=array($item);
+			foreach ($item as $file) {
+				if (empty($file['name']))
+					continue;
+				$base=basename($file['name']);
+				$file['name']=$dir.
+					($slug && preg_match('/(.+?)(\.\w+)?$/',$base,$parts)?
+						(is_callable($slug)?
+							$slug($base):
+							($this->slug($parts[1]).
+								(isset($parts[2])?$parts[2]:''))):
+						$base);
+				$out[$file['name']]=!$file['error'] &&
+					is_uploaded_file($file['tmp_name']) &&
+					(!file_exists($file['name']) || $overwrite) &&
+					(!$func || $fw->call($func,array($file,$name))!==FALSE) &&
+					move_uploaded_file($file['tmp_name'],$file['name']);
+			}
+		}
+		return $out;
+	}
+
+	/**
+	*	Return upload progress in bytes, FALSE on failure
+	*	@return int|FALSE
+	*	@param $id string
+	**/
+	function progress($id) {
+		// ID returned by session.upload_progress.name
+		return ini_get('session.upload_progress.enabled') &&
+			isset($_SESSION[$id]['bytes_processed'])?
+				$_SESSION[$id]['bytes_processed']:FALSE;
+	}
+
+	/**
+	*	HTTP request via cURL
+	*	@return array
+	*	@param $url string
+	*	@param $options array
+	**/
+	protected function _curl($url,$options) {
+		$curl=curl_init($url);
+		curl_setopt($curl,CURLOPT_FOLLOWLOCATION,
+			$options['follow_location']);
+		curl_setopt($curl,CURLOPT_MAXREDIRS,
+			$options['max_redirects']);
+		curl_setopt($curl,CURLOPT_CUSTOMREQUEST,$options['method']);
+		if (isset($options['header']))
+			curl_setopt($curl,CURLOPT_HTTPHEADER,$options['header']);
+		if (isset($options['user_agent']))
+			curl_setopt($curl,CURLOPT_USERAGENT,$options['user_agent']);
+		if (isset($options['content']))
+			curl_setopt($curl,CURLOPT_POSTFIELDS,$options['content']);
+		curl_setopt($curl,CURLOPT_ENCODING,'gzip,deflate');
+		$timeout=isset($options['timeout'])?
+			$options['timeout']:
+			ini_get('default_socket_timeout');
+		curl_setopt($curl,CURLOPT_CONNECTTIMEOUT,$timeout);
+		curl_setopt($curl,CURLOPT_TIMEOUT,$timeout);
+		$headers=array();
+		curl_setopt($curl,CURLOPT_HEADERFUNCTION,
+			// Callback for response headers
+			function($curl,$line) use(&$headers) {
+				if ($trim=trim($line))
+					$headers[]=$trim;
+				return strlen($line);
+			}
+		);
+		curl_setopt($curl,CURLOPT_SSL_VERIFYPEER,FALSE);
+		ob_start();
+		curl_exec($curl);
+		curl_close($curl);
+		$body=ob_get_clean();
+		return array(
+			'body'=>$body,
+			'headers'=>$headers,
+			'engine'=>'cURL',
+			'cached'=>FALSE
+		);
+	}
+
+	/**
+	*	HTTP request via PHP stream wrapper
+	*	@return array
+	*	@param $url string
+	*	@param $options array
+	**/
+	protected function _stream($url,$options) {
+		$eol="\r\n";
+		$options['header']=implode($eol,$options['header']);
+		$body=@file_get_contents($url,FALSE,
+			stream_context_create(array('http'=>$options)));
+		$headers=isset($http_response_header)?
+			$http_response_header:array();
+		$match=NULL;
+		foreach ($headers as $header)
+			if (preg_match('/Content-Encoding: (.+)/',$header,$match))
+				break;
+		if ($match)
+			switch ($match[1]) {
+				case 'gzip':
+					$body=gzdecode($body);
+					break;
+				case 'deflate':
+					$body=gzuncompress($body);
+					break;
+			}
+		return array(
+			'body'=>$body,
+			'headers'=>$headers,
+			'engine'=>'stream',
+			'cached'=>FALSE
+		);
+	}
+
+	/**
+	*	HTTP request via low-level TCP/IP socket
+	*	@return array
+	*	@param $url string
+	*	@param $options array
+	**/
+	protected function _socket($url,$options) {
+		$eol="\r\n";
+		$headers=array();
+		$body='';
+		$parts=parse_url($url);
+		$empty=empty($parts['port']);
+		if ($parts['scheme']=='https') {
+			$parts['host']='ssl://'.$parts['host'];
+			if ($empty)
+				$parts['port']=443;
+		}
+		elseif ($empty)
+			$parts['port']=80;
+		if (empty($parts['path']))
+			$parts['path']='/';
+		if (empty($parts['query']))
+			$parts['query']='';
+		$socket=@fsockopen($parts['host'],$parts['port']);
+		if (!$socket)
+			return FALSE;
+		stream_set_blocking($socket,TRUE);
+		fputs($socket,$options['method'].' '.$parts['path'].
+			($parts['query']?('?'.$parts['query']):'').' HTTP/1.0'.$eol
+		);
+		fputs($socket,implode($eol,$options['header']).$eol.$eol);
+		if (isset($options['content']))
+			fputs($socket,$options['content'].$eol);
+		// Get response
+		$content='';
+		while (!feof($socket) &&
+			($info=stream_get_meta_data($socket)) &&
+			!$info['timed_out'] && $str=fgets($socket,4096))
+			$content.=$str;
+		fclose($socket);
+		$html=explode($eol.$eol,$content,2);
+		$body=isset($html[1])?$html[1]:'';
+		$headers=array_merge($headers,$current=explode($eol,$html[0]));
+		$match=NULL;
+		foreach ($current as $header)
+			if (preg_match('/Content-Encoding: (.+)/',$header,$match))
+				break;
+		if ($match)
+			switch ($match[1]) {
+				case 'gzip':
+					$body=gzdecode($body);
+					break;
+				case 'deflate':
+					$body=gzuncompress($body);
+					break;
+			}
+		if ($options['follow_location'] &&
+			preg_match('/Location: (.+?)'.preg_quote($eol).'/',
+			$html[0],$loc)) {
+			$options['max_redirects']--;
+			return $this->request($loc[1],$options);
+		}
+		return array(
+			'body'=>$body,
+			'headers'=>$headers,
+			'engine'=>'socket',
+			'cached'=>FALSE
+		);
+	}
+
+	/**
+	*	Specify the HTTP request engine to use; If not available,
+	*	fall back to an applicable substitute
+	*	@return string
+	*	@param $arg string
+	**/
+	function engine($arg='curl') {
+		$arg=strtolower($arg);
+		$flags=array(
+			'curl'=>extension_loaded('curl'),
+			'stream'=>ini_get('allow_url_fopen'),
+			'socket'=>function_exists('fsockopen')
+		);
+		if ($flags[$arg])
+			return $this->wrapper=$arg;
+		foreach ($flags as $key=>$val)
+			if ($val)
+				return $this->wrapper=$key;
+		user_error(E_Request);
+	}
+
+	/**
+	*	Replace old headers with new elements
+	*	@return NULL
+	*	@param $old array
+	*	@param $new string|array
+	**/
+	function subst(array &$old,$new) {
+		if (is_string($new))
+			$new=array($new);
+		foreach ($new as $hdr) {
+			$old=preg_grep('/'.preg_quote(strstr($hdr,':',TRUE),'/').':.+/',
+					$old,PREG_GREP_INVERT);
+			array_push($old,$hdr);
+		}
+	}
+
+	/**
+	*	Submit HTTP request; Use HTTP context options (described in
+	*	http://www.php.net/manual/en/context.http.php) if specified;
+	*	Cache the page as instructed by remote server
+	*	@return array|FALSE
+	*	@param $url string
+	*	@param $options array
+	**/
+	function request($url,array $options=NULL) {
+		$fw=Base::instance();
+		$parts=parse_url($url);
+		if (empty($parts['scheme'])) {
+			// Local URL
+			$url=$fw->get('SCHEME').'://'.
+				$fw->get('HOST').
+				($url[0]!='/'?($fw->get('BASE').'/'):'').$url;
+			$parts=parse_url($url);
+		}
+		elseif (!preg_match('/https?/',$parts['scheme']))
+			return FALSE;
+		if (!is_array($options))
+			$options=array();
+		if (empty($options['header']))
+			$options['header']=array();
+		elseif (is_string($options['header']))
+			$options['header']=array($options['header']);
+		if (!$this->wrapper)
+			$this->engine();
+		if ($this->wrapper!='stream') {
+			// PHP streams can't cope with redirects when Host header is set
+			foreach ($options['header'] as &$header)
+				if (preg_match('/^Host:/',$header)) {
+					$header='Host: '.$parts['host'];
+					unset($header);
+					break;
+				}
+			$this->subst($options['header'],'Host: '.$parts['host']);
+		}
+		$this->subst($options['header'],
+			array(
+				'Accept-Encoding: gzip,deflate',
+				'User-Agent: Mozilla/5.0 (compatible; '.php_uname('s').')',
+				'Connection: close'
+			)
+		);
+		if (isset($options['content'])) {
+			if ($options['method']=='POST')
+				$this->subst($options['header'],
+					'Content-Type: application/x-www-form-urlencoded');
+			$this->subst($options['header'],
+				'Content-Length: '.strlen($options['content']));
+		}
+		if (isset($parts['user'],$parts['pass']))
+			$this->subst($options['header'],
+				'Authorization: Basic '.
+					base64_encode($parts['user'].':'.$parts['pass'])
+			);
+		$options+=array(
+			'method'=>'GET',
+			'header'=>$options['header'],
+			'follow_location'=>TRUE,
+			'max_redirects'=>20,
+			'ignore_errors'=>FALSE
+		);
+		$eol="\r\n";
+		if ($fw->get('CACHE') &&
+			preg_match('/GET|HEAD/',$options['method'])) {
+			$cache=Cache::instance();
+			if ($cache->exists(
+				$hash=$fw->hash($options['method'].' '.$url).'.url',$data)) {
+				if (preg_match('/Last-Modified: (.+?)'.preg_quote($eol).'/',
+					implode($eol,$data['headers']),$mod))
+					$this->subst($options['header'],
+						'If-Modified-Since: '.$mod[1]);
+			}
+		}
+		$result=$this->{'_'.$this->wrapper}($url,$options);
+		if ($result && isset($cache)) {
+			if (preg_match('/HTTP\/1\.\d 304/',
+				implode($eol,$result['headers']))) {
+				$result=$cache->get($hash);
+				$result['cached']=TRUE;
+			}
+			elseif (preg_match('/Cache-Control: max-age=(.+?)'.
+				preg_quote($eol).'/',implode($eol,$result['headers']),$exp))
+				$cache->set($hash,$result,$exp[1]);
+		}
+		return $result;
+	}
+
+	/**
+	*	Strip Javascript/CSS files of extraneous whitespaces and comments;
+	*	Return combined output as a minified string
+	*	@return string
+	*	@param $files string|array
+	*	@param $mime string
+	*	@param $header bool
+	*	@param $path string
+	**/
+	function minify($files,$mime=NULL,$header=TRUE,$path='') {
+		$fw=Base::instance();
+		if (is_string($files))
+			$files=$fw->split($files);
+		if (!$mime)
+			$mime=$this->mime($files[0]);
+		preg_match('/\w+$/',$files[0],$ext);
+		$cache=Cache::instance();
+		$dst='';
+		foreach ($fw->split($path?:$fw->get('UI').';./') as $dir)
+			foreach ($files as $file)
+				if (is_file($save=$fw->fixslashes($dir.$file))) {
+					if ($fw->get('CACHE') &&
+						($cached=$cache->exists(
+							$hash=$fw->hash($save).'.'.$ext[0],$data)) &&
+						$cached[0]>filemtime($save))
+						$dst.=$data;
+					else {
+						$data='';
+						$src=$fw->read($save);
+						for ($ptr=0,$len=strlen($src);$ptr<$len;) {
+							if (preg_match('/^@import\h+url'.
+								'\(\h*([\'"])(.+?)\1\h*\)[^;]*;/',
+								substr($src,$ptr),$parts)) {
+								$path=dirname($file);
+								$data.=$this->minify(
+									($path?($path.'/'):'').$parts[2],
+									$mime,$header
+								);
+								$ptr+=strlen($parts[0]);
+								continue;
+							}
+							if ($src[$ptr]=='/') {
+								if ($src[$ptr+1]=='*') {
+									// Multiline comment
+									$str=strstr(
+										substr($src,$ptr+2),'*/',TRUE);
+									$ptr+=strlen($str)+4;
+								}
+								elseif ($src[$ptr+1]=='/') {
+									// Single-line comment
+									$str=strstr(
+										substr($src,$ptr+2),"\n",TRUE);
+									$ptr+=strlen($str)+2;
+								}
+								else {
+									// Presume it's a regex pattern
+									$regex=TRUE;
+									// Backtrack and validate
+									for ($ofs=$ptr;$ofs;$ofs--) {
+										// Pattern should be preceded by
+										// open parenthesis, colon,
+										// object property or operator
+										if (preg_match(
+											'/(return|[(:=!+\-*&|])$/',
+											substr($src,0,$ofs))) {
+											$data.='/';
+											$ptr++;
+											while ($ptr<$len) {
+												$data.=$src[$ptr];
+												$ptr++;
+												if ($src[$ptr-1]=='\\') {
+													$data.=$src[$ptr];
+													$ptr++;
+												}
+												elseif ($src[$ptr-1]=='/')
+													break;
+											}
+											break;
+										}
+										elseif (!ctype_space($src[$ofs-1])) {
+											// Not a regex pattern
+											$regex=FALSE;
+											break;
+										}
+									}
+									if (!$regex) {
+										// Division operator
+										$data.=$src[$ptr];
+										$ptr++;
+									}
+								}
+								continue;
+							}
+							if (in_array($src[$ptr],array('\'','"'))) {
+								$match=$src[$ptr];
+								$data.=$match;
+								$ptr++;
+								// String literal
+								while ($ptr<$len) {
+									$data.=$src[$ptr];
+									$ptr++;
+									if ($src[$ptr-1]=='\\') {
+										$data.=$src[$ptr];
+										$ptr++;
+									}
+									elseif ($src[$ptr-1]==$match)
+										break;
+								}
+								continue;
+							}
+							if (ctype_space($src[$ptr])) {
+								if ($ptr+1<strlen($src) &&
+									preg_match('/[\w'.($ext[0]=='css'?
+										'#\.+\-*()\[\]':'\$').']{2}|'.
+										'[+\-]{2}/',
+										substr($data,-1).$src[$ptr+1]))
+									$data.=' ';
+								$ptr++;
+								continue;
+							}
+							$data.=$src[$ptr];
+							$ptr++;
+						}
+						if ($fw->get('CACHE'))
+							$cache->set($hash,$data);
+						$dst.=$data;
+					}
+				}
+		if (PHP_SAPI!='cli' && $header)
+			header('Content-Type: '.$mime.'; charset='.$fw->get('ENCODING'));
+		return $dst;
+	}
+
+	/**
+	*	Retrieve RSS feed and return as an array
+	*	@return array|FALSE
+	*	@param $url string
+	*	@param $max int
+	*	@param $tags string
+	**/
+	function rss($url,$max=10,$tags=NULL) {
+		if (!$data=$this->request($url))
+			return FALSE;
+		// Suppress errors caused by invalid XML structures
+		libxml_use_internal_errors(TRUE);
+		$xml=simplexml_load_string($data['body'],
+			NULL,LIBXML_NOBLANKS|LIBXML_NOERROR);
+		if (!is_object($xml))
+			return FALSE;
+		$out=array();
+		if (isset($xml->channel)) {
+			$out['source']=(string)$xml->channel->title;
+			$max=min($max,count($xml->channel->item));
+			for ($i=0;$i<$max;$i++) {
+				$item=$xml->channel->item[$i];
+				$list=array(''=>NULL)+$item->getnamespaces(TRUE);
+				$fields=array();
+				foreach ($list as $ns=>$uri)
+					foreach ($item->children($uri) as $key=>$val)
+						$fields[$ns.($ns?':':'').$key]=(string)$val;
+				$out['feed'][]=$fields;
+			}
+		}
+		else
+			return FALSE;
+		Base::instance()->scrub($out,$tags);
+		return $out;
+	}
+
+	/**
+	*	Retrieve information from whois server
+	*	@return string|FALSE
+	*	@param $addr string
+	*	@param $server string
+	**/
+	function whois($addr,$server='whois.internic.net') {
+		$socket=@fsockopen($server,43,$errno,$errstr);
+		if (!$socket)
+			// Can't establish connection
+			return FALSE;
+		// Set connection timeout parameters
+		stream_set_blocking($socket,TRUE);
+		stream_set_timeout($socket,ini_get('default_socket_timeout'));
+		// Send request
+		fputs($socket,$addr."\r\n");
+		$info=stream_get_meta_data($socket);
+		// Get response
+		$response='';
+		while (!feof($socket) && !$info['timed_out']) {
+			$response.=fgets($socket,4096); // MDFK97
+			$info=stream_get_meta_data($socket);
+		}
+		fclose($socket);
+		return $info['timed_out']?FALSE:trim($response);
+	}
+
+	/**
+	*	Return a URL/filesystem-friendly version of string
+	*	@return string
+	*	@param $text string
+	**/
+	function slug($text) {
+		return trim(strtolower(preg_replace('/([^\pL\pN])+/u','-',
+			trim(strtr(str_replace('\'','',$text),
+			array(
+				'Ǎ'=>'A','А'=>'A','Ā'=>'A','Ă'=>'A','Ą'=>'A','Å'=>'A',
+				'Ǻ'=>'A','Ä'=>'Ae','Á'=>'A','À'=>'A','Ã'=>'A','Â'=>'A',
+				'Æ'=>'AE','Ǽ'=>'AE','Б'=>'B','Ç'=>'C','Ć'=>'C','Ĉ'=>'C',
+				'Č'=>'C','Ċ'=>'C','Ц'=>'C','Ч'=>'Ch','Ð'=>'Dj','Đ'=>'Dj',
+				'Ď'=>'Dj','Д'=>'Dj','É'=>'E','Ę'=>'E','Ё'=>'E','Ė'=>'E',
+				'Ê'=>'E','Ě'=>'E','Ē'=>'E','È'=>'E','Е'=>'E','Э'=>'E',
+				'Ë'=>'E','Ĕ'=>'E','Ф'=>'F','Г'=>'G','Ģ'=>'G','Ġ'=>'G',
+				'Ĝ'=>'G','Ğ'=>'G','Х'=>'H','Ĥ'=>'H','Ħ'=>'H','Ï'=>'I',
+				'Ĭ'=>'I','İ'=>'I','Į'=>'I','Ī'=>'I','Í'=>'I','Ì'=>'I',
+				'И'=>'I','Ǐ'=>'I','Ĩ'=>'I','Î'=>'I','IJ'=>'IJ','Ĵ'=>'J',
+				'Й'=>'J','Я'=>'Ja','Ю'=>'Ju','К'=>'K','Ķ'=>'K','Ĺ'=>'L',
+				'Л'=>'L','Ł'=>'L','Ŀ'=>'L','Ļ'=>'L','Ľ'=>'L','М'=>'M',
+				'Н'=>'N','Ń'=>'N','Ñ'=>'N','Ņ'=>'N','Ň'=>'N','Ō'=>'O',
+				'О'=>'O','Ǿ'=>'O','Ǒ'=>'O','Ơ'=>'O','Ŏ'=>'O','Ő'=>'O',
+				'Ø'=>'O','Ö'=>'Oe','Õ'=>'O','Ó'=>'O','Ò'=>'O','Ô'=>'O',
+				'Œ'=>'OE','П'=>'P','Ŗ'=>'R','Р'=>'R','Ř'=>'R','Ŕ'=>'R',
+				'Ŝ'=>'S','Ş'=>'S','Š'=>'S','Ș'=>'S','Ś'=>'S','С'=>'S',
+				'Ш'=>'Sh','Щ'=>'Shch','Ť'=>'T','Ŧ'=>'T','Ţ'=>'T','Ț'=>'T',
+				'Т'=>'T','Ů'=>'U','Ű'=>'U','Ŭ'=>'U','Ũ'=>'U','Ų'=>'U',
+				'Ū'=>'U','Ǜ'=>'U','Ǚ'=>'U','Ù'=>'U','Ú'=>'U','Ü'=>'Ue',
+				'Ǘ'=>'U','Ǖ'=>'U','У'=>'U','Ư'=>'U','Ǔ'=>'U','Û'=>'U',
+				'В'=>'V','Ŵ'=>'W','Ы'=>'Y','Ŷ'=>'Y','Ý'=>'Y','Ÿ'=>'Y',
+				'Ź'=>'Z','З'=>'Z','Ż'=>'Z','Ž'=>'Z','Ж'=>'Zh','á'=>'a',
+				'ă'=>'a','â'=>'a','à'=>'a','ā'=>'a','ǻ'=>'a','å'=>'a',
+				'ä'=>'ae','ą'=>'a','ǎ'=>'a','ã'=>'a','а'=>'a','ª'=>'a',
+				'æ'=>'ae','ǽ'=>'ae','б'=>'b','č'=>'c','ç'=>'c','ц'=>'c',
+				'ċ'=>'c','ĉ'=>'c','ć'=>'c','ч'=>'ch','ð'=>'dj','ď'=>'dj',
+				'д'=>'dj','đ'=>'dj','э'=>'e','é'=>'e','ё'=>'e','ë'=>'e',
+				'ê'=>'e','е'=>'e','ĕ'=>'e','è'=>'e','ę'=>'e','ě'=>'e',
+				'ė'=>'e','ē'=>'e','ƒ'=>'f','ф'=>'f','ġ'=>'g','ĝ'=>'g',
+				'ğ'=>'g','г'=>'g','ģ'=>'g','х'=>'h','ĥ'=>'h','ħ'=>'h',
+				'ǐ'=>'i','ĭ'=>'i','и'=>'i','ī'=>'i','ĩ'=>'i','į'=>'i',
+				'ı'=>'i','ì'=>'i','î'=>'i','í'=>'i','ï'=>'i','ij'=>'ij',
+				'ĵ'=>'j','й'=>'j','я'=>'ja','ю'=>'ju','ķ'=>'k','к'=>'k',
+				'ľ'=>'l','ł'=>'l','ŀ'=>'l','ĺ'=>'l','ļ'=>'l','л'=>'l',
+				'м'=>'m','ņ'=>'n','ñ'=>'n','ń'=>'n','н'=>'n','ň'=>'n',
+				'ʼn'=>'n','ó'=>'o','ò'=>'o','ǒ'=>'o','ő'=>'o','о'=>'o',
+				'ō'=>'o','º'=>'o','ơ'=>'o','ŏ'=>'o','ô'=>'o','ö'=>'oe',
+				'õ'=>'o','ø'=>'o','ǿ'=>'o','œ'=>'oe','п'=>'p','р'=>'r',
+				'ř'=>'r','ŕ'=>'r','ŗ'=>'r','ſ'=>'s','ŝ'=>'s','ș'=>'s',
+				'š'=>'s','ś'=>'s','с'=>'s','ş'=>'s','ш'=>'sh','щ'=>'shch',
+				'ß'=>'ss','ţ'=>'t','т'=>'t','ŧ'=>'t','ť'=>'t','ț'=>'t',
+				'у'=>'u','ǘ'=>'u','ŭ'=>'u','û'=>'u','ú'=>'u','ų'=>'u',
+				'ù'=>'u','ű'=>'u','ů'=>'u','ư'=>'u','ū'=>'u','ǚ'=>'u',
+				'ǜ'=>'u','ǔ'=>'u','ǖ'=>'u','ũ'=>'u','ü'=>'ue','в'=>'v',
+				'ŵ'=>'w','ы'=>'y','ÿ'=>'y','ý'=>'y','ŷ'=>'y','ź'=>'z',
+				'ž'=>'z','з'=>'z','ż'=>'z','ж'=>'zh'
+			)+Base::instance()->get('DIACRITICS'))))),'-');
+	}
+
+	/**
+	*	Return chunk of text from standard Lorem Ipsum passage
+	*	@return string
+	*	@param $count int
+	*	@param $max int
+	*	@param $std bool
+	**/
+	function filler($count=1,$max=20,$std=TRUE) {
+		$out='';
+		if ($std)
+			$out='Lorem ipsum dolor sit amet, consectetur adipisicing elit, '.
+				'sed do eiusmod tempor incididunt ut labore et dolore magna '.
+				'aliqua.';
+		$rnd=explode(' ',
+			'a ab ad accusamus adipisci alias aliquam amet animi aperiam '.
+			'architecto asperiores aspernatur assumenda at atque aut beatae '.
+			'blanditiis cillum commodi consequatur corporis corrupti culpa '.
+			'cum cupiditate debitis delectus deleniti deserunt dicta '.
+			'dignissimos distinctio dolor ducimus duis ea eaque earum eius '.
+			'eligendi enim eos error esse est eum eveniet ex excepteur '.
+			'exercitationem expedita explicabo facere facilis fugiat harum '.
+			'hic id illum impedit in incidunt ipsa iste itaque iure iusto '.
+			'laborum laudantium libero magnam maiores maxime minim minus '.
+			'modi molestiae mollitia nam natus necessitatibus nemo neque '.
+			'nesciunt nihil nisi nobis non nostrum nulla numquam occaecati '.
+			'odio officia omnis optio pariatur perferendis perspiciatis '.
+			'placeat porro possimus praesentium proident quae quia quibus '.
+			'quo ratione recusandae reiciendis rem repellat reprehenderit '.
+			'repudiandae rerum saepe sapiente sequi similique sint soluta '.
+			'suscipit tempora tenetur totam ut ullam unde vel veniam vero '.
+			'vitae voluptas');
+		for ($i=0,$add=$count-(int)$std;$i<$add;$i++) {
+			shuffle($rnd);
+			$words=array_slice($rnd,0,mt_rand(3,$max));
+			$out.=' '.ucfirst(implode(' ',$words)).'.';
+		}
+		return $out;
+	}
+
+}
+
+if (!function_exists('gzdecode')) {
+
+	/**
+	*	Decode gzip-compressed string
+	*	@param $str string
+	**/
+	function gzdecode($str) {
+		$fw=Base::instance();
+		if (!is_dir($tmp=$fw->get('TEMP')))
+			mkdir($tmp,Base::MODE,TRUE);
+		file_put_contents($file=$tmp.'/'.
+			$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'.
+			$fw->hash(uniqid(NULL,TRUE)).'.gz',$str,LOCK_EX);
+		ob_start();
+		readgzfile($file);
+		$out=ob_get_clean();
+		@unlink($file);
+		return $out;
+	}
+
+}

+ 101 - 0
php-fatfree/lib/web/geo.php

@@ -0,0 +1,101 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace Web;
+
+//! Geo plug-in
+class Geo extends \Prefab {
+
+	/**
+	*	Return information about specified Unix time zone
+	*	@return array
+	*	@param $zone string
+	**/
+	function tzinfo($zone) {
+		$ref=new \DateTimeZone($zone);
+		$loc=$ref->getLocation();
+		$trn=$ref->getTransitions($now=time(),$now);
+		$out=array(
+			'offset'=>$ref->
+				getOffset(new \DateTime('now',new \DateTimeZone('GMT')))/3600,
+			'country'=>$loc['country_code'],
+			'latitude'=>$loc['latitude'],
+			'longitude'=>$loc['longitude'],
+			'dst'=>$trn[0]['isdst']
+		);
+		unset($ref);
+		return $out;
+	}
+
+	/**
+	*	Return geolocation data based on specified/auto-detected IP address
+	*	@return array|FALSE
+	*	@param $ip string
+	**/
+	function location($ip=NULL) {
+		$fw=\Base::instance();
+		$web=\Web::instance();
+		if (!$ip)
+			$ip=$fw->get('IP');
+		$public=filter_var($ip,FILTER_VALIDATE_IP,
+			FILTER_FLAG_IPV4|FILTER_FLAG_IPV6|
+			FILTER_FLAG_NO_RES_RANGE|FILTER_FLAG_NO_PRIV_RANGE);
+		if (function_exists('geoip_db_avail') &&
+			geoip_db_avail(GEOIP_CITY_EDITION_REV1) &&
+			$out=@geoip_record_by_name($ip)) {
+			$out['request']=$ip;
+			$out['region_code']=$out['region'];
+			$out['region_name']=geoip_region_name_by_code(
+				$out['country_code'],$out['region']);
+			unset($out['country_code3'],$out['region'],$out['postal_code']);
+			return $out;
+		}
+		if (($req=$web->request('http://www.geoplugin.net/json.gp'.
+			($public?('?ip='.$ip):''))) &&
+			$data=json_decode($req['body'],TRUE)) {
+			$out=array();
+			foreach ($data as $key=>$val)
+				if (!strpos($key,'currency') && $key!=='geoplugin_status'
+					&& $key!=='geoplugin_region')
+					$out[$fw->snakecase(substr($key, 10))]=$val;
+			return $out;
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Return weather data based on specified latitude/longitude
+	*	@return array|FALSE
+	*	@param $latitude float
+	*	@param $longitude float
+	**/
+	function weather($latitude,$longitude) {
+		$fw=\Base::instance();
+		$web=\Web::instance();
+		$query=array(
+			'lat'=>$latitude,
+			'lng'=>$longitude,
+			'username'=>$fw->hash($fw->get('IP'))
+		);
+		return ($req=$web->request(
+			'http://ws.geonames.org/findNearByWeatherJSON?'.
+				http_build_query($query))) &&
+			($data=json_decode($req['body'],TRUE)) &&
+			isset($data['weatherObservation'])?
+			$data['weatherObservation']:
+			FALSE;
+	}
+
+}

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

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

+ 237 - 0
php-fatfree/lib/web/openid.php

@@ -0,0 +1,237 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace Web;
+
+//! OpenID consumer
+class OpenID extends \Magic {
+
+	protected
+		//! OpenID provider endpoint URL
+		$url,
+		//! HTTP request parameters
+		$args=array();
+
+	/**
+	*	Determine OpenID provider
+	*	@return string|FALSE
+	*	@param $proxy string
+	**/
+	protected function discover($proxy) {
+		// Normalize
+		if (!preg_match('/https?:\/\//i',$this->args['identity']))
+			$this->args['identity']='http://'.$this->args['identity'];
+		$url=parse_url($this->args['identity']);
+		// Remove fragment; reconnect parts
+		$this->args['identity']=$url['scheme'].'://'.
+			(isset($url['user'])?
+				($url['user'].
+				(isset($url['pass'])?(':'.$url['pass']):'').'@'):'').
+			strtolower($url['host']).(isset($url['path'])?$url['path']:'/').
+			(isset($url['query'])?('?'.$url['query']):'');
+		// HTML-based discovery of OpenID provider
+		$req=\Web::instance()->
+			request($this->args['identity'],array('proxy'=>$proxy));
+		if (!$req)
+			return FALSE;
+		$type=array_values(preg_grep('/Content-Type:/',$req['headers']));
+		if ($type &&
+			preg_match('/application\/xrds\+xml|text\/xml/',$type[0]) &&
+			($sxml=simplexml_load_string($req['body'])) &&
+			($xrds=json_decode(json_encode($sxml),TRUE)) &&
+			isset($xrds['XRD'])) {
+			// XRDS document
+			$svc=$xrds['XRD']['Service'];
+			if (isset($svc[0]))
+				$svc=$svc[0];
+			if (preg_grep('/http:\/\/specs\.openid\.net\/auth\/2.0\/'.
+					'(?:server|signon)/',$svc['Type'])) {
+				$this->args['provider']=$svc['URI'];
+				if (isset($svc['LocalID']))
+					$this->args['localidentity']=$svc['LocalID'];
+				elseif (isset($svc['CanonicalID']))
+					$this->args['localidentity']=$svc['CanonicalID'];
+			}
+			$this->args['server']=$svc['URI'];
+			if (isset($svc['Delegate']))
+				$this->args['delegate']=$svc['Delegate'];
+		}
+		else {
+			$len=strlen($req['body']);
+			$ptr=0;
+			// Parse document
+			while ($ptr<$len)
+				if (preg_match(
+					'/^<link\b((?:\h+\w+\h*=\h*'.
+					'(?:"(?:.+?)"|\'(?:.+?)\'))*)\h*\/?>/is',
+					substr($req['body'],$ptr),$parts)) {
+					if ($parts[1] &&
+						// Process attributes
+						preg_match_all('/\b(rel|href)\h*=\h*'.
+							'(?:"(.+?)"|\'(.+?)\')/s',$parts[1],$attr,
+							PREG_SET_ORDER)) {
+						$node=array();
+						foreach ($attr as $kv)
+							$node[$kv[1]]=isset($kv[2])?$kv[2]:$kv[3];
+						if (isset($node['rel']) &&
+							preg_match('/openid2?\.(\w+)/',
+								$node['rel'],$var) &&
+							isset($node['href']))
+							$this->args[$var[1]]=$node['href'];
+
+					}
+					$ptr+=strlen($parts[0]);
+				}
+				else
+					$ptr++;
+		}
+		// Get OpenID provider's endpoint URL
+		if (isset($this->args['provider'])) {
+			// OpenID 2.0
+			$this->args['ns']='http://specs.openid.net/auth/2.0';
+			if (isset($this->args['localidentity']))
+				$this->args['identity']=$this->args['localidentity'];
+			if (isset($this->args['trust_root']))
+				$this->args['realm']=$this->args['trust_root'];
+		}
+		elseif (isset($this->args['server'])) {
+			// OpenID 1.1
+			$this->args['ns']='http://openid.net/signon/1.1';
+			if (isset($this->args['delegate']))
+				$this->args['identity']=$this->args['delegate'];
+		}
+		if (isset($this->args['provider'])) {
+			// OpenID 2.0
+			if (empty($this->args['claimed_id']))
+				$this->args['claimed_id']=$this->args['identity'];
+			return $this->args['provider'];
+		}
+		elseif (isset($this->args['server']))
+			// OpenID 1.1
+			return $this->args['server'];
+		else
+			return FALSE;
+	}
+
+	/**
+	*	Initiate OpenID authentication sequence; Return FALSE on failure
+	*	or redirect to OpenID provider URL
+	*	@return bool
+	*	@param $proxy string
+	*	@param $attr array
+	*	@param $reqd string|array
+	**/
+	function auth($proxy=NULL,$attr=array(),array $reqd=NULL) {
+		$fw=\Base::instance();
+		$root=$fw->get('SCHEME').'://'.$fw->get('HOST');
+		if (empty($this->args['trust_root']))
+			$this->args['trust_root']=$root.$fw->get('BASE').'/';
+		if (empty($this->args['return_to']))
+			$this->args['return_to']=$root.$_SERVER['REQUEST_URI'];
+		$this->args['mode']='checkid_setup';
+		if ($this->url=$this->discover($proxy)) {
+			if ($attr) {
+				$this->args['ns.ax']='http://openid.net/srv/ax/1.0';
+				$this->args['ax.mode']='fetch_request';
+				foreach ($attr as $key=>$val)
+					$this->args['ax.type.'.$key]=$val;
+				$this->args['ax.required']=is_string($reqd)?
+					$reqd:implode(',',$reqd);
+			}
+			$var=array();
+			foreach ($this->args as $key=>$val)
+				$var['openid.'.$key]=$val;
+			$fw->reroute($this->url.'?'.http_build_query($var));
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Return TRUE if OpenID verification was successful
+	*	@return bool
+	*	@param $proxy string
+	**/
+	function verified($proxy=NULL) {
+		preg_match_all('/(?<=^|&)openid\.([^=]+)=([^&]+)/',
+			$_SERVER['QUERY_STRING'],$matches,PREG_SET_ORDER);
+		foreach ($matches as $match)
+			$this->args[$match[1]]=urldecode($match[2]);
+		if (isset($this->args['mode']) &&
+			$this->args['mode']!='error' &&
+			$this->url=$this->discover($proxy)) {
+			$this->args['mode']='check_authentication';
+			$var=array();
+			foreach ($this->args as $key=>$val)
+				$var['openid.'.$key]=$val;
+			$req=\Web::instance()->request(
+				$this->url,
+				array(
+					'method'=>'POST',
+					'content'=>http_build_query($var),
+					'proxy'=>$proxy
+				)
+			);
+			return (bool)preg_match('/is_valid:true/i',$req['body']);
+		}
+		return FALSE;
+	}
+
+	/**
+	*	Return OpenID response fields
+	*	@return array
+	**/
+	function response() {
+		return $this->args;
+	}
+
+	/**
+	*	Return TRUE if OpenID request parameter exists
+	*	@return bool
+	*	@param $key string
+	**/
+	function exists($key) {
+		return isset($this->args[$key]);
+	}
+
+	/**
+	*	Bind value to OpenID request parameter
+	*	@return string
+	*	@param $key string
+	*	@param $val string
+	**/
+	function set($key,$val) {
+		return $this->args[$key]=$val;
+	}
+
+	/**
+	*	Return value of OpenID request parameter
+	*	@return mixed
+	*	@param $key string
+	**/
+	function get($key) {
+		return isset($this->args[$key])?$this->args[$key]:NULL;
+	}
+
+	/**
+	*	Remove OpenID request parameter
+	*	@return NULL
+	*	@param $key
+	**/
+	function clear($key) {
+		unset($this->args[$key]);
+	}
+
+}
+

+ 170 - 0
php-fatfree/lib/web/pingback.php

@@ -0,0 +1,170 @@
+<?php
+
+/*
+	Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
+
+	This file is part of the Fat-Free Framework (http://fatfree.sf.net).
+
+	THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
+	ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+	IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
+	PURPOSE.
+
+	Please see the license.txt file for more information.
+*/
+
+namespace Web;
+
+//! Pingback 1.0 protocol (client and server) implementation
+class Pingback extends \Prefab {
+
+	protected
+		//! Transaction history
+		$log;
+
+	/**
+	*	Return TRUE if URL points to a pingback-enabled resource
+	*	@return bool
+	*	@param $url
+	**/
+	protected function enabled($url) {
+		$web=\Web::instance();
+		$req=$web->request($url);
+		$found=FALSE;
+		if ($req && $req['body']) {
+			// Look for pingback header
+			foreach ($req['headers'] as $header)
+				if (preg_match('/^X-Pingback:\h*(.+)/',$header,$href)) {
+					$found=$href[1];
+					break;
+				}
+			if (!$found &&
+				// Scan page for pingback link tag
+				preg_match('/<link\h+(.+?)\h*\/?>/i',$req['body'],$parts) &&
+				preg_match('/rel\h*=\h*"pingback"/i',$parts[1]) &&
+				preg_match('/href\h*=\h*"\h*(.+?)\h*"/i',$parts[1],$href))
+				$found=$href[1];
+		}
+		return $found;
+	}
+
+	/**
+	*	Load local page contents, parse HTML anchor tags, find permalinks,
+	*	and send XML-RPC calls to corresponding pingback servers
+	*	@return NULL
+	*	@param $source string
+	**/
+	function inspect($source) {
+		$fw=\Base::instance();
+		$web=\Web::instance();
+		$parts=parse_url($source);
+		if (empty($parts['scheme']) || empty($parts['host']) ||
+			$parts['host']==$fw->get('HOST')) {
+			$req=$web->request($source);
+			$doc=new \DOMDocument('1.0',$fw->get('ENCODING'));
+			$doc->stricterrorchecking=FALSE;
+			$doc->recover=TRUE;
+			if ($req && @$doc->loadhtml($req['body'])) {
+				// Parse anchor tags
+				$links=$doc->getelementsbytagname('a');
+				foreach ($links as $link) {
+					$permalink=$link->getattribute('href');
+					// Find pingback-enabled resources
+					if ($permalink && $found=$this->enabled($permalink)) {
+						$req=$web->request($found,
+							array(
+								'method'=>'POST',
+								'header'=>'Content-Type: application/xml',
+								'content'=>xmlrpc_encode_request(
+									'pingback.ping',
+									array($source,$permalink),
+									array('encoding'=>$fw->get('ENCODING'))
+								)
+							)
+						);
+						if ($req && $req['body'])
+							$this->log.=date('r').' '.
+								$permalink.' [permalink:'.$found.']'.PHP_EOL.
+								$req['body'].PHP_EOL;
+					}
+				}
+			}
+			unset($doc);
+		}
+	}
+
+	/**
+	*	Receive ping, check if local page is pingback-enabled, verify
+	*	source contents, and return XML-RPC response
+	*	@return string
+	*	@param $func callback
+	*	@param $path string
+	**/
+	function listen($func,$path=NULL) {
+		$fw=\Base::instance();
+		if (PHP_SAPI!='cli') {
+			header('X-Powered-By: '.$fw->get('PACKAGE'));
+			header('Content-Type: application/xml; '.
+				'charset='.$charset=$fw->get('ENCODING'));
+		}
+		if (!$path)
+			$path=$fw->get('BASE');
+		$web=\Web::instance();
+		$args=xmlrpc_decode_request($fw->get('BODY'),$method,$charset);
+		$options=array('encoding'=>$charset);
+		if ($method=='pingback.ping' && isset($args[0],$args[1])) {
+			list($source,$permalink)=$args;
+			$doc=new \DOMDocument('1.0',$fw->get('ENCODING'));
+			// Check local page if pingback-enabled
+			$parts=parse_url($permalink);
+			if ((empty($parts['scheme']) ||
+				$parts['host']==$fw->get('HOST')) &&
+				preg_match('/^'.preg_quote($path,'/').'/'.
+					($fw->get('CASELESS')?'i':''),$parts['path']) &&
+				$this->enabled($permalink)) {
+				// Check source
+				$parts=parse_url($source);
+				if ((empty($parts['scheme']) ||
+					$parts['host']==$fw->get('HOST')) &&
+					($req=$web->request($source)) &&
+					$doc->loadhtml($req['body'])) {
+					$links=$doc->getelementsbytagname('a');
+					foreach ($links as $link) {
+						if ($link->getattribute('href')==$permalink) {
+							call_user_func_array($func,
+								array($source,$req['body']));
+							// Success
+							die(xmlrpc_encode_request(NULL,$source,$options));
+						}
+					}
+					// No link to local page
+					die(xmlrpc_encode_request(NULL,0x11,$options));
+				}
+				// Source failure
+				die(xmlrpc_encode_request(NULL,0x10,$options));
+			}
+			// Doesn't exist (or not pingback-enabled)
+			die(xmlrpc_encode_request(NULL,0x21,$options));
+		}
+		// Access denied
+		die(xmlrpc_encode_request(NULL,0x31,$options));
+	}
+
+	/**
+	*	Return transaction history
+	*	@return string
+	**/
+	function log() {
+		return $this->log;
+	}
+
+	/**
+	*	Instantiate class
+	*	@return object
+	**/
+	function __construct() {
+		// Suppress errors caused by invalid HTML structures
+		libxml_use_internal_errors(TRUE);
+	}
+
+}

+ 45 - 0
php-fatfree/setup.py

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

+ 0 - 0
php-fatfree/tmp/placeholder


+ 20 - 0
php-fatfree/ui/fortune.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Fortunes</title>
+</head>
+<body>
+    <table>
+        <tr>
+            <th>id</th>
+            <th>message</th>
+        </tr>
+        <repeat group="{{@result}}" key="{{@id}}" value="{{@fortune}}">
+            <tr>
+                <td>{{@id}}</td>
+                <td>{{@fortune}}</td>
+            </tr>
+        </repeat>
+    </table>
+</body>
+</html>