commit
						1af0dae049
					
				 61 changed files with 30015 additions and 0 deletions
			
			
		| @ -0,0 +1,6 @@ | |||
| /tmp/ | |||
| /.idea/ | |||
| /vendor/ | |||
| data/* | |||
| !data/.keep | |||
| .DS_Store | |||
| @ -0,0 +1,16 @@ | |||
| # 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 / | |||
| 
 | |||
| RewriteRule ^(app|tmp)\/|\.ini$ - [R=404] | |||
| 
 | |||
| RewriteCond %{REQUEST_FILENAME} !-l | |||
| RewriteCond %{REQUEST_FILENAME} !-f | |||
| RewriteCond %{REQUEST_FILENAME} !-d | |||
| RewriteRule .* index.php [L,QSA] | |||
| RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] | |||
| @ -0,0 +1,20 @@ | |||
| { | |||
| 	"name": "bcosca/fatfree", | |||
| 	"description": "A powerful yet easy-to-use PHP micro-framework designed to help you build dynamic and robust Web applications - fast!", | |||
| 	"homepage": "http://fatfreeframework.com/", | |||
| 	"license": "GPL-3.0", | |||
| 	"require": { | |||
| 		"php": ">=5.4", | |||
| 		"graze/telnet-client": "^2.2", | |||
| 		"tomnomnom/phpwol": "^0.1.1" | |||
| 	}, | |||
| 	"repositories": [ | |||
| 		{ | |||
| 			"type": "vcs", | |||
| 			"url": "https://github.com/bcosca/fatfree" | |||
| 		} | |||
| 	], | |||
| 	"autoload": { | |||
| 		"files": ["lib/base.php"] | |||
| 	} | |||
| } | |||
| @ -0,0 +1,204 @@ | |||
| { | |||
|     "_readme": [ | |||
|         "This file locks the dependencies of your project to a known state", | |||
|         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | |||
|         "This file is @generated automatically" | |||
|     ], | |||
|     "content-hash": "f4784bd93a60f1cf96e2ee40541cf763", | |||
|     "packages": [ | |||
|         { | |||
|             "name": "clue/socket-raw", | |||
|             "version": "v1.4.1", | |||
|             "source": { | |||
|                 "type": "git", | |||
|                 "url": "https://github.com/clue/php-socket-raw.git", | |||
|                 "reference": "00ab102d061f6cdb895e79dd4d69140c7bda31cc" | |||
|             }, | |||
|             "dist": { | |||
|                 "type": "zip", | |||
|                 "url": "https://api.github.com/repos/clue/php-socket-raw/zipball/00ab102d061f6cdb895e79dd4d69140c7bda31cc", | |||
|                 "reference": "00ab102d061f6cdb895e79dd4d69140c7bda31cc", | |||
|                 "shasum": "" | |||
|             }, | |||
|             "require": { | |||
|                 "ext-sockets": "*", | |||
|                 "php": ">=5.3" | |||
|             }, | |||
|             "require-dev": { | |||
|                 "phpunit/phpunit": "^7.0 || ^6.0 || ^5.2 || ^4.8.35" | |||
|             }, | |||
|             "type": "library", | |||
|             "autoload": { | |||
|                 "psr-4": { | |||
|                     "Socket\\Raw\\": "src" | |||
|                 } | |||
|             }, | |||
|             "notification-url": "https://packagist.org/downloads/", | |||
|             "license": [ | |||
|                 "MIT" | |||
|             ], | |||
|             "authors": [ | |||
|                 { | |||
|                     "name": "Christian Lück", | |||
|                     "email": "christian@clue.engineering" | |||
|                 } | |||
|             ], | |||
|             "description": "Simple and lightweight OOP wrapper for PHP's low-level sockets extension (ext-sockets)", | |||
|             "homepage": "https://github.com/clue/php-socket-raw", | |||
|             "keywords": [ | |||
|                 "Socket", | |||
|                 "client", | |||
|                 "datagram", | |||
|                 "dgram", | |||
|                 "icmp", | |||
|                 "ipv6", | |||
|                 "server", | |||
|                 "stream", | |||
|                 "tcp", | |||
|                 "udg", | |||
|                 "udp", | |||
|                 "unix" | |||
|             ], | |||
|             "support": { | |||
|                 "issues": "https://github.com/clue/php-socket-raw/issues", | |||
|                 "source": "https://github.com/clue/php-socket-raw/tree/v1.4.1" | |||
|             }, | |||
|             "time": "2019-10-28T12:32:07+00:00" | |||
|         }, | |||
|         { | |||
|             "name": "graze/telnet-client", | |||
|             "version": "v2.2.2", | |||
|             "source": { | |||
|                 "type": "git", | |||
|                 "url": "https://github.com/graze/telnet-client.git", | |||
|                 "reference": "b8091d6fbf261aea2ff28cf47fb9b0d0853f63d3" | |||
|             }, | |||
|             "dist": { | |||
|                 "type": "zip", | |||
|                 "url": "https://api.github.com/repos/graze/telnet-client/zipball/b8091d6fbf261aea2ff28cf47fb9b0d0853f63d3", | |||
|                 "reference": "b8091d6fbf261aea2ff28cf47fb9b0d0853f63d3", | |||
|                 "shasum": "" | |||
|             }, | |||
|             "require": { | |||
|                 "clue/socket-raw": "^1.3", | |||
|                 "php": ">5.5.0" | |||
|             }, | |||
|             "require-dev": { | |||
|                 "graze/standards": "^2.0", | |||
|                 "mockery/mockery": "^0.9.4", | |||
|                 "phpunit/phpunit": "^5.0", | |||
|                 "squizlabs/php_codesniffer": "^3.5.0", | |||
|                 "symfony/var-dumper": "^3.0" | |||
|             }, | |||
|             "type": "library", | |||
|             "autoload": { | |||
|                 "psr-4": { | |||
|                     "Graze\\TelnetClient\\": "src/" | |||
|                 } | |||
|             }, | |||
|             "notification-url": "https://packagist.org/downloads/", | |||
|             "license": [ | |||
|                 "MIT" | |||
|             ], | |||
|             "authors": [ | |||
|                 { | |||
|                     "name": "John Smith", | |||
|                     "email": "john@graze.com", | |||
|                     "role": "Developer" | |||
|                 }, | |||
|                 { | |||
|                     "name": "Graze Developers", | |||
|                     "email": "developers@graze.com", | |||
|                     "homepage": "http://www.graze.com", | |||
|                     "role": "Development Team" | |||
|                 }, | |||
|                 { | |||
|                     "name": "Bestnetwork", | |||
|                     "email": "reparto.sviluppo@bestnetwork.it" | |||
|                 }, | |||
|                 { | |||
|                     "name": "Dalibor Andzakovic", | |||
|                     "email": "dali@swerve.co.nz" | |||
|                 }, | |||
|                 { | |||
|                     "name": "Marc Ennaji" | |||
|                 }, | |||
|                 { | |||
|                     "name": "Matthias Blaser", | |||
|                     "email": "mb@adfinis.ch" | |||
|                 }, | |||
|                 { | |||
|                     "name": "Christian Hammers", | |||
|                     "email": "chammers@netcologne.de" | |||
|                 } | |||
|             ], | |||
|             "description": "Telnet client written in PHP", | |||
|             "homepage": "https://github.com/graze/telnet-client", | |||
|             "keywords": [ | |||
|                 "client", | |||
|                 "graze", | |||
|                 "php", | |||
|                 "telnet", | |||
|                 "telnet-client" | |||
|             ], | |||
|             "support": { | |||
|                 "issues": "https://github.com/graze/telnet-client/issues", | |||
|                 "source": "https://github.com/graze/telnet-client/tree/master" | |||
|             }, | |||
|             "time": "2020-06-30T00:25:30+00:00" | |||
|         }, | |||
|         { | |||
|             "name": "tomnomnom/phpwol", | |||
|             "version": "0.1.1", | |||
|             "source": { | |||
|                 "type": "git", | |||
|                 "url": "https://github.com/tomnomnom/phpwol.git", | |||
|                 "reference": "678ab79a9a0effac290c2edf37c2a74c6bb6a46f" | |||
|             }, | |||
|             "dist": { | |||
|                 "type": "zip", | |||
|                 "url": "https://api.github.com/repos/tomnomnom/phpwol/zipball/678ab79a9a0effac290c2edf37c2a74c6bb6a46f", | |||
|                 "reference": "678ab79a9a0effac290c2edf37c2a74c6bb6a46f", | |||
|                 "shasum": "" | |||
|             }, | |||
|             "require-dev": { | |||
|                 "phpunit/phpunit": "3.7.*" | |||
|             }, | |||
|             "type": "library", | |||
|             "autoload": { | |||
|                 "psr-0": { | |||
|                     "Phpwol": "./" | |||
|                 } | |||
|             }, | |||
|             "notification-url": "https://packagist.org/downloads/", | |||
|             "license": [ | |||
|                 "MIT" | |||
|             ], | |||
|             "authors": [ | |||
|                 { | |||
|                     "name": "Tom Hudson", | |||
|                     "email": "mail@tomnomnom.com", | |||
|                     "homepage": "http://tomhudson.co.uk/" | |||
|                 } | |||
|             ], | |||
|             "description": "Wake On LAN for PHP", | |||
|             "homepage": "https://github.com/tomnomnom/phpwol/", | |||
|             "support": { | |||
|                 "issues": "https://github.com/tomnomnom/phpwol/issues", | |||
|                 "source": "https://github.com/tomnomnom/phpwol/tree/route" | |||
|             }, | |||
|             "time": "2017-03-21T22:39:03+00:00" | |||
|         } | |||
|     ], | |||
|     "packages-dev": [], | |||
|     "aliases": [], | |||
|     "minimum-stability": "stable", | |||
|     "stability-flags": [], | |||
|     "prefer-stable": false, | |||
|     "prefer-lowest": false, | |||
|     "platform": { | |||
|         "php": ">=5.4" | |||
|     }, | |||
|     "platform-dev": [], | |||
|     "plugin-api-version": "2.0.0" | |||
| } | |||
| @ -0,0 +1,4 @@ | |||
| [globals] | |||
| 
 | |||
| DEBUG=3 | |||
| UI=ui/ | |||
| @ -0,0 +1,173 @@ | |||
| <?php | |||
| 
 | |||
| // Kickstart the framework | |||
| // $f3=require('lib/base.php'); | |||
| require_once './vendor/autoload.php'; | |||
| $f3 = \Base::instance(); | |||
| $f3->set('DB',new DB\Jig('data/')); | |||
| $db=new \DB\Jig('data/'); | |||
| class Item extends \DB\Jig\Mapper { | |||
|     public function __construct() { | |||
|         parent::__construct( \Base::instance()->get('DB'), 'items' ); | |||
|     } | |||
| } | |||
| $f3->set('DEBUG',1); | |||
| if ((float)PCRE_VERSION<8.0) | |||
| 	trigger_error('PCRE version is out of date'); | |||
| 
 | |||
| // Load configuration | |||
| $f3->config('config.ini'); | |||
| 
 | |||
| $f3->route('GET /', | |||
| 		function($f3) { | |||
| 			$f3->set('template', 'home.htm'); | |||
| 			echo View::instance()->render('layout.htm'); | |||
| 		 } | |||
| ); | |||
| $f3->route('GET /admin', | |||
| 		function($f3) { | |||
| 			$f3->set('template', 'home.htm'); | |||
| 			$f3->set('admin', true); | |||
| 			echo View::instance()->render('layout.htm'); | |||
| 		 } | |||
| ); | |||
| $f3->route('GET /add', | |||
| 		function($f3) { | |||
| 			$f3->set('template', 'add.htm'); | |||
| 			echo View::instance()->render('layout.htm'); | |||
| 		 } | |||
| ); | |||
| $f3->route('POST /add', | |||
| 		function($f3) use ($db) { | |||
| 			$item =new Item; | |||
| 			$item->copyFrom(json_decode($f3->get('BODY'))); | |||
| 			$item->save(); | |||
| 
 | |||
| 		 } | |||
| ); | |||
| 
 | |||
| $f3->route('GET /api/items',  | |||
| 	function($f3) { | |||
| 		$item = new Item; | |||
| 		$items = $item->find(); | |||
| 		$list = array_map([$item, 'cast'],$items); | |||
| 		echo json_encode($list); | |||
| 		} | |||
| 	); | |||
| 
 | |||
| $f3->route('POST /sendsignal', function($f3) { | |||
| 	$post = json_decode($f3->get('BODY'), true); | |||
| 	// print_r($post); | |||
| 	// die; | |||
| 	$iparray = preg_split('/\r\n|\r|\n/', $post['item']['ip']); | |||
| 	$messages = []; | |||
| 	switch ($post['item']['channel']) { | |||
| 		case 'telnet': | |||
| 			foreach ($iparray as $ip) { | |||
| 				$result = telnet($ip, $post['item']['port'], $post['action']); | |||
| 				array_push($messages, $result); | |||
| 			} | |||
| 			break; | |||
| 		case 'udp': | |||
| 			foreach ($iparray as $ip) { | |||
| 				$result = udp($ip, $post['item']['port'], $post['action']); | |||
| 				array_push($messages, $result); | |||
| 			} | |||
| 			break; | |||
| 		case 'wol': | |||
| 			foreach ($iparray as $ip) { | |||
| 				$result = wol($post['item']['macAddress'], $post['item']['broadcastIP']); | |||
| 				array_push($messages, $result); | |||
| 			} | |||
| 			break; | |||
| 		default: | |||
| 			array_push($messages, ['success' => false, 'message' => 'No channel set for these equipments']); | |||
| 			break; | |||
| 	} | |||
| 	echo json_encode($messages); | |||
| }); | |||
| 
 | |||
| $f3->route('POST /del', function ($f3) { | |||
| 		$item = new Item; | |||
| 		$target = json_decode($f3->get('BODY'), true); | |||
| 		$items = $item->load(['@name = ?', $target['name']]); | |||
| 		$items->erase(); | |||
| 		echo json_encode(['success' => true, 'message' => 'Deleted']); | |||
| 
 | |||
| }); | |||
| 
 | |||
| function telnet($ip, $port, $action) { | |||
| 		try { | |||
| 			$factory = new \Socket\Raw\Factory(); | |||
| 			$socket = $factory->createClient($ip . ':' . $port, 2); | |||
| 	        $client = \Graze\TelnetClient\TelnetClient::factory(); | |||
| 	        $client->setSocket($socket); | |||
| 	        // $dsn = '192.168.0.100:23'; | |||
| 	        $client->setLineEnding(null); | |||
| 	        $client->setPrompt("Optoma_PJ>"); | |||
| 	        $client->setPromptError("F"); | |||
| 	        try { | |||
| 	        	$conn = $client->connect($ip . ':' . $port);	 | |||
| 	        	$client->setReadTimeout(1); | |||
| 	        	$response = $client->execute($action . "\r"); 	 | |||
| 	        	return ['success' => true, 'message' => 'successfully sent command to ' . $ip]; | |||
| 	        } catch (Exception $e) { | |||
| 	        	return ['success' => false, 'message' => $e->getMessage() . $ip]; | |||
| 	        } | |||
| 			 | |||
| 		} catch (Exception $e) { | |||
| 			return ['success' => false, 'message' => $e->getMessage() . ' ' . $ip];			 | |||
| 		} | |||
| 
 | |||
| } | |||
| 
 | |||
| function udp($ip, $port, $action) { | |||
|         if ($socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP)) { | |||
|             socket_sendto($socket, $action, strlen($action), 0, $ip, $port); | |||
|         	return ['success' => true, 'message' => 'UDP successful']; | |||
|         } else { | |||
|           	return ['success' => false, 'message' => 'UDP not sent...']; | |||
|         } | |||
| 
 | |||
| } | |||
| 
 | |||
| function wol($macAddress, $broadcastIP) { | |||
| 	$f = new \Phpwol\Factory(); | |||
| 	$magicPacket = $f->magicPacket(); | |||
| 	if ($result = $magicPacket->send($macAddress, $broadcastIP)) { | |||
| 		return ['success' => true, 'message' => 'Successful Wake On Lan']; | |||
| 	} else { | |||
| 		return ['success' => false, 'message' => 'Unsuccessful WOL for IP ' . $macAddress]; | |||
| 	} | |||
| } | |||
| 
 | |||
| $f3->route('GET /optomaon',  | |||
| 	function($f3) { | |||
|         $client = \Graze\TelnetClient\TelnetClient::factory(); | |||
|         $dsn = '192.168.0.100:23'; | |||
|         $client->setLineEnding(null); | |||
|         $client->setPrompt("Optoma_PJ>"); | |||
|         $client->setPromptError("F"); | |||
|         $conn = $client->connect($dsn); | |||
|         $client->setReadTimeout(1); | |||
|         $response = $client->execute("~0000 1\r");  | |||
|         print_r($response); | |||
| 	}); | |||
| 
 | |||
| 
 | |||
| function getItems($db) { | |||
| 	return $db->read('items'); | |||
| } | |||
| 
 | |||
| function setItems($db) { | |||
| 	$items = $db->read('items'); | |||
| 
 | |||
| } | |||
| $f3->route('GET /userref', | |||
| 	function($f3) { | |||
| 		$f3->set('content','userref.htm'); | |||
| 		echo View::instance()->render('layout.htm'); | |||
| 	} | |||
| ); | |||
| 
 | |||
| $f3->run(); | |||
| @ -0,0 +1,971 @@ | |||
| CHANGELOG | |||
| 
 | |||
| 3.7.2 (28 May 2020) | |||
| *   CHANGED, View->sandbox: disable escaping when rendering as text/plain, bcosca/fatfree#654 | |||
| *   update HTTP protocol checks, #bcosca/fatfree#1190 | |||
| *   Base->clear: close vulnerability on variable compilation, bcosca/fatfree#1191 | |||
| *   DB\SQL\Mapper: fix empty ID after insert, bcosca/fatfree#1175 | |||
| *   DB\SQL\Mapper: fix using correct key variable for grouped sql pagination sets | |||
| *   Fix return type of 'count' in Cursor->paginate() (bcosca/fatfree#1187) | |||
| *   Bug fix, Web->minify: fix minification of ES6 template literals, bcosca/fatfree#1178 | |||
| *   Bug fix, config: refactoring custom section parser regex, bcosca/fatfree#1149 | |||
| *   Bug fix: token resolve on non-alias reroute paths, ref. 221f0c930f8664565c9825faeb9ed9af0f7a01c8 | |||
| *   Websocket: Improved event handler usage | |||
| *   optimized internal get calls | |||
| *   only use cached lexicon when a $ttl was given | |||
| *   only use money_format up until php7.4, fixes bcosca/fatfree#1174 | |||
| 
 | |||
| 3.7.1 (30. December 2019) | |||
| *   Base->build: Add support for brace-enclosed route tokens | |||
| *   Base->reroute, fix duplicate fragment issue on non-alias routes | |||
| *   DB\SQL\Mapper: fix empty check for pkey when reloading after insert | |||
| *   Web->minify: fix minification with multiple files, [bcosca/fatfree#1152](https://github.com/bcosca/fatfree/issues/1152), [#bcosca/fatfree#1169](https://github.com/bcosca/fatfree/issues/1169) | |||
| 
 | |||
| 3.7.0 (26. November 2019) | |||
| *   NEW: Matrix, added select and walk methods for array processing and validation tools | |||
| *   NEW: Added configurable file locking via LOCK var | |||
| *   NEW: json support for dictionary files | |||
| *   NEW: $die parameter on ONREROUTE hook | |||
| *   NEW: Added SameSite cookie support for php7.3+ (JAR.samesite), [bcosca/fatfree#1165](https://github.com/bcosca/fatfree/issues/1165) | |||
| *   NEW, DB\SQL\Mapper: added updateAll method to batch-update multiple records at once | |||
| *   CHANGED, DB\SQL\Mapper: Throw error on update/erase if the table has no primary key, [#285](https://github.com/bcosca/fatfree-core/issues/285) | |||
| *   Cache, Redis: Added ability to set a Redis password, [#287](https://github.com/bcosca/fatfree-core/issues/287) | |||
| *   DB\SQL\Session: make datatype of data column configurable, [bcosca/fatfree#1130](https://github.com/bcosca/fatfree/issues/1130) | |||
| *   DB\SQL\Mapper: only add adhoc fields in count queries that are used for grouping | |||
| *   DB\SQL\Mapper: fixed inserting an already loaded record again (duplicating), [bcosca/fatfree#1093](https://github.com/bcosca/fatfree/issues/1093) | |||
| *   Magic (Mappers): fix isset check on existing properties | |||
| *   SMTP: added support for Bounce mail recipient ("Sender" header) | |||
| *   OAuth2: make query string encode type configurable, [#268](https://github.com/bcosca/fatfree-core/issues/268) [#269](https://github.com/bcosca/fatfree-core/issues/269) | |||
| *   Web: Added more cyrillic letters to diacritics, [bcosca/fatfree#1158](https://github.com/bcosca/fatfree/issues/1158) | |||
| *   Web: Fixed url string falsely detected as comment section [9ac8e615](https://github.com/bcosca/fatfree-core/commit/9ac8e615ccaf750b49497a3c86161331b24e637f) | |||
| *   Web: added file inspection for mime-type detection, [#270](https://github.com/bcosca/fatfree-core/issues/270), [bcosca/fatfree#1138](https://github.com/bcosca/fatfree/issues/1138) | |||
| *   WS: Fixed processing all queued data frames inside the buffer, [#277](https://github.com/bcosca/fatfree-core/issues/277) | |||
| *   WS: Allow packet size override | |||
| *   Markdown: Support mixed `strong` and `italic` elements, [#276](https://github.com/bcosca/fatfree-core/issues/276) | |||
| *   Markdown: Keep spaces around `=` sign in ini code blocks  | |||
| *   Added route alias key name validation, [#243](https://github.com/bcosca/fatfree-core/issues/243) | |||
| *   Added fragment argument to alias method, [#282](https://github.com/bcosca/fatfree-core/issues/282) | |||
| *   Allow adding fragment to reroute, [#1156](https://github.com/bcosca/fatfree/issues/1156) | |||
| *   Added additional HTTP status codes, [#283](https://github.com/bcosca/fatfree-core/issues/283) | |||
| *   Added X-Forwarded-For IP to log entries, [bcosca/fatfree#1042](https://github.com/bcosca/fatfree/issues/1042) | |||
| *   Bug fix: broken custom date/time formatting, [bcosca/fatfree#1147](https://github.com/bcosca/fatfree/issues/1147) | |||
| *   Bug fix: duplicate UI path rendering edge-case in Views and minify, [bcosca/fatfree#1152](https://github.com/bcosca/fatfree/issues/1152) | |||
| *   Bug fix: unicode chars in custom config section keys, [bcosca/fatfree#1149](https://github.com/bcosca/fatfree/issues/1149) | |||
| *   Bug fix: ensure valid reroute path in location header, [bcosca/fatfree#1140](https://github.com/bcosca/fatfree/issues/1140) | |||
| *   Bug fix: use dictionary path for lexicon caching-hash | |||
| *   Bug fix, php7.3: number format ternary, [bcosca/fatfree#1142](https://github.com/bcosca/fatfree/issues/1142) | |||
| *   fix PHPdoc and variable inspection, [bcosca/fatfree#865](https://github.com/bcosca/fatfree/issues/865), [bcosca/fatfree#1128](https://github.com/bcosca/fatfree/issues/1128) | |||
| 
 | |||
| 3.6.5 (24 December 2018) | |||
| *	NEW: Log, added timestamp to each line | |||
| *	NEW: Auth, added support for custom compare method, [#116](https://github.com/bcosca/fatfree-core/issues/116) | |||
| *	NEW: cache tag support for mongo & jig mapper, ref [#166](https://github.com/bcosca/fatfree-core/issues/116) | |||
| *	NEW: Allow PHP functions as template token filters | |||
| *	Web: Fix double redirect bug when running cURL with open_basedir disabled | |||
| *	Web: Cope with responses from HTTP/2 servers | |||
| *	Web->filler: remove very first space, when $std is false | |||
| *	Web\OAuth2: Cope with HTTP/2 responses | |||
| *	Web\OAuth2: take Content-Type header into account for json decoding, [#250](https://github.com/bcosca/fatfree-core/issues/250) [#251](https://github.com/bcosca/fatfree-core/issues/251) | |||
| *	Web\OAuth2: fixed empty results on some endpoints [#250](https://github.com/bcosca/fatfree-core/issues/250) | |||
| *	DB\SQL\Mapper: optimize mapper->count memory usage | |||
| *	DB\SQL\Mapper: New table alias operator | |||
| *	DB\SQL\Mapper: fix count() performance on non-grouped result sets, [bcosca/fatfree#1114](https://github.com/bcosca/fatfree/issues/1114) | |||
| *	DB\SQL: Support for CTE in postgreSQL, [bcosca/fatfree#1107](https://github.com/bcosca/fatfree/issues/1107), [bcosca/fatfree#1116](https://github.com/bcosca/fatfree/issues/1116), [bcosca/fatfree#1021](https://github.com/bcosca/fatfree/issues/1021) | |||
| *	DB\SQL->log: Remove extraneous whitespace | |||
| *	DB\SQL: Added ability to add inline comments per SQL query | |||
| *	CLI\WS, Refactoring: Streamline socket server | |||
| *	CLI\WS: Add option for dropping query in OAuth2 URI  | |||
| *	CLI\WS: Add URL-safe base64 encoding | |||
| *	CLI\WS: Detect errors in returned JSON values | |||
| *	CLI\WS: Added support for Sec-WebSocket-Protocol header | |||
| *	Matrix->calendar: Allow unix timestamp as date argument | |||
| *	Basket: Access basket item by _id [#260](https://github.com/bcosca/fatfree-core/issues/260) | |||
| *	SMTP: Added TLS 1.2 support [bcosca/fatfree#1115](https://github.com/bcosca/fatfree/issues/1115) | |||
| *	SMTP->send: Respect $log argument | |||
| *	Base->cast: recognize binary and octal numbers in config | |||
| *	Base->cast: add awareness of hexadecimal literals | |||
| *	Base->abort: Remove unnecessary Content-Encoding header | |||
| *	Base->abort: Ensure headers have not been flushed | |||
| *	Base->format: Differentiate between long- and full-date (with localized weekday) formats | |||
| *	Base->format: Conform with intl extension's number output | |||
| *	Enable route handler to override Access-Control headers in response to OPTIONS request, [#257](https://github.com/bcosca/fatfree-core/issues/257) | |||
| *	Augment filters with a var_export function | |||
| *	Bug fix php7.3: Fix template parse regex to be compatible with strict PCRE2 rules for hyphen placement in a character class | |||
| *	Bug fix, Cache->set: update creation time when updating existing cache entries  | |||
| *	Bug fix: incorrect ICU date/time formatting | |||
| *	Bug fix, Jig: lazy write on empty data | |||
| *	Bug fix: Method uppercase to avoid route failure [#252](https://github.com/bcosca/fatfree-core/issues/252) | |||
| *	Fixed error description when (PSR-11) `CONTAINER` fails to resolve a class [#253](https://github.com/bcosca/fatfree-core/issues/253) | |||
| *	Mitigate CSRF predictability/vulnerability | |||
| *	Expose Mapper->factory() method | |||
| 
 | |||
| 3.6.4 (19 April 2018) | |||
| *	NEW: Added Dependency Injection support with CONTAINER variable [#221](https://github.com/bcosca/fatfree-core/issues/221) | |||
| *	NEW: configurable LOGGABLE error codes [#1091](https://github.com/bcosca/fatfree/issues/1091#issuecomment-364674701) | |||
| *	NEW: JAR.lifetime option, [#178](https://github.com/bcosca/fatfree-core/issues/178) | |||
| *	Template: reduced Prefab calls | |||
| *	Template: optimized reflection for better derivative support, [bcosca/fatfree#1088](https://github.com/bcosca/fatfree/issues/1088) | |||
| *	Template: optimized parsing for template attributes and tokens | |||
| *	DB\Mongo: fixed logging with mongodb extention | |||
| *	DB\Jig: added lazy-loading [#7e1cd9b9b89](https://github.com/bcosca/fatfree-core/commit/7e1cd9b9b89c4175d0f6b86ced9d9bd49c04ac39) | |||
| *	DB\Jig\Mapper: Added group feature, bcosca/fatfree#616 | |||
| *	DB\SQL\Mapper: fix PostgreSQL RETURNING ID when no pkey is available, [bcosca/fatfree#1069](https://github.com/bcosca/fatfree/issues/1069), [#230](https://github.com/bcosca/fatfree-core/issues/230) | |||
| *	DB\SQL\Mapper: disable order clause auto-quoting when it's already been quoted | |||
| *	Web->location: add failsafe for geoip_region_name_by_code() [#GB:Bxyn9xn9AgAJ](https://groups.google.com/d/msg/f3-framework/APau4wnwNzE/Bxyn9xn9AgAJ) | |||
| *	Web->request: Added proxy support [#e936361b](https://github.com/bcosca/fatfree-core/commit/e936361bc03010c4c7c38a396562e5e96a8a100d) | |||
| *	Web->mime: Added JFIF format | |||
| *	Markdown: handle line breaks in paragraph blocks, [bcosca/fatfree#1100](https://github.com/bcosca/fatfree/issues/1100) | |||
| *	config: reduced cast calls on parsing config sections | |||
| *	Patch empty SERVER_NAME [bcosca/fatfree#1084](https://github.com/bcosca/fatfree/issues/1084) | |||
| *	Bugfix: unreliable request headers in Web->request() response [bcosca/fatfree#1092](https://github.com/bcosca/fatfree/issues/1092) | |||
| *	Fixed, View->render: utilizing multiple UI paths, [bcosca/fatfree#1083](https://github.com/bcosca/fatfree/issues/1083) | |||
| *	Fixed URL parsing with PHP 5.4 [#247](https://github.com/bcosca/fatfree-core/issues/247) | |||
| *	Fixed PHP 7.2 warnings when session is active prematurely, [#238](https://github.com/bcosca/fatfree-core/issues/238) | |||
| *	Fixed setcookie $expire variable type [#240](https://github.com/bcosca/fatfree-core/issues/240) | |||
| *	Fixed expiration time when updating an existing cookie | |||
| 
 | |||
| 3.6.3 (31 December 2017) | |||
| *	PHP7 fix: remove deprecated (unset) cast | |||
| *	Web->request: restricted follow_location to 3XX responses only | |||
| *	CLI mode: refactored arguments parsing | |||
| *	CLI mode: fixed query string encoding | |||
| *	SMTP: Refactor parsing of attachments | |||
| *	SMTP: clean-up mail headers for multipart messages, [#1065](https://github.com/bcosca/fatfree/issues/1065) | |||
| *	config: fixed performance issues on parsing config files | |||
| *	config: cast command parameters in config entries to php type & constant, [#1030](https://github.com/bcosca/fatfree/issues/1030) | |||
| *	config: reduced registry calls | |||
| *	config: skip hive escaping when resolving dynamic config vars, [#1030](https://github.com/bcosca/fatfree/issues/1030) | |||
| *	Bug fix: Incorrect cookie lifetime computation, [#1070](https://github.com/bcosca/fatfree/issues/1070), [#1016](https://github.com/bcosca/fatfree/issues/1016) | |||
| *	DB\SQL\Mapper: use RETURNING option instead of a sequence query to get lastInsertId in PostgreSQL, [#1069](https://github.com/bcosca/fatfree/issues/1069), [#230](https://github.com/bcosca/fatfree-core/issues/230) | |||
| *	DB\SQL\Session: check if _agent is too long for SQL based sessions [#236](https://github.com/bcosca/fatfree-core/issues/236) | |||
| *	DB\SQL\Session: fix Session handler table creation issue on SQL Server, [#899](https://github.com/bcosca/fatfree/issues/899) | |||
| *	DB\SQL: fix oracle db issue with empty error variable, [#1072](https://github.com/bcosca/fatfree/issues/1072) | |||
| *	DB\SQL\Mapper: fix sorting issues on SQL Server, [#1052](https://github.com/bcosca/fatfree/issues/1052) [#225](https://github.com/bcosca/fatfree-core/issues/225) | |||
| *	Prevent directory traversal attacks on filesystem based cache [#1073](https://github.com/bcosca/fatfree/issues/1073) | |||
| *	Bug fix, Template: PHP constants used in include with attribute, [#983](https://github.com/bcosca/fatfree/issues/983) | |||
| *	Bug fix, Template: Numeric value in expression alters PHP_EOL context | |||
| *	Template: use existing linefeed instead of PHP_EOL, [#1048](https://github.com/bcosca/fatfree/issues/1048) | |||
| *	Template: make newline interpolation handling configurable [#223](https://github.com/bcosca/fatfree-core/issues/223) | |||
| *	Template: add beforerender to Preview | |||
| *	fix custom FORMATS without modifiers | |||
| *	Cache: Refactor Cache->reset for XCache | |||
| *	Cache: loosen reset cache key pattern, [#1041](https://github.com/bcosca/fatfree/issues/1041) | |||
| *	XCache: suffix reset only works if xcache.admin.enable_auth is disabled | |||
| *	Added HTTP 103 as recently approved by the IETF | |||
| *	LDAP changes to for AD flexibility [#227](https://github.com/bcosca/fatfree-core/issues/227) | |||
| *	Hide debug trace from ajax errors when DEBUG=0 [#1071](https://github.com/bcosca/fatfree/issues/1071) | |||
| *	fix View->render using potentially wrong cache entry | |||
| 
 | |||
| 3.6.2 (26 June 2017) | |||
| *   Return a status code > 0 when dying on error [#220](https://github.com/bcosca/fatfree-core/issues/220) | |||
| *   fix SMTP line width [#215](https://github.com/bcosca/fatfree-core/issues/215) | |||
| *   Allow using a custom field for ldap user id checking [#217](https://github.com/bcosca/fatfree-core/issues/217) | |||
| *   NEW: DB\SQL->exists: generic method to check if SQL table exists | |||
| *   Pass handler to route handler and hooks [#1035](https://github.com/bcosca/fatfree/issues/1035) | |||
| *   pass carriage return of multiline dictionary keys | |||
| *   Better Web->slug customization | |||
| *   fix incorrect header issue [#211](https://github.com/bcosca/fatfree-core/issues/211) | |||
| *   fix schema issue on databases with case-sensitive collation, fixes [#209](https://github.com/bcosca/fatfree-core/issues/209) | |||
| *   Add filter for deriving C-locale equivalent of a number | |||
| *   Bug fix: @LANGUAGE remains unchanged after override | |||
| *   abort: added Header pre-check | |||
| *   Assemble URL after ONREROUTE | |||
| *   Add reroute argument to skip script termination | |||
| *   Invoke ONREROUTE after headers are sent | |||
| *   SQLite switch to backtick as quote | |||
| *   Bug fix: Incorrect timing in SQL query logs | |||
| *   DB\SQL\Mapper: Cast return value of count to integer | |||
| *   Patched $_SERVER['REQUEST_URI'] to ensure it contains a relative URI | |||
| *   Tweak debug verbosity | |||
| *   fix php carriage return issue in preview->build [#205](https://github.com/bcosca/fatfree-core/pull/205) | |||
| *   fixed template string resolution [#205](https://github.com/bcosca/fatfree-core/pull/205) | |||
| *   Fixed unexpected default seed on CACHE set [#1028](https://github.com/bcosca/fatfree/issues/1028) | |||
| *   DB\SQL\Mapper: Optimized field escaping on options | |||
| *   Optimize template conversion to PHP file | |||
| 
 | |||
| 3.6.1 (2 April 2017) | |||
| *	NEW: Recaptcha plugin [#194](https://github.com/bcosca/fatfree-core/pull/194) | |||
| *	NEW: MB variable for detecting multibyte support | |||
| *	NEW: DB\SQL: Cache parsed schema for the TTL duration | |||
| *	NEW: quick erase flag on Jig/Mongo/SQL mappers [#193](https://github.com/bcosca/fatfree-core/pull/193) | |||
| *	NEW: Allow OPTIONS method to return a response body [#171](https://github.com/bcosca/fatfree-core/pull/171) | |||
| *	NEW: Add support for Memcached (bcosca/fatfree#997) | |||
| *	NEW: Rudimentary preload resource (HTTP2 server) support via template push() | |||
| *	NEW: Add support for new MongoDB driver [#177](https://github.com/bcosca/fatfree-core/pull/177) | |||
| *	Changed: template filter are all lowercase now | |||
| *	Changed: Fix template lookup inconsistency: removed base dir from UI on render | |||
| *	Changed: count() method now has an options argument [#192](https://github.com/bcosca/fatfree-core/pull/192) | |||
| *	Changed: SMTP, Spit out error message if any | |||
| *	\DB\SQL\Mapper: refactored row count strategy | |||
| *	DB\SQL\Mapper: Allow non-scalar values to be assigned as mapper property | |||
| *	DB\SQL::PARAM_FLOAT: remove cast to float (#106 and bcosca/fatfree#984) (#191) | |||
| *	DB\SQL\mapper->erase: allow empty string | |||
| *	DB\SQL\mapper->insert: fields reset after successful INSERT | |||
| *	Add option to debounce Cursor->paginate subset [#195](https://github.com/bcosca/fatfree-core/pull/195) | |||
| *	View: Don't delete sandboxed variables (#198) | |||
| *	Preview: Optimize compilation of template expressions | |||
| *	Preview: Use shorthand tag for direct rendering | |||
| *	Preview->resolve(): new tweak to allow template persistence as option | |||
| *	Web: Expose diacritics translation table | |||
| *	SMTP: Enable logging of message body only when $log argument is 'verbose' | |||
| *	SMTP: Convert headers to camelcase for consistency | |||
| *	make cache seed more flexible, #164 | |||
| *	Improve trace details for DEBUG>2 | |||
| *	Enable config() to read from an array of input files | |||
| *	Improved alias and reroute regex | |||
| *	Make camelCase and snakeCase Unicode-aware | |||
| *	format: Provision for optional whitespaces | |||
| *	Break APCu-BC dependence | |||
| *	Old PHP 5.3 cleanup | |||
| *	Debug log must include HTTP query | |||
| *	Recognize X-Forwarded-Port header (bcosca/fatfree#1002) | |||
| *	Avoid use of deprecated mcrypt module | |||
| *	Return only the client's IP when using the `X-Forwarded-For` header to deduce an IP address | |||
| *	Remove orphan mutex locks on termination (#157) | |||
| *	Use 80 as default port number to avoid issues when `$_SERVER['SERVER_PORT']` is not existing | |||
| *	fread replaced with readfile() for simple send() usecase | |||
| *	Bug fix: request URI with multiple leading slashes, #203 | |||
| *	Bug fix: Query generates wrong adhoc field value | |||
| *	Bug fix: SMTP stream context issue #200 | |||
| *	Bug fix: child pseudo class selector in minify, bcosca/fatfree#1008 | |||
| *	Bug fix: "Undefined index: CLI" error (#197) | |||
| *	Bug fix: cast Cache-Control expire time to int, bcosca/fatfree#1004 | |||
| *	Bug fix: Avoid issuance of multiple Content-Type headers for nested templates | |||
| *	Bug fix: wildcard token issue with digits (bcosca/fatfree#996) | |||
| *	Bug fix: afterupdate ignored when row does not change | |||
| *	Bug fix: session handler read() method for PHP7 (need strict string) #184 #185 | |||
| *	Bug fix: reroute mocking in CLI mode (#183) | |||
| *	Bug fix: Reroute authoritative relative references (#181) | |||
| *	Bug fix: locales order and charset hyphen | |||
| *	Bug fix: base stripped twice in router (#176) | |||
| 
 | |||
| 3.6.0 (19 November 2016) | |||
| *	NEW: [cli] request type | |||
| *	NEW: console-friendly CLI mode | |||
| *	NEW: lexicon caching | |||
| *	NEW: Silent operator skips startup error check (#125) | |||
| *	NEW: DB\SQL->trans() | |||
| *	NEW: custom config section parser, i.e. [conf > Foo::bar] | |||
| *	NEW: support for cache tags in SQL | |||
| *	NEW: custom FORMATS | |||
| *	NEW: Mongo mapper fields whitelist | |||
| *	NEW: WebSocket server | |||
| *	NEW: Base->extend method (#158) | |||
| *	NEW: Implement framework variable caching via config, i.e. FOO = "bar" | 3600 | |||
| *	NEW: Lightweight OAuth2 client | |||
| *	NEW: SEED variable, configurable app-specific hashing prefix (#149, bcosca/fatfree#951, bcosca/fatfree#884, bcosca/fatfree#629) | |||
| *	NEW: CLI variable | |||
| *	NEW: Web->send, specify custom filename (#124) | |||
| *	NEW: Web->send, added flushing flag (#131) | |||
| *	NEW: Indexed route wildcards, now exposed in PARAMS['*'] | |||
| *	Changed: PHP 5.4 is now the minimum version requirement | |||
| *	Changed: Prevent database wrappers from being cloned | |||
| *	Changed: Router works on PATH instead of URI (#126) NB: PARAMS.0 no longer contains the query string | |||
| *	Changed: Removed ALIASES autobuilding (#118) | |||
| *	Changed: Route wildcards match empty strings (#119) | |||
| *	Changed: Disable default debug highlighting, HIGHLIGHT is false now | |||
| *	General PHP 5.4 optimizations | |||
| *	Optimized config parsing | |||
| *	Optimized Base->recursive | |||
| *	Optimized header extraction | |||
| *	Optimized cache/expire headers | |||
| *	Optimized session_start behaviour (bcosca/fatfree#673) | |||
| *	Optimized reroute regex | |||
| *	Tweaked cookie removal | |||
| *	Better route precedence order | |||
| *	Performance tweak: reduced cache calls | |||
| *	Refactored lexicon (LOCALES) build-up, much faster now | |||
| *	Added turkish locale bug workaround | |||
| *	Geo->tzinfo Update to UTC | |||
| *	Added Xcache reset (bcosca/fatfree#928) | |||
| *	Redis cache: allow db name in dsn | |||
| *	SMTP: Improve server emulation responses | |||
| *	SMTP: Optimize transmission envelope | |||
| *	SMTP: Implement mock transmission | |||
| *	SMTP: Various bug fixes and feature improvements | |||
| *	SMTP: quit on failed authentication | |||
| *	Geo->weather: force metric units | |||
| *	Base->until: Implement CLI interoperability | |||
| *	Base->format: looser plural syntax | |||
| *	Base->format: Force decimal as default number format | |||
| *	Base->merge: Added $keep flag to save result to the hive key | |||
| *	Base->reroute: Allow array as URL argument for aliasing | |||
| *	Base->alias: Allow query string (or array) to be appended to alias | |||
| *	Permit reroute to named routes with URL query segment | |||
| *	Sync COOKIE global on set() | |||
| *	Permit non-hive variables to use JS dot notation | |||
| *	RFC2616: Use absolute URIs for Location header | |||
| *	Matrix->calendar: Check if calendar extension is loaded | |||
| *	Markdown: require start of line/whitespace for text processing (#136) | |||
| *	DB\[SQL|Jig|Mongo]->log(FALSE) disables logging | |||
| *	DB\SQL->exec: Added timestamp toggle to db log | |||
| *	DB\SQL->schema: Remove unnecessary line terminators | |||
| *	DB\SQL\Mapper: allow array filter with empty string | |||
| *	DB\SQL\Mapper: optimized handling for key-less tables | |||
| *	DB\SQL\Mapper: added float support (#106) | |||
| *	DB\SQL\Session: increased default column sizes (#148, bcosca/fatfree#931, bcosca/fatfree#950) | |||
| *	Web: Catch cURL errors | |||
| *	Optimize Web->receive (bcosca/fatfree#930) | |||
| *	Web->minify: fix arbitrary file download vulnerability | |||
| *	Web->request: fix cache control max-age detection (bcosca/fatfree#908) | |||
| *	Web->request: Add request headers & error message to return value (bcosca/fatfree#737) | |||
| *	Web->request: Refactored response to HTTP request | |||
| *	Web->send flush while sending big files | |||
| *	Image->rgb: allow hex strings | |||
| *	Image->captcha: Check if GD module supports TrueType | |||
| *	Image->load: Return FALSE on load failure | |||
| *	Image->resize: keep aspect ratio when only width or height was given | |||
| *	Updated OpenID lib (bcosca/fatfree#965) | |||
| *	Audit->card: add new mastercard "2" BIN range (bcosca/fatfree#954) | |||
| *	Deprecated: Bcrypt class | |||
| *	Preview->render: optimized detection to remove short open PHP tags and allow xml tags (#133) | |||
| *	Display file and line number in exception handler (bcosca/fatfree#967) | |||
| *	Added error reporting level to Base->error and ERROR.level (bcosca/fatfree#957) | |||
| *	Added optional custom cache instance to Session (#141) | |||
| *	CLI-aware mock() | |||
| *	XFRAME and PACKAGE can be switched off now (#128) | |||
| *	Bug fix: wrong time calculation on memcache reset (#170) | |||
| *	Bug fix: encode CLI parameters | |||
| *	Bug fix: Close connection on abort explicitly (#162) | |||
| *	Bug fix: Image->identicon, Avoid double-size sprite rotation (and possible segfault) | |||
| *	Bug fix: Image->render and Image->dump, removed unnecessary 2nd argument (#146) | |||
| *	Bug fix: Magic->offsetset, access property as array element (#147) | |||
| *	Bug fix: multi-line custom template tag parsing (bcosca/fatfree#935) | |||
| *	Bug fix: cache headers on errors (bcosca/fatfree#885) | |||
| *	Bug fix: Web, deprecated CURLOPT_SSL_VERIFYHOST in curl | |||
| *	Bug fix: Web, Invalid user error constant (bcosca/fatfree#962) | |||
| *	Bug fix: Web->request, redirections for domain-less location (#135) | |||
| *	Bug fix: DB\SQL\Mapper, reset changed flag after update (#142, #152) | |||
| *	Bug fix: DB\SQL\Mapper, fix changed flag when using assignment operator #143 #150 #151 | |||
| *	Bug fix: DB\SQL\Mapper, revival of the HAVING clause | |||
| *	Bug fix: DB\SQL\Mapper, pgsql with non-integer primary keys (bcosca/fatfree#916) | |||
| *	Bug fix: DB\SQL\Session, quote table name (bcosca/fatfree#977) | |||
| *	Bug fix: snakeCase returns word starting with underscore (bcosca/fatfree#927) | |||
| *	Bug fix: mock does not populate PATH variable | |||
| *	Bug fix: Geo->weather API key (#129) | |||
| *	Bug fix: Incorrect compilation of array element with zero index | |||
| *	Bug fix: Compilation of array construct is incorrect | |||
| *	Bug fix: Trailing slash redirection on UTF-8 paths (#121) | |||
| 
 | |||
| 3.5.1 (31 December 2015) | |||
| *	NEW: ttl attribute in <include> template tag | |||
| *	NEW: allow anonymous function for template filter | |||
| *	NEW: format modifier for international and custom currency symbol | |||
| *	NEW: Image->data() returns image resource | |||
| *	NEW: extract() get prefixed array keys from an assoc array | |||
| *	NEW: Optimized and faster Template parser with full support for HTML5 empty tags | |||
| *	NEW: Added support for {@token} encapsulation syntax in routes definition | |||
| *	NEW: DB\SQL->exec(), automatically shift to 1-based query arguments | |||
| *	NEW: abort() flush output | |||
| *	Added referenced value to devoid() | |||
| *	Template token filters are now resolved within Preview->token() | |||
| *	Web->_curl: restrict redirections to HTTP | |||
| *	Web->minify(), skip importing of external files | |||
| *	Improved session and error handling in until() | |||
| *	Get the error trace array with the new $format parameter | |||
| *	Better support for unicode URLs | |||
| *	Optimized TZ detection with date_default_timezone_get() | |||
| *	format() Provide default decimal places | |||
| *	Optimize code: remove redundant TTL checks | |||
| *	Optimized timeout handling in Web->request() | |||
| *	Improved PHPDoc hints | |||
| *	Added missing russian DIACRITICS letters | |||
| *	DB\Cursor: allow child implementation of reset() | |||
| *	DB\Cursor: Copyfrom now does an internal call to set() | |||
| *	DB\SQL: Provide the ability to disable SQL logging | |||
| *	DB\SQL: improved query analysis to trigger fetchAll | |||
| *	DB\SQL\Mapper: added support for binary table columns | |||
| *	SQL,JIG,MONGO,CACHE Session handlers refactored and optimized | |||
| *	SMTP Refactoring and optimization | |||
| *	Bug fix: SMTP, Align quoted_printable_encode() with SMTP specs (dot-stuffing) | |||
| *	Bug fix: SMTP, Send buffered optional headers to output | |||
| *	Bug fix: SMTP, Content-Transfer-Encoding for non-TLS connections | |||
| *	Bug fix: SMTP, Single attachment error | |||
| *	Bug fix: Cursor->load not always mapping to first record | |||
| *	Bug fix: dry SQL mapper should not trigger 'load' | |||
| *	Bug fix: Code highlighting on empty text | |||
| *	Bug fix: Image->resize, round dimensions instead of cast | |||
| *	Bug fix: whitespace handling in $f3->compile() | |||
| *	Bug fix: TTL of `View` and `Preview` (`Template`) | |||
| *	Bug fix: token filter regex | |||
| *	Bug fix: Template, empty attributes | |||
| *	Bug fix: Preview->build() greedy regex | |||
| *	Bug fix: Web->minify() single-line comment on last line | |||
| *	Bug fix: Web->request(), follow_location with cURL and open_basedir | |||
| *	Bug fix: Web->send() Single quotes around filename not interpreted correctly by some browsers | |||
| 
 | |||
| 3.5.0 (2 June 2015) | |||
| *	NEW: until() method for long polling | |||
| *	NEW: abort() to disconnect HTTP client (and continue execution) | |||
| *	NEW: SQL Mapper->required() returns TRUE if field is not nullable | |||
| *	NEW: PREMAP variable for allowing prefixes to handlers named after HTTP verbs | |||
| *	NEW: [configs] section to allow config includes | |||
| *	NEW: Test->passed() returns TRUE if no test failed | |||
| *	NEW: SQL mapper changed() function | |||
| *	NEW: fatfree-core composer support | |||
| *	NEW: constants() method to expose constants | |||
| *	NEW: Preview->filter() for configurable token filters | |||
| *	NEW: CORS variable for Cross-Origin Resource Sharing support, #731 | |||
| *	Change in behavior: Switch to htmlspecialchars for escaping | |||
| *	Change in behavior: No movement in cursor position after erase(), #797 | |||
| *	Change in behavior: ERROR.trace is a multiline string now | |||
| *	Change in behavior: Strict token recognition in <include> href attribute | |||
| *	Router fix: loose method search | |||
| *	Better route precedence order, #12 | |||
| *	Preserve contents of ROUTES, #723 | |||
| *	Alias: allow array of parameters | |||
| *	Improvements on reroute method | |||
| *	Fix for custom Jig session files | |||
| *	Audit: better mobile detection | |||
| *	Audit: add argument to test string as browser agent | |||
| *	DB mappers: abort insert/update/erase from hooks, #684 | |||
| *	DB mappers: Allow array inputs in copyfrom() | |||
| *	Cache,SQL,Jig,Mongo Session: custom callback for suspect sessions | |||
| *	Fix for unexpected HIVE values when defining an empty HIVE array | |||
| *	SQL mapper: check for results from CALL and EXEC queries, #771 | |||
| *	SQL mapper: consider SQL schema prefix, #820 | |||
| *	SQL mapper: write to log before execution to | |||
| 	enable tracking of PDOStatement error | |||
| *	Add SQL Mapper->table() to return table name | |||
| *	Allow override of the schema in SQL Mapper->schema() | |||
| *	Improvement: Keep JIG table as reference, #758 | |||
| *	Expand regex to include whitespaces in SQL DB dsn, #817 | |||
| *	View: Removed reserved variables $fw and $implicit | |||
| *	Add missing newlines after template expansion | |||
| *	Web->receive: fix for complex field names, #806 | |||
| *	Web: Improvements in socket engine | |||
| *	Web: customizable user_agent for all engines, #822 | |||
| *	SMTP: Provision for Content-ID in attachments | |||
| *	Image + minify: allow absolute paths | |||
| *	Promote framework error to E_USER_ERROR | |||
| *	Geo->weather switch to OpenWeather | |||
| *	Expose mask() and grab() methods for routing | |||
| *	Expose trace() method to expose the debug backtrace | |||
| *	Implement recursion strategy using IteratorAggregate, #714 | |||
| *	Exempt whitespace between % and succeeding operator from being minified, #773 | |||
| *	Optimized error detection and ONERROR handler, fatfree-core#18 | |||
| *	Tweak error log output | |||
| *	Optimized If-Modified-Since cache header usage | |||
| *	Improved APCu compatibility, #724 | |||
| *	Bug fix: Web::send fails on filename with spaces, #810 | |||
| *	Bug fix: overwrite limit in findone() | |||
| *	Bug fix: locale-specific edge cases affecting SQL schema, #772 | |||
| *	Bug fix: Newline stripping in config() | |||
| *	Bug fix: bracket delimited identifier for sybase and dblib driver | |||
| *	Bug fix: Mongo mapper collection->count driver compatibility | |||
| *	Bug fix: SQL Mapper->set() forces adhoc value if already defined | |||
| *	Bug fix: Mapper ignores HAVING clause | |||
| *	Bug fix: Constructor invocation in call() | |||
| *	Bug fix: Wrong element returned by ajax/sync request | |||
| *	Bug fix: handling of non-consecutive compound key members | |||
| *	Bug fix: Virtual fields not retrieved when group option is present, #757 | |||
| *	Bug fix: group option generates incorrect SQL query, #757 | |||
| *	Bug fix: ONERROR does not receive PARAMS on fatal error | |||
| 
 | |||
| 3.4.0 (1 January 2015) | |||
| *	NEW: [redirects] section | |||
| *	NEW: Custom config sections | |||
| *	NEW: User-defined AUTOLOAD function | |||
| *	NEW: ONREROUTE variable | |||
| *	NEW: Provision for in-memory Jig database (#727) | |||
| *	Return run() result (#687) | |||
| *	Pass result of run() to mock() (#687) | |||
| *	Add port suffix to REALM variable | |||
| *	New attribute in <include> tag to extend hive | |||
| *	Adjust unit tests and clean up templates | |||
| *	Expose header-related methods | |||
| *	Web->request: allow content array | |||
| *	Preserve contents of ROUTES (#723) | |||
| *	Smart detection of PHP functions in template expressions | |||
| *	Add afterrender() hook to View class | |||
| *	Implement ArrayAccess and magic properties on hive | |||
| *	Improvement on mocking of superglobals and request body | |||
| *	Fix table creation for pgsql handled sessions | |||
| *	Add QUERY to hive | |||
| *	Exempt E_NOTICE from default error_reporting() | |||
| *	Add method to build alias routes from template, fixes #693 | |||
| *	Fix dangerous caching of cookie values | |||
| *	Fix multiple encoding in nested templates | |||
| *	Fix node attribute parsing for empty/zero values | |||
| *	Apply URL encoding on BASE to emulate v2 behavior (#123) | |||
| *	Improve Base->map performance (#595) | |||
| *	Add simple backtrace for fatal errors | |||
| *	Count Cursor->load() results (#581) | |||
| *	Add form field name to Web->receive() callback arguments | |||
| *	Fix missing newlines after template expansion | |||
| *	Fix overwrite of ENCODING variable | |||
| *	limit & offset workaround for SQL Server, fixes #671 | |||
| *	SQL Mapper->find: GROUP BY SQL compliant statement | |||
| *	Bug fix: Missing abstract method fields() | |||
| *	Bug fix: Auto escaping does not work with mapper objects (#710) | |||
| *	Bug fix: 'with' attribute in <include> tag raise error when no token | |||
| 	inside | |||
| *	View rendering: optional Content-Type header | |||
| *	Bug fix: Undefined variable: cache (#705) | |||
| *	Bug fix: Routing does not work if project base path includes valid | |||
| 	special URI character (#704) | |||
| *	Bug fix: Template hash collision (#702) | |||
| *	Bug fix: Property visibility is incorrect (#697) | |||
| *	Bug fix: Missing Allow header on HTTP 405 response | |||
| *	Bug fix: Double quotes in lexicon files (#681) | |||
| *	Bug fix: Space should not be mandatory in ICU pluralization format string | |||
| *	Bug fix: Incorrect log entry when SQL query contains a question mark | |||
| *	Bug fix: Error stack trace | |||
| *	Bug fix: Cookie expiration (#665) | |||
| *	Bug fix: OR operator (||) parsed incorrectly | |||
| *	Bug fix: Routing treatment of * wildcard character | |||
| *	Bug fix:  Mapper copyfrom() method doesn't allow class/object callbacks | |||
| 	(#590) | |||
| *	Bug fix: exists() creates elements/properties (#591) | |||
| *	Bug fix: Wildcard in routing pattern consumes entire query string (#592) | |||
| *	Bug fix: Workaround bug in latest MongoDB driver | |||
| *	Bug fix: Default error handler silently fails for AJAX request with | |||
| 	DEBUG>0 (#599) | |||
| *	Bug fix: Mocked BODY overwritten (#601) | |||
| *	Bug fix: Undefined pkey (#607) | |||
| 
 | |||
| 3.3.0 (8 August 2014) | |||
| *	NEW: Attribute in <include> tag to extend hive | |||
| *	NEW: Image overlay with transparency and alignment control | |||
| *	NEW: Allow redirection of specified route patterns to a URL | |||
| *	Bug fix: Missing AND operator in SQL Server schema query (Issue #576) | |||
| *	Count Cursor->load() results (Feature request #581) | |||
| *	Mapper copyfrom() method doesn't allow class/object callbacks (Issue #590) | |||
| *	Bug fix: exists() creates elements/properties (Issue #591) | |||
| *	Bug fix: Wildcard in routing pattern consumes entire query string | |||
| 	(Issue #592) | |||
| *	Tweak Base->map performance (Issue #595) | |||
| *	Bug fix: Default error handler silently fails for AJAX request with | |||
| 	DEBUG>0 (Issue #599) | |||
| *	Bug fix: Mocked BODY overwritten (Issue #601) | |||
| *	Bug fix: Undefined pkey (Issue #607) | |||
| *	Bug fix: beforeupdate() position (Issue #633) | |||
| *	Bug fix: exists() return value for cached keys | |||
| *	Bug fix: Missing error code in UNLOAD handler | |||
| *	Bug fix: OR operator (||) parsed incorrectly | |||
| *	Add input name parameter to custom slug function | |||
| *	Apply URL encoding on BASE to emulate v2 behavior (Issue #123) | |||
| *	Reduce mapper update() iterations | |||
| *	Bug fix: Routing treatment of * wildcard character | |||
| *	SQL Mapper->find: GROUP BY SQL compliant statement | |||
| *	Work around bug in latest MongoDB driver | |||
| *	Work around probable race condition and optimize cache access | |||
| *	View rendering: Optional Content-Type header | |||
| *	Fix missing newlines after template expansion | |||
| *	Add form field name to Web->receive() callback arguments | |||
| *	Quick reference: add RAW variable | |||
| 
 | |||
| 3.2.2 (19 March 2014) | |||
| *	NEW: Locales set automatically (Feature request #522) | |||
| *	NEW: Mapper dbtype() | |||
| *	NEW: before- and after- triggers for all mappers | |||
| *	NEW: Decode HTML5 entities if PHP>5.3 detected (Feature request #552) | |||
| *	NEW: Send credentials only if AUTH is present in the SMTP extension | |||
| 	response (Feature request #545) | |||
| *	NEW: BITMASK variable to allow ENT_COMPAT override | |||
| *	NEW: Redis support for caching | |||
| *	Enable SMTP feature detection | |||
| *	Enable extended ICU custom date format (Feature request #555) | |||
| *	Enable custom time ICU format | |||
| *	Add option to turn off session table creation (Feature request #557) | |||
| *	Enhanced template token rendering and custom filters (Feature request | |||
| 	#550) | |||
| *	Avert multiple loads in DB-managed sessions (Feature request #558) | |||
| *	Add EXEC to associative fetch | |||
| *	Bug fix: Building template tokens breaks on inline OR condition (Issue | |||
| 	#573) | |||
| *	Bug fix: SMTP->send does not use the $log parameter (Issue #571) | |||
| *	Bug fix: Allow setting sqlsrv primary keys on insert (Issue #570) | |||
| *	Bug fix: Generated query for obtaining table schema in sqlsrv incorrect | |||
| 	(Bug #565) | |||
| *	Bug fix: SQL mapper flag set even when value has not changed (Bug #562) | |||
| *	Bug fix: Add XFRAME config option (Feature request #546) | |||
| *	Bug fix: Incorrect parsing of comments (Issue #541) | |||
| *	Bug fix: Multiple Set-Cookie headers (Issue #533) | |||
| *	Bug fix: Mapper is dry after save() | |||
| *	Bug fix: Prevent infinite loop when error handler is triggered | |||
| 	(Issue #361) | |||
| *	Bug fix: Mapper tweaks not passing primary keys as arguments | |||
| *	Bug fix: Zero indexes in dot-notated arrays fail to compile | |||
| *	Bug fix: Prevent GROUP clause double-escaping | |||
| *	Bug fix: Regression of zlib compression bug | |||
| *	Bug fix: Method copyto() does not include ad hoc fields | |||
| *	Check existence of OpenID mode (Issue #529) | |||
| *	Generate a 404 when a tokenized class doesn't exist | |||
| *	Fix SQLite quotes (Issue #521) | |||
| *	Bug fix: BASE is incorrect on Windows | |||
| 
 | |||
| 3.2.1 (7 January 2014) | |||
| *	NEW: EMOJI variable, UTF->translate(), UTF->emojify(), and UTF->strrev() | |||
| *	Allow empty strings in config() | |||
| *	Add support for turning off php://input buffering via RAW | |||
| 	(FALSE by default) | |||
| *	Add Cursor->load() and Cursor->find() TTL support | |||
| *	Support Web->receive() large file downloads via PUT | |||
| *	ONERROR safety check | |||
| *	Fix session CSRF cookie detection | |||
| *	Framework object now passed to route handler contructors | |||
| *	Allow override of DIACRITICS | |||
| *	Various code optimizations | |||
| *	Support log disabling (Issue #483) | |||
| *	Implicit mapper load() on authentication | |||
| *	Declare abstract methods for Cursor derivatives | |||
| *	Support single-quoted HTML/XML attributes (Feature request #503) | |||
| *	Relax property visibility of mappers and derivatives | |||
| *	Deprecated: {{~ ~}} instructions and {{* *}} comments; Use {~ ~} and | |||
| 	{* *} instead | |||
| *	Minor fix: Audit->ipv4() return value | |||
| *	Bug fix: Backslashes in BASE not converted on Windows | |||
| *	Bug fix: UTF->substr() with negative offset and specified length | |||
| *	Bug fix: Replace named URL tokens on render() | |||
| *	Bug fix: BASE is not empty when run from document root | |||
| *	Bug fix: stringify() recursion | |||
| 
 | |||
| 3.2.0 (18 December 2013) | |||
| *	NEW: Automatic CSRF protection (with IP and User-Agent checks) for | |||
| 	sessions mapped to SQL-, Jig-, Mongo- and Cache-based backends | |||
| *	NEW: Named routes | |||
| *	NEW: PATH variable; returns the URL relative to BASE | |||
| *	NEW: Image->captcha() color parameters | |||
| *	NEW: Ability to access MongoCuror thru the cursor() method | |||
| *	NEW: Mapper->fields() method returns array of field names | |||
| *	NEW: Mapper onload(), oninsert(), onupdate(), and onerase() event | |||
| 	listeners/triggers | |||
| *	NEW: Preview class (a lightweight template engine) | |||
| *	NEW: rel() method derives path from URL relative to BASE; useful for | |||
| 	rerouting | |||
| *	NEW: PREFIX variable for prepending a string to a dictionary term; | |||
| 	Enable support for prefixed dictionary arrays and .ini files (Feature | |||
| 	request #440) | |||
| *	NEW: Google static map plugin | |||
| *	NEW: devoid() method | |||
| *	Introduce clean(); similar to scrub(), except that arg is passed by | |||
| 	value | |||
| *	Use $ttl for cookie expiration (Issue #457) | |||
| *	Fix needs_rehash() cost comparison | |||
| *	Add pass-by-reference argument to exists() so if method returns TRUE, | |||
| 	a subsequent get() is unnecessary | |||
| *	Improve MySQL support | |||
| *	Move esc(), raw(), and dupe() to View class where they more | |||
| 	appropriately belong | |||
| *	Allow user-defined fields in SQL mapper constructor (Feature request | |||
| 	#450) | |||
| *	Re-implement the pre-3.0 template resolve() feature | |||
| *	Remove redundant instances of session_commit() | |||
| *	Add support for input filtering in Mapper->copyfrom() | |||
| *	Prevent intrusive behavior of Mapper->copyfrom() | |||
| *	Support multiple SQL primary keys | |||
| *	Support custom tag attributes/inline tokens defined at runtime | |||
| 	(Feature request #438) | |||
| *	Broader support for HTTP basic auth | |||
| *	Prohibit Jig _id clear() | |||
| *	Add support for detailed stringify() output | |||
| *	Add base directory to UI path as fallback | |||
| *	Support Test->expect() chaining | |||
| *	Support __tostring() in stringify() | |||
| *	Trigger error on invalid CAPTCHA length (Issue #458) | |||
| *	Bug fix: exists() pass-by-reference argument returns incorrect value | |||
| *	Bug fix: DB Exec does not return affected row if query contains a | |||
| 	sub-SELECT (Issue #437) | |||
| *	Improve seed generator and add code for detecting of acceptable | |||
| 	limits in Image->captcha() (Feature request #460) | |||
| *	Add decimal format ICU extension | |||
| *	Bug fix: 404-reported URI contains HTTP query | |||
| *	Bug fix: Data type detection in DB->schema() | |||
| *	Bug fix: TZ initialization | |||
| *	Bug fix: paginate() passes incorrect argument to count() | |||
| *	Bug fix: Incorrect query when reloading after insert() | |||
| *	Bug fix: SQL preg_match error in pdo_type matching (Issue #447) | |||
| *	Bug fix: Missing merge() function (Issue #444) | |||
| *	Bug fix: BASE misdefined in command line mode | |||
| *	Bug fix: Stringifying hive may run infinite (Issue #436) | |||
| *	Bug fix: Incomplete stringify() when DEBUG<3 (Issue #432) | |||
| *	Bug fix: Redirection of basic auth (Issue #430) | |||
| *	Bug fix: Filter only PHP code (including short tags) in templates | |||
| *	Bug fix: Markdown paragraph parser does not convert PHP code blocks | |||
| 	properly | |||
| *	Bug fix: identicon() colors on same keys are randomized | |||
| *	Bug fix: quotekey() fails on aliased keys | |||
| *	Bug fix: Missing _id in Jig->find() return value | |||
| *	Bug fix: LANGUAGE/LOCALES handling | |||
| *	Bug fix: Loose comparison in stringify() | |||
| 
 | |||
| 3.1.2 (5 November 2013) | |||
| *	Abandon .chm help format; Package API documentation in plain HTML; | |||
| 	(Launch lib/api/index.html in your browser) | |||
| *	Deprecate BAIL in favor of HALT (default: TRUE) | |||
| *	Revert to 3.1.0 autoload behavior; Add support for lowercase folder | |||
| 	names | |||
| *	Allow Spring-style HTTP method overrides | |||
| *	Add support for SQL Server-based sessions | |||
| *	Capture full X-Forwarded-For header | |||
| *	Add protection against malicious scripts; Extra check if file was really | |||
| 	uploaded | |||
| *	Pass-thru page limit in return value of Cursor->paginate() | |||
| *	Optimize code: Implement single-pass escaping | |||
| *	Short circuit Jig->find() if source file is empty | |||
| *	Bug fix: PHP globals passed by reference in hive() result (Issue #424) | |||
| *	Bug fix: ZIP mime type incorrect behavior | |||
| *	Bug fix: Jig->erase() filter malfunction | |||
| *	Bug fix: Mongo->select() group | |||
| *	Bug fix: Unknown bcrypt constant | |||
| 
 | |||
| 3.1.1 (13 October 2013) | |||
| *	NEW: Support OpenID attribute exchange | |||
| *	NEW: BAIL variable enables/disables continuance of execution on non-fatal | |||
| 	errors | |||
| *	Deprecate BAIL in favor of HALT (default: FALSE) | |||
| *	Add support for Oracle | |||
| *	Mark cached queries in log (Feature Request #405) | |||
| *	Implement Bcrypt->needs_reshash() | |||
| *	Add entropy to SQL cache hash; Add uuid() method to DB backends | |||
| *	Find real document root; Simplify debug paths | |||
| *	Permit OpenID required fields to be declared as comma-separated string or | |||
| 	array | |||
| *	Pass modified filename as argument to user-defined function in | |||
| 	Web->receive() | |||
| *	Quote keys in optional SQL clauses (Issue #408) | |||
| *	Allow UNLOAD to override fatal error detection (Issue #404) | |||
| *	Mutex operator precedence error (Issue #406) | |||
| *	Bug fix: exists() malfunction (Issue #401) | |||
| *	Bug fix: Jig mapper triggers error when loading from CACHE (Issue #403) | |||
| *	Bug fix: Array index check | |||
| *	Bug fix: OpenID verified() return value | |||
| *	Bug fix: Basket->find() should return a set of results (Issue #407); | |||
| 	Also implemented findone() for consistency with mappers | |||
| *	Bug fix: PostgreSQL last insert ID (Issue #410) | |||
| *	Bug fix: $port component URL overwritten by _socket() | |||
| *	Bug fix: Calculation of elapsed time | |||
| 
 | |||
| 3.1.0 (20 August 2013) | |||
| *	NEW: Web->filler() returns a chunk of text from the standard | |||
| 	Lorem Ipsum passage | |||
| *	Change in behavior: Drop support for JSON serialization | |||
| *	SQL->exec() now returns value of RETURNING clause | |||
| *	Add support for $ttl argument in count() (Issue #393) | |||
| *	Allow UI to be overridden by custom $path | |||
| *	Return result of PDO primitives: begintransaction(), rollback(), and | |||
| 	commit() | |||
| *	Full support for PHP 5.5 | |||
| *	Flush buffers only when DEBUG=0 | |||
| *	Support class->method, class::method, and lambda functions as | |||
| 	Web->basic() arguments | |||
| *	Commit session on Basket->save() | |||
| *	Optional enlargement in Image->resize() | |||
| *	Support authentication on hosts running PHP-CGI | |||
| *	Change visibility level of Cache properties | |||
| *	Prevent ONERROR recursion | |||
| *	Work around Apache pre-2.4 VirtualDocumentRoot bug | |||
| *	Prioritize cURL in HTTP engine detection | |||
| *	Bug fix: Minify tricky JS | |||
| *	Bug fix: desktop() detection | |||
| *	Bug fix: Double-slash on TEMP-relative path | |||
| *	Bug fix: Cursor mapping of first() and last() records | |||
| *	Bug fix: Premature end of Web->receive() on multiple files | |||
| *	Bug fix: German umlaute to its corresponding grammatically-correct | |||
| 	equivalent | |||
| 
 | |||
| 3.0.9 (12 June 2013) | |||
| *	NEW: Web->whois() | |||
| *	NEW: Template <switch> <case> tags | |||
| *	Improve CACHE consistency | |||
| *	Case-insensitive MIME type detection | |||
| *	Support pre-PHP 5.3.4 in Prefab->instance() | |||
| *	Refactor isdesktop() and ismobile(); Add isbot() | |||
| *	Add support for Markdown strike-through | |||
| *	Work around ODBC's lack of quote() support | |||
| *	Remove useless Prefab destructor | |||
| *	Support multiple cache instances | |||
| *	Bug fix: Underscores in OpenId keys mangled | |||
| *	Refactor format() | |||
| *	Numerous tweaks | |||
| *	Bug fix: MongoId object not preserved | |||
| *	Bug fix: Double-quotes included in lexicon() string (Issue #341) | |||
| *	Bug fix: UTF-8 formatting mangled on Windows (Issue #342) | |||
| *	Bug fix: Cache->load() error when CACHE is FALSE (Issue #344) | |||
| *	Bug fix: send() ternary expression | |||
| *	Bug fix: Country code constants | |||
| 
 | |||
| 3.0.8 (17 May 2013) | |||
| *	NEW: Bcrypt lightweight hashing library\ | |||
| *	Return total number of records in superset in Cursor->paginate() | |||
| *	ONERROR short-circuit (Enhancement #334) | |||
| *	Apply quotes/backticks on DB identifiers | |||
| *	Allow enabling/disabling of SQL log | |||
| *	Normalize glob() behavior (Issue #330) | |||
| *	Bug fix: mbstring 2-byte text truncation (Issue #325) | |||
| *	Bug fix: Unsupported operand types (Issue #324) | |||
| 
 | |||
| 3.0.7 (2 May 2013) | |||
| *	NEW: route() now allows an array of routing patterns as first argument; | |||
| 	support array as first argument of map() | |||
| *	NEW: entropy() for calculating password strength (NIST 800-63) | |||
| *	NEW: AGENT variable containing auto-detected HTTP user agent string | |||
| *	NEW: ismobile() and isdesktop() methods | |||
| *	NEW: Prefab class and descendants now accept constructor arguments | |||
| *	Change in behavior: Cache->exists() now returns timestamp and TTL of | |||
| 	cache entry or FALSE if not found (Feature request #315) | |||
| *	Preserve timestamp and TTL when updating cache entry (Feature request | |||
| 	#316) | |||
| *	Improved currency formatting with C99 compliance | |||
| *	Suppress unnecessary program halt at startup caused by misconfigured | |||
| 	server | |||
| *	Add support for dashes in custom attribute names in templates | |||
| *	Bug fix: Routing precedene (Issue #313) | |||
| *	Bug fix: Remove Jig _id element from document property | |||
| *	Bug fix: Web->rss() error when not enough items in the feed (Issue #299) | |||
| *	Bug fix: Web engine fallback (Issue #300) | |||
| *	Bug fix: <strong> and <em> formatting | |||
| *	Bug fix: Text rendering of text with trailing punctuation (Issue #303) | |||
| *	Bug fix: Incorrect regex in SMTP | |||
| 
 | |||
| 3.0.6 (31 Mar 2013) | |||
| *	NEW: Image->crop() | |||
| *	Modify documentation blocks for PHPDoc interoperability | |||
| *	Allow user to control whether Base->rerouet() uses a permanent or | |||
| 	temporary redirect | |||
| *	Allow JAR elements to be set individually | |||
| *	Refactor DB\SQL\Mapper->insert() to cope with autoincrement fields | |||
| *	Trigger error when captcha() font is missing | |||
| *	Remove unnecessary markdown regex recursion | |||
| *	Check for scalars instead of DB\SQL strings | |||
| *	Implement more comprehensive diacritics table | |||
| *	Add option for disabling 401 errors when basic auth() fails | |||
| *	Add markdown syntax highlighting for Apache configuration | |||
| *	Markdown->render() deprecated to remove dependency on UI variable; | |||
| 	Feature replaced by Markdown->convert() to enable translation from | |||
| 	markdown string to HTML | |||
| *	Optimize factory() code of all data mappers | |||
| *	Apply backticks on MySQL table names | |||
| *	Bug fix: Routing failure when directory path contains a tilde (Issue #291) | |||
| *	Bug fix: Incorrect markdown parsing of strong/em sequences and inline HTML | |||
| *	Bug fix: Cached page not echoed (Issue #278) | |||
| *	Bug fix: Object properties not escaped when rendering | |||
| *	Bug fix: OpenID error response ignored | |||
| *	Bug fix: memcache_get_extended_stats() timeout | |||
| *	Bug fix: Base->set() doesn't pass TTL to Cache->set() | |||
| *	Bug fix: Base->scrub() ignores pass-thru * argument (Issue #274) | |||
| 
 | |||
| 3.0.5 (16 Feb 2013) | |||
| *	NEW: Markdown class with PHP, HTML, and .ini syntax highlighting support | |||
| *	NEW: Options for caching of select() and find() results | |||
| *	NEW: Web->acceptable() | |||
| *	Add send() argument for forcing downloads | |||
| *	Provide read() option for applying Unix LF as standard line ending | |||
| *	Bypass lexicon() call if LANGUAGE is undefined | |||
| *	Load fallback language dictionary if LANGUAGE is undefined | |||
| *	map() now checks existence of class/methods for non-tokenized URLs | |||
| *	Improve error reporting of non-existent Template methods | |||
| *	Address output buffer issues on some servers | |||
| *	Bug fix: Setting DEBUG to 0 won't suppress the stack trace when the | |||
| 	content type is application/json (Issue #257) | |||
| *	Bug fix: Image dump/render additional arguments shifted | |||
| *	Bug fix: ob_clean() causes buffer issues with zlib compression | |||
| *	Bug fix: minify() fails when commenting CSS @ rules (Issue #251) | |||
| *	Bug fix: Handling of commas inside quoted strings | |||
| *	Bug fix: Glitch in stringify() handling of closures | |||
| *	Bug fix: dry() in mappers returns TRUE despite being hydrated by | |||
| 	factory() (Issue #265) | |||
| *	Bug fix: expect() not handling flags correctly | |||
| *	Bug fix: weather() fails when server is unreachable | |||
| 
 | |||
| 3.0.4 (29 Jan 2013) | |||
| *	NEW: Support for ICU/CLDR pluralization | |||
| *	NEW: User-defined FALLBACK language | |||
| *	NEW: minify() now recognizes CSS @import directives | |||
| *	NEW: UTF->bom() returns byte order mark for UTF-8 encoding | |||
| *	Expose SQL\Mapper->schema() | |||
| *	Change in behavior: Send error response as JSON string if AJAX request is | |||
| 	detected | |||
| *	Deprecated: afind*() methods | |||
| *	Discard output buffer in favor of debug output | |||
| *	Make _id available to Jig queries | |||
| *	Magic class now implements ArrayAccess | |||
| *	Abort execution on startup errors | |||
| *	Suppress stack trace on DEBUG level 0 | |||
| *	Allow single = as equality operator in Jig query expressions | |||
| *	Abort OpenID discovery if Web->request() fails | |||
| *	Mimic PHP *RECURSION* in stringify() | |||
| *	Modify Jig parser to allow wildcard-search using preg_match() | |||
| *	Abort execution after error() execution | |||
| *	Concatenate cached/uncached minify() iterations; Prevent spillover | |||
| 	caching of previous minify() result | |||
| *	Work around obscure PHP session id regeneration bug | |||
| *	Revise algorithm for Jig filter involving undefined fields (Issue #230) | |||
| *	Use checkdnsrr() instead of gethostbyname() in DNSBL check | |||
| *	Auto-adjust pagination to cursor boundaries | |||
| *	Add Romanian diacritics | |||
| *	Bug fix: Root namespace reference and sorting with undefined Jig fields | |||
| *	Bug fix: Greedy receive() regex | |||
| *	Bug fix: Default LANGUAGE always 'en' | |||
| *	Bug fix: minify() hammers cache backend | |||
| *	Bug fix: Previous values of primary keys not saved during factory() | |||
| 	instantiation | |||
| *	Bug fix: Jig find() fails when search key is not present in all records | |||
| *	Bug fix: Jig SORT_DESC (Issue #233) | |||
| *	Bug fix: Error reporting (Issue #225) | |||
| *	Bug fix: language() return value | |||
| 
 | |||
| 3.0.3 (29 Dec 2013) | |||
| *	NEW: [ajax] and [sync] routing pattern modifiers | |||
| *	NEW: Basket class (session-based pseudo-mapper, shopping cart, etc.) | |||
| *	NEW: Test->message() method | |||
| *	NEW: DB profiling via DB->log() | |||
| *	NEW: Matrix->calendar() | |||
| *	NEW: Audit->card() and Audit->mod10() for credit card verification | |||
| *	NEW: Geo->weather() | |||
| *	NEW: Base->relay() accepts comma-separated callbacks; but unlike | |||
| 	Base->chain(), result of previous callback becomes argument of the next | |||
| *	Numerous performance tweaks | |||
| *	Interoperability with new MongoClient class | |||
| *	Web->request() now recognizes gzip and deflate encoding | |||
| *	Differences in behavior of Web->request() engines rectified | |||
| *	mutex() now uses an ID as argument (instead of filename to make it clear | |||
| 	that specified file is not the target being locked, but a primitive | |||
| 	cross-platform semaphore) | |||
| *	DB\SQL\Mapper field _id now returned even in the absence of any | |||
| 	auto-increment field | |||
| *	Magic class spinned off as a separate file | |||
| *	ISO 3166-1 alpha-2 table updated | |||
| *	Apache redirect emulation for PHP 5.4 CLI server mode | |||
| *	Framework instance now passed as argument to any user-defined shutdown | |||
| 	function | |||
| *	Cache engine now used as storage for Web->minify() output | |||
| *	Flag added for enabling/disabling Image class filter history | |||
| *	Bug fix: Trailing routing token consumes HTTP query | |||
| *	Bug fix: LANGUAGE spills over to LOCALES setting | |||
| *	Bug fix: Inconsistent dry() return value | |||
| *	Bug fix: URL-decoding | |||
| 
 | |||
| 3.0.2 (23 Dec 2013) | |||
| *	NEW: Syntax-highlighted stack traces via Base->highlight(); boolean | |||
| 	HIGHLIGHT global variable can be used to enable/disable this feature | |||
| *	NEW: Template engine <ignore> tag | |||
| *	NEW: Image->captcha() | |||
| *	NEW: DNSBL-based spammer detection (ported from 2.x) | |||
| *	NEW: paginate(), first(), and last() methods for data mappers | |||
| *	NEW: X-HTTP-Method-Override header now recognized | |||
| *	NEW: Base->chain() method for executing callbacks in succession | |||
| *	NEW: HOST global variable; derived from either $_SERVER['SERVER_NAME'] or | |||
| 	gethostname() | |||
| *	NEW: REALM global variable representing full canonical URI | |||
| *	NEW: Auth plug-in | |||
| *	NEW: Pingback plug-in (implements both Pingback 1.0 protocol client and | |||
| 	server) | |||
| *	NEW: DEBUG verbosity can now reach up to level 3; Base->stringify() drills | |||
| 	down to object properties at this setting | |||
| *	NEW: HTTP PATCH method added to recognized HTTP ReST methods | |||
| *	Web->slug() now trims trailing dashes | |||
| *	Web->request() now allows relative local URLs as argument | |||
| *	Use of PARAMS in route handlers now unnecessary; framework now passes two | |||
| 	arguments to route handlers: the framework object instance and an array | |||
| 	containing the captured values of tokens in route patterns | |||
| *	Standardized timeout settings among Web->request() backends | |||
| *	Session IDs regenerated for additional security | |||
| *	Automatic HTTP 404 responses by Base->call() now restricted to route | |||
| 	handlers | |||
| *	Empty comments in ini-style files now parsed properly | |||
| *	Use file_get_contents() in methods that don't involve high concurrency | |||
| 
 | |||
| 3.0.1 (14 Dec 2013) | |||
| *	Major rewrite of much of the framework's core features | |||
| @ -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 | |||
| @ -0,0 +1,191 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! Data validator | |||
| class Audit extends Prefab { | |||
| 
 | |||
| 	//@{ User agents | |||
| 	const | |||
| 		UA_Mobile='android|blackberry|phone|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=[]; | |||
| 		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 | |||
| 	*	@param $agent string | |||
| 	**/ | |||
| 	function isdesktop($agent=NULL) { | |||
| 		if (!isset($agent)) | |||
| 			$agent=Base::instance()->AGENT; | |||
| 		return (bool)preg_match('/('.self::UA_Desktop.')/i',$agent) && | |||
| 			!$this->ismobile($agent); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return TRUE if user agent is a mobile device | |||
| 	*	@return bool | |||
| 	*	@param $agent string | |||
| 	**/ | |||
| 	function ismobile($agent=NULL) { | |||
| 		if (!isset($agent)) | |||
| 			$agent=Base::instance()->AGENT; | |||
| 		return (bool)preg_match('/('.self::UA_Mobile.')/i',$agent); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return TRUE if user agent is a Web bot | |||
| 	*	@return bool | |||
| 	*	@param $agent string | |||
| 	**/ | |||
| 	function isbot($agent=NULL) { | |||
| 		if (!isset($agent)) | |||
| 			$agent=Base::instance()->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}$|'. | |||
| 				'^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)\d{12}$/',$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)); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,262 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! 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, | |||
| 		//! Custom compare function | |||
| 		$func; | |||
| 
 | |||
| 	/** | |||
| 	*	Jig storage handler | |||
| 	*	@return bool | |||
| 	*	@param $id string | |||
| 	*	@param $pw string | |||
| 	*	@param $realm string | |||
| 	**/ | |||
| 	protected function _jig($id,$pw,$realm) { | |||
| 		$success = (bool) | |||
| 			call_user_func_array( | |||
| 				[$this->mapper,'load'], | |||
| 				[ | |||
| 					array_merge( | |||
| 						[ | |||
| 							'@'.$this->args['id'].'==?'. | |||
| 							($this->func?'':' AND @'.$this->args['pw'].'==?'). | |||
| 							(isset($this->args['realm'])? | |||
| 								(' AND @'.$this->args['realm'].'==?'):''), | |||
| 							$id | |||
| 						], | |||
| 						($this->func?[]:[$pw]), | |||
| 						(isset($this->args['realm'])?[$realm]:[]) | |||
| 					) | |||
| 				] | |||
| 			); | |||
| 		if ($success && $this->func) | |||
| 			$success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw'])); | |||
| 		return $success; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	MongoDB storage handler | |||
| 	*	@return bool | |||
| 	*	@param $id string | |||
| 	*	@param $pw string | |||
| 	*	@param $realm string | |||
| 	**/ | |||
| 	protected function _mongo($id,$pw,$realm) { | |||
| 		$success = (bool) | |||
| 			$this->mapper->load( | |||
| 				[$this->args['id']=>$id]+ | |||
| 				($this->func?[]:[$this->args['pw']=>$pw])+ | |||
| 				(isset($this->args['realm'])? | |||
| 					[$this->args['realm']=>$realm]:[]) | |||
| 			); | |||
| 		if ($success && $this->func) | |||
| 			$success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw'])); | |||
| 		return $success; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	SQL storage handler | |||
| 	*	@return bool | |||
| 	*	@param $id string | |||
| 	*	@param $pw string | |||
| 	*	@param $realm string | |||
| 	**/ | |||
| 	protected function _sql($id,$pw,$realm) { | |||
| 		$success = (bool) | |||
| 			call_user_func_array( | |||
| 				[$this->mapper,'load'], | |||
| 				[ | |||
| 					array_merge( | |||
| 						[ | |||
| 							$this->args['id'].'=?'. | |||
| 							($this->func?'':' AND '.$this->args['pw'].'=?'). | |||
| 							(isset($this->args['realm'])? | |||
| 								(' AND '.$this->args['realm'].'=?'):''), | |||
| 							$id | |||
| 						], | |||
| 						($this->func?[]:[$pw]), | |||
| 						(isset($this->args['realm'])?[$realm]:[]) | |||
| 					) | |||
| 				] | |||
| 			); | |||
| 		if ($success && $this->func) | |||
| 			$success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw'])); | |||
| 		return $success; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	LDAP storage handler | |||
| 	*	@return bool | |||
| 	*	@param $id string | |||
| 	*	@param $pw string | |||
| 	**/ | |||
| 	protected function _ldap($id,$pw) { | |||
| 		$port=(int)($this->args['port']?:389); | |||
| 		$filter=$this->args['filter']=$this->args['filter']?:"uid=".$id; | |||
| 		$this->args['attr']=$this->args['attr']?:["uid"]; | |||
| 		array_walk($this->args['attr'], | |||
| 		function($attr)use(&$filter,$id) { | |||
| 			$filter=str_ireplace($attr."=*",$attr."=".$id,$filter);}); | |||
| 		$dc=@ldap_connect($this->args['dc'],$port); | |||
| 		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'], | |||
| 				$filter,$this->args['attr'])) && | |||
| 			ldap_count_entries($dc,$result) && | |||
| 			($info=ldap_get_entries($dc,$result)) && | |||
| 			$info['count']==1 && | |||
| 			@ldap_bind($dc,$info[0]['dn'],$pw) && | |||
| 			@ldap_close($dc)) { | |||
| 			return in_array($id,(array_map(function($value){return $value[0];}, | |||
| 				array_intersect_key($info[0], | |||
| 					array_flip($this->args['attr'])))),TRUE); | |||
| 		} | |||
| 		user_error(self::E_LDAP,E_USER_ERROR); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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->HOST); | |||
| 			if (strtolower($this->args['scheme'])=='tls') { | |||
| 				$dialog('STARTTLS'); | |||
| 				stream_socket_enable_crypto( | |||
| 					$socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); | |||
| 				$dialog('EHLO '.$fw->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,E_USER_ERROR); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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->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 | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function __construct($storage,array $args=NULL,$func=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; | |||
| 		$this->func=$func; | |||
| 	} | |||
| 
 | |||
| } | |||
								
									
										File diff suppressed because it is too large
									
								
							
						
					| @ -0,0 +1,239 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! Session-based pseudo-mapper | |||
| class Basket extends Magic { | |||
| 
 | |||
| 	//@{ Error messages | |||
| 	const | |||
| 		E_Field='Undefined field %s'; | |||
| 	//@} | |||
| 
 | |||
| 	protected | |||
| 		//! Session key | |||
| 		$key, | |||
| 		//! Current item identifier | |||
| 		$id, | |||
| 		//! Current item contents | |||
| 		$item=[]; | |||
| 
 | |||
| 	/** | |||
| 	*	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),E_USER_ERROR); | |||
| 		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 | |||
| 	*	@param $key string | |||
| 	*	@param $val mixed | |||
| 	**/ | |||
| 	function find($key=NULL,$val=NULL) { | |||
| 		$out=[]; | |||
| 		if (isset($_SESSION[$this->key])) { | |||
| 			foreach ($_SESSION[$this->key] as $id=>$item) | |||
| 				if (!isset($key) || | |||
| 					array_key_exists($key,$item) && $item[$key]==$val || | |||
| 					$key=='_id' && $id==$val) { | |||
| 					$obj=clone($this); | |||
| 					$obj->id=$id; | |||
| 					$obj->item=$item; | |||
| 					$out[]=$obj; | |||
| 				} | |||
| 		} | |||
| 		return $out; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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 []; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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=[]; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Empty basket | |||
| 	*	@return NULL | |||
| 	**/ | |||
| 	function drop() { | |||
| 		unset($_SESSION[$this->key]); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Hydrate item using hive array variable | |||
| 	*	@return NULL | |||
| 	*	@param $var array|string | |||
| 	**/ | |||
| 	function copyfrom($var) { | |||
| 		if (is_string($var)) | |||
| 			$var=\Base::instance()->$var; | |||
| 		foreach ($var as $key=>$val) | |||
| 			$this->set($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 []; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Instantiate class | |||
| 	*	@return void | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function __construct($key='basket') { | |||
| 		$this->key=$key; | |||
| 		if (session_status()!=PHP_SESSION_ACTIVE) | |||
| 			session_start(); | |||
| 		Base::instance()->sync('SESSION'); | |||
| 		$this->reset(); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,96 @@ | |||
| <?php | |||
| 
 | |||
| /** | |||
| *	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| * | |||
| *	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| * | |||
| *	This is free software: you can redistribute it and/or modify it under the | |||
| *	terms of the GNU General Public License as published by the Free Software | |||
| *	Foundation, either version 3 of the License, or later. | |||
| * | |||
| *	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| *	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| *	General Public License for more details. | |||
| * | |||
| *	You should have received a copy of the GNU General Public License along | |||
| *	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| * | |||
| **/ | |||
| 
 | |||
| /** | |||
| *	Lightweight password hashing library (PHP 5.5+ only) | |||
| *	@deprecated Use http://php.net/manual/en/ref.password.php instead | |||
| **/ | |||
| 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,E_USER_ERROR); | |||
| 		$len=22; | |||
| 		if ($salt) { | |||
| 			if (!preg_match('/^[[:alnum:]\.\/]{'.$len.',}$/',$salt)) | |||
| 				user_error(self::E_SaltArg,E_USER_ERROR); | |||
| 		} | |||
| 		else { | |||
| 			$raw=16; | |||
| 			$iv=''; | |||
| 			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; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,487 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace CLI; | |||
| 
 | |||
| //! RFC6455 WebSocket server | |||
| class WS { | |||
| 
 | |||
| 	const | |||
| 		//! UUID magic string | |||
| 		Magic='258EAFA5-E914-47DA-95CA-C5AB0DC85B11', | |||
| 		//! Max packet size | |||
| 		Packet=65536; | |||
| 
 | |||
| 	//@{ Mask bits for first byte of header | |||
| 	const | |||
| 		Text=0x01, | |||
| 		Binary=0x02, | |||
| 		Close=0x08, | |||
| 		Ping=0x09, | |||
| 		Pong=0x0a, | |||
| 		OpCode=0x0f, | |||
| 		Finale=0x80; | |||
| 	//@} | |||
| 
 | |||
| 	//@{ Mask bits for second byte of header | |||
| 	const | |||
| 		Length=0x7f; | |||
| 	//@} | |||
| 
 | |||
| 	protected | |||
| 		$addr, | |||
| 		$ctx, | |||
| 		$wait, | |||
| 		$sockets, | |||
| 		$protocol, | |||
| 		$agents=[], | |||
| 		$events=[]; | |||
| 
 | |||
| 	/** | |||
| 	*	Allocate stream socket | |||
| 	*	@return NULL | |||
| 	*	@param $socket resource | |||
| 	**/ | |||
| 	function alloc($socket) { | |||
| 		if (is_bool($buf=$this->read($socket))) | |||
| 			return; | |||
| 		// Get WebSocket headers | |||
| 		$hdrs=[]; | |||
| 		$EOL="\r\n"; | |||
| 		$verb=NULL; | |||
| 		$uri=NULL; | |||
| 		foreach (explode($EOL,trim($buf)) as $line) | |||
| 			if (preg_match('/^(\w+)\s(.+)\sHTTP\/[\d.]{1,3}$/', | |||
| 				trim($line),$match)) { | |||
| 				$verb=$match[1]; | |||
| 				$uri=$match[2]; | |||
| 			} | |||
| 			else | |||
| 			if (preg_match('/^(.+): (.+)/',trim($line),$match)) | |||
| 				// Standardize header | |||
| 				$hdrs[ | |||
| 					strtr( | |||
| 						ucwords( | |||
| 							strtolower( | |||
| 								strtr($match[1],'-',' ') | |||
| 							) | |||
| 						),' ','-' | |||
| 					) | |||
| 				]=$match[2]; | |||
| 			else { | |||
| 				$this->close($socket); | |||
| 				return; | |||
| 			} | |||
| 		if (empty($hdrs['Upgrade']) && | |||
| 			empty($hdrs['Sec-Websocket-Key'])) { | |||
| 			// Not a WebSocket request | |||
| 			if ($verb && $uri) | |||
| 				$this->write( | |||
| 					$socket, | |||
| 					'HTTP/1.1 400 Bad Request'.$EOL. | |||
| 					'Connection: close'.$EOL.$EOL | |||
| 				); | |||
| 			$this->close($socket); | |||
| 			return; | |||
| 		} | |||
| 		// Handshake | |||
| 		$buf='HTTP/1.1 101 Switching Protocols'.$EOL. | |||
| 			'Upgrade: websocket'.$EOL. | |||
| 			'Connection: Upgrade'.$EOL; | |||
| 		if (isset($hdrs['Sec-Websocket-Protocol'])) | |||
| 			$buf.='Sec-WebSocket-Protocol: '. | |||
| 				$hdrs['Sec-Websocket-Protocol'].$EOL; | |||
| 		$buf.='Sec-WebSocket-Accept: '. | |||
| 			base64_encode( | |||
| 				sha1($hdrs['Sec-Websocket-Key'].WS::Magic,TRUE) | |||
| 			).$EOL.$EOL; | |||
| 		if ($this->write($socket,$buf)) { | |||
| 			// Connect agent to server | |||
| 			$this->sockets[(int)$socket]=$socket; | |||
| 			$this->agents[(int)$socket]= | |||
| 				new Agent($this,$socket,$verb,$uri,$hdrs); | |||
| 		} | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Close stream socket | |||
| 	*	@return NULL | |||
| 	*	@param $socket resource | |||
| 	**/ | |||
| 	function close($socket) { | |||
| 		if (isset($this->agents[(int)$socket])) | |||
| 			unset($this->sockets[(int)$socket],$this->agents[(int)$socket]); | |||
| 		stream_socket_shutdown($socket,STREAM_SHUT_WR); | |||
| 		@fclose($socket); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Read from stream socket | |||
| 	*	@return string|FALSE | |||
| 	*	@param $socket resource | |||
| 	*	@param $len int | |||
| 	**/ | |||
| 	function read($socket,$len=0) { | |||
| 		if (!$len) | |||
| 			$len=WS::Packet; | |||
| 		if (is_string($buf=@fread($socket,$len)) && | |||
| 			strlen($buf) && strlen($buf)<$len) | |||
| 			return $buf; | |||
| 		if (isset($this->events['error']) && | |||
| 			is_callable($func=$this->events['error'])) | |||
| 			$func($this); | |||
| 		$this->close($socket); | |||
| 		return FALSE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Write to stream socket | |||
| 	*	@return int|FALSE | |||
| 	*	@param $socket resource | |||
| 	*	@param $buf string | |||
| 	**/ | |||
| 	function write($socket,$buf) { | |||
| 		for ($i=0,$bytes=0;$i<strlen($buf);$i+=$bytes) { | |||
| 			if (($bytes=@fwrite($socket,substr($buf,$i))) && | |||
| 				@fflush($socket)) | |||
| 				continue; | |||
| 			if (isset($this->events['error']) && | |||
| 				is_callable($func=$this->events['error'])) | |||
| 				$func($this); | |||
| 			$this->close($socket); | |||
| 			return FALSE; | |||
| 		} | |||
| 		return $bytes; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return socket agents | |||
| 	*	@return array | |||
| 	*	@param $uri string | |||
| 	***/ | |||
| 	function agents($uri=NULL) { | |||
| 		return array_filter( | |||
| 			$this->agents, | |||
| 			/** | |||
| 			 * @var $val Agent | |||
| 			 * @return bool | |||
| 			 */ | |||
| 			function($val) use($uri) { | |||
| 				return $uri?($val->uri()==$uri):TRUE; | |||
| 			} | |||
| 		); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return event handlers | |||
| 	*	@return array | |||
| 	**/ | |||
| 	function events() { | |||
| 		return $this->events; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Bind function to event handler | |||
| 	*	@return object | |||
| 	*	@param $event string | |||
| 	*	@param $func callable | |||
| 	**/ | |||
| 	function on($event,$func) { | |||
| 		$this->events[$event]=$func; | |||
| 		return $this; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Terminate server | |||
| 	**/ | |||
| 	function kill() { | |||
| 		die; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Execute the server process | |||
| 	**/ | |||
| 	function run() { | |||
| 		// Assign signal handlers | |||
| 		declare(ticks=1); | |||
| 		pcntl_signal(SIGINT,[$this,'kill']); | |||
| 		pcntl_signal(SIGTERM,[$this,'kill']); | |||
| 		gc_enable(); | |||
| 		// Activate WebSocket listener | |||
| 		$listen=stream_socket_server( | |||
| 			$this->addr,$errno,$errstr, | |||
| 			STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, | |||
| 			$this->ctx | |||
| 		); | |||
| 		$socket=socket_import_stream($listen); | |||
| 		register_shutdown_function(function() use($listen) { | |||
| 			foreach ($this->sockets as $socket) | |||
| 				if ($socket!=$listen) | |||
| 					$this->close($socket); | |||
| 			$this->close($listen); | |||
| 			if (isset($this->events['stop']) && | |||
| 				is_callable($func=$this->events['stop'])) | |||
| 				$func($this); | |||
| 		}); | |||
| 		if ($errstr) | |||
| 			user_error($errstr,E_USER_ERROR); | |||
| 		if (isset($this->events['start']) && | |||
| 			is_callable($func=$this->events['start'])) | |||
| 			$func($this); | |||
| 		$this->sockets=[(int)$listen=>$listen]; | |||
| 		$empty=[]; | |||
| 		$wait=$this->wait; | |||
| 		while (TRUE) { | |||
| 			$active=$this->sockets; | |||
| 			$mark=microtime(TRUE); | |||
| 			$count=@stream_select( | |||
| 				$active,$empty,$empty,(int)$wait,round(1e6*($wait-(int)$wait)) | |||
| 			); | |||
| 			if (is_bool($count) && $wait) { | |||
| 				if (isset($this->events['error']) && | |||
| 					is_callable($func=$this->events['error'])) | |||
| 					$func($this); | |||
| 				die; | |||
| 			} | |||
| 			if ($count) { | |||
| 				// Process active connections | |||
| 				foreach ($active as $socket) { | |||
| 					if (!is_resource($socket)) | |||
| 						continue; | |||
| 					if ($socket==$listen) { | |||
| 						if ($socket=@stream_socket_accept($listen,0)) | |||
| 							$this->alloc($socket); | |||
| 						else | |||
| 						if (isset($this->events['error']) && | |||
| 							is_callable($func=$this->events['error'])) | |||
| 							$func($this); | |||
| 					} | |||
| 					else { | |||
| 						$id=(int)$socket; | |||
| 						if (isset($this->agents[$id])) | |||
| 							$this->agents[$id]->fetch(); | |||
| 					} | |||
| 				} | |||
| 				$wait-=microtime(TRUE)-$mark; | |||
| 				while ($wait<1e-6) { | |||
| 					$wait+=$this->wait; | |||
| 					$count=0; | |||
| 				} | |||
| 			} | |||
| 			if (!$count) { | |||
| 				$mark=microtime(TRUE); | |||
| 				foreach ($this->sockets as $id=>$socket) { | |||
| 					if (!is_resource($socket)) | |||
| 						continue; | |||
| 					if ($socket!=$listen && | |||
| 						isset($this->agents[$id]) && | |||
| 						isset($this->events['idle']) && | |||
| 						is_callable($func=$this->events['idle'])) | |||
| 						$func($this->agents[$id]); | |||
| 				} | |||
| 				$wait=$this->wait-microtime(TRUE)+$mark; | |||
| 			} | |||
| 			gc_collect_cycles(); | |||
| 		} | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	@param $addr string | |||
| 	*	@param $ctx resource | |||
| 	*	@param $wait int | |||
| 	**/ | |||
| 	function __construct($addr,$ctx=NULL,$wait=60) { | |||
| 		$this->addr=$addr; | |||
| 		$this->ctx=$ctx?:stream_context_create(); | |||
| 		$this->wait=$wait; | |||
| 		$this->events=[]; | |||
| 	} | |||
| 
 | |||
| } | |||
| 
 | |||
| //! RFC6455 remote socket | |||
| class Agent { | |||
| 
 | |||
| 	protected | |||
| 		$server, | |||
| 		$id, | |||
| 		$socket, | |||
| 		$flag, | |||
| 		$verb, | |||
| 		$uri, | |||
| 		$headers; | |||
| 
 | |||
| 	/** | |||
| 	*	Return server instance | |||
| 	*	@return WS | |||
| 	**/ | |||
| 	function server() { | |||
| 		return $this->server; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return socket ID | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function id() { | |||
| 		return $this->id; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return socket | |||
| 	*	@return resource | |||
| 	**/ | |||
| 	function socket() { | |||
| 		return $this->socket; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return request method | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function verb() { | |||
| 		return $this->verb; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return request URI | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function uri() { | |||
| 		return $this->uri; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return socket headers | |||
| 	*	@return array | |||
| 	**/ | |||
| 	function headers() { | |||
| 		return $this->headers; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Frame and transmit payload | |||
| 	*	@return string|FALSE | |||
| 	*	@param $op int | |||
| 	*	@param $data string | |||
| 	**/ | |||
| 	function send($op,$data='') { | |||
| 		$server=$this->server; | |||
| 		$mask=WS::Finale | $op & WS::OpCode; | |||
| 		$len=strlen($data); | |||
| 		$buf=''; | |||
| 		if ($len>0xffff) | |||
| 			$buf=pack('CCNN',$mask,0x7f,$len); | |||
| 		elseif ($len>0x7d) | |||
| 			$buf=pack('CCn',$mask,0x7e,$len); | |||
| 		else | |||
| 			$buf=pack('CC',$mask,$len); | |||
| 		$buf.=$data; | |||
| 		if (is_bool($server->write($this->socket,$buf))) | |||
| 			return FALSE; | |||
| 		if (!in_array($op,[WS::Pong,WS::Close]) && | |||
| 			isset($this->server->events['send']) && | |||
| 			is_callable($func=$this->server->events['send'])) | |||
| 			$func($this,$op,$data); | |||
| 		return $data; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Retrieve and unmask payload | |||
| 	*	@return bool|NULL | |||
| 	**/ | |||
| 	function fetch() { | |||
| 		// Unmask payload | |||
| 		$server=$this->server; | |||
| 		if (is_bool($buf=$server->read($this->socket))) | |||
| 			return FALSE; | |||
| 		while($buf) { | |||
| 			$op=ord($buf[0]) & WS::OpCode; | |||
| 			$len=ord($buf[1]) & WS::Length; | |||
| 			$pos=2; | |||
| 			if ($len==0x7e) { | |||
| 				$len=ord($buf[2])*256+ord($buf[3]); | |||
| 				$pos+=2; | |||
| 			} | |||
| 			else | |||
| 			if ($len==0x7f) { | |||
| 				for ($i=0,$len=0;$i<8;$i++) | |||
| 					$len=$len*256+ord($buf[$i+2]); | |||
| 				$pos+=8; | |||
| 			} | |||
| 			for ($i=0,$mask=[];$i<4;$i++) | |||
| 				$mask[$i]=ord($buf[$pos+$i]); | |||
| 			$pos+=4; | |||
| 			if (strlen($buf)<$len+$pos) | |||
| 				return FALSE; | |||
| 			for ($i=0,$data='';$i<$len;$i++) | |||
| 				$data.=chr(ord($buf[$pos+$i])^$mask[$i%4]); | |||
| 			// Dispatch | |||
| 			switch ($op & WS::OpCode) { | |||
| 			case WS::Ping: | |||
| 				$this->send(WS::Pong); | |||
| 				break; | |||
| 			case WS::Close: | |||
| 				$server->close($this->socket); | |||
| 				break; | |||
| 			case WS::Text: | |||
| 				$data=trim($data); | |||
| 			case WS::Binary: | |||
| 				if (isset($this->server->events['receive']) && | |||
| 					is_callable($func=$this->server->events['receive'])) | |||
| 					$func($this,$op,$data); | |||
| 				break; | |||
| 			} | |||
| 			$buf = substr($buf, $len+$pos); | |||
| 		} | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Destroy object | |||
| 	**/ | |||
| 	function __destruct() { | |||
| 		if (isset($this->server->events['disconnect']) && | |||
| 			is_callable($func=$this->server->events['disconnect'])) | |||
| 			$func($this); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	@param $server WS | |||
| 	*	@param $socket resource | |||
| 	*	@param $verb string | |||
| 	*	@param $uri string | |||
| 	*	@param $hdrs array | |||
| 	**/ | |||
| 	function __construct($server,$socket,$verb,$uri,array $hdrs) { | |||
| 		$this->server=$server; | |||
| 		$this->id=stream_socket_get_name($socket,TRUE); | |||
| 		$this->socket=$socket; | |||
| 		$this->verb=$verb; | |||
| 		$this->uri=$uri; | |||
| 		$this->headers=$hdrs; | |||
| 
 | |||
| 		if (isset($server->events['connect']) && | |||
| 			is_callable($func=$server->events['connect'])) | |||
| 			$func($this); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -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} | |||
| @ -0,0 +1,388 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace DB; | |||
| 
 | |||
| //! Simple cursor implementation | |||
| abstract class Cursor extends \Magic implements \IteratorAggregate { | |||
| 
 | |||
| 	//@{ Error messages | |||
| 	const | |||
| 		E_Field='Undefined field %s'; | |||
| 	//@} | |||
| 
 | |||
| 	protected | |||
| 		//! Query results | |||
| 		$query=[], | |||
| 		//! Current position | |||
| 		$ptr=0, | |||
| 		//! Event listeners | |||
| 		$trigger=[]; | |||
| 
 | |||
| 	/** | |||
| 	*	Return database type | |||
| 	*	@return string | |||
| 	**/ | |||
| 	abstract function dbtype(); | |||
| 
 | |||
| 	/** | |||
| 	*	Return field names | |||
| 	*	@return array | |||
| 	**/ | |||
| 	abstract function fields(); | |||
| 
 | |||
| 	/** | |||
| 	*	Return fields of mapper object as an associative array | |||
| 	*	@return array | |||
| 	*	@param $obj object | |||
| 	**/ | |||
| 	abstract function cast($obj=NULL); | |||
| 
 | |||
| 	/** | |||
| 	*	Return records (array of mapper objects) that match criteria | |||
| 	*	@return array | |||
| 	*	@param $filter string|array | |||
| 	*	@param $options array | |||
| 	*	@param $ttl int | |||
| 	**/ | |||
| 	abstract function find($filter=NULL,array $options=NULL,$ttl=0); | |||
| 
 | |||
| 	/** | |||
| 	*	Count records that match criteria | |||
| 	*	@return int | |||
| 	*	@param $filter array | |||
| 	*	@param $options array | |||
| 	*	@param $ttl int | |||
| 	**/ | |||
| 	abstract function count($filter=NULL,array $options=NULL,$ttl=0); | |||
| 
 | |||
| 	/** | |||
| 	*	Insert new record | |||
| 	*	@return array | |||
| 	**/ | |||
| 	abstract function insert(); | |||
| 
 | |||
| 	/** | |||
| 	*	Update current record | |||
| 	*	@return array | |||
| 	**/ | |||
| 	abstract function update(); | |||
| 
 | |||
| 	/** | |||
| 	*	Hydrate mapper object using hive array variable | |||
| 	*	@return NULL | |||
| 	*	@param $var array|string | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	abstract function copyfrom($var,$func=NULL); | |||
| 
 | |||
| 	/** | |||
| 	*	Populate hive array variable with mapper fields | |||
| 	*	@return NULL | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	abstract function copyto($key); | |||
| 
 | |||
| 	/** | |||
| 	*	Get cursor's equivalent external iterator | |||
| 	*	Causes a fatal error in PHP 5.3.5 if uncommented | |||
| 	*	return ArrayIterator | |||
| 	**/ | |||
| 	abstract function getiterator(); | |||
| 
 | |||
| 
 | |||
| 	/** | |||
| 	*	Return TRUE if current cursor position is not mapped to any record | |||
| 	*	@return bool | |||
| 	**/ | |||
| 	function dry() { | |||
| 		return empty($this->query[$this->ptr]); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return first record (mapper object) that matches criteria | |||
| 	*	@return static|FALSE | |||
| 	*	@param $filter string|array | |||
| 	*	@param $options array | |||
| 	*	@param $ttl int | |||
| 	**/ | |||
| 	function findone($filter=NULL,array $options=NULL,$ttl=0) { | |||
| 		if (!$options) | |||
| 			$options=[]; | |||
| 		// Override limit | |||
| 		$options['limit']=1; | |||
| 		return ($data=$this->find($filter,$options,$ttl))?$data[0]:FALSE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return array containing subset of records matching criteria, | |||
| 	*	total number of records in superset, specified limit, number of | |||
| 	*	subsets available, and actual subset position | |||
| 	*	@return array | |||
| 	*	@param $pos int | |||
| 	*	@param $size int | |||
| 	*	@param $filter string|array | |||
| 	*	@param $options array | |||
| 	*	@param $ttl int | |||
| 	*	@param $bounce bool | |||
| 	**/ | |||
| 	function paginate( | |||
| 		$pos=0,$size=10,$filter=NULL,array $options=NULL,$ttl=0,$bounce=TRUE) { | |||
| 		$total=$this->count($filter,$options,$ttl); | |||
| 		$count=(int)ceil($total/$size); | |||
| 		if ($bounce) | |||
| 			$pos=max(0,min($pos,$count-1)); | |||
| 		return [ | |||
| 			'subset'=>($bounce || $pos<$count)?$this->find($filter, | |||
| 				array_merge( | |||
| 					$options?:[], | |||
| 					['limit'=>$size,'offset'=>$pos*$size] | |||
| 				), | |||
| 				$ttl | |||
| 			):[], | |||
| 			'total'=>$total, | |||
| 			'limit'=>$size, | |||
| 			'count'=>$count, | |||
| 			'pos'=>$bounce?($pos<$count?$pos:0):$pos | |||
| 		]; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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) { | |||
| 		$this->reset(); | |||
| 		return ($this->query=$this->find($filter,$options,$ttl)) && | |||
| 			$this->skip(0)?$this->query[$this->ptr]:FALSE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return the count of records loaded | |||
| 	*	@return int | |||
| 	**/ | |||
| 	function loaded() { | |||
| 		return count($this->query); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Map to first record in cursor | |||
| 	*	@return mixed | |||
| 	**/ | |||
| 	function first() { | |||
| 		return $this->skip(-$this->ptr); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Map to last record in cursor | |||
| 	*	@return mixed | |||
| 	**/ | |||
| 	function last() { | |||
| 		return $this->skip(($ofs=count($this->query)-$this->ptr)?$ofs-1:0); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Map to nth record relative to current cursor position | |||
| 	*	@return mixed | |||
| 	*	@param $ofs int | |||
| 	**/ | |||
| 	function skip($ofs=1) { | |||
| 		$this->ptr+=$ofs; | |||
| 		return $this->ptr>-1 && $this->ptr<count($this->query)? | |||
| 			$this->query[$this->ptr]:FALSE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Map next record | |||
| 	*	@return mixed | |||
| 	**/ | |||
| 	function next() { | |||
| 		return $this->skip(); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Map previous record | |||
| 	*	@return mixed | |||
| 	**/ | |||
| 	function prev() { | |||
| 		return $this->skip(-1); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 * Return whether current iterator position is valid. | |||
| 	 */ | |||
| 	function valid() { | |||
| 		return !$this->dry(); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Save mapped record | |||
| 	*	@return mixed | |||
| 	**/ | |||
| 	function save() { | |||
| 		return $this->query?$this->update():$this->insert(); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Delete current record | |||
| 	*	@return int|bool | |||
| 	**/ | |||
| 	function erase() { | |||
| 		$this->query=array_slice($this->query,0,$this->ptr,TRUE)+ | |||
| 			array_slice($this->query,$this->ptr,NULL,TRUE); | |||
| 		$this->skip(0); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define onload trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function onload($func) { | |||
| 		return $this->trigger['load']=$func; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define beforeinsert trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function beforeinsert($func) { | |||
| 		return $this->trigger['beforeinsert']=$func; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define afterinsert trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function afterinsert($func) { | |||
| 		return $this->trigger['afterinsert']=$func; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define oninsert trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function oninsert($func) { | |||
| 		return $this->afterinsert($func); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define beforeupdate trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function beforeupdate($func) { | |||
| 		return $this->trigger['beforeupdate']=$func; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define afterupdate trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function afterupdate($func) { | |||
| 		return $this->trigger['afterupdate']=$func; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define onupdate trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function onupdate($func) { | |||
| 		return $this->afterupdate($func); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define beforesave trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function beforesave($func) { | |||
| 		$this->trigger['beforeinsert']=$func; | |||
| 		$this->trigger['beforeupdate']=$func; | |||
| 		return $func; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define aftersave trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function aftersave($func) { | |||
| 		$this->trigger['afterinsert']=$func; | |||
| 		$this->trigger['afterupdate']=$func; | |||
| 		return $func; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define onsave trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function onsave($func) { | |||
| 		return $this->aftersave($func); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define beforeerase trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function beforeerase($func) { | |||
| 		return $this->trigger['beforeerase']=$func; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define aftererase trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function aftererase($func) { | |||
| 		return $this->trigger['aftererase']=$func; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Define onerase trigger | |||
| 	*	@return callback | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function onerase($func) { | |||
| 		return $this->aftererase($func); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Reset cursor | |||
| 	*	@return NULL | |||
| 	**/ | |||
| 	function reset() { | |||
| 		$this->query=[]; | |||
| 		$this->ptr=0; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,175 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace DB; | |||
| 
 | |||
| //! In-memory/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, | |||
| 		//! Memory-held data | |||
| 		$data, | |||
| 		//! lazy load/save files | |||
| 		$lazy; | |||
| 
 | |||
| 	/** | |||
| 	*	Read data from memory/file | |||
| 	*	@return array | |||
| 	*	@param $file string | |||
| 	**/ | |||
| 	function &read($file) { | |||
| 		if (!$this->dir || !is_file($dst=$this->dir.$file)) { | |||
| 			if (!isset($this->data[$file])) | |||
| 				$this->data[$file]=[]; | |||
| 			return $this->data[$file]; | |||
| 		} | |||
| 		if ($this->lazy && isset($this->data[$file])) | |||
| 			return $this->data[$file]; | |||
| 		$fw=\Base::instance(); | |||
| 		$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; | |||
| 		} | |||
| 		$this->data[$file] = $data; | |||
| 		return $this->data[$file]; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Write data to memory/file | |||
| 	*	@return int | |||
| 	*	@param $file string | |||
| 	*	@param $data array | |||
| 	**/ | |||
| 	function write($file,array $data=NULL) { | |||
| 		if (!$this->dir || $this->lazy) | |||
| 			return count($this->data[$file]=$data); | |||
| 		$fw=\Base::instance(); | |||
| 		switch ($this->format) { | |||
| 			case self::FORMAT_JSON: | |||
| 				$out=json_encode($data,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 profiler results (or disable logging) | |||
| 	*	@param $flag bool | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function log($flag=TRUE) { | |||
| 		if ($flag) | |||
| 			return $this->log; | |||
| 		$this->log=FALSE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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 ($this->lazy) // intentional | |||
| 			$this->data=[]; | |||
| 		if (!$this->dir) | |||
| 			$this->data=[]; | |||
| 		elseif ($glob=@glob($this->dir.'/*',GLOB_NOSORT)) | |||
| 			foreach ($glob as $file) | |||
| 				@unlink($file); | |||
| 	} | |||
| 
 | |||
| 	//! Prohibit cloning | |||
| 	private function __clone() { | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Instantiate class | |||
| 	*	@param $dir string | |||
| 	*	@param $format int | |||
| 	**/ | |||
| 	function __construct($dir=NULL,$format=self::FORMAT_JSON,$lazy=FALSE) { | |||
| 		if ($dir && !is_dir($dir)) | |||
| 			mkdir($dir,\Base::MODE,TRUE); | |||
| 		$this->uuid=\Base::instance()->hash($this->dir=$dir); | |||
| 		$this->format=$format; | |||
| 		$this->lazy=$lazy; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	save file on destruction | |||
| 	**/ | |||
| 	function __destruct() { | |||
| 		if ($this->lazy) { | |||
| 			$this->lazy = FALSE; | |||
| 			foreach ($this->data?:[] as $file => $data) | |||
| 				$this->write($file,$data); | |||
| 		} | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,541 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace DB\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=[], | |||
| 		//! field map-reduce handlers | |||
| 		$_reduce; | |||
| 
 | |||
| 	/** | |||
| 	*	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),E_USER_ERROR); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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 | |||
| 	**/ | |||
| 	function factory($id,$row) { | |||
| 		$mapper=clone($this); | |||
| 		$mapper->reset(); | |||
| 		$mapper->id=$id; | |||
| 		foreach ($row as $field=>$val) | |||
| 			$mapper->document[$field]=$val; | |||
| 		$mapper->query=[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+['_id'=>$this->id]; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Convert tokens in string expression to variable names | |||
| 	*	@return string | |||
| 	*	@param $str string | |||
| 	**/ | |||
| 	function token($str) { | |||
| 		$str=preg_replace_callback( | |||
| 			'/(?<!\w)@(\w(?:[\w\.\[\]])*)/', | |||
| 			function($token) { | |||
| 				// Convert from JS dot notation to PHP array notation | |||
| 				return '$'.preg_replace_callback( | |||
| 					'/(\.\w+)|\[((?:[^\[\]]*|(?R))*)\]/', | |||
| 					function($expr) { | |||
| 						$fw=\Base::instance(); | |||
| 						return | |||
| 							'['. | |||
| 							($expr[1]? | |||
| 								$fw->stringify(substr($expr[1],1)): | |||
| 								(preg_match('/^\w+/', | |||
| 									$mix=$this->token($expr[2]))? | |||
| 									$fw->stringify($mix): | |||
| 									$mix)). | |||
| 							']'; | |||
| 					}, | |||
| 					$token[1] | |||
| 				); | |||
| 			}, | |||
| 			$str | |||
| 		); | |||
| 		return trim($str); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return records that match criteria | |||
| 	*	@return static[]|FALSE | |||
| 	*	@param $filter array | |||
| 	*	@param $options array | |||
| 	*	@param $ttl int|array | |||
| 	*	@param $log bool | |||
| 	**/ | |||
| 	function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) { | |||
| 		if (!$options) | |||
| 			$options=[]; | |||
| 		$options+=[ | |||
| 			'order'=>NULL, | |||
| 			'limit'=>0, | |||
| 			'offset'=>0, | |||
| 			'group'=>NULL, | |||
| 		]; | |||
| 		$fw=\Base::instance(); | |||
| 		$cache=\Cache::instance(); | |||
| 		$db=$this->db; | |||
| 		$now=microtime(TRUE); | |||
| 		$data=[]; | |||
| 		$tag=''; | |||
| 		if (is_array($ttl)) | |||
| 			list($ttl,$tag)=$ttl; | |||
| 		if (!$fw->CACHE || !$ttl || !($cached=$cache->exists( | |||
| 			$hash=$fw->hash($this->db->dir(). | |||
| 				$fw->stringify([$filter,$options])).($tag?'.'.$tag:'').'.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:[1=>$args]; | |||
| 				$keys=$vals=[]; | |||
| 				$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['group'])) { | |||
| 				$cols=array_reverse($fw->split($options['group'])); | |||
| 				// sort into groups | |||
| 				$data=$this->sort($data,$options['group']); | |||
| 				foreach($data as $i=>&$row) { | |||
| 					if (!isset($prev)) { | |||
| 						$prev=$row; | |||
| 						$prev_i=$i; | |||
| 					} | |||
| 					$drop=false; | |||
| 					foreach ($cols as $col) | |||
| 						if ($prev_i!=$i && array_key_exists($col,$row) && | |||
| 							array_key_exists($col,$prev) && $row[$col]==$prev[$col]) | |||
| 							// reduce/modify | |||
| 							$drop=!isset($this->_reduce[$col]) || call_user_func_array( | |||
| 								$this->_reduce[$col][0],[&$prev,&$row])!==FALSE; | |||
| 						elseif (isset($this->_reduce[$col])) { | |||
| 							$null=null; | |||
| 							// initial | |||
| 							call_user_func_array($this->_reduce[$col][0],[&$row,&$null]); | |||
| 						} | |||
| 					if ($drop) | |||
| 						unset($data[$i]); | |||
| 					else { | |||
| 						$prev=&$row; | |||
| 						$prev_i=$i; | |||
| 					} | |||
| 					unset($row); | |||
| 				} | |||
| 				// finalize | |||
| 				if ($this->_reduce[$col][1]) | |||
| 					foreach($data as $i=>&$row) { | |||
| 						$row=call_user_func($this->_reduce[$col][1],$row); | |||
| 						if (!$row) | |||
| 							unset($data[$i]); | |||
| 						unset($row); | |||
| 					} | |||
| 			} | |||
| 			if (isset($options['order'])) | |||
| 				$data=$this->sort($data,$options['order']); | |||
| 			$data=array_slice($data, | |||
| 				$options['offset'],$options['limit']?:NULL,TRUE); | |||
| 			if ($fw->CACHE && $ttl) | |||
| 				// Save to cache backend | |||
| 				$cache->set($hash,$data,$ttl); | |||
| 		} | |||
| 		$out=[]; | |||
| 		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; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Sort a collection | |||
| 	*	@param $data | |||
| 	*	@param $cond | |||
| 	*	@return mixed | |||
| 	*/ | |||
| 	protected function sort($data,$cond) { | |||
| 		$cols=\Base::instance()->split($cond); | |||
| 		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)=[$val1[$col],$val2[$col]]; | |||
| 					if ($out=strnatcmp($v1,$v2)* | |||
| 						(($order==SORT_ASC)*2-1)) | |||
| 						return $out; | |||
| 				} | |||
| 				return 0; | |||
| 			} | |||
| 		); | |||
| 		return $data; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Add reduce handler for grouped fields | |||
| 	*	@param $key string | |||
| 	*	@param $handler callback | |||
| 	*	@param $finalize callback | |||
| 	*/ | |||
| 	function reduce($key,$handler,$finalize=null){ | |||
| 		$this->_reduce[$key]=[$handler,$finalize]; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Count records that match criteria | |||
| 	*	@return int | |||
| 	*	@param $filter array | |||
| 	*	@param $options array | |||
| 	*	@param $ttl int|array | |||
| 	**/ | |||
| 	function count($filter=NULL,array $options=NULL,$ttl=0) { | |||
| 		$now=microtime(TRUE); | |||
| 		$out=count($this->find($filter,$options,$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:[]; | |||
| 		$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; | |||
| 		$pkey=['_id'=>$this->id]; | |||
| 		if (isset($this->trigger['beforeinsert']) && | |||
| 			\Base::instance()->call($this->trigger['beforeinsert'], | |||
| 				[$this,$pkey])===FALSE) | |||
| 			return $this->document; | |||
| 		$data[$id]=$this->document; | |||
| 		$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'], | |||
| 				[$this,$pkey]); | |||
| 		$this->load(['@_id=?',$this->id]); | |||
| 		return $this->document; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Update current record | |||
| 	*	@return array | |||
| 	**/ | |||
| 	function update() { | |||
| 		$db=$this->db; | |||
| 		$now=microtime(TRUE); | |||
| 		$data=&$db->read($this->file); | |||
| 		if (isset($this->trigger['beforeupdate']) && | |||
| 			\Base::instance()->call($this->trigger['beforeupdate'], | |||
| 				[$this,['_id'=>$this->id]])===FALSE) | |||
| 			return $this->document; | |||
| 		$data[$this->id]=$this->document; | |||
| 		$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'], | |||
| 				[$this,['_id'=>$this->id]]); | |||
| 		return $this->document; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Delete current record | |||
| 	*	@return bool | |||
| 	*	@param $filter array | |||
| 	*	@param $quick bool | |||
| 	**/ | |||
| 	function erase($filter=NULL,$quick=FALSE) { | |||
| 		$db=$this->db; | |||
| 		$now=microtime(TRUE); | |||
| 		$data=&$db->read($this->file); | |||
| 		$pkey=['_id'=>$this->id]; | |||
| 		if ($filter) { | |||
| 			foreach ($this->find($filter,NULL,FALSE) as $mapper) | |||
| 				if (!$mapper->erase(null,$quick)) | |||
| 					return FALSE; | |||
| 			return TRUE; | |||
| 		} | |||
| 		elseif (isset($this->id)) { | |||
| 			unset($data[$this->id]); | |||
| 			parent::erase(); | |||
| 		} | |||
| 		else | |||
| 			return FALSE; | |||
| 		if (!$quick && isset($this->trigger['beforeerase']) && | |||
| 			\Base::instance()->call($this->trigger['beforeerase'], | |||
| 				[$this,$pkey])===FALSE) | |||
| 			return FALSE; | |||
| 		$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:[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 (!$quick && isset($this->trigger['aftererase'])) | |||
| 			\Base::instance()->call($this->trigger['aftererase'], | |||
| 				[$this,$pkey]); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Reset cursor | |||
| 	*	@return NULL | |||
| 	**/ | |||
| 	function reset() { | |||
| 		$this->id=NULL; | |||
| 		$this->document=[]; | |||
| 		parent::reset(); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Hydrate mapper object using hive array variable | |||
| 	*	@return NULL | |||
| 	*	@param $var array|string | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function copyfrom($var,$func=NULL) { | |||
| 		if (is_string($var)) | |||
| 			$var=\Base::instance()->$var; | |||
| 		if ($func) | |||
| 			$var=call_user_func($func,$var); | |||
| 		foreach ($var as $key=>$val) | |||
| 			$this->set($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); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Retrieve external iterator for fields | |||
| 	*	@return object | |||
| 	**/ | |||
| 	function getiterator() { | |||
| 		return new \ArrayIterator($this->cast()); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Instantiate class | |||
| 	*	@return void | |||
| 	*	@param $db object | |||
| 	*	@param $file string | |||
| 	**/ | |||
| 	function __construct(\DB\Jig $db,$file) { | |||
| 		$this->db=$db; | |||
| 		$this->file=$file; | |||
| 		$this->reset(); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,194 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace DB\Jig; | |||
| 
 | |||
| //! Jig-managed session handler | |||
| class Session extends Mapper { | |||
| 
 | |||
| 	protected | |||
| 		//! Session ID | |||
| 		$sid, | |||
| 		//! Anti-CSRF token | |||
| 		$_csrf, | |||
| 		//! User agent | |||
| 		$_agent, | |||
| 		//! IP, | |||
| 		$_ip, | |||
| 		//! Suspect callback | |||
| 		$onsuspect; | |||
| 
 | |||
| 	/** | |||
| 	*	Open session | |||
| 	*	@return TRUE | |||
| 	*	@param $path string | |||
| 	*	@param $name string | |||
| 	**/ | |||
| 	function open($path,$name) { | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Close session | |||
| 	*	@return TRUE | |||
| 	**/ | |||
| 	function close() { | |||
| 		$this->reset(); | |||
| 		$this->sid=NULL; | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return session data in serialized format | |||
| 	*	@return string | |||
| 	*	@param $id string | |||
| 	**/ | |||
| 	function read($id) { | |||
| 		$this->load(['@session_id=?',$this->sid=$id]); | |||
| 		if ($this->dry()) | |||
| 			return ''; | |||
| 		if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { | |||
| 			$fw=\Base::instance(); | |||
| 			if (!isset($this->onsuspect) || | |||
| 				$fw->call($this->onsuspect,[$this,$id])===FALSE) { | |||
| 				// NB: `session_destroy` can't be called at that stage; | |||
| 				// `session_start` not completed | |||
| 				$this->destroy($id); | |||
| 				$this->close(); | |||
| 				unset($fw->{'COOKIE.'.session_name()}); | |||
| 				$fw->error(403); | |||
| 			} | |||
| 		} | |||
| 		return $this->get('data'); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Write session data | |||
| 	*	@return TRUE | |||
| 	*	@param $id string | |||
| 	*	@param $data string | |||
| 	**/ | |||
| 	function write($id,$data) { | |||
| 		$this->set('session_id',$id); | |||
| 		$this->set('data',$data); | |||
| 		$this->set('ip',$this->_ip); | |||
| 		$this->set('agent',$this->_agent); | |||
| 		$this->set('stamp',time()); | |||
| 		$this->save(); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Destroy session | |||
| 	*	@return TRUE | |||
| 	*	@param $id string | |||
| 	**/ | |||
| 	function destroy($id) { | |||
| 		$this->erase(['@session_id=?',$id]); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Garbage collector | |||
| 	*	@return TRUE | |||
| 	*	@param $max int | |||
| 	**/ | |||
| 	function cleanup($max) { | |||
| 		$this->erase(['@stamp+?<?',$max,time()]); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return session id (if session has started) | |||
| 	 *	@return string|NULL | |||
| 	 **/ | |||
| 	function sid() { | |||
| 		return $this->sid; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return anti-CSRF token | |||
| 	 *	@return string | |||
| 	 **/ | |||
| 	function csrf() { | |||
| 		return $this->_csrf; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return IP address | |||
| 	 *	@return string | |||
| 	 **/ | |||
| 	function ip() { | |||
| 		return $this->_ip; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return Unix timestamp | |||
| 	*	@return string|FALSE | |||
| 	**/ | |||
| 	function stamp() { | |||
| 		if (!$this->sid) | |||
| 			session_start(); | |||
| 		return $this->dry()?FALSE:$this->get('stamp'); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return HTTP user agent | |||
| 	*	@return string|FALSE | |||
| 	**/ | |||
| 	function agent() { | |||
| 		return $this->_agent; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Instantiate class | |||
| 	*	@param $db \DB\Jig | |||
| 	*	@param $file string | |||
| 	*	@param $onsuspect callback | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function __construct(\DB\Jig $db,$file='sessions',$onsuspect=NULL,$key=NULL) { | |||
| 		parent::__construct($db,$file); | |||
| 		$this->onsuspect=$onsuspect; | |||
| 		session_set_save_handler( | |||
| 			[$this,'open'], | |||
| 			[$this,'close'], | |||
| 			[$this,'read'], | |||
| 			[$this,'write'], | |||
| 			[$this,'destroy'], | |||
| 			[$this,'cleanup'] | |||
| 		); | |||
| 		register_shutdown_function('session_commit'); | |||
| 		$fw=\Base::instance(); | |||
| 		$headers=$fw->HEADERS; | |||
| 		$this->_csrf=$fw->hash($fw->SEED. | |||
| 			extension_loaded('openssl')? | |||
| 				implode(unpack('L',openssl_random_pseudo_bytes(4))): | |||
| 				mt_rand() | |||
| 			); | |||
| 		if ($key) | |||
| 			$fw->$key=$this->_csrf; | |||
| 		$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; | |||
| 		$this->_ip=$fw->IP; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,145 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace DB; | |||
| 
 | |||
| //! MongoDB wrapper | |||
| class Mongo { | |||
| 
 | |||
| 	//@{ | |||
| 	const | |||
| 		E_Profiler='MongoDB profiler is disabled'; | |||
| 	//@} | |||
| 
 | |||
| 	protected | |||
| 		//! UUID | |||
| 		$uuid, | |||
| 		//! Data source name | |||
| 		$dsn, | |||
| 		//! MongoDB object | |||
| 		$db, | |||
| 		//! Legacy flag | |||
| 		$legacy, | |||
| 		//! 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 (or disable logging) | |||
| 	*	@param $flag bool | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function log($flag=TRUE) { | |||
| 		if ($flag) { | |||
| 			$cursor=$this->db->selectcollection('system.profile')->find(); | |||
| 			foreach (iterator_to_array($cursor) as $frame) | |||
| 				if (!preg_match('/\.system\..+$/',$frame['ns'])) | |||
| 					$this->log.=date('r',$this->legacy() ? | |||
| 						$frame['ts']->sec : (round((string)$frame['ts'])/1000)). | |||
| 						' ('.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; | |||
| 		} else { | |||
| 			$this->log=FALSE; | |||
| 			if ($this->legacy) | |||
| 				$this->db->setprofilinglevel(-1); | |||
| 			else | |||
| 				$this->db->command(['profile'=>-1]); | |||
| 		} | |||
| 		return $this->log; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Intercept native call to re-enable profiler | |||
| 	*	@return int | |||
| 	**/ | |||
| 	function drop() { | |||
| 		$out=$this->db->drop(); | |||
| 		if ($this->log!==FALSE) { | |||
| 			if ($this->legacy) | |||
| 				$this->db->setprofilinglevel(2); | |||
| 			else | |||
| 				$this->db->command(['profile'=>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([$this->db,$func],$args); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return TRUE if legacy driver is loaded | |||
| 	*	@return bool | |||
| 	**/ | |||
| 	function legacy() { | |||
| 		return $this->legacy; | |||
| 	} | |||
| 
 | |||
| 	//! Prohibit cloning | |||
| 	private function __clone() { | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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); | |||
| 		if ($this->legacy=class_exists('\MongoClient')) { | |||
| 			$this->db=new \MongoDB(new \MongoClient($dsn,$options?:[]),$dbname); | |||
| 			$this->db->setprofilinglevel(2); | |||
| 		} | |||
| 		else { | |||
| 			$this->db=(new \MongoDB\Client($dsn,$options?:[]))->$dbname; | |||
| 			$this->db->command(['profile'=>2]); | |||
| 		} | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,405 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace DB\Mongo; | |||
| 
 | |||
| //! MongoDB mapper | |||
| class Mapper extends \DB\Cursor { | |||
| 
 | |||
| 	protected | |||
| 		//! MongoDB wrapper | |||
| 		$db, | |||
| 		//! Legacy flag | |||
| 		$legacy, | |||
| 		//! Mongo collection | |||
| 		$collection, | |||
| 		//! Mongo document | |||
| 		$document=[], | |||
| 		//! Mongo cursor | |||
| 		$cursor, | |||
| 		//! Defined fields | |||
| 		$fields; | |||
| 
 | |||
| 	/** | |||
| 	*	Return database type | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function dbtype() { | |||
| 		return 'Mongo'; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return TRUE if field is defined | |||
| 	*	@return bool | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function exists($key) { | |||
| 		return array_key_exists($key,$this->document); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Assign value to field | |||
| 	*	@return scalar|FALSE | |||
| 	*	@param $key string | |||
| 	*	@param $val scalar | |||
| 	**/ | |||
| 	function set($key,$val) { | |||
| 		return $this->document[$key]=$val; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Retrieve value of field | |||
| 	*	@return scalar|FALSE | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function &get($key) { | |||
| 		if ($this->exists($key)) | |||
| 			return $this->document[$key]; | |||
| 		user_error(sprintf(self::E_Field,$key),E_USER_ERROR); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Delete field | |||
| 	*	@return NULL | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function clear($key) { | |||
| 		unset($this->document[$key]); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Convert array to mapper object | |||
| 	*	@return static | |||
| 	*	@param $row array | |||
| 	**/ | |||
| 	function factory($row) { | |||
| 		$mapper=clone($this); | |||
| 		$mapper->reset(); | |||
| 		foreach ($row as $key=>$val) | |||
| 			$mapper->document[$key]=$val; | |||
| 		$mapper->query=[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 static[] | |||
| 	*	@param $fields string | |||
| 	*	@param $filter array | |||
| 	*	@param $options array | |||
| 	*	@param $ttl int|array | |||
| 	**/ | |||
| 	function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) { | |||
| 		if (!$options) | |||
| 			$options=[]; | |||
| 		$options+=[ | |||
| 			'group'=>NULL, | |||
| 			'order'=>NULL, | |||
| 			'limit'=>0, | |||
| 			'offset'=>0 | |||
| 		]; | |||
| 		$tag=''; | |||
| 		if (is_array($ttl)) | |||
| 			list($ttl,$tag)=$ttl; | |||
| 		$fw=\Base::instance(); | |||
| 		$cache=\Cache::instance(); | |||
| 		if (!($cached=$cache->exists($hash=$fw->hash($this->db->dsn(). | |||
| 			$fw->stringify([$fields,$filter,$options])).($tag?'.'.$tag:'').'.mongo', | |||
| 			$result)) || !$ttl || $cached[0]+$ttl<microtime(TRUE)) { | |||
| 			if ($options['group']) { | |||
| 				$grp=$this->collection->group( | |||
| 					$options['group']['keys'], | |||
| 					$options['group']['initial'], | |||
| 					$options['group']['reduce'], | |||
| 					[ | |||
| 						'condition'=>$filter, | |||
| 						'finalize'=>$options['group']['finalize'] | |||
| 					] | |||
| 				); | |||
| 				$tmp=$this->db->selectcollection( | |||
| 					$fw->HOST.'.'.$fw->BASE.'.'. | |||
| 					uniqid(NULL,TRUE).'.tmp' | |||
| 				); | |||
| 				$tmp->batchinsert($grp['retval'],['w'=>1]); | |||
| 				$filter=[]; | |||
| 				$collection=$tmp; | |||
| 			} | |||
| 			else { | |||
| 				$filter=$filter?:[]; | |||
| 				$collection=$this->collection; | |||
| 			} | |||
| 			if ($this->legacy) { | |||
| 				$this->cursor=$collection->find($filter,$fields?:[]); | |||
| 				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=[]; | |||
| 				while ($this->cursor->hasnext()) | |||
| 					$result[]=$this->cursor->getnext(); | |||
| 			} | |||
| 			else { | |||
| 				$this->cursor=$collection->find($filter,[ | |||
| 					'sort'=>$options['order'], | |||
| 					'limit'=>$options['limit'], | |||
| 					'skip'=>$options['offset'] | |||
| 				]); | |||
| 				$result=$this->cursor->toarray(); | |||
| 			} | |||
| 			if ($options['group']) | |||
| 				$tmp->drop(); | |||
| 			if ($fw->CACHE && $ttl) | |||
| 				// Save to cache backend | |||
| 				$cache->set($hash,$result,$ttl); | |||
| 		} | |||
| 		$out=[]; | |||
| 		foreach ($result as $doc) | |||
| 			$out[]=$this->factory($doc); | |||
| 		return $out; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return records that match criteria | |||
| 	*	@return static[] | |||
| 	*	@param $filter array | |||
| 	*	@param $options array | |||
| 	*	@param $ttl int|array | |||
| 	**/ | |||
| 	function find($filter=NULL,array $options=NULL,$ttl=0) { | |||
| 		if (!$options) | |||
| 			$options=[]; | |||
| 		$options+=[ | |||
| 			'group'=>NULL, | |||
| 			'order'=>NULL, | |||
| 			'limit'=>0, | |||
| 			'offset'=>0 | |||
| 		]; | |||
| 		return $this->select($this->fields,$filter,$options,$ttl); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Count records that match criteria | |||
| 	*	@return int | |||
| 	*	@param $filter array | |||
| 	*	@param $options array | |||
| 	*	@param $ttl int|array | |||
| 	**/ | |||
| 	function count($filter=NULL,array $options=NULL,$ttl=0) { | |||
| 		$fw=\Base::instance(); | |||
| 		$cache=\Cache::instance(); | |||
| 		$tag=''; | |||
| 		if (is_array($ttl)) | |||
| 			list($ttl,$tag)=$ttl; | |||
| 		if (!($cached=$cache->exists($hash=$fw->hash($fw->stringify( | |||
| 			[$filter])).($tag?'.'.$tag:'').'.mongo',$result)) || !$ttl || | |||
| 			$cached[0]+$ttl<microtime(TRUE)) { | |||
| 			$result=$this->collection->count($filter?:[]); | |||
| 			if ($fw->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:[]; | |||
| 		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'], | |||
| 				[$this,['_id'=>$this->document['_id']]])===FALSE) | |||
| 			return $this->document; | |||
| 		if ($this->legacy) { | |||
| 			$this->collection->insert($this->document); | |||
| 			$pkey=['_id'=>$this->document['_id']]; | |||
| 		} | |||
| 		else { | |||
| 			$result=$this->collection->insertone($this->document); | |||
| 			$pkey=['_id'=>$result->getinsertedid()]; | |||
| 		} | |||
| 		if (isset($this->trigger['afterinsert'])) | |||
| 			\Base::instance()->call($this->trigger['afterinsert'], | |||
| 				[$this,$pkey]); | |||
| 		$this->load($pkey); | |||
| 		return $this->document; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Update current record | |||
| 	*	@return array | |||
| 	**/ | |||
| 	function update() { | |||
| 		$pkey=['_id'=>$this->document['_id']]; | |||
| 		if (isset($this->trigger['beforeupdate']) && | |||
| 			\Base::instance()->call($this->trigger['beforeupdate'], | |||
| 				[$this,$pkey])===FALSE) | |||
| 			return $this->document; | |||
| 		$upsert=['upsert'=>TRUE]; | |||
| 		if ($this->legacy) | |||
| 			$this->collection->update($pkey,$this->document,$upsert); | |||
| 		else | |||
| 			$this->collection->replaceone($pkey,$this->document,$upsert); | |||
| 		if (isset($this->trigger['afterupdate'])) | |||
| 			\Base::instance()->call($this->trigger['afterupdate'], | |||
| 				[$this,$pkey]); | |||
| 		return $this->document; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Delete current record | |||
| 	*	@return bool | |||
| 	*	@param $quick bool | |||
| 	*	@param $filter array | |||
| 	**/ | |||
| 	function erase($filter=NULL,$quick=TRUE) { | |||
| 		if ($filter) { | |||
| 			if (!$quick) { | |||
| 				foreach ($this->find($filter) as $mapper) | |||
| 					if (!$mapper->erase()) | |||
| 						return FALSE; | |||
| 				return TRUE; | |||
| 			} | |||
| 			return $this->legacy? | |||
| 				$this->collection->remove($filter): | |||
| 				$this->collection->deletemany($filter); | |||
| 		} | |||
| 		$pkey=['_id'=>$this->document['_id']]; | |||
| 		if (isset($this->trigger['beforeerase']) && | |||
| 			\Base::instance()->call($this->trigger['beforeerase'], | |||
| 				[$this,$pkey])===FALSE) | |||
| 			return FALSE; | |||
| 		$result=$this->legacy? | |||
| 			$this->collection->remove(['_id'=>$this->document['_id']]): | |||
| 			$this->collection->deleteone(['_id'=>$this->document['_id']]); | |||
| 		parent::erase(); | |||
| 		if (isset($this->trigger['aftererase'])) | |||
| 			\Base::instance()->call($this->trigger['aftererase'], | |||
| 				[$this,$pkey]); | |||
| 		return $result; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Reset cursor | |||
| 	*	@return NULL | |||
| 	**/ | |||
| 	function reset() { | |||
| 		$this->document=[]; | |||
| 		parent::reset(); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Hydrate mapper object using hive array variable | |||
| 	*	@return NULL | |||
| 	*	@param $var array|string | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function copyfrom($var,$func=NULL) { | |||
| 		if (is_string($var)) | |||
| 			$var=\Base::instance()->$var; | |||
| 		if ($func) | |||
| 			$var=call_user_func($func,$var); | |||
| 		foreach ($var as $key=>$val) | |||
| 			$this->set($key,$val); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Populate hive array variable with mapper fields | |||
| 	*	@return NULL | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function copyto($key) { | |||
| 		$var=&\Base::instance()->ref($key); | |||
| 		foreach ($this->document as $key=>$field) | |||
| 			$var[$key]=$field; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return field names | |||
| 	*	@return array | |||
| 	**/ | |||
| 	function fields() { | |||
| 		return array_keys($this->document); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return the cursor from last query | |||
| 	*	@return object|NULL | |||
| 	**/ | |||
| 	function cursor() { | |||
| 		return $this->cursor; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Retrieve external iterator for fields | |||
| 	*	@return object | |||
| 	**/ | |||
| 	function getiterator() { | |||
| 		return new \ArrayIterator($this->cast()); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Instantiate class | |||
| 	*	@return void | |||
| 	*	@param $db object | |||
| 	*	@param $collection string | |||
| 	*	@param $fields array | |||
| 	**/ | |||
| 	function __construct(\DB\Mongo $db,$collection,$fields=NULL) { | |||
| 		$this->db=$db; | |||
| 		$this->legacy=$db->legacy(); | |||
| 		$this->collection=$db->selectcollection($collection); | |||
| 		$this->fields=$fields; | |||
| 		$this->reset(); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,194 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace DB\Mongo; | |||
| 
 | |||
| //! MongoDB-managed session handler | |||
| class Session extends Mapper { | |||
| 
 | |||
| 	protected | |||
| 		//! Session ID | |||
| 		$sid, | |||
| 		//! Anti-CSRF token | |||
| 		$_csrf, | |||
| 		//! User agent | |||
| 		$_agent, | |||
| 		//! IP, | |||
| 		$_ip, | |||
| 		//! Suspect callback | |||
| 		$onsuspect; | |||
| 
 | |||
| 	/** | |||
| 	*	Open session | |||
| 	*	@return TRUE | |||
| 	*	@param $path string | |||
| 	*	@param $name string | |||
| 	**/ | |||
| 	function open($path,$name) { | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Close session | |||
| 	*	@return TRUE | |||
| 	**/ | |||
| 	function close() { | |||
| 		$this->reset(); | |||
| 		$this->sid=NULL; | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return session data in serialized format | |||
| 	*	@return string | |||
| 	*	@param $id string | |||
| 	**/ | |||
| 	function read($id) { | |||
| 		$this->load(['session_id'=>$this->sid=$id]); | |||
| 		if ($this->dry()) | |||
| 			return ''; | |||
| 		if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { | |||
| 			$fw=\Base::instance(); | |||
| 			if (!isset($this->onsuspect) || | |||
| 				$fw->call($this->onsuspect,[$this,$id])===FALSE) { | |||
| 				// NB: `session_destroy` can't be called at that stage; | |||
| 				// `session_start` not completed | |||
| 				$this->destroy($id); | |||
| 				$this->close(); | |||
| 				unset($fw->{'COOKIE.'.session_name()}); | |||
| 				$fw->error(403); | |||
| 			} | |||
| 		} | |||
| 		return $this->get('data'); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Write session data | |||
| 	*	@return TRUE | |||
| 	*	@param $id string | |||
| 	*	@param $data string | |||
| 	**/ | |||
| 	function write($id,$data) { | |||
| 		$this->set('session_id',$id); | |||
| 		$this->set('data',$data); | |||
| 		$this->set('ip',$this->_ip); | |||
| 		$this->set('agent',$this->_agent); | |||
| 		$this->set('stamp',time()); | |||
| 		$this->save(); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Destroy session | |||
| 	*	@return TRUE | |||
| 	*	@param $id string | |||
| 	**/ | |||
| 	function destroy($id) { | |||
| 		$this->erase(['session_id'=>$id]); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Garbage collector | |||
| 	*	@return TRUE | |||
| 	*	@param $max int | |||
| 	**/ | |||
| 	function cleanup($max) { | |||
| 		$this->erase(['$where'=>'this.stamp+'.$max.'<'.time()]); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return session id (if session has started) | |||
| 	 *	@return string|NULL | |||
| 	 **/ | |||
| 	function sid() { | |||
| 		return $this->sid; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return anti-CSRF token | |||
| 	 *	@return string | |||
| 	 **/ | |||
| 	function csrf() { | |||
| 		return $this->_csrf; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return IP address | |||
| 	 *	@return string | |||
| 	 **/ | |||
| 	function ip() { | |||
| 		return $this->_ip; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return Unix timestamp | |||
| 	 *	@return string|FALSE | |||
| 	 **/ | |||
| 	function stamp() { | |||
| 		if (!$this->sid) | |||
| 			session_start(); | |||
| 		return $this->dry()?FALSE:$this->get('stamp'); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return HTTP user agent | |||
| 	 *	@return string | |||
| 	 **/ | |||
| 	function agent() { | |||
| 		return $this->_agent; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Instantiate class | |||
| 	*	@param $db \DB\Mongo | |||
| 	*	@param $table string | |||
| 	*	@param $onsuspect callback | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL,$key=NULL) { | |||
| 		parent::__construct($db,$table); | |||
| 		$this->onsuspect=$onsuspect; | |||
| 		session_set_save_handler( | |||
| 			[$this,'open'], | |||
| 			[$this,'close'], | |||
| 			[$this,'read'], | |||
| 			[$this,'write'], | |||
| 			[$this,'destroy'], | |||
| 			[$this,'cleanup'] | |||
| 		); | |||
| 		register_shutdown_function('session_commit'); | |||
| 		$fw=\Base::instance(); | |||
| 		$headers=$fw->HEADERS; | |||
| 		$this->_csrf=$fw->hash($fw->SEED. | |||
| 			extension_loaded('openssl')? | |||
| 				implode(unpack('L',openssl_random_pseudo_bytes(4))): | |||
| 				mt_rand() | |||
| 			); | |||
| 		if ($key) | |||
| 			$fw->$key=$this->_csrf; | |||
| 		$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; | |||
| 		$this->_ip=$fw->IP; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,523 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace DB; | |||
| 
 | |||
| //! PDO wrapper | |||
| class SQL { | |||
| 
 | |||
| 	//@{ Error messages | |||
| 	const | |||
| 		E_PKey='Table %s does not have a primary key'; | |||
| 	//@} | |||
| 
 | |||
| 	const | |||
| 		PARAM_FLOAT='float'; | |||
| 
 | |||
| 	protected | |||
| 		//! UUID | |||
| 		$uuid, | |||
| 		//! Raw PDO | |||
| 		$pdo, | |||
| 		//! Data source name | |||
| 		$dsn, | |||
| 		//! Database engine | |||
| 		$engine, | |||
| 		//! Database name | |||
| 		$dbname, | |||
| 		//! Transaction flag | |||
| 		$trans=FALSE, | |||
| 		//! Number of rows affected by query | |||
| 		$rows=0, | |||
| 		//! SQL log | |||
| 		$log; | |||
| 
 | |||
| 	/** | |||
| 	*	Begin SQL transaction | |||
| 	*	@return bool | |||
| 	**/ | |||
| 	function begin() { | |||
| 		$out=$this->pdo->begintransaction(); | |||
| 		$this->trans=TRUE; | |||
| 		return $out; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Rollback SQL transaction | |||
| 	*	@return bool | |||
| 	**/ | |||
| 	function rollback() { | |||
| 		$out=$this->pdo->rollback(); | |||
| 		$this->trans=FALSE; | |||
| 		return $out; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Commit SQL transaction | |||
| 	*	@return bool | |||
| 	**/ | |||
| 	function commit() { | |||
| 		$out=$this->pdo->commit(); | |||
| 		$this->trans=FALSE; | |||
| 		return $out; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return transaction flag | |||
| 	*	@return bool | |||
| 	**/ | |||
| 	function trans() { | |||
| 		return $this->trans; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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; | |||
| 			case 'resource': | |||
| 				return \PDO::PARAM_LOB; | |||
| 			case 'float': | |||
| 				return self::PARAM_FLOAT; | |||
| 			default: | |||
| 				return \PDO::PARAM_STR; | |||
| 		} | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Cast value to PHP type | |||
| 	*	@return mixed | |||
| 	*	@param $type string | |||
| 	*	@param $val mixed | |||
| 	**/ | |||
| 	function value($type,$val) { | |||
| 		switch ($type) { | |||
| 			case self::PARAM_FLOAT: | |||
| 				if (!is_string($val)) | |||
| 					$val=str_replace(',','.',$val); | |||
| 				return $val; | |||
| 			case \PDO::PARAM_NULL: | |||
| 				return NULL; | |||
| 			case \PDO::PARAM_INT: | |||
| 				return (int)$val; | |||
| 			case \PDO::PARAM_BOOL: | |||
| 				return (bool)$val; | |||
| 			case \PDO::PARAM_STR: | |||
| 				return (string)$val; | |||
| 			case \PDO::PARAM_LOB: | |||
| 				return (binary)$val; | |||
| 		} | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Execute SQL statement(s) | |||
| 	*	@return array|int|FALSE | |||
| 	*	@param $cmds string|array | |||
| 	*	@param $args string|array | |||
| 	*	@param $ttl int|array | |||
| 	*	@param $log bool | |||
| 	*	@param $stamp bool | |||
| 	**/ | |||
| 	function exec($cmds,$args=NULL,$ttl=0,$log=TRUE,$stamp=FALSE) { | |||
| 		$tag=''; | |||
| 		if (is_array($ttl)) | |||
| 			list($ttl,$tag)=$ttl; | |||
| 		$auto=FALSE; | |||
| 		if (is_null($args)) | |||
| 			$args=[]; | |||
| 		elseif (is_scalar($args)) | |||
| 			$args=[1=>$args]; | |||
| 		if (is_array($cmds)) { | |||
| 			if (count($args)<($count=count($cmds))) | |||
| 				// Apply arguments to SQL commands | |||
| 				$args=array_fill(0,$count,$args); | |||
| 			if (!$this->trans) { | |||
| 				$this->begin(); | |||
| 				$auto=TRUE; | |||
| 			} | |||
| 		} | |||
| 		else { | |||
| 			$count=1; | |||
| 			$cmds=[$cmds]; | |||
| 			$args=[$args]; | |||
| 		} | |||
| 		if ($this->log===FALSE) | |||
| 			$log=FALSE; | |||
| 		$fw=\Base::instance(); | |||
| 		$cache=\Cache::instance(); | |||
| 		$result=FALSE; | |||
| 		for ($i=0;$i<$count;$i++) { | |||
| 			$cmd=$cmds[$i]; | |||
| 			$arg=$args[$i]; | |||
| 			// ensure 1-based arguments | |||
| 			if (array_key_exists(0,$arg)) { | |||
| 				array_unshift($arg,''); | |||
| 				unset($arg[0]); | |||
| 			} | |||
| 			if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd)) | |||
| 				continue; | |||
| 			$now=microtime(TRUE); | |||
| 			$keys=$vals=[]; | |||
| 			if ($fw->CACHE && $ttl && ($cached=$cache->exists( | |||
| 				$hash=$fw->hash($this->dsn.$cmd. | |||
| 				$fw->stringify($arg)).($tag?'.'.$tag:'').'.sql',$result)) && | |||
| 				$cached[0]+$ttl>microtime(TRUE)) { | |||
| 				foreach ($arg as $key=>$val) { | |||
| 					$vals[]=$fw->stringify(is_array($val)?$val[0]:$val); | |||
| 					$keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). | |||
| 						'/'; | |||
| 				} | |||
| 				if ($log) | |||
| 					$this->log.=($stamp?(date('r').' '):'').'('. | |||
| 						sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. | |||
| 						'[CACHED] '. | |||
| 						preg_replace($keys,$vals, | |||
| 							str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; | |||
| 			} | |||
| 			elseif (is_object($query=$this->pdo->prepare($cmd))) { | |||
| 				foreach ($arg as $key=>$val) { | |||
| 					if (is_array($val)) { | |||
| 						// User-specified data type | |||
| 						$query->bindvalue($key,$val[0], | |||
| 							$val[1]==self::PARAM_FLOAT?\PDO::PARAM_STR:$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))==self::PARAM_FLOAT? | |||
| 								\PDO::PARAM_STR:$type); | |||
| 						$vals[]=$fw->stringify($this->value($type,$val)); | |||
| 					} | |||
| 					$keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). | |||
| 						'/'; | |||
| 				} | |||
| 				if ($log) | |||
| 					$this->log.=($stamp?(date('r').' '):'').'(-0ms) '. | |||
| 						preg_replace($keys,$vals, | |||
| 							str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; | |||
| 				$query->execute(); | |||
| 				if ($log) | |||
| 					$this->log=str_replace('(-0ms)', | |||
| 						'('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms)', | |||
| 						$this->log); | |||
| 				if (($error=$query->errorinfo()) && $error[0]!=\PDO::ERR_NONE) { | |||
| 					// Statement-level error occurred | |||
| 					if ($this->trans) | |||
| 						$this->rollback(); | |||
| 					user_error('PDOStatement: '.$error[2],E_USER_ERROR); | |||
| 				} | |||
| 				if (preg_match('/(?:^[\s\(]*'. | |||
| 					'(?:WITH|EXPLAIN|SELECT|PRAGMA|SHOW)|RETURNING)\b/is',$cmd) || | |||
| 					(preg_match('/^\s*(?:CALL|EXEC)\b/is',$cmd) && | |||
| 						$query->columnCount())) { | |||
| 					$result=$query->fetchall(\PDO::FETCH_ASSOC); | |||
| 					// Work around SQLite quote bug | |||
| 					if (preg_match('/sqlite2?/',$this->engine)) | |||
| 						foreach ($result as $pos=>$rec) { | |||
| 							unset($result[$pos]); | |||
| 							$result[$pos]=[]; | |||
| 							foreach ($rec as $key=>$val) | |||
| 								$result[$pos][trim($key,'\'"[]`')]=$val; | |||
| 						} | |||
| 					$this->rows=count($result); | |||
| 					if ($fw->CACHE && $ttl) | |||
| 						// Save to cache backend | |||
| 						$cache->set($hash,$result,$ttl); | |||
| 				} | |||
| 				else | |||
| 					$this->rows=$result=$query->rowcount(); | |||
| 				$query->closecursor(); | |||
| 				unset($query); | |||
| 			} | |||
| 			elseif (($error=$this->errorinfo()) && $error[0]!=\PDO::ERR_NONE) { | |||
| 				// PDO-level error occurred | |||
| 				if ($this->trans) | |||
| 					$this->rollback(); | |||
| 				user_error('PDO: '.$error[2],E_USER_ERROR); | |||
| 			} | |||
| 
 | |||
| 		} | |||
| 		if ($this->trans && $auto) | |||
| 			$this->commit(); | |||
| 		return $result; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return number of rows affected by last query | |||
| 	*	@return int | |||
| 	**/ | |||
| 	function count() { | |||
| 		return $this->rows; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return SQL profiler results (or disable logging) | |||
| 	*	@return string | |||
| 	*	@param $flag bool | |||
| 	**/ | |||
| 	function log($flag=TRUE) { | |||
| 		if ($flag) | |||
| 			return $this->log; | |||
| 		$this->log=FALSE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return TRUE if table exists | |||
| 	*	@return bool | |||
| 	*	@param $table string | |||
| 	**/ | |||
| 	function exists($table) { | |||
| 		$mode=$this->pdo->getAttribute(\PDO::ATTR_ERRMODE); | |||
| 		$this->pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_SILENT); | |||
| 		$out=$this->pdo-> | |||
| 			query('SELECT 1 FROM '.$this->quotekey($table).' LIMIT 1'); | |||
| 		$this->pdo->setAttribute(\PDO::ATTR_ERRMODE,$mode); | |||
| 		return is_object($out); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Retrieve schema of SQL table | |||
| 	*	@return array|FALSE | |||
| 	*	@param $table string | |||
| 	*	@param $fields array|string | |||
| 	*	@param $ttl int|array | |||
| 	**/ | |||
| 	function schema($table,$fields=NULL,$ttl=0) { | |||
| 		$fw=\Base::instance(); | |||
| 		$cache=\Cache::instance(); | |||
| 		if ($fw->CACHE && $ttl && | |||
| 			($cached=$cache->exists( | |||
| 				$hash=$fw->hash($this->dsn.$table).'.schema',$result)) && | |||
| 			$cached[0]+$ttl>microtime(TRUE)) | |||
| 			return $result; | |||
| 		if (strpos($table,'.')) | |||
| 			list($schema,$table)=explode('.',$table); | |||
| 		// Supported engines | |||
| 		$cmd=[ | |||
| 			'sqlite2?'=>[ | |||
| 				'PRAGMA table_info(`'.$table.'`)', | |||
| 				'name','type','dflt_value','notnull',0,'pk',TRUE], | |||
| 			'mysql'=>[ | |||
| 				'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`', | |||
| 				'Field','Type','Default','Null','YES','Key','PRI'], | |||
| 			'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>[ | |||
| 				'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'=>[ | |||
| 				'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); | |||
| 		$conv=[ | |||
| 			'int\b|integer'=>\PDO::PARAM_INT, | |||
| 			'bool'=>\PDO::PARAM_BOOL, | |||
| 			'blob|bytea|image|binary'=>\PDO::PARAM_LOB, | |||
| 			'float|real|double|decimal|numeric'=>self::PARAM_FLOAT, | |||
| 			'.+'=>\PDO::PARAM_STR | |||
| 		]; | |||
| 		foreach ($cmd as $key=>$val) | |||
| 			if (preg_match('/'.$key.'/',$this->engine)) { | |||
| 				$rows=[]; | |||
| 				foreach ($this->exec($val[0],NULL) as $row) | |||
| 					if (!$fields || in_array($row[$val[1]],$fields)) { | |||
| 						foreach ($conv as $regex=>$type) | |||
| 							if (preg_match('/'.$regex.'/i',$row[$val[2]])) | |||
| 								break; | |||
| 						$rows[$row[$val[1]]]=[ | |||
| 							'type'=>$row[$val[2]], | |||
| 							'pdo_type'=>$type, | |||
| 							'default'=>is_string($row[$val[3]])? | |||
| 								preg_replace('/^\s*([\'"])(.*)\1\s*/','\2', | |||
| 								$row[$val[3]]):$row[$val[3]], | |||
| 							'nullable'=>$row[$val[4]]==$val[5], | |||
| 							'pkey'=>$row[$val[6]]==$val[7] | |||
| 						]; | |||
| 					} | |||
| 				if ($fw->CACHE && $ttl) | |||
| 					// Save to cache backend | |||
| 					$cache->set($hash,$rows,$ttl); | |||
| 				return $rows; | |||
| 			} | |||
| 		user_error(sprintf(self::E_PKey,$table),E_USER_ERROR); | |||
| 		return FALSE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Quote string | |||
| 	*	@return string | |||
| 	*	@param $val mixed | |||
| 	*	@param $type int | |||
| 	**/ | |||
| 	function quote($val,$type=\PDO::PARAM_STR) { | |||
| 		return $this->engine=='odbc'? | |||
| 			(is_string($val)? | |||
| 				\Base::instance()->stringify(str_replace('\'','\'\'',$val)): | |||
| 				$val): | |||
| 			$this->pdo->quote($val,$type); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return UUID | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function uuid() { | |||
| 		return $this->uuid; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return parent object | |||
| 	*	@return \PDO | |||
| 	**/ | |||
| 	function pdo() { | |||
| 		return $this->pdo; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return database engine | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function driver() { | |||
| 		return $this->engine; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return server version | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function version() { | |||
| 		return $this->pdo->getattribute(\PDO::ATTR_SERVER_VERSION); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return database name | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function name() { | |||
| 		return $this->dbname; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return quoted identifier name | |||
| 	*	@return string | |||
| 	*	@param $key | |||
| 	*	@param bool $split | |||
| 	 **/ | |||
| 	function quotekey($key, $split=TRUE) { | |||
| 		$delims=[ | |||
| 			'sqlite2?|mysql'=>'``', | |||
| 			'pgsql|oci'=>'""', | |||
| 			'mssql|sqlsrv|odbc|sybase|dblib'=>'[]' | |||
| 		]; | |||
| 		$use=''; | |||
| 		foreach ($delims as $engine=>$delim) | |||
| 			if (preg_match('/'.$engine.'/',$this->engine)) { | |||
| 				$use=$delim; | |||
| 				break; | |||
| 			} | |||
| 		return $use[0].($split ? implode($use[1].'.'.$use[0],explode('.',$key)) | |||
| 			: $key).$use[1]; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Redirect call to PDO object | |||
| 	*	@return mixed | |||
| 	*	@param $func string | |||
| 	*	@param $args array | |||
| 	**/ | |||
| 	function __call($func,array $args) { | |||
| 		return call_user_func_array([$this->pdo,$func],$args); | |||
| 	} | |||
| 
 | |||
| 	//! Prohibit cloning | |||
| 	private function __clone() { | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Instantiate class | |||
| 	*	@param $dsn string | |||
| 	*	@param $user string | |||
| 	*	@param $pw string | |||
| 	*	@param $options array | |||
| 	**/ | |||
| 	function __construct($dsn,$user=NULL,$pw=NULL,array $options=NULL) { | |||
| 		$fw=\Base::instance(); | |||
| 		$this->uuid=$fw->hash($this->dsn=$dsn); | |||
| 		if (preg_match('/^.+?(?:dbname|database)=(.+?)(?=;|$)/is',$dsn,$parts)) | |||
| 			$this->dbname=$parts[1]; | |||
| 		if (!$options) | |||
| 			$options=[]; | |||
| 		if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql') | |||
| 			$options+=[\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '. | |||
| 				strtolower(str_replace('-','',$fw->ENCODING)).';']; | |||
| 		$this->pdo=new \PDO($dsn,$user,$pw,$options); | |||
| 		$this->engine=$this->pdo->getattribute(\PDO::ATTR_DRIVER_NAME); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,759 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace DB\SQL; | |||
| 
 | |||
| //! SQL data mapper | |||
| class Mapper extends \DB\Cursor { | |||
| 
 | |||
| 	//@{ Error messages | |||
| 	const | |||
| 		E_PKey='Table %s does not have a primary key'; | |||
| 	//@} | |||
| 
 | |||
| 	protected | |||
| 		//! PDO wrapper | |||
| 		$db, | |||
| 		//! Database engine | |||
| 		$engine, | |||
| 		//! SQL table | |||
| 		$source, | |||
| 		//! SQL table (quoted) | |||
| 		$table, | |||
| 		//! Alias for SQL table | |||
| 		$as, | |||
| 		//! Last insert ID | |||
| 		$_id, | |||
| 		//! Defined fields | |||
| 		$fields, | |||
| 		//! Adhoc fields | |||
| 		$adhoc=[], | |||
| 		//! Dynamic properties | |||
| 		$props=[]; | |||
| 
 | |||
| 	/** | |||
| 	*	Return database type | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function dbtype() { | |||
| 		return 'SQL'; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return mapped table | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function table() { | |||
| 		return $this->source; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return TRUE if any/specified field value has changed | |||
| 	*	@return bool | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function changed($key=NULL) { | |||
| 		if (isset($key)) | |||
| 			return $this->fields[$key]['changed']; | |||
| 		foreach($this->fields as $key=>$field) | |||
| 			if ($field['changed']) | |||
| 				return TRUE; | |||
| 		return FALSE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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]['initial']!==$val || | |||
| 				$this->fields[$key]['default']!==$val && is_null($val)) | |||
| 				$this->fields[$key]['changed']=TRUE; | |||
| 			return $this->fields[$key]['value']=$val; | |||
| 		} | |||
| 		// Adjust result on existing expressions | |||
| 		if (isset($this->adhoc[$key])) | |||
| 			$this->adhoc[$key]['value']=$val; | |||
| 		elseif (is_string($val)) | |||
| 			// Parenthesize expression in case it's a subquery | |||
| 			$this->adhoc[$key]=['expr'=>'('.$val.')','value'=>NULL]; | |||
| 		else | |||
| 			$this->props[$key]=$val; | |||
| 		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']; | |||
| 		elseif (array_key_exists($key,$this->props)) | |||
| 			return $this->props[$key]; | |||
| 		user_error(sprintf(self::E_Field,$key),E_USER_ERROR); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Clear value of field | |||
| 	*	@return NULL | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function clear($key) { | |||
| 		if (array_key_exists($key,$this->adhoc)) | |||
| 			unset($this->adhoc[$key]); | |||
| 		else | |||
| 			unset($this->props[$key]); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Invoke dynamic method | |||
| 	*	@return mixed | |||
| 	*	@param $func string | |||
| 	*	@param $args array | |||
| 	**/ | |||
| 	function __call($func,$args) { | |||
| 		return call_user_func_array( | |||
| 			(array_key_exists($func,$this->props)? | |||
| 				$this->props[$func]: | |||
| 				$this->$func),$args | |||
| 		); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Convert array to mapper object | |||
| 	*	@return static | |||
| 	*	@param $row array | |||
| 	**/ | |||
| 	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; | |||
| 			$mapper->{$var}[$key]['initial']=$val; | |||
| 			if ($var=='fields' && $mapper->{$var}[$key]['pkey']) | |||
| 				$mapper->{$var}[$key]['previous']=$val; | |||
| 		} | |||
| 		$mapper->query=[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 arguments | |||
| 	*	@return array | |||
| 	*	@param $fields string | |||
| 	*	@param $filter string|array | |||
| 	*	@param $options array | |||
| 	**/ | |||
| 	function stringify($fields,$filter=NULL,array $options=NULL) { | |||
| 		if (!$options) | |||
| 			$options=[]; | |||
| 		$options+=[ | |||
| 			'group'=>NULL, | |||
| 			'order'=>NULL, | |||
| 			'limit'=>0, | |||
| 			'offset'=>0, | |||
| 			'comment'=>NULL | |||
| 		]; | |||
| 		$db=$this->db; | |||
| 		$sql='SELECT '.$fields.' FROM '.$this->table; | |||
| 		if (isset($this->as)) | |||
| 			$sql.=' AS '.$this->db->quotekey($this->as); | |||
| 		$args=[]; | |||
| 		if (is_array($filter)) { | |||
| 			$args=isset($filter[1]) && is_array($filter[1])? | |||
| 				$filter[1]: | |||
| 				array_slice($filter,1,NULL,TRUE); | |||
| 			$args=is_array($args)?$args:[1=>$args]; | |||
| 			list($filter)=$filter; | |||
| 		} | |||
| 		if ($filter) | |||
| 			$sql.=' WHERE '.$filter; | |||
| 		if ($options['group']) { | |||
| 			$sql.=' GROUP BY '.implode(',',array_map( | |||
| 				function($str) use($db) { | |||
| 					return preg_replace_callback( | |||
| 						'/\b(\w+[._\-\w]*)\h*(HAVING.+|$)/i', | |||
| 						function($parts) use($db) { | |||
| 							return $db->quotekey($parts[1]). | |||
| 								(isset($parts[2])?(' '.$parts[2]):''); | |||
| 						}, | |||
| 						$str | |||
| 					); | |||
| 				}, | |||
| 				explode(',',$options['group']))); | |||
| 		} | |||
| 		if ($options['order']) { | |||
| 			$char=substr($db->quotekey(''),0,1);// quoting char | |||
| 			$order=' ORDER BY '.(is_bool(strpos($options['order'],$char))? | |||
| 				implode(',',array_map(function($str) use($db) { | |||
| 					return preg_match('/^\h*(\w+[._\-\w]*)'. | |||
| 						'(?:\h+((?:ASC|DESC)[\w\h]*))?\h*$/i', | |||
| 						$str,$parts)? | |||
| 						($db->quotekey($parts[1]). | |||
| 						(isset($parts[2])?(' '.$parts[2]):'')):$str; | |||
| 				},explode(',',$options['order']))): | |||
| 				$options['order']); | |||
| 		} | |||
| 		// SQL Server fixes | |||
| 		if (preg_match('/mssql|sqlsrv|odbc/', $this->engine) && | |||
| 			($options['limit'] || $options['offset'])) { | |||
| 			// order by pkey when no ordering option was given | |||
| 			if (!$options['order']) | |||
| 				foreach ($this->fields as $key=>$field) | |||
| 					if ($field['pkey']) { | |||
| 						$order=' ORDER BY '.$db->quotekey($key); | |||
| 						break; | |||
| 					} | |||
| 			$ofs=$options['offset']?(int)$options['offset']:0; | |||
| 			$lmt=$options['limit']?(int)$options['limit']:0; | |||
| 			if (strncmp($db->version(),'11',2)>=0) { | |||
| 				// SQL Server >= 2012 | |||
| 				$sql.=$order.' OFFSET '.$ofs.' ROWS'; | |||
| 				if ($lmt) | |||
| 					$sql.=' FETCH NEXT '.$lmt.' ROWS ONLY'; | |||
| 			} | |||
| 			else { | |||
| 				// SQL Server 2008 | |||
| 				$sql=preg_replace('/SELECT/', | |||
| 					'SELECT '. | |||
| 					($lmt>0?'TOP '.($ofs+$lmt):'').' ROW_NUMBER() '. | |||
| 					'OVER ('.$order.') AS rnum,',$sql.$order,1); | |||
| 				$sql='SELECT * FROM ('.$sql.') x WHERE rnum > '.($ofs); | |||
| 			} | |||
| 		} | |||
| 		else { | |||
| 			if (isset($order)) | |||
| 				$sql.=$order; | |||
| 			if ($options['limit']) | |||
| 				$sql.=' LIMIT '.(int)$options['limit']; | |||
| 			if ($options['offset']) | |||
| 				$sql.=' OFFSET '.(int)$options['offset']; | |||
| 		} | |||
| 		if ($options['comment']) | |||
| 			$sql.="\n".' /* '.$options['comment'].' */'; | |||
| 		return [$sql,$args]; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Build query string and execute | |||
| 	*	@return static[] | |||
| 	*	@param $fields string | |||
| 	*	@param $filter string|array | |||
| 	*	@param $options array | |||
| 	*	@param $ttl int|array | |||
| 	**/ | |||
| 	function select($fields,$filter=NULL,array $options=NULL,$ttl=0) { | |||
| 		list($sql,$args)=$this->stringify($fields,$filter,$options); | |||
| 		$result=$this->db->exec($sql,$args,$ttl); | |||
| 		$out=[]; | |||
| 		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); | |||
| 				} | |||
| 				unset($val); | |||
| 			} | |||
| 			$out[]=$this->factory($row); | |||
| 			unset($row); | |||
| 		} | |||
| 		return $out; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return records that match criteria | |||
| 	*	@return static[] | |||
| 	*	@param $filter string|array | |||
| 	*	@param $options array | |||
| 	*	@param $ttl int|array | |||
| 	**/ | |||
| 	function find($filter=NULL,array $options=NULL,$ttl=0) { | |||
| 		if (!$options) | |||
| 			$options=[]; | |||
| 		$options+=[ | |||
| 			'group'=>NULL, | |||
| 			'order'=>NULL, | |||
| 			'limit'=>0, | |||
| 			'offset'=>0 | |||
| 		]; | |||
| 		$adhoc=''; | |||
| 		foreach ($this->adhoc as $key=>$field) | |||
| 			$adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key); | |||
| 		return $this->select( | |||
| 			($options['group'] && !preg_match('/mysql|sqlite/',$this->engine)? | |||
| 				$options['group']: | |||
| 				implode(',',array_map([$this->db,'quotekey'], | |||
| 					array_keys($this->fields)))).$adhoc,$filter,$options,$ttl); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Count records that match criteria | |||
| 	*	@return int | |||
| 	*	@param $filter string|array | |||
| 	*	@param $options array | |||
| 	*	@param $ttl int|array | |||
| 	**/ | |||
| 	function count($filter=NULL,array $options=NULL,$ttl=0) { | |||
| 		$adhoc=[]; | |||
| 		// with grouping involved, we need to wrap the actualy query and count the results | |||
| 		if ($subquery_mode=($options && !empty($options['group']))) { | |||
| 			$group_string=preg_replace('/HAVING.+$/i','',$options['group']); | |||
| 			$group_fields=array_flip(array_map('trim',explode(',',$group_string))); | |||
| 			foreach ($this->adhoc as $key=>$field) | |||
| 				// add adhoc fields that are used for grouping | |||
| 				if (isset($group_fields[$key])) | |||
| 					$adhoc[]=$field['expr'].' AS '.$this->db->quotekey($key); | |||
| 			$fields=implode(',',$adhoc); | |||
| 			if (empty($fields)) | |||
| 				// Select at least one field, ideally the grouping fields | |||
| 				// or sqlsrv fails | |||
| 				$fields=$group_string; | |||
| 			if (preg_match('/mssql|dblib|sqlsrv/',$this->engine)) | |||
| 				$fields='TOP 100 PERCENT '.$fields; | |||
| 		} else { | |||
| 			// for simple count just add a new adhoc counter | |||
| 			$fields='COUNT(*) AS '.$this->db->quotekey('_rows'); | |||
| 		} | |||
| 		list($sql,$args)=$this->stringify($fields,$filter,$options); | |||
| 		if ($subquery_mode) | |||
| 			$sql='SELECT COUNT(*) AS '.$this->db->quotekey('_rows').' '. | |||
| 				'FROM ('.$sql.') AS '.$this->db->quotekey('_temp'); | |||
| 		$result=$this->db->exec($sql,$args,$ttl); | |||
| 		unset($this->adhoc['_rows']); | |||
| 		return (int)$result[0]['_rows']; | |||
| 	} | |||
| 	/** | |||
| 	*	Return record at specified offset using same criteria as | |||
| 	*	previous load() call and make it active | |||
| 	*	@return static | |||
| 	*	@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['initial']=$field['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 (!$dry && isset($this->trigger['load'])) | |||
| 			\Base::instance()->call($this->trigger['load'],$this); | |||
| 		return $out; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Insert new record | |||
| 	*	@return static | |||
| 	**/ | |||
| 	function insert() { | |||
| 		$args=[]; | |||
| 		$actr=0; | |||
| 		$nctr=0; | |||
| 		$fields=''; | |||
| 		$values=''; | |||
| 		$filter=''; | |||
| 		$pkeys=[]; | |||
| 		$nkeys=[]; | |||
| 		$ckeys=[]; | |||
| 		$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'], | |||
| 				[$this,$pkeys])===FALSE) | |||
| 			return $this; | |||
| 		if ($this->valid()) | |||
| 			// duplicate record | |||
| 			foreach ($this->fields as $key=>&$field) { | |||
| 				$field['changed']=true; | |||
| 				if ($field['pkey'] && !$inc && $field['pdo_type']==\PDO::PARAM_INT | |||
| 					&& !$field['nullable']) | |||
| 					$inc=$key; | |||
| 				unset($field); | |||
| 			} | |||
| 		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'] && | |||
| 					is_null($field['default'])) | |||
| 					$inc=$key; | |||
| 				$filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; | |||
| 				$nkeys[$nctr+1]=[$field['value'],$field['pdo_type']]; | |||
| 				$nctr++; | |||
| 			} | |||
| 			if ($field['changed'] && $key!=$inc) { | |||
| 				$fields.=($actr?',':'').$this->db->quotekey($key); | |||
| 				$values.=($actr?',':'').'?'; | |||
| 				$args[$actr+1]=[$field['value'],$field['pdo_type']]; | |||
| 				$actr++; | |||
| 				$ckeys[]=$key; | |||
| 			} | |||
| 			unset($field); | |||
| 		} | |||
| 		if ($fields) { | |||
| 			$add=$aik=''; | |||
| 			if ($this->engine=='pgsql' && !empty($pkeys)) { | |||
| 				$names=array_keys($pkeys); | |||
| 				$aik=end($names); | |||
| 				$add=' RETURNING '.$this->db->quotekey($aik); | |||
| 			} | |||
| 			$lID=$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.')'.$add,$args | |||
| 			); | |||
| 			if ($this->engine=='pgsql' && $lID && $aik) | |||
| 				$this->_id=$lID[0][$aik]; | |||
| 			elseif ($this->engine!='oci') | |||
| 				$this->_id=$this->db->lastinsertid(); | |||
| 			// Reload to obtain default and auto-increment field values | |||
| 			if ($reload=(($inc && $this->_id) || $filter)) | |||
| 				$this->load($inc? | |||
| 					[$inc.'=?',$this->db->value( | |||
| 						$this->fields[$inc]['pdo_type'],$this->_id)]: | |||
| 					[$filter,$nkeys]); | |||
| 			if (isset($this->trigger['afterinsert'])) | |||
| 				\Base::instance()->call($this->trigger['afterinsert'], | |||
| 					[$this,$pkeys]); | |||
| 			// reset changed flag after calling afterinsert | |||
| 			if (!$reload) | |||
| 				foreach ($this->fields as $key=>&$field) { | |||
| 					$field['changed']=FALSE; | |||
| 					$field['initial']=$field['value']; | |||
| 					unset($field); | |||
| 				} | |||
| 		} | |||
| 		return $this; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Update current record | |||
| 	*	@return static | |||
| 	**/ | |||
| 	function update() { | |||
| 		$args=[]; | |||
| 		$ctr=0; | |||
| 		$pairs=''; | |||
| 		$pkeys=[]; | |||
| 		foreach ($this->fields as $key=>$field) | |||
| 			if ($field['pkey']) | |||
| 				$pkeys[$key]=$field['previous']; | |||
| 		if (isset($this->trigger['beforeupdate']) && | |||
| 			\Base::instance()->call($this->trigger['beforeupdate'], | |||
| 				[$this,$pkeys])===FALSE) | |||
| 			return $this; | |||
| 		foreach ($this->fields as $key=>$field) | |||
| 			if ($field['changed']) { | |||
| 				$pairs.=($pairs?',':'').$this->db->quotekey($key).'=?'; | |||
| 				$args[++$ctr]=[$field['value'],$field['pdo_type']]; | |||
| 			} | |||
| 		if ($pairs) { | |||
| 			$filter=''; | |||
| 			foreach ($this->fields as $key=>$field) | |||
| 				if ($field['pkey']) { | |||
| 					$filter.=($filter?' AND ':' WHERE '). | |||
| 						$this->db->quotekey($key).'=?'; | |||
| 					$args[++$ctr]=[$field['previous'],$field['pdo_type']]; | |||
| 				} | |||
| 			if (!$filter) | |||
| 				user_error(sprintf(self::E_PKey,$this->source),E_USER_ERROR); | |||
| 			$sql='UPDATE '.$this->table.' SET '.$pairs.$filter; | |||
| 			$this->db->exec($sql,$args); | |||
| 		} | |||
| 		if (isset($this->trigger['afterupdate'])) | |||
| 			\Base::instance()->call($this->trigger['afterupdate'], | |||
| 				[$this,$pkeys]); | |||
| 		// reset changed flag after calling afterupdate | |||
| 		foreach ($this->fields as $key=>&$field) { | |||
| 				$field['changed']=FALSE; | |||
| 				$field['initial']=$field['value']; | |||
| 				unset($field); | |||
| 			} | |||
| 		return $this; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 * batch-update multiple records at once | |||
| 	 * @param string|array $filter | |||
| 	 * @return int | |||
| 	 */ | |||
| 	function updateAll($filter=NULL) { | |||
| 		$args=[]; | |||
| 		$ctr=$out=0; | |||
| 		$pairs=''; | |||
| 		foreach ($this->fields as $key=>$field) | |||
| 			if ($field['changed']) { | |||
| 				$pairs.=($pairs?',':'').$this->db->quotekey($key).'=?'; | |||
| 				$args[++$ctr]=[$field['value'],$field['pdo_type']]; | |||
| 			} | |||
| 		if ($filter) | |||
| 			if (is_array($filter)) { | |||
| 				$cond=array_shift($filter); | |||
| 				$args=array_merge($args,$filter); | |||
| 				$filter=' WHERE '.$cond; | |||
| 			} else | |||
| 				$filter=' WHERE '.$filter; | |||
| 		if ($pairs) { | |||
| 			$sql='UPDATE '.$this->table.' SET '.$pairs.$filter; | |||
| 			$out = $this->db->exec($sql,$args); | |||
| 		} | |||
| 		// reset changed flag after calling afterupdate | |||
| 		foreach ($this->fields as $key=>&$field) { | |||
| 			$field['changed']=FALSE; | |||
| 			$field['initial']=$field['value']; | |||
| 			unset($field); | |||
| 		} | |||
| 		return $out; | |||
| 	} | |||
| 
 | |||
| 
 | |||
| 	/** | |||
| 	*	Delete current record | |||
| 	*	@return int | |||
| 	*	@param $quick bool | |||
| 	*	@param $filter string|array | |||
| 	**/ | |||
| 	function erase($filter=NULL,$quick=TRUE) { | |||
| 		if (isset($filter)) { | |||
| 			if (!$quick) { | |||
| 				$out=0; | |||
| 				foreach ($this->find($filter) as $mapper) | |||
| 					$out+=$mapper->erase(); | |||
| 				return $out; | |||
| 			} | |||
| 			$args=[]; | |||
| 			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:[1=>$args]; | |||
| 				list($filter)=$filter; | |||
| 			} | |||
| 			return $this->db-> | |||
| 				exec('DELETE FROM '.$this->table. | |||
| 				($filter?' WHERE '.$filter:'').';',$args); | |||
| 		} | |||
| 		$args=[]; | |||
| 		$ctr=0; | |||
| 		$filter=''; | |||
| 		$pkeys=[]; | |||
| 		foreach ($this->fields as $key=>&$field) { | |||
| 			if ($field['pkey']) { | |||
| 				$filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; | |||
| 				$args[$ctr+1]=[$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); | |||
| 		} | |||
| 		if (!$filter) | |||
| 			user_error(sprintf(self::E_PKey,$this->source),E_USER_ERROR); | |||
| 		foreach ($this->adhoc as &$field) { | |||
| 			$field['value']=NULL; | |||
| 			unset($field); | |||
| 		} | |||
| 		parent::erase(); | |||
| 		if (isset($this->trigger['beforeerase']) && | |||
| 			\Base::instance()->call($this->trigger['beforeerase'], | |||
| 				[$this,$pkeys])===FALSE) | |||
| 			return 0; | |||
| 		$out=$this->db-> | |||
| 			exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args); | |||
| 		if (isset($this->trigger['aftererase'])) | |||
| 			\Base::instance()->call($this->trigger['aftererase'], | |||
| 				[$this,$pkeys]); | |||
| 		return $out; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Reset cursor | |||
| 	*	@return NULL | |||
| 	**/ | |||
| 	function reset() { | |||
| 		foreach ($this->fields as &$field) { | |||
| 			$field['value']=NULL; | |||
| 			$field['initial']=NULL; | |||
| 			$field['changed']=FALSE; | |||
| 			if ($field['pkey']) | |||
| 				$field['previous']=NULL; | |||
| 			unset($field); | |||
| 		} | |||
| 		foreach ($this->adhoc as &$field) { | |||
| 			$field['value']=NULL; | |||
| 			unset($field); | |||
| 		} | |||
| 		parent::reset(); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Hydrate mapper object using hive array variable | |||
| 	*	@return NULL | |||
| 	*	@param $var array|string | |||
| 	*	@param $func callback | |||
| 	**/ | |||
| 	function copyfrom($var,$func=NULL) { | |||
| 		if (is_string($var)) | |||
| 			$var=\Base::instance()->$var; | |||
| 		if ($func) | |||
| 			$var=call_user_func($func,$var); | |||
| 		foreach ($var as $key=>$val) | |||
| 			if (in_array($key,array_keys($this->fields))) | |||
| 				$this->set($key,$val); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Populate hive array variable with mapper fields | |||
| 	*	@return NULL | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function copyto($key) { | |||
| 		$var=&\Base::instance()->ref($key); | |||
| 		foreach ($this->fields+$this->adhoc as $key=>$field) | |||
| 			$var[$key]=$field['value']; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return schema and, if the first argument is provided, update it | |||
| 	*	@return array | |||
| 	*	@param $fields NULL|array | |||
| 	**/ | |||
| 	function schema($fields=null) { | |||
| 		if ($fields) | |||
| 			$this->fields = $fields; | |||
| 		return $this->fields; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return field names | |||
| 	*	@return array | |||
| 	*	@param $adhoc bool | |||
| 	**/ | |||
| 	function fields($adhoc=TRUE) { | |||
| 		return array_keys($this->fields+($adhoc?$this->adhoc:[])); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return TRUE if field is not nullable | |||
| 	*	@return bool | |||
| 	*	@param $field string | |||
| 	**/ | |||
| 	function required($field) { | |||
| 		return isset($this->fields[$field]) && | |||
| 			!$this->fields[$field]['nullable']; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Retrieve external iterator for fields | |||
| 	*	@return object | |||
| 	**/ | |||
| 	function getiterator() { | |||
| 		return new \ArrayIterator($this->cast()); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Assign alias for table | |||
| 	*	@param $alias string | |||
| 	**/ | |||
| 	function alias($alias) { | |||
| 		$this->as=$alias; | |||
| 		return $this; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Instantiate class | |||
| 	*	@param $db \DB\SQL | |||
| 	*	@param $table string | |||
| 	*	@param $fields array|string | |||
| 	*	@param $ttl int|array | |||
| 	**/ | |||
| 	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(); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,222 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace DB\SQL; | |||
| 
 | |||
| //! SQL-managed session handler | |||
| class Session extends Mapper { | |||
| 
 | |||
| 	protected | |||
| 		//! Session ID | |||
| 		$sid, | |||
| 		//! Anti-CSRF token | |||
| 		$_csrf, | |||
| 		//! User agent | |||
| 		$_agent, | |||
| 		//! IP, | |||
| 		$_ip, | |||
| 		//! Suspect callback | |||
| 		$onsuspect; | |||
| 
 | |||
| 	/** | |||
| 	*	Open session | |||
| 	*	@return TRUE | |||
| 	*	@param $path string | |||
| 	*	@param $name string | |||
| 	**/ | |||
| 	function open($path,$name) { | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Close session | |||
| 	*	@return TRUE | |||
| 	**/ | |||
| 	function close() { | |||
| 		$this->reset(); | |||
| 		$this->sid=NULL; | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return session data in serialized format | |||
| 	*	@return string | |||
| 	*	@param $id string | |||
| 	**/ | |||
| 	function read($id) { | |||
| 		$this->load(['session_id=?',$this->sid=$id]); | |||
| 		if ($this->dry()) | |||
| 			return ''; | |||
| 		if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { | |||
| 			$fw=\Base::instance(); | |||
| 			if (!isset($this->onsuspect) || | |||
| 				$fw->call($this->onsuspect,[$this,$id])===FALSE) { | |||
| 				//NB: `session_destroy` can't be called at that stage (`session_start` not completed) | |||
| 				$this->destroy($id); | |||
| 				$this->close(); | |||
| 				unset($fw->{'COOKIE.'.session_name()}); | |||
| 				$fw->error(403); | |||
| 			} | |||
| 		} | |||
| 		return $this->get('data'); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Write session data | |||
| 	*	@return TRUE | |||
| 	*	@param $id string | |||
| 	*	@param $data string | |||
| 	**/ | |||
| 	function write($id,$data) { | |||
| 		$this->set('session_id',$id); | |||
| 		$this->set('data',$data); | |||
| 		$this->set('ip',$this->_ip); | |||
| 		$this->set('agent',$this->_agent); | |||
| 		$this->set('stamp',time()); | |||
| 		$this->save(); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Destroy session | |||
| 	*	@return TRUE | |||
| 	*	@param $id string | |||
| 	**/ | |||
| 	function destroy($id) { | |||
| 		$this->erase(['session_id=?',$id]); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Garbage collector | |||
| 	*	@return TRUE | |||
| 	*	@param $max int | |||
| 	**/ | |||
| 	function cleanup($max) { | |||
| 		$this->erase(['stamp+?<?',$max,time()]); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return session id (if session has started) | |||
| 	*	@return string|NULL | |||
| 	**/ | |||
| 	function sid() { | |||
| 		return $this->sid; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return anti-CSRF token | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function csrf() { | |||
| 		return $this->_csrf; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return IP address | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function ip() { | |||
| 		return $this->_ip; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return Unix timestamp | |||
| 	*	@return string|FALSE | |||
| 	**/ | |||
| 	function stamp() { | |||
| 		if (!$this->sid) | |||
| 			session_start(); | |||
| 		return $this->dry()?FALSE:$this->get('stamp'); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return HTTP user agent | |||
| 	*	@return string | |||
| 	**/ | |||
| 	function agent() { | |||
| 		return $this->_agent; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Instantiate class | |||
| 	*	@param $db \DB\SQL | |||
| 	*	@param $table string | |||
| 	*	@param $force bool | |||
| 	*	@param $onsuspect callback | |||
| 	*	@param $key string | |||
| 	*	@param $type string, column type for data field | |||
| 	**/ | |||
| 	function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL,$key=NULL,$type='TEXT') { | |||
| 		if ($force) { | |||
| 			$eol="\n"; | |||
| 			$tab="\t"; | |||
| 			$sqlsrv=preg_match('/mssql|sqlsrv|sybase/',$db->driver()); | |||
| 			$db->exec( | |||
| 				($sqlsrv? | |||
| 					('IF NOT EXISTS (SELECT * FROM sysobjects WHERE '. | |||
| 						'name='.$db->quote($table).' AND xtype=\'U\') '. | |||
| 						'CREATE TABLE dbo.'): | |||
| 					('CREATE TABLE IF NOT EXISTS '. | |||
| 						((($name=$db->name())&&$db->driver()!='pgsql')? | |||
| 							($db->quotekey($name,FALSE).'.'):''))). | |||
| 				$db->quotekey($table,FALSE).' ('.$eol. | |||
| 					($sqlsrv?$tab.$db->quotekey('id').' INT IDENTITY,'.$eol:''). | |||
| 					$tab.$db->quotekey('session_id').' VARCHAR(255),'.$eol. | |||
| 					$tab.$db->quotekey('data').' '.$type.','.$eol. | |||
| 					$tab.$db->quotekey('ip').' VARCHAR(45),'.$eol. | |||
| 					$tab.$db->quotekey('agent').' VARCHAR(300),'.$eol. | |||
| 					$tab.$db->quotekey('stamp').' INTEGER,'.$eol. | |||
| 					$tab.'PRIMARY KEY ('.$db->quotekey($sqlsrv?'id':'session_id').')'.$eol. | |||
| 				($sqlsrv?',CONSTRAINT [UK_session_id] UNIQUE(session_id)':''). | |||
| 				');' | |||
| 			); | |||
| 		} | |||
| 		parent::__construct($db,$table); | |||
| 		$this->onsuspect=$onsuspect; | |||
| 		session_set_save_handler( | |||
| 			[$this,'open'], | |||
| 			[$this,'close'], | |||
| 			[$this,'read'], | |||
| 			[$this,'write'], | |||
| 			[$this,'destroy'], | |||
| 			[$this,'cleanup'] | |||
| 		); | |||
| 		register_shutdown_function('session_commit'); | |||
| 		$fw=\Base::instance(); | |||
| 		$headers=$fw->HEADERS; | |||
| 		$this->_csrf=$fw->hash($fw->SEED. | |||
| 			extension_loaded('openssl')? | |||
| 				implode(unpack('L',openssl_random_pseudo_bytes(4))): | |||
| 				mt_rand() | |||
| 			); | |||
| 		if ($key) | |||
| 			$fw->$key=$this->_csrf; | |||
| 		$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; | |||
| 		if (strlen($this->_agent) > 300) { | |||
| 			$this->_agent = substr($this->_agent, 0, 300); | |||
| 		} | |||
| 		$this->_ip=$fw->IP; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,42 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! 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([self::$fw,$func],$args); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,616 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! Image manipulation tools | |||
| class Image { | |||
| 
 | |||
| 	//@{ Messages | |||
| 	const | |||
| 		E_Color='Invalid color specified: %s', | |||
| 		E_File='File not found', | |||
| 		E_Font='CAPTCHA font not found', | |||
| 		E_TTF='No TrueType support in GD module', | |||
| 		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|string | |||
| 	**/ | |||
| 	function rgb($color) { | |||
| 		if (is_string($color)) | |||
| 			$color=hexdec($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),E_USER_ERROR); | |||
| 		$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=NULL,$height=NULL,$crop=TRUE,$enlarge=TRUE) { | |||
| 		if (is_null($width) && is_null($height)) | |||
| 			return $this; | |||
| 		$origw=$this->width(); | |||
| 		$origh=$this->height(); | |||
| 		if (is_null($width)) | |||
| 			$width=round(($height/$origh)*$origw); | |||
| 		if (is_null($height)) | |||
| 			$height=round(($width/$origw)*$origh); | |||
| 		// Adjust dimensions; retain aspect ratio | |||
| 		$ratio=$origw/$origh; | |||
| 		if (!$crop) { | |||
| 			if ($width/$ratio<=$height) | |||
| 				$height=round($width/$ratio); | |||
| 			else | |||
| 				$width=round($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=round($origh*$width/$height); | |||
| 				imagecopyresampled($tmp,$this->data, | |||
| 					0,0,($origw-$cropw)/2,0,$width,$height,$cropw,$origh); | |||
| 			} | |||
| 			else { | |||
| 				$croph=round($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=[ | |||
| 			[.5,1,1,0,1,1], | |||
| 			[.5,0,1,0,.5,1,0,1], | |||
| 			[.5,0,1,0,1,1,.5,1,1,.5], | |||
| 			[0,.5,.5,0,1,.5,.5,1,.5,.5], | |||
| 			[0,.5,1,0,1,1,0,1,1,.5], | |||
| 			[1,0,1,1,.5,1,1,.5,.5,.5], | |||
| 			[0,0,1,0,1,.5,0,0,.5,1,0,1], | |||
| 			[0,0,.5,0,1,.5,.5,1,0,1,.5,.5], | |||
| 			[.5,0,.5,.5,1,.5,1,1,.5,1,.5,.5,0,.5], | |||
| 			[0,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1], | |||
| 			[0,.5,.5,1,1,.5,.5,0,1,0,1,1,0,1], | |||
| 			[.5,0,1,0,1,1,.5,1,1,.75,.5,.5,1,.25], | |||
| 			[0,.5,.5,0,.5,.5,1,0,1,.5,.5,1,.5,.5,0,1], | |||
| 			[0,0,1,0,1,1,0,1,1,.5,.5,.25,.5,.75,0,.5,.5,.25], | |||
| 			[0,.5,.5,.5,.5,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1], | |||
| 			[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); | |||
| 				$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); | |||
| 				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),E_USER_ERROR); | |||
| 			return FALSE; | |||
| 		} | |||
| 		if (!function_exists('imagettftext')) { | |||
| 			user_error(self::E_TTF,E_USER_ERROR); | |||
| 			return FALSE; | |||
| 		} | |||
| 		$fw=Base::instance(); | |||
| 		foreach ($fw->split($path?:$fw->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=[]; | |||
| 				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->$key=$seed; | |||
| 				return $this->save(); | |||
| 			} | |||
| 		user_error(self::E_Font,E_USER_ERROR); | |||
| 		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()->PACKAGE); | |||
| 		} | |||
| 		call_user_func_array( | |||
| 			'image'.$format, | |||
| 			array_merge([$this->data,NULL],$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([$this->data,NULL],$args) | |||
| 		); | |||
| 		return ob_get_clean(); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return image resource | |||
| 	*	@return resource | |||
| 	**/ | |||
| 	function data() { | |||
| 		return $this->data; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Save current state | |||
| 	*	@return object | |||
| 	**/ | |||
| 	function save() { | |||
| 		$fw=Base::instance(); | |||
| 		if ($this->flag) { | |||
| 			if (!is_dir($dir=$fw->TEMP)) | |||
| 				mkdir($dir,Base::MODE,TRUE); | |||
| 			$this->count++; | |||
| 			$fw->write($dir.'/'.$fw->SEED.'.'. | |||
| 				$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->TEMP. | |||
| 			$fw->SEED.'.'.$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|FALSE | |||
| 	*	@param $str string | |||
| 	**/ | |||
| 	function load($str) { | |||
| 		if (!$this->data=@imagecreatefromstring($str)) | |||
| 			return FALSE; | |||
| 		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=NULL) { | |||
| 		$this->flag=$flag; | |||
| 		if ($file) { | |||
| 			$fw=Base::instance(); | |||
| 			// Create image from file | |||
| 			$this->file=$file; | |||
| 			if (!isset($path)) | |||
| 				$path=$fw->UI.';./'; | |||
| 			foreach ($fw->split($path,FALSE) as $dir) | |||
| 				if (is_file($dir.$file)) | |||
| 					return $this->load($fw->read($dir.$file)); | |||
| 			user_error(self::E_File,E_USER_ERROR); | |||
| 		} | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Wrap-up | |||
| 	*	@return NULL | |||
| 	**/ | |||
| 	function __destruct() { | |||
| 		if (is_resource($this->data)) { | |||
| 			imagedestroy($this->data); | |||
| 			$fw=Base::instance(); | |||
| 			$path=$fw->TEMP.$fw->SEED.'.'.$fw->hash($this->file); | |||
| 			if ($glob=@glob($path.'*.png',GLOB_NOSORT)) | |||
| 				foreach ($glob as $match) | |||
| 					if (preg_match('/-(\d+)\.png/',$match)) | |||
| 						@unlink($match); | |||
| 		} | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,71 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! 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(); | |||
| 		foreach (preg_split('/\r?\n|\r/',trim($text)) as $line) | |||
| 			$fw->write( | |||
| 				$this->file, | |||
| 				date($format). | |||
| 				(isset($_SERVER['REMOTE_ADDR'])? | |||
| 					(' ['.$_SERVER['REMOTE_ADDR']. | |||
| 					(($fwd=filter_var($fw->get('HEADERS.X-Forwarded-For'), | |||
| 						FILTER_VALIDATE_IP))?(' ('.$fwd.')'):'') | |||
| 					.']'):'').' '. | |||
| 				trim($line).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->LOGS)) | |||
| 			mkdir($dir,Base::MODE,TRUE); | |||
| 		$this->file=$dir.$file; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,139 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! PHP magic wrapper | |||
| abstract class Magic implements ArrayAccess { | |||
| 
 | |||
| 	/** | |||
| 	*	Return TRUE if key is not empty | |||
| 	*	@return bool | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	abstract function exists($key); | |||
| 
 | |||
| 	/** | |||
| 	*	Bind value to key | |||
| 	*	@return mixed | |||
| 	*	@param $key string | |||
| 	*	@param $val mixed | |||
| 	**/ | |||
| 	abstract function set($key,$val); | |||
| 
 | |||
| 	/** | |||
| 	*	Retrieve contents of key | |||
| 	*	@return mixed | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	abstract function &get($key); | |||
| 
 | |||
| 	/** | |||
| 	*	Unset key | |||
| 	*	@return NULL | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	abstract function clear($key); | |||
| 
 | |||
| 	/** | |||
| 	*	Convenience method for checking property value | |||
| 	*	@return mixed | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function offsetexists($key) { | |||
| 		return Base::instance()->visible($this,$key)? | |||
| 			isset($this->$key): | |||
| 			($this->exists($key) && $this->get($key)!==NULL); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Convenience method for assigning property value | |||
| 	*	@return mixed | |||
| 	*	@param $key string | |||
| 	*	@param $val mixed | |||
| 	**/ | |||
| 	function offsetset($key,$val) { | |||
| 		return Base::instance()->visible($this,$key)? | |||
| 			($this->$key=$val):$this->set($key,$val); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Convenience method for retrieving property value | |||
| 	*	@return mixed | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function &offsetget($key) { | |||
| 		if (Base::instance()->visible($this,$key)) | |||
| 			$val=&$this->$key; | |||
| 		else | |||
| 			$val=&$this->get($key); | |||
| 		return $val; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Convenience method for removing property value | |||
| 	*	@return NULL | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function offsetunset($key) { | |||
| 		if (Base::instance()->visible($this,$key)) | |||
| 			unset($this->$key); | |||
| 		else | |||
| 			$this->clear($key); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Alias for offsetexists() | |||
| 	*	@return mixed | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function __isset($key) { | |||
| 		return $this->offsetexists($key); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Alias for offsetset() | |||
| 	*	@return mixed | |||
| 	*	@param $key string | |||
| 	*	@param $val mixed | |||
| 	**/ | |||
| 	function __set($key,$val) { | |||
| 		return $this->offsetset($key,$val); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Alias for offsetget() | |||
| 	*	@return mixed | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function &__get($key) { | |||
| 		$val=&$this->offsetget($key); | |||
| 		return $val; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Alias for offsetunset() | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function __unset($key) { | |||
| 		$this->offsetunset($key); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,569 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! 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->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"><'. | |||
| 								$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].'></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]. | |||
| 								($match[5]? | |||
| 									('<span class="ini_value">'. | |||
| 										$match[5].'</span>'):''); | |||
| 						else | |||
| 							$out.=$match[0]; | |||
| 						if (isset($match[6])) | |||
| 							$out.=$match[6]; | |||
| 					} | |||
| 					$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]); | |||
| 			$str=preg_replace_callback( | |||
| 				'/([^<>\[]+)?(<[\?%].+?[\?%]>|<.+?>|\[.+?\]\s*\(.+?\))|'. | |||
| 				'(.+)/s', | |||
| 				function($expr) { | |||
| 					$tmp=''; | |||
| 					if (isset($expr[4])) | |||
| 						$tmp.=$this->esc($expr[4]); | |||
| 					else { | |||
| 						if (isset($expr[1])) | |||
| 							$tmp.=$this->esc($expr[1]); | |||
| 						$tmp.=$expr[2]; | |||
| 						if (isset($expr[3])) | |||
| 							$tmp.=$this->esc($expr[3]); | |||
| 					} | |||
| 					return $tmp; | |||
| 				}, | |||
| 				$str | |||
| 			); | |||
| 			$str=preg_replace('/\s{2}\r?\n/','<br />',$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( | |||
| 				'/(?<=\s|^)(?<!\\\\)([*_])([*_]?)([*_]?)(.*?)(?!\\\\)\3\2\1(?=[\s[:punct:]]|$)/', | |||
| 				function($expr) { | |||
| 					if ($expr[3]) | |||
| 						return '<strong><em>'.$expr[4].'</em></strong>'; | |||
| 					if ($expr[2]) | |||
| 						return '<strong>'.$expr[4].'</strong>'; | |||
| 					return '<em>'.$expr[4].'</em>'; | |||
| 				}, | |||
| 				preg_replace( | |||
| 					'/(?<!\\\\)~~(.*?)(?!\\\\)~~(?=[\s[:punct:]]|$)/', | |||
| 					'<del>\1</del>', | |||
| 					$tmp=$str | |||
| 				) | |||
| 			); | |||
| 		return $str; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Process image span | |||
| 	*	@return string | |||
| 	*	@param $str string | |||
| 	**/ | |||
| 	protected function _img($str) { | |||
| 		return preg_replace_callback( | |||
| 			'/!(?:\[(.+?)\])?\h*\(<?(.*?)>?(?:\h*"(.*?)"\h*)?\)/', | |||
| 			function($expr) { | |||
| 				return '<img src="'.$expr[2].'"'. | |||
| 					(empty($expr[1])? | |||
| 						'': | |||
| 						(' alt="'.$this->esc($expr[1]).'"')). | |||
| 					(empty($expr[3])? | |||
| 						'': | |||
| 						(' title="'.$this->esc($expr[3]).'"')).' />'; | |||
| 			}, | |||
| 			$str | |||
| 		); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Process anchor span | |||
| 	*	@return string | |||
| 	*	@param $str string | |||
| 	**/ | |||
| 	protected function _a($str) { | |||
| 		return preg_replace_callback( | |||
| 			'/(?<!\\\\)\[(.+?)(?!\\\\)\]\h*\(<?(.*?)>?(?:\h*"(.*?)"\h*)?\)/', | |||
| 			function($expr) { | |||
| 				return '<a href="'.$this->esc($expr[2]).'"'. | |||
| 					(empty($expr[3])? | |||
| 						'': | |||
| 						(' title="'.$this->esc($expr[3]).'"')). | |||
| 					'>'.$this->scan($expr[1]).'</a>'; | |||
| 			}, | |||
| 			$str | |||
| 		); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Auto-convert links | |||
| 	*	@return string | |||
| 	*	@param $str string | |||
| 	**/ | |||
| 	protected function _auto($str) { | |||
| 		return preg_replace_callback( | |||
| 			'/`.*?<(.+?)>.*?`|<(.+?)>/', | |||
| 			function($expr) { | |||
| 				if (empty($expr[1]) && parse_url($expr[2],PHP_URL_SCHEME)) { | |||
| 					$expr[2]=$this->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) { | |||
| 		return preg_replace_callback( | |||
| 			'/`` (.+?) ``|(?<!\\\\)`(.+?)(?!\\\\)`/', | |||
| 			function($expr) { | |||
| 				return '<code>'. | |||
| 					$this->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=[ | |||
| 				'...'=>'…', | |||
| 				'(tm)'=>'™', | |||
| 				'(r)'=>'®', | |||
| 				'(c)'=>'©' | |||
| 			]; | |||
| 		foreach ($this->special as $key=>$val) | |||
| 			$str=preg_replace('/'.preg_quote($key,'/').'/i',$val,$str); | |||
| 		return htmlspecialchars($str,ENT_COMPAT, | |||
| 			Base::instance()->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=['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=[ | |||
| 				'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' | |||
| 			]; | |||
| 		} | |||
| 		// 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) { | |||
| 							return (empty($expr[2]))? | |||
| 								// Anchor | |||
| 								('<a href="'.$this->esc($match[2]).'"'. | |||
| 								(empty($match[3])? | |||
| 									'': | |||
| 									(' title="'. | |||
| 										$this->esc($match[3]).'"')).'>'. | |||
| 								// Link | |||
| 								$this->scan( | |||
| 									empty($expr[3])? | |||
| 										(empty($expr[1])? | |||
| 											$expr[4]: | |||
| 											$expr[1]): | |||
| 										$expr[3] | |||
| 								).'</a>'): | |||
| 								// Image | |||
| 								('<img src="'.$match[2].'"'. | |||
| 								(empty($expr[2])? | |||
| 									'': | |||
| 									(' alt="'. | |||
| 										$this->esc($expr[3]).'"')). | |||
| 								(empty($match[3])? | |||
| 									'': | |||
| 									(' title="'. | |||
| 										$this->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( | |||
| 							[$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); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,139 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! Generic array utilities | |||
| class Matrix extends Prefab { | |||
| 
 | |||
| 	/** | |||
| 	*	Retrieve values from a specified column of a multi-dimensional | |||
| 	*	array variable | |||
| 	*	@return array | |||
| 	*	@param $var array | |||
| 	*	@param $col mixed | |||
| 	**/ | |||
| 	function pick(array $var,$col) { | |||
| 		return array_map( | |||
| 			function($row) use($col) { | |||
| 				return $row[$col]; | |||
| 			}, | |||
| 			$var | |||
| 		); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 * select a subset of fields from an input array | |||
| 	 * @param string|array $fields splittable string or array | |||
| 	 * @param string|array $data hive key or array | |||
| 	 * @return array | |||
| 	 */ | |||
| 	function select($fields, $data) { | |||
| 		return array_intersect_key(is_array($data) ? $data : \Base::instance()->get($data), | |||
| 			array_flip(is_array($fields) ? $fields : \Base::instance()->split($fields))); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 * walk with a callback function through a subset of fields from an input array | |||
| 	 * the callback receives the value, index-key and the full input array as parameters | |||
| 	 * set value parameter as reference and you're able to modify the data as well | |||
| 	 * @param string|array $fields splittable string or array of fields | |||
| 	 * @param string|array $data hive key or input array | |||
| 	 * @param callable $callback (mixed &$value, string $key, array $data) | |||
| 	 * @return array modified subset data | |||
| 	 */ | |||
| 	function walk($fields, $data, $callback) { | |||
| 		$subset=$this->select($fields, $data); | |||
| 		array_walk($subset, $callback, $data); | |||
| 		return $subset; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Rotate a two-dimensional array variable | |||
| 	*	@return NULL | |||
| 	*	@param $var array | |||
| 	**/ | |||
| 	function transpose(array &$var) { | |||
| 		$out=[]; | |||
| 		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)=[$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|int | |||
| 	*	@param $first int | |||
| 	**/ | |||
| 	function calendar($date='now',$first=0) { | |||
| 		$out=FALSE; | |||
| 		if (extension_loaded('calendar')) { | |||
| 			if (is_string($date)) | |||
| 				$date=strtotime($date); | |||
| 			$parts=getdate($date); | |||
| 			$days=cal_days_in_month(CAL_GREGORIAN,$parts['mon'],$parts['year']); | |||
| 			$ref=date('w',strtotime(date('Y-m',$parts[0]).'-01'))+(7-$first)%7; | |||
| 			$out=[]; | |||
| 			for ($i=0;$i<$days;$i++) | |||
| 				$out[floor(($ref+$i)/7)][($ref+$i)%7]=$i+1; | |||
| 		} | |||
| 		return $out; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,196 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! Cache-based session handler | |||
| class Session { | |||
| 
 | |||
| 	protected | |||
| 		//! Session ID | |||
| 		$sid, | |||
| 		//! Anti-CSRF token | |||
| 		$_csrf, | |||
| 		//! User agent | |||
| 		$_agent, | |||
| 		//! IP, | |||
| 		$_ip, | |||
| 		//! Suspect callback | |||
| 		$onsuspect, | |||
| 		//! Cache instance | |||
| 		$_cache; | |||
| 
 | |||
| 	/** | |||
| 	*	Open session | |||
| 	*	@return TRUE | |||
| 	*	@param $path string | |||
| 	*	@param $name string | |||
| 	**/ | |||
| 	function open($path,$name) { | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Close session | |||
| 	*	@return TRUE | |||
| 	**/ | |||
| 	function close() { | |||
| 		$this->sid=NULL; | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return session data in serialized format | |||
| 	*	@return string | |||
| 	*	@param $id string | |||
| 	**/ | |||
| 	function read($id) { | |||
| 		$this->sid=$id; | |||
| 		if (!$data=$this->_cache->get($id.'.@')) | |||
| 			return ''; | |||
| 		if ($data['ip']!=$this->_ip || $data['agent']!=$this->_agent) { | |||
| 			$fw=Base::instance(); | |||
| 			if (!isset($this->onsuspect) || | |||
| 				$fw->call($this->onsuspect,[$this,$id])===FALSE) { | |||
| 				//NB: `session_destroy` can't be called at that stage (`session_start` not completed) | |||
| 				$this->destroy($id); | |||
| 				$this->close(); | |||
| 				unset($fw->{'COOKIE.'.session_name()}); | |||
| 				$fw->error(403); | |||
| 			} | |||
| 		} | |||
| 		return $data['data']; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Write session data | |||
| 	*	@return TRUE | |||
| 	*	@param $id string | |||
| 	*	@param $data string | |||
| 	**/ | |||
| 	function write($id,$data) { | |||
| 		$fw=Base::instance(); | |||
| 		$jar=$fw->JAR; | |||
| 		$this->_cache->set($id.'.@', | |||
| 			[ | |||
| 				'data'=>$data, | |||
| 				'ip'=>$this->_ip, | |||
| 				'agent'=>$this->_agent, | |||
| 				'stamp'=>time() | |||
| 			], | |||
| 			$jar['expire'] | |||
| 		); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Destroy session | |||
| 	*	@return TRUE | |||
| 	*	@param $id string | |||
| 	**/ | |||
| 	function destroy($id) { | |||
| 		$this->_cache->clear($id.'.@'); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Garbage collector | |||
| 	*	@return TRUE | |||
| 	*	@param $max int | |||
| 	**/ | |||
| 	function cleanup($max) { | |||
| 		$this->_cache->reset('.@',$max); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return session id (if session has started) | |||
| 	 *	@return string|NULL | |||
| 	 **/ | |||
| 	function sid() { | |||
| 		return $this->sid; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return anti-CSRF token | |||
| 	 *	@return string | |||
| 	 **/ | |||
| 	function csrf() { | |||
| 		return $this->_csrf; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return IP address | |||
| 	 *	@return string | |||
| 	 **/ | |||
| 	function ip() { | |||
| 		return $this->_ip; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return Unix timestamp | |||
| 	 *	@return string|FALSE | |||
| 	 **/ | |||
| 	function stamp() { | |||
| 		if (!$this->sid) | |||
| 			session_start(); | |||
| 		return $this->_cache->exists($this->sid.'.@',$data)? | |||
| 			$data['stamp']:FALSE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 *	Return HTTP user agent | |||
| 	 *	@return string | |||
| 	 **/ | |||
| 	function agent() { | |||
| 		return $this->_agent; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Instantiate class | |||
| 	*	@param $onsuspect callback | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function __construct($onsuspect=NULL,$key=NULL,$cache=null) { | |||
| 		$this->onsuspect=$onsuspect; | |||
| 		$this->_cache=$cache?:Cache::instance(); | |||
| 		session_set_save_handler( | |||
| 			[$this,'open'], | |||
| 			[$this,'close'], | |||
| 			[$this,'read'], | |||
| 			[$this,'write'], | |||
| 			[$this,'destroy'], | |||
| 			[$this,'cleanup'] | |||
| 		); | |||
| 		register_shutdown_function('session_commit'); | |||
| 		$fw=\Base::instance(); | |||
| 		$headers=$fw->HEADERS; | |||
| 		$this->_csrf=$fw->hash($fw->SEED. | |||
| 			extension_loaded('openssl')? | |||
| 				implode(unpack('L',openssl_random_pseudo_bytes(4))): | |||
| 				mt_rand() | |||
| 			); | |||
| 		if ($key) | |||
| 			$fw->$key=$this->_csrf; | |||
| 		$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; | |||
| 		$this->_ip=$fw->IP; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,360 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! 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, | |||
| 		//! TLS/SSL stream context | |||
| 		$context, | |||
| 		//! 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); | |||
| 		if (isset($this->headers[$key])) | |||
| 			$val=&$this->headers[$key]; | |||
| 		else | |||
| 			$val=NULL; | |||
| 		return $val; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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|string | |||
| 	*	@param $mock bool | |||
| 	**/ | |||
| 	protected function dialog($cmd=NULL,$log=TRUE,$mock=FALSE) { | |||
| 		$reply=''; | |||
| 		if ($mock) { | |||
| 			$host=str_replace('ssl://','',$this->host); | |||
| 			switch ($cmd) { | |||
| 			case NULL: | |||
| 				$reply='220 '.$host.' ESMTP ready'."\n"; | |||
| 				break; | |||
| 			case 'DATA': | |||
| 				$reply='354 Go ahead'."\n"; | |||
| 				break; | |||
| 			case 'QUIT': | |||
| 				$reply='221 '.$host.' closing connection'."\n"; | |||
| 				break; | |||
| 			default: | |||
| 				$reply='250 OK'."\n"; | |||
| 				break; | |||
| 			} | |||
| 		} | |||
| 		else { | |||
| 			$socket=&$this->socket; | |||
| 			if ($cmd) | |||
| 				fputs($socket,$cmd."\r\n"); | |||
| 			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) { | |||
| 			if ($cmd) | |||
| 				$this->log.=$cmd."\n"; | |||
| 			$this->log.=str_replace("\r",'',$reply); | |||
| 		} | |||
| 		return $reply; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Add e-mail attachment | |||
| 	*	@return NULL | |||
| 	*	@param $file string | |||
| 	*	@param $alias string | |||
| 	*	@param $cid string | |||
| 	**/ | |||
| 	function attach($file,$alias=NULL,$cid=NULL) { | |||
| 		if (!is_file($file)) | |||
| 			user_error(sprintf(self::E_Attach,$file),E_USER_ERROR); | |||
| 		if ($alias) | |||
| 			$file=[$alias,$file]; | |||
| 		$this->attachments[]=['filename'=>$file,'cid'=>$cid]; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Transmit message | |||
| 	*	@return bool | |||
| 	*	@param $message string | |||
| 	*	@param $log bool|string | |||
| 	*	@param $mock bool | |||
| 	**/ | |||
| 	function send($message,$log=TRUE,$mock=FALSE) { | |||
| 		if ($this->scheme=='ssl' && !extension_loaded('openssl')) | |||
| 			return FALSE; | |||
| 		// Message should not be blank | |||
| 		if (!$message) | |||
| 			user_error(self::E_Blank,E_USER_ERROR); | |||
| 		$fw=Base::instance(); | |||
| 		// Retrieve headers | |||
| 		$headers=$this->headers; | |||
| 		// Connect to the server | |||
| 		if (!$mock) { | |||
| 			$socket=&$this->socket; | |||
| 			$socket=@stream_socket_client($this->host.':'.$this->port, | |||
| 				$errno,$errstr,ini_get('default_socket_timeout'), | |||
| 				STREAM_CLIENT_CONNECT,$this->context); | |||
| 			if (!$socket) { | |||
| 				$fw->error(500,$errstr); | |||
| 				return FALSE; | |||
| 			} | |||
| 			stream_set_blocking($socket,TRUE); | |||
| 		} | |||
| 		// Get server's initial response | |||
| 		$this->dialog(NULL,$log,$mock); | |||
| 		// Announce presence | |||
| 		$reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock); | |||
| 		if (strtolower($this->scheme)=='tls') { | |||
| 			$this->dialog('STARTTLS',$log,$mock); | |||
| 			if (!$mock) { | |||
| 				$method=STREAM_CRYPTO_METHOD_TLS_CLIENT; | |||
| 				if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { | |||
| 					$method|=STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; | |||
| 					$method|=STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; | |||
| 				} | |||
| 				stream_socket_enable_crypto($socket,TRUE,$method); | |||
| 			} | |||
| 			$reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock); | |||
| 		} | |||
| 		$message=wordwrap($message,998); | |||
| 		if (preg_match('/8BITMIME/',$reply)) | |||
| 			$headers['Content-Transfer-Encoding']='8bit'; | |||
| 		else { | |||
| 			$headers['Content-Transfer-Encoding']='quoted-printable'; | |||
| 			$message=preg_replace('/^\.(.+)/m', | |||
| 				'..$1',quoted_printable_encode($message)); | |||
| 		} | |||
| 		if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) { | |||
| 			// Authenticate | |||
| 			$this->dialog('AUTH LOGIN',$log,$mock); | |||
| 			$this->dialog(base64_encode($this->user),$log,$mock); | |||
| 			$reply=$this->dialog(base64_encode($this->pw),$log,$mock); | |||
| 			if (!preg_match('/^235\s.*/',$reply)) { | |||
| 				$this->dialog('QUIT',$log,$mock); | |||
| 				if (!$mock && $socket) | |||
| 					fclose($socket); | |||
| 				return FALSE; | |||
| 			} | |||
| 		} | |||
| 		if (empty($headers['Message-Id'])) | |||
| 			$headers['Message-Id']='<'.uniqid('',TRUE).'@'.$this->host.'>'; | |||
| 		if (empty($headers['Date'])) | |||
| 			$headers['Date']=date('r'); | |||
| 		// Required headers | |||
| 		$reqd=['From','To','Subject']; | |||
| 		foreach ($reqd as $id) | |||
| 			if (empty($headers[$id])) | |||
| 				user_error(sprintf(self::E_Header,$id),E_USER_ERROR); | |||
| 		$eol="\r\n"; | |||
| 		// Stringify headers | |||
| 		foreach ($headers as $key=>&$val) { | |||
| 			if (in_array($key,['From','To','Cc','Bcc'])) { | |||
| 				$email=''; | |||
| 				preg_match_all('/(?:".+?" )?(?:<.+?>|[^ ,]+)/', | |||
| 					$val,$matches,PREG_SET_ORDER); | |||
| 				foreach ($matches as $raw) | |||
| 					$email.=($email?', ':''). | |||
| 						(preg_match('/<.+?>/',$raw[0])? | |||
| 							$raw[0]: | |||
| 							('<'.$raw[0].'>')); | |||
| 				$val=$email; | |||
| 			} | |||
| 			unset($val); | |||
| 		} | |||
| 		$from=isset($headers['Sender'])?$headers['Sender']:strstr($headers['From'],'<'); | |||
| 		unset($headers['Sender']); | |||
| 		// Start message dialog | |||
| 		$this->dialog('MAIL FROM: '.$from,$log,$mock); | |||
| 		foreach ($fw->split($headers['To']. | |||
| 			(isset($headers['Cc'])?(';'.$headers['Cc']):''). | |||
| 			(isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst) { | |||
| 			$this->dialog('RCPT TO: '.strstr($dst,'<'),$log,$mock); | |||
| 		} | |||
| 		$this->dialog('DATA',$log,$mock); | |||
| 		if ($this->attachments) { | |||
| 			// Replace Content-Type | |||
| 			$type=$headers['Content-Type']; | |||
| 			unset($headers['Content-Type']); | |||
| 			$enc=$headers['Content-Transfer-Encoding']; | |||
| 			unset($headers['Content-Transfer-Encoding']); | |||
| 			$hash=uniqid(NULL,TRUE); | |||
| 			// Send mail headers | |||
| 			$out='Content-Type: multipart/mixed; boundary="'.$hash.'"'.$eol; | |||
| 			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.='Content-Transfer-Encoding: '.$enc.$eol; | |||
| 			$out.=$eol; | |||
| 			$out.=$message.$eol; | |||
| 			foreach ($this->attachments as $attachment) { | |||
| 				if (is_array($attachment['filename'])) | |||
| 					list($alias,$file)=$attachment['filename']; | |||
| 				else | |||
| 					$alias=basename($file=$attachment['filename']); | |||
| 				$out.='--'.$hash.$eol; | |||
| 				$out.='Content-Type: application/octet-stream'.$eol; | |||
| 				$out.='Content-Transfer-Encoding: base64'.$eol; | |||
| 				if ($attachment['cid']) | |||
| 					$out.='Content-Id: '.$attachment['cid'].$eol; | |||
| 				$out.='Content-Disposition: attachment; '. | |||
| 					'filename="'.$alias.'"'.$eol; | |||
| 				$out.=$eol; | |||
| 				$out.=chunk_split(base64_encode( | |||
| 					file_get_contents($file))).$eol; | |||
| 			} | |||
| 			$out.=$eol; | |||
| 			$out.='--'.$hash.'--'.$eol; | |||
| 			$out.='.'; | |||
| 			$this->dialog($out,preg_match('/verbose/i',$log),$mock); | |||
| 		} | |||
| 		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,preg_match('/verbose/i',$log),$mock); | |||
| 		} | |||
| 		$this->dialog('QUIT',$log,$mock); | |||
| 		if (!$mock && $socket) | |||
| 			fclose($socket); | |||
| 		return TRUE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Instantiate class | |||
| 	*	@param $host string | |||
| 	*	@param $port int | |||
| 	*	@param $scheme string | |||
| 	*	@param $user string | |||
| 	*	@param $pw string | |||
| 	*	@param $ctx resource | |||
| 	**/ | |||
| 	function __construct( | |||
| 		$host='localhost',$port=25,$scheme=NULL,$user=NULL,$pw=NULL,$ctx=NULL) { | |||
| 		$this->headers=[ | |||
| 			'MIME-Version'=>'1.0', | |||
| 			'Content-Type'=>'text/plain; '. | |||
| 				'charset='.Base::instance()->ENCODING | |||
| 		]; | |||
| 		$this->host=strtolower((($this->scheme=strtolower($scheme))=='ssl'? | |||
| 			'ssl':'tcp').'://'.$host); | |||
| 		$this->port=$port; | |||
| 		$this->user=$user; | |||
| 		$this->pw=$pw; | |||
| 		$this->context=stream_context_create($ctx); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,353 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! XML-style template engine | |||
| class Template extends Preview { | |||
| 
 | |||
| 	//@{ Error messages | |||
| 	const | |||
| 		E_Method='Call to undefined method %s()'; | |||
| 	//@} | |||
| 
 | |||
| 	protected | |||
| 		//! Template tags | |||
| 		$tags, | |||
| 		//! Custom tag handlers | |||
| 		$custom=[]; | |||
| 
 | |||
| 	/** | |||
| 	*	Template -set- tag handler | |||
| 	*	@return string | |||
| 	*	@param $node array | |||
| 	**/ | |||
| 	protected function _set(array $node) { | |||
| 		$out=''; | |||
| 		foreach ($node['@attrib'] as $key=>$val) | |||
| 			$out.='$'.$key.'='. | |||
| 				(preg_match('/\{\{(.+?)\}\}/',$val)? | |||
| 					$this->token($val): | |||
| 					Base::instance()->stringify($val)).'; '; | |||
| 		return '<?php '.$out.'?>'; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Template -include- tag handler | |||
| 	*	@return string | |||
| 	*	@param $node array | |||
| 	**/ | |||
| 	protected function _include(array $node) { | |||
| 		$attrib=$node['@attrib']; | |||
| 		$hive=isset($attrib['with']) && | |||
| 			($attrib['with']=$this->token($attrib['with'])) && | |||
| 			preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/', | |||
| 				$attrib['with'],$pairs,PREG_SET_ORDER)? | |||
| 					('['.implode(',', | |||
| 						array_map(function($pair) { | |||
| 							return '\''.$pair[1].'\'=>'. | |||
| 								(preg_match('/^\'.*\'$/',$pair[2]) || | |||
| 									preg_match('/\$/',$pair[2])? | |||
| 									$pair[2]:Base::instance()->stringify( | |||
| 										Base::instance()->cast($pair[2]))); | |||
| 						},$pairs)).']+get_defined_vars()'): | |||
| 					'get_defined_vars()'; | |||
| 		$ttl=isset($attrib['ttl'])?(int)$attrib['ttl']:0; | |||
| 		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'])).','. | |||
| 					'NULL,'.$hive.','.$ttl.'); ?>'); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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']).'?:[]) 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=[$pos,$block]; | |||
| 			elseif (isset($block['false'])) | |||
| 				$false=[$pos,$block]; | |||
| 		if (isset($true,$false) && $true[0]>$false[0]) | |||
| 			// Reverse <true> and <false> blocks | |||
| 			list($node[$true[0]],$node[$false[0]])=[$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 | |||
| 	**/ | |||
| 	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 string | |||
| 	*	@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([$this,$func],$args); | |||
| 		user_error(sprintf(self::E_Method,$func),E_USER_ERROR); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Parse string for template directives and tokens | |||
| 	*	@return array | |||
| 	*	@param $text string | |||
| 	**/ | |||
| 	function parse($text) { | |||
| 		$text=parent::parse($text); | |||
| 		// Build tree structure | |||
| 		for ($ptr=0,$w=5,$len=strlen($text),$tree=[],$tmp='';$ptr<$len;) | |||
| 			if (preg_match('/^(.{0,'.$w.'}?)<(\/?)(?:F3:)?'. | |||
| 				'('.$this->tags.')\b((?:\s+[\w.:@!-]+'. | |||
| 				'(?:\h*=\h*(?:"(?:.*?)"|\'(?:.*?)\'))?|'. | |||
| 				'\h*\{\{.+?\}\})*)\h*(\/?)>/is', | |||
| 				substr($text,$ptr),$match)) { | |||
| 				if (strlen($tmp) || $match[1]) | |||
| 					$tree[]=$tmp.$match[1]; | |||
| 				// Element node | |||
| 				if ($match[2]) { | |||
| 					// Find matching start tag | |||
| 					$stack=[]; | |||
| 					for($i=count($tree)-1;$i>=0;$i--) { | |||
| 						$item=$tree[$i]; | |||
| 						if (is_array($item) && | |||
| 							array_key_exists($match[3],$item) && | |||
| 							!isset($item[$match[3]][0])) { | |||
| 							// Start tag found | |||
| 							$tree[$i][$match[3]]+=array_reverse($stack); | |||
| 							$tree=array_slice($tree,0,$i+1); | |||
| 							break; | |||
| 						} | |||
| 						else $stack[]=$item; | |||
| 					} | |||
| 				} | |||
| 				else { | |||
| 					// Start tag | |||
| 					$node=&$tree[][$match[3]]; | |||
| 					$node=[]; | |||
| 					if ($match[4]) { | |||
| 						// Process attributes | |||
| 						preg_match_all( | |||
| 							'/(?:(\{\{.+?\}\})|([^\s\/"\'=]+))'. | |||
| 							'\h*(?:=\h*(?:"(.*?)"|\'(.*?)\'))?/s', | |||
| 							$match[4],$attr,PREG_SET_ORDER); | |||
| 						foreach ($attr as $kv) | |||
| 							if (!empty($kv[1]) && !isset($kv[3]) && !isset($kv[4])) | |||
| 								$node['@attrib'][]=$kv[1]; | |||
| 							else | |||
| 								$node['@attrib'][$kv[1]?:$kv[2]]= | |||
| 									(isset($kv[3]) && $kv[3]!==''? | |||
| 										$kv[3]: | |||
| 										(isset($kv[4]) && $kv[4]!==''? | |||
| 											$kv[4]:NULL)); | |||
| 					} | |||
| 				} | |||
| 				$tmp=''; | |||
| 				$ptr+=strlen($match[0]); | |||
| 				$w=5; | |||
| 			} | |||
| 			else { | |||
| 				// Text node | |||
| 				$tmp.=substr($text,$ptr,$w); | |||
| 				$ptr+=$w; | |||
| 				if ($w<50) | |||
| 					$w++; | |||
| 			} | |||
| 		if (strlen($tmp)) | |||
| 			// Append trailing text | |||
| 			$tree[]=$tmp; | |||
| 		// Break references | |||
| 		unset($node); | |||
| 		return $tree; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Class constructor | |||
| 	*	return object | |||
| 	**/ | |||
| 	function __construct() { | |||
| 		$ref=new ReflectionClass(get_called_class()); | |||
| 		$this->tags=''; | |||
| 		foreach ($ref->getmethods() as $method) | |||
| 			if (preg_match('/^_(?=[[:alpha:]])/',$method->name)) | |||
| 				$this->tags.=(strlen($this->tags)?'|':''). | |||
| 					substr($method->name,1); | |||
| 		parent::__construct(); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,98 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! Unit test kit | |||
| class Test { | |||
| 
 | |||
| 	//@{ Reporting level | |||
| 	const | |||
| 		FLAG_False=0, | |||
| 		FLAG_True=1, | |||
| 		FLAG_Both=2; | |||
| 	//@} | |||
| 
 | |||
| 	protected | |||
| 		//! Test results | |||
| 		$data=[], | |||
| 		//! Success indicator | |||
| 		$passed=TRUE, | |||
| 		//! Reporting level | |||
| 		$level; | |||
| 
 | |||
| 	/** | |||
| 	*	Return test results | |||
| 	*	@return array | |||
| 	**/ | |||
| 	function results() { | |||
| 		return $this->data; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return FALSE if at least one test case fails | |||
| 	*	@return bool | |||
| 	**/ | |||
| 	function passed() { | |||
| 		return $this->passed; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	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=['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; | |||
| 		} | |||
| 		if (!$out && $this->passed) | |||
| 			$this->passed=FALSE; | |||
| 		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; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,199 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| //! 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=[ | |||
| 			':('=>'\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()->EMOJI; | |||
| 		return $this->translate(str_replace(array_keys($map), | |||
| 			array_values($map),$str)); | |||
| 	} | |||
| 
 | |||
| } | |||
								
									
										File diff suppressed because it is too large
									
								
							
						
					| @ -0,0 +1,111 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace 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=[ | |||
| 			'offset'=>$ref-> | |||
| 				getOffset(new \DateTime('now',new \DateTimeZone('UTC')))/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->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']=''; | |||
| 			if (!empty($out['country_code']) && !empty($out['region'])) | |||
| 				$out['region_name']=geoip_region_name_by_code( | |||
| 					$out['country_code'],$out['region'] | |||
| 				); | |||
| 			unset($out['country_code3'],$out['region'],$out['postal_code']); | |||
| 			return $out; | |||
| 		} | |||
| 		if (($req=$web->request('http://www.geoplugin.net/json.gp'. | |||
| 			($public?('?ip='.$ip):''))) && | |||
| 			$data=json_decode($req['body'],TRUE)) { | |||
| 			$out=[]; | |||
| 			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 | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function weather($latitude,$longitude,$key) { | |||
| 		$fw=\Base::instance(); | |||
| 		$web=\Web::instance(); | |||
| 		$query=[ | |||
| 			'lat'=>$latitude, | |||
| 			'lon'=>$longitude, | |||
| 			'APPID'=>$key, | |||
| 			'units'=>'metric' | |||
| 		]; | |||
| 		return ($req=$web->request( | |||
| 			'http://api.openweathermap.org/data/2.5/weather?'. | |||
| 				http_build_query($query)))? | |||
| 			json_decode($req['body'],TRUE): | |||
| 			FALSE; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,58 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace Web\Google; | |||
| 
 | |||
| //! Google ReCAPTCHA v2 plug-in | |||
| class Recaptcha { | |||
| 
 | |||
| 	const | |||
| 		//! API URL | |||
| 		URL_Recaptcha='https://www.google.com/recaptcha/api/siteverify'; | |||
| 
 | |||
| 	/** | |||
| 	 *	Verify reCAPTCHA response | |||
| 	 *	@param string $secret | |||
| 	 *	@param string $response | |||
| 	 *	@return bool | |||
| 	 **/ | |||
| 	static function verify($secret,$response=NULL) { | |||
| 		$fw=\Base::instance(); | |||
| 		if (!isset($response)) | |||
| 			$response=$fw->{'POST.g-recaptcha-response'}; | |||
| 		if (!$response) | |||
| 			return FALSE; | |||
| 		$web=\Web::instance(); | |||
| 		$out=$web->request(self::URL_Recaptcha,[ | |||
| 			'method'=>'POST', | |||
| 			'content'=>http_build_query([ | |||
| 				'secret'=>$secret, | |||
| 				'response'=>$response, | |||
| 				'remoteip'=>$fw->IP | |||
| 			]), | |||
| 		]); | |||
| 		return isset($out['body']) && | |||
| 			($json=json_decode($out['body'],TRUE)) && | |||
| 			isset($json['success']) && $json['success']; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,65 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace 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; | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,163 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace Web; | |||
| 
 | |||
| //! Lightweight OAuth2 client | |||
| class OAuth2 extends \Magic { | |||
| 
 | |||
| 	protected | |||
| 		//! Scopes and claims | |||
| 		$args=[], | |||
| 		//! Encoding | |||
| 		$enc_type = PHP_QUERY_RFC1738; | |||
| 
 | |||
| 	/** | |||
| 	*	Return OAuth2 authentication URI | |||
| 	*	@return string | |||
| 	*	@param $endpoint string | |||
| 	*	@param $query bool | |||
| 	**/ | |||
| 	function uri($endpoint,$query=TRUE) { | |||
| 		return $endpoint.($query?('?'. | |||
| 				http_build_query($this->args,null,'&',$this->enc_type)):''); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Send request to API/token endpoint | |||
| 	*	@return string|FALSE | |||
| 	*	@param $uri string | |||
| 	*	@param $method string | |||
| 	*	@param $token array | |||
| 	**/ | |||
| 	function request($uri,$method,$token=NULL) { | |||
| 		$web=\Web::instance(); | |||
| 		$options=[ | |||
| 			'method'=>$method, | |||
| 			'content'=>http_build_query($this->args,null,'&',$this->enc_type), | |||
| 			'header'=>['Accept: application/json'] | |||
| 		]; | |||
| 		if ($token) | |||
| 			array_push($options['header'],'Authorization: Bearer '.$token); | |||
| 		elseif ($method=='POST' && isset($this->args['client_id'])) | |||
| 			array_push($options['header'],'Authorization: Basic '. | |||
| 				base64_encode( | |||
| 					$this->args['client_id'].':'. | |||
| 					$this->args['client_secret'] | |||
| 				) | |||
| 			); | |||
| 		$response=$web->request($uri,$options); | |||
| 		if ($response['error']) | |||
| 			user_error($response['error'],E_USER_ERROR); | |||
| 		if (isset($response['body'])) { | |||
| 			if (preg_grep('/^Content-Type:.*application\/json/i', | |||
| 				$response['headers'])) { | |||
| 				$token=json_decode($response['body'],TRUE); | |||
| 				if (isset($token['error_description'])) | |||
| 					user_error($token['error_description'],E_USER_ERROR); | |||
| 				if (isset($token['error'])) | |||
| 					user_error($token['error'],E_USER_ERROR); | |||
| 				return $token; | |||
| 			} | |||
| 			else | |||
| 				return $response['body']; | |||
| 		} | |||
| 		return FALSE; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Parse JSON Web token | |||
| 	*	@return array | |||
| 	*	@param $token string | |||
| 	**/ | |||
| 	function jwt($token) { | |||
| 		return json_decode( | |||
| 			base64_decode( | |||
| 				str_replace(['-','_'],['+','/'],explode('.',$token)[1]) | |||
| 			), | |||
| 			TRUE | |||
| 		); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	 * change default url encoding type, i.E. PHP_QUERY_RFC3986 | |||
| 	 * @param $type | |||
| 	 */ | |||
| 	function setEncoding($type) { | |||
| 		$this->enc_type = $type; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	URL-safe base64 encoding | |||
| 	*	@return array | |||
| 	*	@param $data string | |||
| 	**/ | |||
| 	function b64url($data) { | |||
| 		return trim(strtr(base64_encode($data),'+/','-_'),'='); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return TRUE if scope/claim exists | |||
| 	*	@return bool | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function exists($key) { | |||
| 		return isset($this->args[$key]); | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Bind value to scope/claim | |||
| 	*	@return string | |||
| 	*	@param $key string | |||
| 	*	@param $val string | |||
| 	**/ | |||
| 	function set($key,$val) { | |||
| 		return $this->args[$key]=$val; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Return value of scope/claim | |||
| 	*	@return mixed | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function &get($key) { | |||
| 		if (isset($this->args[$key])) | |||
| 			$val=&$this->args[$key]; | |||
| 		else | |||
| 			$val=NULL; | |||
| 		return $val; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Remove scope/claim | |||
| 	*	@return NULL | |||
| 	*	@param $key string | |||
| 	**/ | |||
| 	function clear($key=NULL) { | |||
| 		if ($key) | |||
| 			unset($this->args[$key]); | |||
| 		else | |||
| 			$this->args=[]; | |||
| 	} | |||
| 
 | |||
| } | |||
| 
 | |||
| @ -0,0 +1,248 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace Web; | |||
| 
 | |||
| //! OpenID consumer | |||
| class OpenID extends \Magic { | |||
| 
 | |||
| 	protected | |||
| 		//! OpenID provider endpoint URL | |||
| 		$url, | |||
| 		//! HTTP request parameters | |||
| 		$args=[]; | |||
| 
 | |||
| 	/** | |||
| 	*	Determine OpenID provider | |||
| 	*	@return string|FALSE | |||
| 	*	@param $proxy string | |||
| 	**/ | |||
| 	protected function discover($proxy) { | |||
| 		// Normalize | |||
| 		if (!preg_match('/https?:\/\//i',$this->args['endpoint'])) | |||
| 			$this->args['endpoint']='http://'.$this->args['endpoint']; | |||
| 		$url=parse_url($this->args['endpoint']); | |||
| 		// Remove fragment; reconnect parts | |||
| 		$this->args['endpoint']=$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['endpoint'],['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]; | |||
| 			$svc_type=is_array($svc['Type'])?$svc['Type']:array($svc['Type']); | |||
| 			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=[]; | |||
| 						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 $reqd=NULL) { | |||
| 		$fw=\Base::instance(); | |||
| 		$root=$fw->SCHEME.'://'.$fw->HOST; | |||
| 		if (empty($this->args['trust_root'])) | |||
| 			$this->args['trust_root']=$root.$fw->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=[]; | |||
| 			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=[]; | |||
| 			foreach ($this->args as $key=>$val) | |||
| 				$var['openid.'.$key]=$val; | |||
| 			$req=\Web::instance()->request( | |||
| 				$this->url, | |||
| 				[ | |||
| 					'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) { | |||
| 		if (isset($this->args[$key])) | |||
| 			$val=&$this->args[$key]; | |||
| 		else | |||
| 			$val=NULL; | |||
| 		return $val; | |||
| 	} | |||
| 
 | |||
| 	/** | |||
| 	*	Remove OpenID request parameter | |||
| 	*	@return NULL | |||
| 	*	@param $key | |||
| 	**/ | |||
| 	function clear($key) { | |||
| 		unset($this->args[$key]); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,176 @@ | |||
| <?php | |||
| 
 | |||
| /* | |||
| 
 | |||
| 	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved. | |||
| 
 | |||
| 	This file is part of the Fat-Free Framework (http://fatfreeframework.com). | |||
| 
 | |||
| 	This is free software: you can redistribute it and/or modify it under the | |||
| 	terms of the GNU General Public License as published by the Free Software | |||
| 	Foundation, either version 3 of the License, or later. | |||
| 
 | |||
| 	Fat-Free Framework is distributed in the hope that it will be useful, | |||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | |||
| 	General Public License for more details. | |||
| 
 | |||
| 	You should have received a copy of the GNU General Public License along | |||
| 	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>. | |||
| 
 | |||
| */ | |||
| 
 | |||
| namespace 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['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->HOST) { | |||
| 			$req=$web->request($source); | |||
| 			$doc=new \DOMDocument('1.0',$fw->ENCODING); | |||
| 			$doc->stricterrorchecking=FALSE; | |||
| 			$doc->recover=TRUE; | |||
| 			if (@$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, | |||
| 							[ | |||
| 								'method'=>'POST', | |||
| 								'header'=>'Content-Type: application/xml', | |||
| 								'content'=>xmlrpc_encode_request( | |||
| 									'pingback.ping', | |||
| 									[$source,$permalink], | |||
| 									['encoding'=>$fw->ENCODING] | |||
| 								) | |||
| 							] | |||
| 						); | |||
| 						if ($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->PACKAGE); | |||
| 			header('Content-Type: application/xml; '. | |||
| 				'charset='.$charset=$fw->ENCODING); | |||
| 		} | |||
| 		if (!$path) | |||
| 			$path=$fw->BASE; | |||
| 		$web=\Web::instance(); | |||
| 		$args=xmlrpc_decode_request($fw->BODY,$method,$charset); | |||
| 		$options=['encoding'=>$charset]; | |||
| 		if ($method=='pingback.ping' && isset($args[0],$args[1])) { | |||
| 			list($source,$permalink)=$args; | |||
| 			$doc=new \DOMDocument('1.0',$fw->ENCODING); | |||
| 			// Check local page if pingback-enabled | |||
| 			$parts=parse_url($permalink); | |||
| 			if ((empty($parts['scheme']) || | |||
| 				$parts['host']==$fw->HOST) && | |||
| 				preg_match('/^'.preg_quote($path,'/').'/'. | |||
| 					($fw->CASELESS?'i':''),$parts['path']) && | |||
| 				$this->enabled($permalink)) { | |||
| 				// Check source | |||
| 				$parts=parse_url($source); | |||
| 				if ((empty($parts['scheme']) || | |||
| 					$parts['host']==$fw->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,[$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); | |||
| 	} | |||
| 
 | |||
| } | |||
| @ -0,0 +1,8 @@ | |||
| { | |||
|   "name": "Glowing Bulbs Power Manager Interface", | |||
|   "short_name": "IZZ Control", | |||
|   "lang": "hu", | |||
|   "start_url": "/192.168.0.200", | |||
|   "display": "standalone", | |||
|   "theme_color": "#979ea5" | |||
| } | |||
| @ -0,0 +1,2 @@ | |||
| #IzControl# | |||
| IzControl is an application that makes is possible to easily control appliances over the network using UDP, telnet and other protocols. | |||
| @ -0,0 +1,6 @@ | |||
| /* Reset */ | |||
| html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,abbr,address,cite,code,del,dfn,em,img,ins,kbd,q,samp,small,strong,sub,sup,var,b,i,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,figcaption,figure,footer,header,hgroup,dir,menu,nav,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{line-height:1}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}nav ul{list-style:none}ol{list-style:decimal}ul{list-style:disc}ul ul{list-style:circle}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}a{margin:0;padding:0;font-size:100%;vertical-align:baseline;background:transparent}ins{text-decoration:underline}mark{background:none}del{text-decoration:line-through}abbr[title],dfn[title]{border-bottom:1px dotted;cursor:help}table{border-collapse:collapse;border-spacing:0}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}input,select,a img{vertical-align:middle} | |||
| /* Typography */ | |||
| *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;max-width:100%}html{height:100%;font-size:100%;font-family:Verdana, sans-serif;overflow-y:scroll;-webkit-text-size-adjust:100%}body{margin:0;min-height:100%;overflow:hidden}body,pre,label,input,button,select,textarea{font:normal 100%/1.25 Verdana, sans-serif;vertical-align:top}a{display:inline-block}p,ul,ol{margin:1.25em 0}h1{font-size:2em;line-height:1.25em;margin:0.625em 0}h2{font-size:1.5em;line-height:1.6667em;margin:0.8333em 0}h3{font-size:1.25em;line-height:1em;margin:1em 0}h4{font-size:1em;line-height:1.25em;margin:1.25em 0}h5{font-size:0.8125em;line-height:1.5385em;margin:1.5385em 0}h6{font-size:0.6875em;line-height:1.8182em;margin:1.8182em 0}blockquote{margin:0 3em}caption{font-weight:bold}ul,ol,dir,menu,dd{margin-left:3em}ul,dir,menu{list-style:disc}ol{list-style:decimal}sub,sup{font-size:75%;line-height:0;vertical-align:baseline;position:relative}sub{top:0.5em}sup{top:-0.5em}label{display:inline-block}input[type="text"],input[type="password"],input[type="file"]{padding:1px;border:1px solid #999;margin:-4px 0 0 0}select,textarea{padding:0;border:1px solid #999;margin:-4px 0 0 0}fieldset{padding:0.625em;border:1px solid #ccc;margin-bottom:0.625em}input[type="radio"],input[type="checkbox"]{height:1em;vertical-align:top;margin:0.125em}div,table{overflow:hidden} | |||
| /* Fluid Fonts */ | |||
| @media screen and (max-width:960px){body{font-size:0.81255em}} | |||
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					| @ -0,0 +1,96 @@ | |||
| body { | |||
| 	display: flex; | |||
| 	align-items: center; | |||
| 	justify-content: center; | |||
| 	font-family: Verdana, sans-serif; | |||
| 	font-size: 14px; | |||
| 	background-color: #979ea5; | |||
| } | |||
| 
 | |||
| div.buttons { | |||
| 	display: flex; | |||
| 	justify-content: center; | |||
| } | |||
| 
 | |||
| label { | |||
| 	padding: 10px; | |||
| 	border: 1px solid grey; | |||
| 	margin-bottom: 10px; | |||
| 	background: #a7a7a7; | |||
| } | |||
| input:focus {  | |||
|     outline: none !important; | |||
|     border-color: #719ECE; | |||
|     box-shadow: 0 0 10px #719ECE; | |||
| } | |||
| textarea:focus {  | |||
|     outline: none !important; | |||
|     border-color: #719ECE; | |||
|     box-shadow: 0 0 10px #719ECE; | |||
| } | |||
| 
 | |||
| .addbtn { | |||
| 	border: 1px solid grey; | |||
| 	background-color: #a7a7a7; | |||
| 	padding: 5px; | |||
| 	margin: 3px; | |||
| 	text-decoration: none; | |||
| 	color: #000; | |||
| } | |||
| .darker { | |||
| 	background-image: linear-gradient(#b1b1b1, #868686); | |||
| 	padding: 4px 7px; | |||
| } | |||
| input, textarea { | |||
| 	background-color: #979ea5; | |||
| 	margin: 10px !important; | |||
| 	border: 1px solid grey !important; | |||
| 
 | |||
| } | |||
| 
 | |||
| #app {text-align: center;} | |||
| 
 | |||
| .messages { | |||
| 	color: green; | |||
|     padding: 5px; | |||
|     border: 1px solid green; | |||
|     margin-bottom: 10px; | |||
| } | |||
| .messages.error { | |||
| 	color: #c72f2f; | |||
| 	border: 1px solid #c72f2f; | |||
| } | |||
| .button { | |||
| 	display: flex; | |||
| 	flex-direction: column; | |||
| 	align-items: center; | |||
| 	justify-content: center; | |||
| 	font-family: Verdana, sans-serif; | |||
| 	font-size: 14px; | |||
| 	/*text-transform: capitalize;*/ | |||
| 	width: 120px; | |||
| 	height: 120px; | |||
| 	margin: 20px; | |||
| 	background-image: url(/ui/images/btn.png); | |||
| 	background-position: center center; | |||
| 	background-size: contain; | |||
| 	text-decoration: none; | |||
| 	color: #111; | |||
| 
 | |||
| } | |||
| 
 | |||
| p { | |||
| 	margin: 0; | |||
| } | |||
| 
 | |||
| .button div { | |||
| 	display: flex; | |||
| } | |||
| 
 | |||
| @media screen and (max-width:48em) { | |||
| 
 | |||
| 	body { | |||
| 		font-size:1em; | |||
| 	} | |||
| 
 | |||
| } | |||
								
									Binary file not shown.
								
							
						
					
								
									Binary file not shown.
								
							
						
					| After Width: | Height: | Size: 434 KiB | 
								
									Binary file not shown.
								
							
						
					
								
									Binary file not shown.
								
							
						
					
								
									Binary file not shown.
								
							
						
					| @ -0,0 +1,190 @@ | |||
| <div id="app"> | |||
| 	<div v-for="message in messages" :class="{'error': !message.success}" class="messages">{{message.message}}</div> | |||
| 	<p v-show="sending" style="position:absolute;top: 10px; right: 10px;">Parancs küldése, kérem várjon.</p> | |||
| 	<p v-if="loading">No equipments, please add one.</p> | |||
| 	<div class="buttons" v-else> | |||
| 		<div class="button" v-for="item in items"> | |||
| 					<div> | |||
| 						<p>{{item.name}}</p> | |||
| 					</div> | |||
| 					<div> | |||
| 						<a v-on:click="sendsignal(item, item.oncommand)" href="javascript:void(0)" class="addbtn darker"><i class="fa fa-play" aria-hidden="true"></i></a> | |||
| 						<a v-on:click="sendsignal(item, item.offcommand)" href="javascript:void(0)" class="addbtn darker"><i class="fa fa-stop" aria-hidden="true"></i></a> | |||
| 					</div> | |||
| 					<?php if ($admin): ?> | |||
| 					<div> | |||
| 						<a v-on:click="delitem(item)" href="javascript:void(0)" class="addbtn darker"><i class="fa fa-trash" aria-hidden="true"></i></a> | |||
| 						 | |||
| 					</div>						 | |||
| 					<?php endif ?> | |||
| 
 | |||
| 
 | |||
| 		</div> | |||
| 
 | |||
| 		 | |||
| 	</div> | |||
| 	<?php if ($admin): ?> | |||
| 	<div> | |||
| 		<a href="javascript:void(0)" class="addbtn" v-on:click="add">{{addtext}}</a> | |||
| 		<div class="add" v-show="adding"> | |||
| 			<form> | |||
| 				<div> | |||
| 					<div> | |||
| 						<label for="channel"> | |||
| 							Channel<br> | |||
| 							<label>UDP<br><input type="radio" name="channel" value="udp" v-model="newitem.channel"></label>	 | |||
| 							<label>TELNET<br><input type="radio" name="channel" value="telnet" v-model="newitem.channel">	</label> | |||
| 							<label>WOL<br><input type="radio" name="channel" value="wol" v-model="newitem.channel">	</label> | |||
| 						</label> | |||
| 					</div> | |||
| 					<label for="name" v-show="newitem.channel != ''"> | |||
| 						Add a name for this resource<br> | |||
| 						<input type="text" name="name" v-model="newitem.name">	<br> | |||
| 					</label> | |||
| 					<label for="oncommand" v-show="['udp', 'telnet'].indexOf(newitem.channel) >= 0"> | |||
| 						What is the on command?<br> | |||
| 						<input type="text" name="oncommand" v-model="newitem.oncommand">	<br> | |||
| 					</label>			 | |||
| 					<label for="ontime" v-show="newitem.channel != ''"> | |||
| 						When to send the on command?<br> | |||
| 						<input type="time" name="ontime" v-model="newitem.ontime">	<br> | |||
| 					</label>			 | |||
| 					<label for="offcommand" v-show="['udp', 'telnet'].indexOf(newitem.channel) >= 0"> | |||
| 						What is the off command?<br> | |||
| 						<input type="text" name="offcommand" v-model="newitem.offcommand">	<br> | |||
| 					</label>			 | |||
| 					<label for="offtime" v-show="['udp', 'telnet'].indexOf(newitem.channel) >= 0"> | |||
| 						When to send the off command?<br> | |||
| 						<input type="time" name="offtime" v-model="newitem.offtime">	<br> | |||
| 					</label> | |||
| 
 | |||
| 					<div v-show="newitem.channel != ''"> | |||
| 						<label for="days"> | |||
| 							Days<br> | |||
| 							<label>monday<br><input type="checkbox" v-model="newitem.days" value="monday"></label>	 | |||
| 							<label>tuesday<br><input type="checkbox" v-model="newitem.days" value="tuesday"></label>	 | |||
| 							<label>wednesday<br><input type="checkbox" v-model="newitem.days" value="wednesday"></label>	 | |||
| 							<label>thursday<br><input type="checkbox" v-model="newitem.days" value="thursday"></label>	 | |||
| 							<label>friday<br><input type="checkbox" v-model="newitem.days" value="friday"></label>	 | |||
| 							<label>saturday<br><input type="checkbox" v-model="newitem.days" value="saturday"></label>	 | |||
| 							<label>sunday<br><input type="checkbox" v-model="newitem.days" value="sunday"></label>	 | |||
| 						</label> | |||
| 					</div> | |||
| 					<div v-show="['udp', 'telnet'].indexOf(newitem.channel) >= 0"> | |||
| 						<label for="ip"> | |||
| 							What ip-s to send the signal to? (Each IP: new line!)<br> | |||
| 							<textarea name="ip" v-model="newitem.ip" rows="10"> | |||
| 								 | |||
| 							</textarea> | |||
| 						</label> | |||
| 						<label for="port" v-show="newitem.channel != ''"> | |||
| 							What is the network port?<br> | |||
| 							<input type="text" name="port" v-model="newitem.port">	<br> | |||
| 						</label> | |||
| 					</div> | |||
| 					<div v-show="newitem.channel === 'wol'"> | |||
| 						<label for="macAdress"> | |||
| 							What MAC Addresses to send the signal to? (Each MAC Address: new line!)<br> | |||
| 							<textarea name="macAdress" v-model="newitem.macAddress" rows="10"> | |||
| 								 | |||
| 							</textarea> | |||
| 
 | |||
| 						</label> | |||
| 						<label for="broadcastIP"> | |||
| 							What is the network Broadcast IP? (usually 255.255.255.255)<br> | |||
| 							<input type="text" name="broadcastIP" v-model="newitem.broadcastIP">	<br> | |||
| 						</label> | |||
| 					</div> | |||
| 					<a class="addbtn" v-show="submittable" href="javascript:void(0)" v-on:click="plug"><i class="fa fa-plug" aria-hidden="true"></i>Plug it in!</a> | |||
| 
 | |||
| 					 | |||
| 				</div> | |||
| 			</form> | |||
| 		</div> | |||
| 	</div> | |||
| 			 | |||
| 	<?php endif ?> | |||
| 
 | |||
| </div> | |||
| 
 | |||
| 
 | |||
| 
 | |||
| <script> | |||
| var app = new Vue({ | |||
|   el: '#app', | |||
|   data: { | |||
|     items: [], | |||
|     sending: false, | |||
|     adding: false, | |||
|     messages: [], | |||
|     newitem: { | |||
|     	name: '', | |||
|     	oncommand: '', | |||
|     	ontime: '', | |||
|     	offcommand: '', | |||
|     	offtime: '', | |||
|     	days: [], | |||
|     	channel: '', | |||
|     	port: '', | |||
|     	ip: '', | |||
|     	broadcastIP: '', | |||
|     	macAddress: '' | |||
| 
 | |||
|     }, | |||
|   }, | |||
|   mounted () { | |||
|   	this.getitems() | |||
|   }, | |||
|   computed: { | |||
|   	loading() {return this.items.length === 0}, | |||
|   	addtext: function() { | |||
|   		if (this.adding == false) { return 'Add new group or element'} else {return 'Back'} | |||
|   	}, | |||
|   	submittable: function() { | |||
|   		if ( | |||
|   			this.newitem.name != '' && | |||
|   			this.newitem.channel != '' | |||
|   			) {return true} else {return false} | |||
|   	} | |||
|   }, | |||
|   methods: { | |||
|   	add: function() { | |||
|   		this.adding = !this.adding  | |||
|   	}, | |||
|   	sendsignal: function(item, action) { | |||
|   		if (this.sending) { return alert('Please wait, the signal is sending')} | |||
|   		this.sending = true | |||
|   		console.log(`sending signal ${action} to ${item.name}`) | |||
|   		 | |||
|   		axios | |||
|   		  .post('sendsignal', {'item': item, 'action': action}).then(response => { | |||
|   		  	this.messages = response.data | |||
|   		  	this.sending = false | |||
|   		  }, error => { | |||
|   		  	alert(error) | |||
|   		  	this.sending = false | |||
|   		  }) | |||
|   	}, | |||
|   	delitem: function(item) { | |||
|   		if (confirm('Are you sure?')) { | |||
| 	  		axios | |||
| 	  		  .post('del', item).then(response => { | |||
| 	  		  	this.getitems() | |||
| 	  		  })  			 | |||
| 	  		} | |||
|   	}, | |||
|   	getitems: function() { | |||
|     	axios | |||
|       	  .get('api/items') | |||
|       	  .then(response => (this.items = response.data)) | |||
|   	}, | |||
|   	plug: function() { | |||
|   		axios | |||
|   		  .post('add', this.newitem).then(response => { | |||
|   		  	this.items.push(this.newitem) | |||
|   		  }) | |||
|   		  this.adding = false | |||
|   	} | |||
|   } | |||
| }) | |||
| </script> | |||
| After Width: | Height: | Size: 2.0 KiB | 
| After Width: | Height: | Size: 6.1 KiB | 
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because it is too large
									
								
							
						
					| @ -0,0 +1,29 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| 	<head> | |||
| 		<meta charset="<?php echo $ENCODING; ?>" /> | |||
| 		<link rel="manifest" href="manifest.json"> | |||
| 		<meta name="mobile-web-app-capable" content="yes"> | |||
| 		<meta name="apple-mobile-web-app-capable" content="yes"> | |||
| 		<meta name="application-name" content="IZZ Control"> | |||
| 		<meta name="apple-mobile-web-app-title" content="IZZ Control"> | |||
| 		<meta name="theme-color" content="#979ea5"> | |||
| 		<meta name="msapplication-navbutton-color" content="#979ea5"> | |||
| 		<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> | |||
| 		<meta name="msapplication-starturl" content="/192.168.0.200"> | |||
| 		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | |||
| 		<title>IzzoControl</title> | |||
| 		<base href="<?php echo $SCHEME.'://'.$HOST.':'.$PORT.$BASE.'/'; ?>" /> | |||
| 		<link rel="stylesheet" href="lib/code.css" type="text/css" /> | |||
| 		<link rel="stylesheet" href="ui/css/base.css" type="text/css" /> | |||
| 		<link rel="stylesheet" href="ui/css/theme.css" type="text/css" /> | |||
| 		<link rel="stylesheet" href="ui/css/font-awesome.min.css" type="text/css" /> | |||
| 		<script src="ui/js/vue.js" type="text/javascript" charset="utf-8"></script> | |||
| 		<script src="ui/js/axios.min.js" type="text/javascript" charset="utf-8"></script> | |||
| 
 | |||
| 
 | |||
| 	</head> | |||
| 	<body> | |||
| 		<?php echo $this->render(Base::instance()->get('template')); ?> | |||
| 	</body> | |||
| </html> | |||
					Loading…
					
					
				
		Reference in new issue