commit 1af0dae0497f770c10c21f7acace61c1d18c23f4 Author: Enzo Angiulli Date: Mon Nov 23 23:10:34 2020 +0100 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..55a9c8e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/tmp/ +/.idea/ +/vendor/ +data/* +!data/.keep +.DS_Store diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..2be86c5 --- /dev/null +++ b/.htaccess @@ -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] diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..435152d --- /dev/null +++ b/composer.json @@ -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"] + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..5b2c092 --- /dev/null +++ b/composer.lock @@ -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" +} diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..8911dfc --- /dev/null +++ b/config.ini @@ -0,0 +1,4 @@ +[globals] + +DEBUG=3 +UI=ui/ diff --git a/data/.keep b/data/.keep new file mode 100644 index 0000000..e69de29 diff --git a/index.php b/index.php new file mode 100644 index 0000000..afdd9b5 --- /dev/null +++ b/index.php @@ -0,0 +1,173 @@ +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(); diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md new file mode 100644 index 0000000..b406321 --- /dev/null +++ b/lib/CHANGELOG.md @@ -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 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 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 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 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 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 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: and 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 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 diff --git a/lib/COPYING b/lib/COPYING new file mode 100755 index 0000000..3c7236c --- /dev/null +++ b/lib/COPYING @@ -0,0 +1,621 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +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 diff --git a/lib/audit.php b/lib/audit.php new file mode 100644 index 0000000..8fcc95a --- /dev/null +++ b/lib/audit.php @@ -0,0 +1,191 @@ +. + +*/ + +//! 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)); + } + +} diff --git a/lib/auth.php b/lib/auth.php new file mode 100644 index 0000000..a150ce4 --- /dev/null +++ b/lib/auth.php @@ -0,0 +1,262 @@ +. + +*/ + +//! 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; + } + +} diff --git a/lib/base.php b/lib/base.php new file mode 100644 index 0000000..ea72e7c --- /dev/null +++ b/lib/base.php @@ -0,0 +1,3584 @@ +. + +*/ + +//! Factory class for single-instance objects +abstract class Prefab { + + /** + * Return class instance + * @return static + **/ + static function instance() { + if (!Registry::exists($class=get_called_class())) { + $ref=new ReflectionClass($class); + $args=func_get_args(); + Registry::set($class, + $args?$ref->newinstanceargs($args):new $class); + } + return Registry::get($class); + } + +} + +//! Base structure +final class Base extends Prefab implements ArrayAccess { + + //@{ Framework details + const + PACKAGE='Fat-Free Framework', + VERSION='3.7.2-Release'; + //@} + + //@{ HTTP status codes (RFC 2616) + const + HTTP_100='Continue', + HTTP_101='Switching Protocols', + HTTP_103='Early Hints', + HTTP_200='OK', + HTTP_201='Created', + HTTP_202='Accepted', + HTTP_203='Non-Authorative Information', + HTTP_204='No Content', + HTTP_205='Reset Content', + HTTP_206='Partial Content', + HTTP_300='Multiple Choices', + HTTP_301='Moved Permanently', + HTTP_302='Found', + HTTP_303='See Other', + HTTP_304='Not Modified', + HTTP_305='Use Proxy', + HTTP_307='Temporary Redirect', + HTTP_308='Permanent Redirect', + HTTP_400='Bad Request', + HTTP_401='Unauthorized', + HTTP_402='Payment Required', + HTTP_403='Forbidden', + HTTP_404='Not Found', + HTTP_405='Method Not Allowed', + HTTP_406='Not Acceptable', + HTTP_407='Proxy Authentication Required', + HTTP_408='Request Timeout', + HTTP_409='Conflict', + HTTP_410='Gone', + HTTP_411='Length Required', + HTTP_412='Precondition Failed', + HTTP_413='Request Entity Too Large', + HTTP_414='Request-URI Too Long', + HTTP_415='Unsupported Media Type', + HTTP_416='Requested Range Not Satisfiable', + HTTP_417='Expectation Failed', + HTTP_421='Misdirected Request', + HTTP_422='Unprocessable Entity', + HTTP_423='Locked', + HTTP_429='Too Many Requests', + HTTP_451='Unavailable For Legal Reasons', + HTTP_500='Internal Server Error', + HTTP_501='Not Implemented', + HTTP_502='Bad Gateway', + HTTP_503='Service Unavailable', + HTTP_504='Gateway Timeout', + HTTP_505='HTTP Version Not Supported', + HTTP_507='Insufficient Storage', + HTTP_511='Network Authentication Required'; + //@} + + const + //! Mapped PHP globals + GLOBALS='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV', + //! HTTP verbs + VERBS='GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS', + //! Default directory permissions + MODE=0755, + //! Syntax highlighting stylesheet + CSS='code.css'; + + //@{ Request types + const + REQ_SYNC=1, + REQ_AJAX=2, + REQ_CLI=4; + //@} + + //@{ Error messages + const + E_Pattern='Invalid routing pattern: %s', + E_Named='Named route does not exist: %s', + E_Alias='Invalid named route alias: %s', + E_Fatal='Fatal error: %s', + E_Open='Unable to open %s', + E_Routes='No routes specified', + E_Class='Invalid class %s', + E_Method='Invalid method %s', + E_Hive='Invalid hive key %s'; + //@} + + private + //! Globals + $hive, + //! Initial settings + $init, + //! Language lookup sequence + $languages, + //! Mutex locks + $locks=[], + //! Default fallback language + $fallback='en'; + + /** + * Sync PHP global with corresponding hive key + * @return array + * @param $key string + **/ + function sync($key) { + return $this->hive[$key]=&$GLOBALS['_'.$key]; + } + + /** + * Return the parts of specified hive key + * @return array + * @param $key string + **/ + private function cut($key) { + return preg_split('/\[\h*[\'"]?(.+?)[\'"]?\h*\]|(->)|\./', + $key,NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE); + } + + /** + * Replace tokenized URL with available token values + * @return string + * @param $url array|string + * @param $args array + **/ + function build($url,$args=[]) { + $args+=$this->hive['PARAMS']; + if (is_array($url)) + foreach ($url as &$var) { + $var=$this->build($var,$args); + unset($var); + } + else { + $i=0; + $url=preg_replace_callback('/(\{)?@(\w+)(?(1)\})|(\*)/', + function($match) use(&$i,$args) { + if (isset($match[2]) && + array_key_exists($match[2],$args)) + return $args[$match[2]]; + if (isset($match[3]) && + array_key_exists($match[3],$args)) { + if (!is_array($args[$match[3]])) + return $args[$match[3]]; + $i++; + return $args[$match[3]][$i-1]; + } + return $match[0]; + },$url); + } + return $url; + } + + /** + * Parse string containing key-value pairs + * @return array + * @param $str string + **/ + function parse($str) { + preg_match_all('/(\w+|\*)\h*=\h*(?:\[(.+?)\]|(.+?))(?=,|$)/', + $str,$pairs,PREG_SET_ORDER); + $out=[]; + foreach ($pairs as $pair) + if ($pair[2]) { + $out[$pair[1]]=[]; + foreach (explode(',',$pair[2]) as $val) + array_push($out[$pair[1]],$val); + } + else + $out[$pair[1]]=trim($pair[3]); + return $out; + } + + /** + * Cast string variable to PHP type or constant + * @param $val + * @return mixed + */ + function cast($val) { + if (preg_match('/^(?:0x[0-9a-f]+|0[0-7]+|0b[01]+)$/i',$val)) + return intval($val,0); + if (is_numeric($val)) + return $val+0; + $val=trim($val); + if (preg_match('/^\w+$/i',$val) && defined($val)) + return constant($val); + return $val; + } + + /** + * Convert JS-style token to PHP expression + * @return string + * @param $str string + * @param $evaluate bool compile expressions as well or only convert variable access + **/ + function compile($str, $evaluate=TRUE) { + return (!$evaluate) + ? preg_replace_callback( + '/^@(\w+)((?:\..+|\[(?:(?:[^\[\]]*|(?R))*)\])*)/', + function($expr) { + $str='$'.$expr[1]; + if (isset($expr[2])) + $str.=preg_replace_callback( + '/\.([^.\[\]]+)|\[((?:[^\[\]\'"]*|(?R))*)\]/', + function($sub) { + $val=isset($sub[2]) ? $sub[2] : $sub[1]; + if (ctype_digit($val)) + $val=(int)$val; + $out='['.$this->export($val).']'; + return $out; + }, + $expr[2] + ); + return $str; + }, + $str + ) + : preg_replace_callback( + '/(?|::)\w+)?)'. + '((?:\.\w+|\[(?:(?:[^\[\]]*|(?R))*)\]|(?:\->|::)\w+|\()*)/', + function($expr) { + $str='$'.$expr[1]; + if (isset($expr[2])) + $str.=preg_replace_callback( + '/\.(\w+)(\()?|\[((?:[^\[\]]*|(?R))*)\]/', + function($sub) { + if (empty($sub[2])) { + if (ctype_digit($sub[1])) + $sub[1]=(int)$sub[1]; + $out='['. + (isset($sub[3])? + $this->compile($sub[3]): + $this->export($sub[1])). + ']'; + } + else + $out=function_exists($sub[1])? + $sub[0]: + ('['.$this->export($sub[1]).']'.$sub[2]); + return $out; + }, + $expr[2] + ); + return $str; + }, + $str + ); + } + + /** + * Get hive key reference/contents; Add non-existent hive keys, + * array elements, and object properties by default + * @return mixed + * @param $key string + * @param $add bool + * @param $var mixed + **/ + function &ref($key,$add=TRUE,&$var=NULL) { + $null=NULL; + $parts=$this->cut($key); + if ($parts[0]=='SESSION') { + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); + $this->sync('SESSION'); + } + elseif (!preg_match('/^\w+$/',$parts[0])) + user_error(sprintf(self::E_Hive,$this->stringify($key)), + E_USER_ERROR); + if (is_null($var)) { + if ($add) + $var=&$this->hive; + else + $var=$this->hive; + } + $obj=FALSE; + foreach ($parts as $part) + if ($part=='->') + $obj=TRUE; + elseif ($obj) { + $obj=FALSE; + if (!is_object($var)) + $var=new stdClass; + if ($add || property_exists($var,$part)) + $var=&$var->$part; + else { + $var=&$null; + break; + } + } + else { + if (!is_array($var)) + $var=[]; + if ($add || array_key_exists($part,$var)) + $var=&$var[$part]; + else { + $var=&$null; + break; + } + } + return $var; + } + + /** + * Return TRUE if hive key is set + * (or return timestamp and TTL if cached) + * @return bool + * @param $key string + * @param $val mixed + **/ + function exists($key,&$val=NULL) { + $val=$this->ref($key,FALSE); + return isset($val)? + TRUE: + (Cache::instance()->exists($this->hash($key).'.var',$val)?:FALSE); + } + + /** + * Return TRUE if hive key is empty and not cached + * @param $key string + * @param $val mixed + * @return bool + **/ + function devoid($key,&$val=NULL) { + $val=$this->ref($key,FALSE); + return empty($val) && + (!Cache::instance()->exists($this->hash($key).'.var',$val) || + !$val); + } + + /** + * Bind value to hive key + * @return mixed + * @param $key string + * @param $val mixed + * @param $ttl int + **/ + function set($key,$val,$ttl=0) { + $time=(int)$this->hive['TIME']; + if (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) { + $this->set('REQUEST'.$expr[2],$val); + if ($expr[1]=='COOKIE') { + $parts=$this->cut($key); + $jar=$this->unserialize($this->serialize($this->hive['JAR'])); + unset($jar['lifetime']); + if (version_compare(PHP_VERSION, '7.3.0') >= 0) { + unset($jar['expire']); + if (isset($_COOKIE[$parts[1]])) + setcookie($parts[1],NULL,['expires'=>0]+$jar); + if ($ttl) + $jar['expires']=$time+$ttl; + setcookie($parts[1],$val,$jar); + } else { + unset($jar['samesite']); + if (isset($_COOKIE[$parts[1]])) + call_user_func_array('setcookie', + array_merge([$parts[1],NULL],['expire'=>0]+$jar)); + if ($ttl) + $jar['expire']=$time+$ttl; + call_user_func_array('setcookie',[$parts[1],$val]+$jar); + } + $_COOKIE[$parts[1]]=$val; + return $val; + } + } + else switch ($key) { + case 'CACHE': + $val=Cache::instance()->load($val); + break; + case 'ENCODING': + ini_set('default_charset',$val); + if (extension_loaded('mbstring')) + mb_internal_encoding($val); + break; + case 'FALLBACK': + $this->fallback=$val; + $lang=$this->language($this->hive['LANGUAGE']); + case 'LANGUAGE': + if (!isset($lang)) + $val=$this->language($val); + $lex=$this->lexicon($this->hive['LOCALES'],$ttl); + case 'LOCALES': + if (isset($lex) || $lex=$this->lexicon($val,$ttl)) + foreach ($lex as $dt=>$dd) { + $ref=&$this->ref($this->hive['PREFIX'].$dt); + $ref=$dd; + unset($ref); + } + break; + case 'TZ': + date_default_timezone_set($val); + break; + } + $ref=&$this->ref($key); + $ref=$val; + if (preg_match('/^JAR\b/',$key)) { + if ($key=='JAR.lifetime') + $this->set('JAR.expire',$val==0?0: + (is_int($val)?$time+$val:strtotime($val))); + else { + if ($key=='JAR.expire') + $this->hive['JAR']['lifetime']=max(0,$val-$time); + $jar=$this->unserialize($this->serialize($this->hive['JAR'])); + unset($jar['expire']); + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + if (version_compare(PHP_VERSION, '7.3.0') >= 0) + session_set_cookie_params($jar); + else { + unset($jar['samesite']); + call_user_func_array('session_set_cookie_params',$jar); + } + } + } + if ($ttl) + // Persist the key-value pair + Cache::instance()->set($this->hash($key).'.var',$val,$ttl); + return $ref; + } + + /** + * Retrieve contents of hive key + * @return mixed + * @param $key string + * @param $args string|array + **/ + function get($key,$args=NULL) { + if (is_string($val=$this->ref($key,FALSE)) && !is_null($args)) + return call_user_func_array( + [$this,'format'], + array_merge([$val],is_array($args)?$args:[$args]) + ); + if (is_null($val)) { + // Attempt to retrieve from cache + if (Cache::instance()->exists($this->hash($key).'.var',$data)) + return $data; + } + return $val; + } + + /** + * Unset hive key + * @param $key string + **/ + function clear($key) { + // Normalize array literal + $cache=Cache::instance(); + $parts=$this->cut($key); + if ($key=='CACHE') + // Clear cache contents + $cache->reset(); + elseif (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) { + $this->clear('REQUEST'.$expr[2]); + if ($expr[1]=='COOKIE') { + $parts=$this->cut($key); + $jar=$this->hive['JAR']; + unset($jar['lifetime']); + $jar['expire']=0; + if (version_compare(PHP_VERSION, '7.3.0') >= 0) { + $jar['expires']=$jar['expire']; + unset($jar['expire']); + setcookie($parts[1],NULL,$jar); + } else { + unset($jar['samesite']); + call_user_func_array('setcookie', + array_merge([$parts[1],NULL],$jar)); + } + unset($_COOKIE[$parts[1]]); + } + } + elseif ($parts[0]=='SESSION') { + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); + if (empty($parts[1])) { + // End session + session_unset(); + session_destroy(); + $this->clear('COOKIE.'.session_name()); + } + $this->sync('SESSION'); + } + if (!isset($parts[1]) && array_key_exists($parts[0],$this->init)) + // Reset global to default value + $this->hive[$parts[0]]=$this->init[$parts[0]]; + else { + $val=preg_replace('/^(\$hive)/','$this->hive', + $this->compile('@hive.'.$key, FALSE)); + eval('unset('.$val.');'); + if ($parts[0]=='SESSION') { + session_commit(); + session_start(); + } + if ($cache->exists($hash=$this->hash($key).'.var')) + // Remove from cache + $cache->clear($hash); + } + } + + /** + * Return TRUE if hive variable is 'on' + * @return bool + * @param $key string + **/ + function checked($key) { + $ref=&$this->ref($key); + return $ref=='on'; + } + + /** + * Return TRUE if property has public visibility + * @return bool + * @param $obj object + * @param $key string + **/ + function visible($obj,$key) { + if (property_exists($obj,$key)) { + $ref=new ReflectionProperty(get_class($obj),$key); + $out=$ref->ispublic(); + unset($ref); + return $out; + } + return FALSE; + } + + /** + * Multi-variable assignment using associative array + * @param $vars array + * @param $prefix string + * @param $ttl int + **/ + function mset(array $vars,$prefix='',$ttl=0) { + foreach ($vars as $key=>$val) + $this->set($prefix.$key,$val,$ttl); + } + + /** + * Publish hive contents + * @return array + **/ + function hive() { + return $this->hive; + } + + /** + * Copy contents of hive variable to another + * @return mixed + * @param $src string + * @param $dst string + **/ + function copy($src,$dst) { + $ref=&$this->ref($dst); + return $ref=$this->ref($src,FALSE); + } + + /** + * Concatenate string to hive string variable + * @return string + * @param $key string + * @param $val string + **/ + function concat($key,$val) { + $ref=&$this->ref($key); + $ref.=$val; + return $ref; + } + + /** + * Swap keys and values of hive array variable + * @return array + * @param $key string + * @public + **/ + function flip($key) { + $ref=&$this->ref($key); + return $ref=array_combine(array_values($ref),array_keys($ref)); + } + + /** + * Add element to the end of hive array variable + * @return mixed + * @param $key string + * @param $val mixed + **/ + function push($key,$val) { + $ref=&$this->ref($key); + $ref[]=$val; + return $val; + } + + /** + * Remove last element of hive array variable + * @return mixed + * @param $key string + **/ + function pop($key) { + $ref=&$this->ref($key); + return array_pop($ref); + } + + /** + * Add element to the beginning of hive array variable + * @return mixed + * @param $key string + * @param $val mixed + **/ + function unshift($key,$val) { + $ref=&$this->ref($key); + array_unshift($ref,$val); + return $val; + } + + /** + * Remove first element of hive array variable + * @return mixed + * @param $key string + **/ + function shift($key) { + $ref=&$this->ref($key); + return array_shift($ref); + } + + /** + * Merge array with hive array variable + * @return array + * @param $key string + * @param $src string|array + * @param $keep bool + **/ + function merge($key,$src,$keep=FALSE) { + $ref=&$this->ref($key); + if (!$ref) + $ref=[]; + $out=array_merge($ref,is_string($src)?$this->hive[$src]:$src); + if ($keep) + $ref=$out; + return $out; + } + + /** + * Extend hive array variable with default values from $src + * @return array + * @param $key string + * @param $src string|array + * @param $keep bool + **/ + function extend($key,$src,$keep=FALSE) { + $ref=&$this->ref($key); + if (!$ref) + $ref=[]; + $out=array_replace_recursive( + is_string($src)?$this->hive[$src]:$src,$ref); + if ($keep) + $ref=$out; + return $out; + } + + /** + * Convert backslashes to slashes + * @return string + * @param $str string + **/ + function fixslashes($str) { + return $str?strtr($str,'\\','/'):$str; + } + + /** + * Split comma-, semi-colon, or pipe-separated string + * @return array + * @param $str string + * @param $noempty bool + **/ + function split($str,$noempty=TRUE) { + return array_map('trim', + preg_split('/[,;|]/',$str,0,$noempty?PREG_SPLIT_NO_EMPTY:0)); + } + + /** + * Convert PHP expression/value to compressed exportable string + * @return string + * @param $arg mixed + * @param $stack array + **/ + function stringify($arg,array $stack=NULL) { + if ($stack) { + foreach ($stack as $node) + if ($arg===$node) + return '*RECURSION*'; + } + else + $stack=[]; + switch (gettype($arg)) { + case 'object': + $str=''; + foreach (get_object_vars($arg) as $key=>$val) + $str.=($str?',':''). + $this->export($key).'=>'. + $this->stringify($val, + array_merge($stack,[$arg])); + return get_class($arg).'::__set_state(['.$str.'])'; + case 'array': + $str=''; + $num=isset($arg[0]) && + ctype_digit(implode('',array_keys($arg))); + foreach ($arg as $key=>$val) + $str.=($str?',':''). + ($num?'':($this->export($key).'=>')). + $this->stringify($val,array_merge($stack,[$arg])); + return '['.$str.']'; + default: + return $this->export($arg); + } + } + + /** + * Flatten array values and return as CSV string + * @return string + * @param $args array + **/ + function csv(array $args) { + return implode(',',array_map('stripcslashes', + array_map([$this,'stringify'],$args))); + } + + /** + * Convert snakecase string to camelcase + * @return string + * @param $str string + **/ + function camelcase($str) { + return preg_replace_callback( + '/_(\pL)/u', + function($match) { + return strtoupper($match[1]); + }, + $str + ); + } + + /** + * Convert camelcase string to snakecase + * @return string + * @param $str string + **/ + function snakecase($str) { + return strtolower(preg_replace('/(?!^)\p{Lu}/u','_\0',$str)); + } + + /** + * Return -1 if specified number is negative, 0 if zero, + * or 1 if the number is positive + * @return int + * @param $num mixed + **/ + function sign($num) { + return $num?($num/abs($num)):0; + } + + /** + * Extract values of array whose keys start with the given prefix + * @return array + * @param $arr array + * @param $prefix string + **/ + function extract($arr,$prefix) { + $out=[]; + foreach (preg_grep('/^'.preg_quote($prefix,'/').'/',array_keys($arr)) + as $key) + $out[substr($key,strlen($prefix))]=$arr[$key]; + return $out; + } + + /** + * Convert class constants to array + * @return array + * @param $class object|string + * @param $prefix string + **/ + function constants($class,$prefix='') { + $ref=new ReflectionClass($class); + return $this->extract($ref->getconstants(),$prefix); + } + + /** + * Generate 64bit/base36 hash + * @return string + * @param $str + **/ + function hash($str) { + return str_pad(base_convert( + substr(sha1($str),-16),16,36),11,'0',STR_PAD_LEFT); + } + + /** + * Return Base64-encoded equivalent + * @return string + * @param $data string + * @param $mime string + **/ + function base64($data,$mime) { + return 'data:'.$mime.';base64,'.base64_encode($data); + } + + /** + * Convert special characters to HTML entities + * @return string + * @param $str string + **/ + function encode($str) { + return @htmlspecialchars($str,$this->hive['BITMASK'], + $this->hive['ENCODING'])?:$this->scrub($str); + } + + /** + * Convert HTML entities back to characters + * @return string + * @param $str string + **/ + function decode($str) { + return htmlspecialchars_decode($str,$this->hive['BITMASK']); + } + + /** + * Invoke callback recursively for all data types + * @return mixed + * @param $arg mixed + * @param $func callback + * @param $stack array + **/ + function recursive($arg,$func,$stack=[]) { + if ($stack) { + foreach ($stack as $node) + if ($arg===$node) + return $arg; + } + switch (gettype($arg)) { + case 'object': + $ref=new ReflectionClass($arg); + if ($ref->iscloneable()) { + $arg=clone($arg); + $cast=is_a($arg,'IteratorAggregate')? + iterator_to_array($arg):get_object_vars($arg); + foreach ($cast as $key=>$val) + $arg->$key=$this->recursive( + $val,$func,array_merge($stack,[$arg])); + } + return $arg; + case 'array': + $copy=[]; + foreach ($arg as $key=>$val) + $copy[$key]=$this->recursive($val,$func, + array_merge($stack,[$arg])); + return $copy; + } + return $func($arg); + } + + /** + * Remove HTML tags (except those enumerated) and non-printable + * characters to mitigate XSS/code injection attacks + * @return mixed + * @param $arg mixed + * @param $tags string + **/ + function clean($arg,$tags=NULL) { + return $this->recursive($arg, + function($val) use($tags) { + if ($tags!='*') + $val=trim(strip_tags($val, + '<'.implode('><',$this->split($tags)).'>')); + return trim(preg_replace( + '/[\x00-\x08\x0B\x0C\x0E-\x1F]/','',$val)); + } + ); + } + + /** + * Similar to clean(), except that variable is passed by reference + * @return mixed + * @param $var mixed + * @param $tags string + **/ + function scrub(&$var,$tags=NULL) { + return $var=$this->clean($var,$tags); + } + + /** + * Return locale-aware formatted string + * @return string + **/ + function format() { + $args=func_get_args(); + $val=array_shift($args); + // Get formatting rules + $conv=localeconv(); + return preg_replace_callback( + '/\{\s*(?P\d+)\s*(?:,\s*(?P\w+)\s*'. + '(?:,\s*(?P(?:\w+(?:\s*\{.+?\}\s*,?\s*)?)*)'. + '(?:,\s*(?P.+?))?)?)?\s*\}/', + function($expr) use($args,$conv) { + /** + * @var string $pos + * @var string $mod + * @var string $type + * @var string $prop + */ + extract($expr); + /** + * @var string $thousands_sep + * @var string $negative_sign + * @var string $positive_sign + * @var string $frac_digits + * @var string $decimal_point + * @var string $int_curr_symbol + * @var string $currency_symbol + */ + extract($conv); + if (!array_key_exists($pos,$args)) + return $expr[0]; + if (isset($type)) { + if (isset($this->hive['FORMATS'][$type])) + return $this->call( + $this->hive['FORMATS'][$type], + [ + $args[$pos], + isset($mod)?$mod:null, + isset($prop)?$prop:null + ] + ); + switch ($type) { + case 'plural': + preg_match_all('/(?\w+)'. + '(?:\s*\{\s*(?.+?)\s*\})/', + $mod,$matches,PREG_SET_ORDER); + $ord=['zero','one','two']; + foreach ($matches as $match) { + /** @var string $tag */ + /** @var string $data */ + extract($match); + if (isset($ord[$args[$pos]]) && + $tag==$ord[$args[$pos]] || $tag=='other') + return str_replace('#',$args[$pos],$data); + } + case 'number': + if (isset($mod)) + switch ($mod) { + case 'integer': + return number_format( + $args[$pos],0,'',$thousands_sep); + case 'currency': + $int=$cstm=FALSE; + if (isset($prop) && + $cstm=!$int=($prop=='int')) + $currency_symbol=$prop; + if (!$cstm && + function_exists('money_format') && + version_compare(PHP_VERSION,'7.4.0')<0) + return money_format( + '%'.($int?'i':'n'),$args[$pos]); + $fmt=[ + 0=>'(nc)',1=>'(n c)', + 2=>'(nc)',10=>'+nc', + 11=>'+n c',12=>'+ nc', + 20=>'nc+',21=>'n c+', + 22=>'nc +',30=>'n+c', + 31=>'n +c',32=>'n+ c', + 40=>'nc+',41=>'n c+', + 42=>'nc +',100=>'(cn)', + 101=>'(c n)',102=>'(cn)', + 110=>'+cn',111=>'+c n', + 112=>'+ cn',120=>'cn+', + 121=>'c n+',122=>'cn +', + 130=>'+cn',131=>'+c n', + 132=>'+ cn',140=>'c+n', + 141=>'c+ n',142=>'c +n' + ]; + if ($args[$pos]<0) { + $sgn=$negative_sign; + $pre='n'; + } + else { + $sgn=$positive_sign; + $pre='p'; + } + return str_replace( + ['+','n','c'], + [$sgn,number_format( + abs($args[$pos]), + $frac_digits, + $decimal_point, + $thousands_sep), + $int?$int_curr_symbol + :$currency_symbol], + $fmt[(int)( + (${$pre.'_cs_precedes'}%2). + (${$pre.'_sign_posn'}%5). + (${$pre.'_sep_by_space'}%3) + )] + ); + case 'percent': + return number_format( + $args[$pos]*100,0,$decimal_point, + $thousands_sep).'%'; + } + $frac=$args[$pos]-(int)$args[$pos]; + return number_format( + $args[$pos], + isset($prop)? + $prop: + ($frac?strlen($frac)-2:0), + $decimal_point,$thousands_sep); + case 'date': + if (empty($mod) || $mod=='short') + $prop='%x'; + elseif ($mod=='full') + $prop='%A, %d %B %Y'; + elseif ($mod!='custom') + $prop='%d %B %Y'; + return strftime($prop,$args[$pos]); + case 'time': + if (empty($mod) || $mod=='short') + $prop='%X'; + elseif ($mod!='custom') + $prop='%r'; + return strftime($prop,$args[$pos]); + default: + return $expr[0]; + } + } + return $args[$pos]; + }, + $val + ); + } + + /** + * Return string representation of expression + * @return string + * @param $expr mixed + **/ + function export($expr) { + return var_export($expr,TRUE); + } + + /** + * Assign/auto-detect language + * @return string + * @param $code string + **/ + function language($code) { + $code=preg_replace('/\h+|;q=[0-9.]+/','',$code); + $code.=($code?',':'').$this->fallback; + $this->languages=[]; + foreach (array_reverse(explode(',',$code)) as $lang) + if (preg_match('/^(\w{2})(?:-(\w{2}))?\b/i',$lang,$parts)) { + // Generic language + array_unshift($this->languages,$parts[1]); + if (isset($parts[2])) { + // Specific language + $parts[0]=$parts[1].'-'.($parts[2]=strtoupper($parts[2])); + array_unshift($this->languages,$parts[0]); + } + } + $this->languages=array_unique($this->languages); + $locales=[]; + $windows=preg_match('/^win/i',PHP_OS); + // Work around PHP's Turkish locale bug + foreach (preg_grep('/^(?!tr)/i',$this->languages) as $locale) { + if ($windows) { + $parts=explode('-',$locale); + $locale=@constant('ISO::LC_'.$parts[0]); + if (isset($parts[1]) && + $country=@constant('ISO::CC_'.strtolower($parts[1]))) + $locale.='-'.$country; + } + $locale=str_replace('-','_',$locale); + $locales[]=$locale.'.'.ini_get('default_charset'); + $locales[]=$locale; + } + setlocale(LC_ALL,$locales); + return $this->hive['LANGUAGE']=implode(',',$this->languages); + } + + /** + * Return lexicon entries + * @return array + * @param $path string + * @param $ttl int + **/ + function lexicon($path,$ttl=0) { + $languages=$this->languages?:explode(',',$this->fallback); + $cache=Cache::instance(); + if ($ttl && $cache->exists( + $hash=$this->hash(implode(',',$languages).$path).'.dic',$lex)) + return $lex; + $lex=[]; + foreach ($languages as $lang) + foreach ($this->split($path) as $dir) + if ((is_file($file=($base=$dir.$lang).'.php') || + is_file($file=$base.'.php')) && + is_array($dict=require($file))) + $lex+=$dict; + elseif (is_file($file=$base.'.json') && + is_array($dict=json_decode(file_get_contents($file), true))) + $lex+=$dict; + elseif (is_file($file=$base.'.ini')) { + preg_match_all( + '/(?<=^|\n)(?:'. + '\[(?.+?)\]|'. + '(?[^\h\r\n;].*?)\h*=\h*'. + '(?(?:\\\\\h*\r?\n|.+?)*)'. + ')(?=\r?\n|$)/', + $this->read($file),$matches,PREG_SET_ORDER); + if ($matches) { + $prefix=''; + foreach ($matches as $match) + if ($match['prefix']) + $prefix=$match['prefix'].'.'; + elseif (!array_key_exists( + $key=$prefix.$match['lval'],$lex)) + $lex[$key]=trim(preg_replace( + '/\\\\\h*\r?\n/',"\n",$match['rval'])); + } + } + if ($ttl) + $cache->set($hash,$lex,$ttl); + return $lex; + } + + /** + * Return string representation of PHP value + * @return string + * @param $arg mixed + **/ + function serialize($arg) { + switch (strtolower($this->hive['SERIALIZER'])) { + case 'igbinary': + return igbinary_serialize($arg); + default: + return serialize($arg); + } + } + + /** + * Return PHP value derived from string + * @return string + * @param $arg mixed + **/ + function unserialize($arg) { + switch (strtolower($this->hive['SERIALIZER'])) { + case 'igbinary': + return igbinary_unserialize($arg); + default: + return unserialize($arg); + } + } + + /** + * Send HTTP status header; Return text equivalent of status code + * @return string + * @param $code int + **/ + function status($code) { + $reason=@constant('self::HTTP_'.$code); + if (!$this->hive['CLI'] && !headers_sent()) + header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason); + return $reason; + } + + /** + * Send cache metadata to HTTP client + * @param $secs int + **/ + function expire($secs=0) { + if (!$this->hive['CLI'] && !headers_sent()) { + $secs=(int)$secs; + if ($this->hive['PACKAGE']) + header('X-Powered-By: '.$this->hive['PACKAGE']); + if ($this->hive['XFRAME']) + header('X-Frame-Options: '.$this->hive['XFRAME']); + header('X-XSS-Protection: 1; mode=block'); + header('X-Content-Type-Options: nosniff'); + if ($this->hive['VERB']=='GET' && $secs) { + $time=microtime(TRUE); + header_remove('Pragma'); + header('Cache-Control: max-age='.$secs); + header('Expires: '.gmdate('r',$time+$secs)); + header('Last-Modified: '.gmdate('r')); + } + else { + header('Pragma: no-cache'); + header('Cache-Control: no-cache, no-store, must-revalidate'); + header('Expires: '.gmdate('r',0)); + } + } + } + + /** + * Return HTTP user agent + * @return string + **/ + function agent() { + $headers=$this->hive['HEADERS']; + return isset($headers['X-Operamini-Phone-UA'])? + $headers['X-Operamini-Phone-UA']: + (isset($headers['X-Skyfire-Phone'])? + $headers['X-Skyfire-Phone']: + (isset($headers['User-Agent'])? + $headers['User-Agent']:'')); + } + + /** + * Return TRUE if XMLHttpRequest detected + * @return bool + **/ + function ajax() { + $headers=$this->hive['HEADERS']; + return isset($headers['X-Requested-With']) && + $headers['X-Requested-With']=='XMLHttpRequest'; + } + + /** + * Sniff IP address + * @return string + **/ + function ip() { + $headers=$this->hive['HEADERS']; + return isset($headers['Client-IP'])? + $headers['Client-IP']: + (isset($headers['X-Forwarded-For'])? + explode(',',$headers['X-Forwarded-For'])[0]: + (isset($_SERVER['REMOTE_ADDR'])? + $_SERVER['REMOTE_ADDR']:'')); + } + + /** + * Return filtered stack trace as a formatted string (or array) + * @return string|array + * @param $trace array|NULL + * @param $format bool + **/ + function trace(array $trace=NULL,$format=TRUE) { + if (!$trace) { + $trace=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $frame=$trace[0]; + if (isset($frame['file']) && $frame['file']==__FILE__) + array_shift($trace); + } + $debug=$this->hive['DEBUG']; + $trace=array_filter( + $trace, + function($frame) use($debug) { + return isset($frame['file']) && + ($debug>1 || + ($frame['file']!=__FILE__ || $debug) && + (empty($frame['function']) || + !preg_match('/^(?:(?:trigger|user)_error|'. + '__call|call_user_func)/',$frame['function']))); + } + ); + if (!$format) + return $trace; + $out=''; + $eol="\n"; + // Analyze stack trace + foreach ($trace as $frame) { + $line=''; + if (isset($frame['class'])) + $line.=$frame['class'].$frame['type']; + if (isset($frame['function'])) + $line.=$frame['function'].'('. + ($debug>2 && isset($frame['args'])? + $this->csv($frame['args']):'').')'; + $src=$this->fixslashes(str_replace($_SERVER['DOCUMENT_ROOT']. + '/','',$frame['file'])).':'.$frame['line']; + $out.='['.$src.'] '.$line.$eol; + } + return $out; + } + + /** + * Log error; Execute ONERROR handler if defined, else display + * default error page (HTML for synchronous requests, JSON string + * for AJAX requests) + * @param $code int + * @param $text string + * @param $trace array + * @param $level int + **/ + function error($code,$text='',array $trace=NULL,$level=0) { + $prior=$this->hive['ERROR']; + $header=$this->status($code); + $req=$this->hive['VERB'].' '.$this->hive['PATH']; + if ($this->hive['QUERY']) + $req.='?'.$this->hive['QUERY']; + if (!$text) + $text='HTTP '.$code.' ('.$req.')'; + $trace=$this->trace($trace); + $loggable=$this->hive['LOGGABLE']; + if (!is_array($loggable)) + $loggable=$this->split($loggable); + foreach ($loggable as $status) + if ($status=='*' || + preg_match('/^'.preg_replace('/\D/','\d',$status).'$/',$code)) { + error_log($text); + foreach (explode("\n",$trace) as $nexus) + if ($nexus) + error_log($nexus); + break; + } + if ($highlight=!$this->hive['CLI'] && !$this->hive['AJAX'] && + $this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS)) + $trace=$this->highlight($trace); + $this->hive['ERROR']=[ + 'status'=>$header, + 'code'=>$code, + 'text'=>$text, + 'trace'=>$trace, + 'level'=>$level + ]; + $this->expire(-1); + $handler=$this->hive['ONERROR']; + $this->hive['ONERROR']=NULL; + $eol="\n"; + if ((!$handler || + $this->call($handler,[$this,$this->hive['PARAMS']], + 'beforeroute,afterroute')===FALSE) && + !$prior && !$this->hive['CLI'] && !$this->hive['QUIET']) + echo $this->hive['AJAX']? + json_encode( + array_diff_key( + $this->hive['ERROR'], + $this->hive['DEBUG']? + []: + ['trace'=>1] + ) + ): + (''.$eol. + ''.$eol. + ''. + ''.$code.' '.$header.''. + ($highlight? + (''):''). + ''.$eol. + ''.$eol. + '

'.$header.'

'.$eol. + '

'.$this->encode($text?:$req).'

'.$eol. + ($this->hive['DEBUG']?('
'.$trace.'
'.$eol):''). + ''.$eol. + ''); + if ($this->hive['HALT']) + die(1); + } + + /** + * Mock HTTP request + * @return mixed + * @param $pattern string + * @param $args array + * @param $headers array + * @param $body string + **/ + function mock($pattern, + array $args=NULL,array $headers=NULL,$body=NULL) { + if (!$args) + $args=[]; + $types=['sync','ajax','cli']; + preg_match('/([\|\w]+)\h+(?:@(\w+)(?:(\(.+?)\))*|([^\h]+))'. + '(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts); + $verb=strtoupper($parts[1]); + if ($parts[2]) { + if (empty($this->hive['ALIASES'][$parts[2]])) + user_error(sprintf(self::E_Named,$parts[2]),E_USER_ERROR); + $parts[4]=$this->hive['ALIASES'][$parts[2]]; + $parts[4]=$this->build($parts[4], + isset($parts[3])?$this->parse($parts[3]):[]); + } + if (empty($parts[4])) + user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR); + $url=parse_url($parts[4]); + parse_str(@$url['query'],$GLOBALS['_GET']); + if (preg_match('/GET|HEAD/',$verb)) + $GLOBALS['_GET']=array_merge($GLOBALS['_GET'],$args); + $GLOBALS['_POST']=$verb=='POST'?$args:[]; + $GLOBALS['_REQUEST']=array_merge($GLOBALS['_GET'],$GLOBALS['_POST']); + foreach ($headers?:[] as $key=>$val) + $_SERVER['HTTP_'.strtr(strtoupper($key),'-','_')]=$val; + $this->hive['VERB']=$verb; + $this->hive['PATH']=$url['path']; + $this->hive['URI']=$this->hive['BASE'].$url['path']; + if ($GLOBALS['_GET']) + $this->hive['URI'].='?'.http_build_query($GLOBALS['_GET']); + $this->hive['BODY']=''; + if (!preg_match('/GET|HEAD/',$verb)) + $this->hive['BODY']=$body?:http_build_query($args); + $this->hive['AJAX']=isset($parts[5]) && + preg_match('/ajax/i',$parts[5]); + $this->hive['CLI']=isset($parts[5]) && + preg_match('/cli/i',$parts[5]); + return $this->run(); + } + + /** + * Assemble url from alias name + * @return string + * @param $name string + * @param $params array|string + * @param $query string|array + * @param $fragment string + **/ + function alias($name,$params=[],$query=NULL,$fragment=NULL) { + if (!is_array($params)) + $params=$this->parse($params); + if (empty($this->hive['ALIASES'][$name])) + user_error(sprintf(self::E_Named,$name),E_USER_ERROR); + $url=$this->build($this->hive['ALIASES'][$name],$params); + if (is_array($query)) + $query=http_build_query($query); + return $url.($query?('?'.$query):'').($fragment?'#'.$fragment:''); + } + + /** + * Bind handler to route pattern + * @return NULL + * @param $pattern string|array + * @param $handler callback + * @param $ttl int + * @param $kbps int + **/ + function route($pattern,$handler,$ttl=0,$kbps=0) { + $types=['sync','ajax','cli']; + $alias=null; + if (is_array($pattern)) { + foreach ($pattern as $item) + $this->route($item,$handler,$ttl,$kbps); + return; + } + preg_match('/([\|\w]+)\h+(?:(?:@?(.+?)\h*:\h*)?(@(\w+)|[^\h]+))'. + '(?:\h+\[('.implode('|',$types).')\])?/u',$pattern,$parts); + if (isset($parts[2]) && $parts[2]) { + if (!preg_match('/^\w+$/',$parts[2])) + user_error(sprintf(self::E_Alias,$parts[2]),E_USER_ERROR); + $this->hive['ALIASES'][$alias=$parts[2]]=$parts[3]; + } + elseif (!empty($parts[4])) { + if (empty($this->hive['ALIASES'][$parts[4]])) + user_error(sprintf(self::E_Named,$parts[4]),E_USER_ERROR); + $parts[3]=$this->hive['ALIASES'][$alias=$parts[4]]; + } + if (empty($parts[3])) + user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR); + $type=empty($parts[5])?0:constant('self::REQ_'.strtoupper($parts[5])); + foreach ($this->split($parts[1]) as $verb) { + if (!preg_match('/'.self::VERBS.'/',$verb)) + $this->error(501,$verb.' '.$this->hive['URI']); + $this->hive['ROUTES'][$parts[3]][$type][strtoupper($verb)]= + [$handler,$ttl,$kbps,$alias]; + } + } + + /** + * Reroute to specified URI + * @return NULL + * @param $url array|string + * @param $permanent bool + * @param $die bool + **/ + function reroute($url=NULL,$permanent=FALSE,$die=TRUE) { + if (!$url) + $url=$this->hive['REALM']; + if (is_array($url)) + $url=call_user_func_array([$this,'alias'],$url); + elseif (preg_match('/^(?:@([^\/()?#]+)(?:\((.+?)\))*(\?[^#]+)*(#.+)*)/', + $url,$parts) && isset($this->hive['ALIASES'][$parts[1]])) + $url=$this->build($this->hive['ALIASES'][$parts[1]], + isset($parts[2])?$this->parse($parts[2]):[]). + (isset($parts[3])?$parts[3]:'').(isset($parts[4])?$parts[4]:''); + else + $url=$this->build($url); + if (($handler=$this->hive['ONREROUTE']) && + $this->call($handler,[$url,$permanent,$die])!==FALSE) + return; + if ($url[0]!='/' && !preg_match('/^\w+:\/\//i',$url)) + $url='/'.$url; + if ($url[0]=='/' && (empty($url[1]) || $url[1]!='/')) { + $port=$this->hive['PORT']; + $port=in_array($port,[80,443])?'':(':'.$port); + $url=$this->hive['SCHEME'].'://'. + $this->hive['HOST'].$port.$this->hive['BASE'].$url; + } + if ($this->hive['CLI']) + $this->mock('GET '.$url.' [cli]'); + else { + header('Location: '.$url); + $this->status($permanent?301:302); + if ($die) + die; + } + } + + /** + * Provide ReST interface by mapping HTTP verb to class method + * @return NULL + * @param $url string + * @param $class string|object + * @param $ttl int + * @param $kbps int + **/ + function map($url,$class,$ttl=0,$kbps=0) { + if (is_array($url)) { + foreach ($url as $item) + $this->map($item,$class,$ttl,$kbps); + return; + } + foreach (explode('|',self::VERBS) as $method) + $this->route($method.' '.$url,is_string($class)? + $class.'->'.$this->hive['PREMAP'].strtolower($method): + [$class,$this->hive['PREMAP'].strtolower($method)], + $ttl,$kbps); + } + + /** + * Redirect a route to another URL + * @return NULL + * @param $pattern string|array + * @param $url string + * @param $permanent bool + */ + function redirect($pattern,$url,$permanent=TRUE) { + if (is_array($pattern)) { + foreach ($pattern as $item) + $this->redirect($item,$url,$permanent); + return; + } + $this->route($pattern,function($fw) use($url,$permanent) { + $fw->reroute($url,$permanent); + }); + } + + /** + * Return TRUE if IPv4 address exists in DNSBL + * @return bool + * @param $ip string + **/ + function blacklisted($ip) { + if ($this->hive['DNSBL'] && + !in_array($ip, + is_array($this->hive['EXEMPT'])? + $this->hive['EXEMPT']: + $this->split($this->hive['EXEMPT']))) { + // Reverse IPv4 dotted quad + $rev=implode('.',array_reverse(explode('.',$ip))); + foreach (is_array($this->hive['DNSBL'])? + $this->hive['DNSBL']: + $this->split($this->hive['DNSBL']) as $server) + // DNSBL lookup + if (checkdnsrr($rev.'.'.$server,'A')) + return TRUE; + } + return FALSE; + } + + /** + * Applies the specified URL mask and returns parameterized matches + * @return $args array + * @param $pattern string + * @param $url string|NULL + **/ + function mask($pattern,$url=NULL) { + if (!$url) + $url=$this->rel($this->hive['URI']); + $case=$this->hive['CASELESS']?'i':''; + $wild=preg_quote($pattern,'/'); + $i=0; + while (is_int($pos=strpos($wild,'\*'))) { + $wild=substr_replace($wild,'(?P<_'.$i.'>[^\?]*)',$pos,2); + $i++; + } + preg_match('/^'. + preg_replace( + '/((\\\{)?@(\w+\b)(?(2)\\\}))/', + '(?P<\3>[^\/\?]+)', + $wild).'\/?$/'.$case.'um',$url,$args); + foreach (array_keys($args) as $key) { + if (preg_match('/^_\d+$/',$key)) { + if (empty($args['*'])) + $args['*']=$args[$key]; + else { + if (is_string($args['*'])) + $args['*']=[$args['*']]; + array_push($args['*'],$args[$key]); + } + unset($args[$key]); + } + elseif (is_numeric($key) && $key) + unset($args[$key]); + } + return $args; + } + + /** + * Match routes against incoming URI + * @return mixed + **/ + function run() { + if ($this->blacklisted($this->hive['IP'])) + // Spammer detected + $this->error(403); + if (!$this->hive['ROUTES']) + // No routes defined + user_error(self::E_Routes,E_USER_ERROR); + // Match specific routes first + $paths=[]; + foreach ($keys=array_keys($this->hive['ROUTES']) as $key) { + $path=preg_replace('/@\w+/','*@',$key); + if (substr($path,-1)!='*') + $path.='+'; + $paths[]=$path; + } + $vals=array_values($this->hive['ROUTES']); + array_multisort($paths,SORT_DESC,$keys,$vals); + $this->hive['ROUTES']=array_combine($keys,$vals); + // Convert to BASE-relative URL + $req=urldecode($this->hive['PATH']); + $preflight=FALSE; + if ($cors=(isset($this->hive['HEADERS']['Origin']) && + $this->hive['CORS']['origin'])) { + $cors=$this->hive['CORS']; + header('Access-Control-Allow-Origin: '.$cors['origin']); + header('Access-Control-Allow-Credentials: '. + $this->export($cors['credentials'])); + $preflight= + isset($this->hive['HEADERS']['Access-Control-Request-Method']); + } + $allowed=[]; + foreach ($this->hive['ROUTES'] as $pattern=>$routes) { + if (!$args=$this->mask($pattern,$req)) + continue; + ksort($args); + $route=NULL; + $ptr=$this->hive['CLI']?self::REQ_CLI:$this->hive['AJAX']+1; + if (isset($routes[$ptr][$this->hive['VERB']]) || + isset($routes[$ptr=0])) + $route=$routes[$ptr]; + if (!$route) + continue; + if (isset($route[$this->hive['VERB']]) && !$preflight) { + if ($this->hive['VERB']=='GET' && + preg_match('/.+\/$/',$this->hive['PATH'])) + $this->reroute(substr($this->hive['PATH'],0,-1). + ($this->hive['QUERY']?('?'.$this->hive['QUERY']):'')); + list($handler,$ttl,$kbps,$alias)=$route[$this->hive['VERB']]; + // Capture values of route pattern tokens + $this->hive['PARAMS']=$args; + // Save matching route + $this->hive['ALIAS']=$alias; + $this->hive['PATTERN']=$pattern; + if ($cors && $cors['expose']) + header('Access-Control-Expose-Headers: '. + (is_array($cors['expose'])? + implode(',',$cors['expose']):$cors['expose'])); + if (is_string($handler)) { + // Replace route pattern tokens in handler if any + $handler=preg_replace_callback('/({)?@(\w+\b)(?(1)})/', + function($id) use($args) { + $pid=count($id)>2?2:1; + return isset($args[$id[$pid]])? + $args[$id[$pid]]: + $id[0]; + }, + $handler + ); + if (preg_match('/(.+)\h*(?:->|::)/',$handler,$match) && + !class_exists($match[1])) + $this->error(404); + } + // Process request + $result=NULL; + $body=''; + $now=microtime(TRUE); + if (preg_match('/GET|HEAD/',$this->hive['VERB']) && $ttl) { + // Only GET and HEAD requests are cacheable + $headers=$this->hive['HEADERS']; + $cache=Cache::instance(); + $cached=$cache->exists( + $hash=$this->hash($this->hive['VERB'].' '. + $this->hive['URI']).'.url',$data); + if ($cached) { + if (isset($headers['If-Modified-Since']) && + strtotime($headers['If-Modified-Since'])+ + $ttl>$now) { + $this->status(304); + die; + } + // Retrieve from cache backend + list($headers,$body,$result)=$data; + if (!$this->hive['CLI']) + array_walk($headers,'header'); + $this->expire($cached[0]+$ttl-$now); + } + else + // Expire HTTP client-cached page + $this->expire($ttl); + } + else + $this->expire(0); + if (!strlen($body)) { + if (!$this->hive['RAW'] && !$this->hive['BODY']) + $this->hive['BODY']=file_get_contents('php://input'); + ob_start(); + // Call route handler + $result=$this->call($handler,[$this,$args,$handler], + 'beforeroute,afterroute'); + $body=ob_get_clean(); + if (isset($cache) && !error_get_last()) { + // Save to cache backend + $cache->set($hash,[ + // Remove cookies + preg_grep('/Set-Cookie\:/',headers_list(), + PREG_GREP_INVERT),$body,$result],$ttl); + } + } + $this->hive['RESPONSE']=$body; + if (!$this->hive['QUIET']) { + if ($kbps) { + $ctr=0; + foreach (str_split($body,1024) as $part) { + // Throttle output + $ctr++; + if ($ctr/$kbps>($elapsed=microtime(TRUE)-$now) && + !connection_aborted()) + usleep(1e6*($ctr/$kbps-$elapsed)); + echo $part; + } + } + else + echo $body; + } + if ($result || $this->hive['VERB']!='OPTIONS') + return $result; + } + $allowed=array_merge($allowed,array_keys($route)); + } + if (!$allowed) + // URL doesn't match any route + $this->error(404); + elseif (!$this->hive['CLI']) { + if (!preg_grep('/Allow:/',$headers_send=headers_list())) + // Unhandled HTTP method + header('Allow: '.implode(',',array_unique($allowed))); + if ($cors) { + if (!preg_grep('/Access-Control-Allow-Methods:/',$headers_send)) + header('Access-Control-Allow-Methods: OPTIONS,'. + implode(',',$allowed)); + if ($cors['headers'] && + !preg_grep('/Access-Control-Allow-Headers:/',$headers_send)) + header('Access-Control-Allow-Headers: '. + (is_array($cors['headers'])? + implode(',',$cors['headers']): + $cors['headers'])); + if ($cors['ttl']>0) + header('Access-Control-Max-Age: '.$cors['ttl']); + } + if ($this->hive['VERB']!='OPTIONS') + $this->error(405); + } + return FALSE; + } + + /** + * Loop until callback returns TRUE (for long polling) + * @return mixed + * @param $func callback + * @param $args array + * @param $timeout int + **/ + function until($func,$args=NULL,$timeout=60) { + if (!$args) + $args=[]; + $time=time(); + $max=ini_get('max_execution_time'); + $limit=max(0,($max?min($timeout,$max):$timeout)-1); + $out=''; + // Turn output buffering on + ob_start(); + // Not for the weak of heart + while ( + // No error occurred + !$this->hive['ERROR'] && + // Got time left? + time()-$time+1<$limit && + // Still alive? + !connection_aborted() && + // Restart session + !headers_sent() && + (session_status()==PHP_SESSION_ACTIVE || session_start()) && + // CAUTION: Callback will kill host if it never becomes truthy! + !$out=$this->call($func,$args)) { + if (!$this->hive['CLI']) + session_commit(); + // Hush down + sleep(1); + } + ob_flush(); + flush(); + return $out; + } + + /** + * Disconnect HTTP client; + * Set FcgidOutputBufferSize to zero if server uses mod_fcgid; + * Disable mod_deflate when rendering text/html output + **/ + function abort() { + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); + $out=''; + while (ob_get_level()) + $out=ob_get_clean().$out; + if (!headers_sent()) { + header('Content-Length: '.strlen($out)); + header('Connection: close'); + } + session_commit(); + echo $out; + flush(); + if (function_exists('fastcgi_finish_request')) + fastcgi_finish_request(); + } + + /** + * Grab the real route handler behind the string expression + * @return string|array + * @param $func string + * @param $args array + **/ + function grab($func,$args=NULL) { + if (preg_match('/(.+)\h*(->|::)\h*(.+)/s',$func,$parts)) { + // Convert string to executable PHP callback + if (!class_exists($parts[1])) + user_error(sprintf(self::E_Class,$parts[1]),E_USER_ERROR); + if ($parts[2]=='->') { + if (is_subclass_of($parts[1],'Prefab')) + $parts[1]=call_user_func($parts[1].'::instance'); + elseif (isset($this->hive['CONTAINER'])) { + $container=$this->hive['CONTAINER']; + if (is_object($container) && is_callable([$container,'has']) + && $container->has($parts[1])) // PSR11 + $parts[1]=call_user_func([$container,'get'],$parts[1]); + elseif (is_callable($container)) + $parts[1]=call_user_func($container,$parts[1],$args); + elseif (is_string($container) && + is_subclass_of($container,'Prefab')) + $parts[1]=call_user_func($container.'::instance')-> + get($parts[1]); + else + user_error(sprintf(self::E_Class, + $this->stringify($parts[1])), + E_USER_ERROR); + } + else { + $ref=new ReflectionClass($parts[1]); + $parts[1]=method_exists($parts[1],'__construct') && $args? + $ref->newinstanceargs($args): + $ref->newinstance(); + } + } + $func=[$parts[1],$parts[3]]; + } + return $func; + } + + /** + * Execute callback/hooks (supports 'class->method' format) + * @return mixed|FALSE + * @param $func callback + * @param $args mixed + * @param $hooks string + **/ + function call($func,$args=NULL,$hooks='') { + if (!is_array($args)) + $args=[$args]; + // Grab the real handler behind the string representation + if (is_string($func)) + $func=$this->grab($func,$args); + // Execute function; abort if callback/hook returns FALSE + if (!is_callable($func)) + // No route handler + if ($hooks=='beforeroute,afterroute') { + $allowed=[]; + if (is_array($func)) + $allowed=array_intersect( + array_map('strtoupper',get_class_methods($func[0])), + explode('|',self::VERBS) + ); + header('Allow: '.implode(',',$allowed)); + $this->error(405); + } + else + user_error(sprintf(self::E_Method, + is_string($func)?$func:$this->stringify($func)), + E_USER_ERROR); + $obj=FALSE; + if (is_array($func)) { + $hooks=$this->split($hooks); + $obj=TRUE; + } + // Execute pre-route hook if any + if ($obj && $hooks && in_array($hook='beforeroute',$hooks) && + method_exists($func[0],$hook) && + call_user_func_array([$func[0],$hook],$args)===FALSE) + return FALSE; + // Execute callback + $out=call_user_func_array($func,$args?:[]); + if ($out===FALSE) + return FALSE; + // Execute post-route hook if any + if ($obj && $hooks && in_array($hook='afterroute',$hooks) && + method_exists($func[0],$hook) && + call_user_func_array([$func[0],$hook],$args)===FALSE) + return FALSE; + return $out; + } + + /** + * Execute specified callbacks in succession; Apply same arguments + * to all callbacks + * @return array + * @param $funcs array|string + * @param $args mixed + **/ + function chain($funcs,$args=NULL) { + $out=[]; + foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func) + $out[]=$this->call($func,$args); + return $out; + } + + /** + * Execute specified callbacks in succession; Relay result of + * previous callback as argument to the next callback + * @return array + * @param $funcs array|string + * @param $args mixed + **/ + function relay($funcs,$args=NULL) { + foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func) + $args=[$this->call($func,$args)]; + return array_shift($args); + } + + /** + * Configure framework according to .ini-style file settings; + * If optional 2nd arg is provided, template strings are interpreted + * @return object + * @param $source string|array + * @param $allow bool + **/ + function config($source,$allow=FALSE) { + if (is_string($source)) + $source=$this->split($source); + if ($allow) + $preview=Preview::instance(); + foreach ($source as $file) { + preg_match_all( + '/(?<=^|\n)(?:'. + '\[(?
.+?)\]|'. + '(?[^\h\r\n;].*?)\h*=\h*'. + '(?(?:\\\\\h*\r?\n|.+?)*)'. + ')(?=\r?\n|$)/', + $this->read($file), + $matches,PREG_SET_ORDER); + if ($matches) { + $sec='globals'; + $cmd=[]; + foreach ($matches as $match) { + if ($match['section']) { + $sec=$match['section']; + if (preg_match( + '/^(?!(?:global|config|route|map|redirect)s\b)'. + '(.*?)(?:\s*[:>])/i',$sec,$msec) && + !$this->exists($msec[1])) + $this->set($msec[1],NULL); + preg_match('/^(config|route|map|redirect)s\b|'. + '^(.+?)\s*\>\s*(.*)/i',$sec,$cmd); + continue; + } + if ($allow) + foreach (['lval','rval'] as $ndx) + $match[$ndx]=$preview-> + resolve($match[$ndx],NULL,0,FALSE,FALSE); + if (!empty($cmd)) { + isset($cmd[3])? + $this->call($cmd[3], + [$match['lval'],$match['rval'],$cmd[2]]): + call_user_func_array( + [$this,$cmd[1]], + array_merge([$match['lval']], + str_getcsv($cmd[1]=='config'? + $this->cast($match['rval']): + $match['rval'])) + ); + } + else { + $rval=preg_replace( + '/\\\\\h*(\r?\n)/','\1',$match['rval']); + $ttl=NULL; + if (preg_match('/^(.+)\|\h*(\d+)$/',$rval,$tmp)) { + array_shift($tmp); + list($rval,$ttl)=$tmp; + } + $args=array_map( + function($val) { + $val=$this->cast($val); + if (is_string($val)) + $val=strlen($val)? + preg_replace('/\\\\"/','"',$val): + NULL; + return $val; + }, + // Mark quoted strings with 0x00 whitespace + str_getcsv(preg_replace( + '/(?[^:]+)(?:\:(?.+))?/', + $sec,$parts); + $func=isset($parts['func'])?$parts['func']:NULL; + $custom=(strtolower($parts['section'])!='globals'); + if ($func) + $args=[$this->call($func,$args)]; + if (count($args)>1) + $args=[$args]; + if (isset($ttl)) + $args=array_merge($args,[$ttl]); + call_user_func_array( + [$this,'set'], + array_merge( + [ + ($custom?($parts['section'].'.'):''). + $match['lval'] + ], + $args + ) + ); + } + } + } + } + return $this; + } + + /** + * Create mutex, invoke callback then drop ownership when done + * @return mixed + * @param $id string + * @param $func callback + * @param $args mixed + **/ + function mutex($id,$func,$args=NULL) { + if (!is_dir($tmp=$this->hive['TEMP'])) + mkdir($tmp,self::MODE,TRUE); + // Use filesystem lock + if (is_file($lock=$tmp. + $this->hive['SEED'].'.'.$this->hash($id).'.lock') && + filemtime($lock)+ini_get('max_execution_time')locks[$id]=$lock; + $out=$this->call($func,$args); + fclose($handle); + @unlink($lock); + unset($this->locks[$id]); + return $out; + } + + /** + * Read file (with option to apply Unix LF as standard line ending) + * @return string + * @param $file string + * @param $lf bool + **/ + function read($file,$lf=FALSE) { + $out=@file_get_contents($file); + return $lf?preg_replace('/\r\n|\r/',"\n",$out):$out; + } + + /** + * Exclusive file write + * @return int|FALSE + * @param $file string + * @param $data mixed + * @param $append bool + **/ + function write($file,$data,$append=FALSE) { + return file_put_contents($file,$data,$this->hive['LOCK']|($append?FILE_APPEND:0)); + } + + /** + * Apply syntax highlighting + * @return string + * @param $text string + **/ + function highlight($text) { + $out=''; + $pre=FALSE; + $text=trim($text); + if ($text && !preg_match('/^<\?php/',$text)) { + $text=''. + $this->encode($token[1]).''): + ('>'.$this->encode($token))). + ''; + return $out?(''.$out.''):$text; + } + + /** + * Dump expression with syntax highlighting + * @param $expr mixed + **/ + function dump($expr) { + echo $this->highlight($this->stringify($expr)); + } + + /** + * Return path (and query parameters) relative to the base directory + * @return string + * @param $url string + **/ + function rel($url) { + return preg_replace('/^(?:https?:\/\/)?'. + preg_quote($this->hive['BASE'],'/').'(\/.*|$)/','\1',$url); + } + + /** + * Namespace-aware class autoloader + * @return mixed + * @param $class string + **/ + protected function autoload($class) { + $class=$this->fixslashes(ltrim($class,'\\')); + /** @var callable $func */ + $func=NULL; + if (is_array($path=$this->hive['AUTOLOAD']) && + isset($path[1]) && is_callable($path[1])) + list($path,$func)=$path; + foreach ($this->split($this->hive['PLUGINS'].';'.$path) as $auto) + if ($func && is_file($file=$func($auto.$class).'.php') || + is_file($file=$auto.$class.'.php') || + is_file($file=$auto.strtolower($class).'.php') || + is_file($file=strtolower($auto.$class).'.php')) + return require($file); + } + + /** + * Execute framework/application shutdown sequence + * @param $cwd string + **/ + function unload($cwd) { + chdir($cwd); + if (!($error=error_get_last()) && + session_status()==PHP_SESSION_ACTIVE) + session_commit(); + foreach ($this->locks as $lock) + @unlink($lock); + $handler=$this->hive['UNLOAD']; + if ((!$handler || $this->call($handler,$this)===FALSE) && + $error && in_array($error['type'], + [E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR])) + // Fatal error detected + $this->error(500, + sprintf(self::E_Fatal,$error['message']),[$error]); + } + + /** + * Convenience method for checking hive key + * @return mixed + * @param $key string + **/ + function offsetexists($key) { + return $this->exists($key); + } + + /** + * Convenience method for assigning hive value + * @return mixed + * @param $key string + * @param $val mixed + **/ + function offsetset($key,$val) { + return $this->set($key,$val); + } + + /** + * Convenience method for retrieving hive value + * @return mixed + * @param $key string + **/ + function &offsetget($key) { + $val=&$this->ref($key); + return $val; + } + + /** + * Convenience method for removing hive key + * @param $key string + **/ + function offsetunset($key) { + $this->clear($key); + } + + /** + * Alias for offsetexists() + * @return mixed + * @param $key string + **/ + function __isset($key) { + return $this->offsetexists($key); + } + + /** + * Alias for offsetset() + * @return mixed + * @param $key string + * @param $val mixed + **/ + function __set($key,$val) { + return $this->offsetset($key,$val); + } + + /** + * Alias for offsetget() + * @return mixed + * @param $key string + **/ + function &__get($key) { + $val=&$this->offsetget($key); + return $val; + } + + /** + * Alias for offsetunset() + * @param $key string + **/ + function __unset($key) { + $this->offsetunset($key); + } + + /** + * Call function identified by hive key + * @return mixed + * @param $key string + * @param $args array + **/ + function __call($key,array $args) { + if ($this->exists($key,$val)) + return call_user_func_array($val,$args); + user_error(sprintf(self::E_Method,$key),E_USER_ERROR); + } + + //! Prohibit cloning + private function __clone() { + } + + //! Bootstrap + function __construct() { + // Managed directives + ini_set('default_charset',$charset='UTF-8'); + if (extension_loaded('mbstring')) + mb_internal_encoding($charset); + ini_set('display_errors',0); + // Deprecated directives + @ini_set('magic_quotes_gpc',0); + @ini_set('register_globals',0); + // Intercept errors/exceptions; PHP5.3-compatible + $check=error_reporting((E_ALL|E_STRICT)&~(E_NOTICE|E_USER_NOTICE)); + set_exception_handler( + function($obj) { + /** @var Exception $obj */ + $this->hive['EXCEPTION']=$obj; + $this->error(500, + $obj->getmessage().' '. + '['.$obj->getFile().':'.$obj->getLine().']', + $obj->gettrace()); + } + ); + set_error_handler( + function($level,$text,$file,$line) { + if ($level & error_reporting()) + $this->error(500,$text,NULL,$level); + } + ); + if (!isset($_SERVER['SERVER_NAME']) || $_SERVER['SERVER_NAME']==='') + $_SERVER['SERVER_NAME']=gethostname(); + $headers=[]; + if ($cli=PHP_SAPI=='cli') { + // Emulate HTTP request + $_SERVER['REQUEST_METHOD']='GET'; + if (!isset($_SERVER['argv'][1])) { + $_SERVER['argc']++; + $_SERVER['argv'][1]='/'; + } + $req=$query=''; + if (substr($_SERVER['argv'][1],0,1)=='/') { + $req=$_SERVER['argv'][1]; + $query=parse_url($req,PHP_URL_QUERY); + } else { + foreach($_SERVER['argv'] as $i=>$arg) { + if (!$i) continue; + if (preg_match('/^\-(\-)?(\w+)(?:\=(.*))?$/',$arg,$m)) { + foreach($m[1]?[$m[2]]:str_split($m[2]) as $k) + $query.=($query?'&':'').urlencode($k).'='; + if (isset($m[3])) + $query.=urlencode($m[3]); + } else + $req.='/'.$arg; + } + if (!$req) + $req='/'; + if ($query) + $req.='?'.$query; + } + $_SERVER['REQUEST_URI']=$req; + parse_str($query,$GLOBALS['_GET']); + } + elseif (function_exists('getallheaders')) { + foreach (getallheaders() as $key=>$val) { + $tmp=strtoupper(strtr($key,'-','_')); + // TODO: use ucwords delimiters for php 5.4.32+ & 5.5.16+ + $key=strtr(ucwords(strtolower(strtr($key,'-',' '))),' ','-'); + $headers[$key]=$val; + if (isset($_SERVER['HTTP_'.$tmp])) + $headers[$key]=&$_SERVER['HTTP_'.$tmp]; + } + } + else { + if (isset($_SERVER['CONTENT_LENGTH'])) + $headers['Content-Length']=&$_SERVER['CONTENT_LENGTH']; + if (isset($_SERVER['CONTENT_TYPE'])) + $headers['Content-Type']=&$_SERVER['CONTENT_TYPE']; + foreach (array_keys($_SERVER) as $key) + if (substr($key,0,5)=='HTTP_') + $headers[strtr(ucwords(strtolower(strtr( + substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key]; + } + if (isset($headers['X-HTTP-Method-Override'])) + $_SERVER['REQUEST_METHOD']=$headers['X-HTTP-Method-Override']; + elseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method'])) + $_SERVER['REQUEST_METHOD']=strtoupper($_POST['_method']); + $scheme=isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' || + isset($headers['X-Forwarded-Proto']) && + $headers['X-Forwarded-Proto']=='https'?'https':'http'; + // Create hive early on to expose header methods + $this->hive=['HEADERS'=>&$headers]; + if (function_exists('apache_setenv')) { + // Work around Apache pre-2.4 VirtualDocumentRoot bug + $_SERVER['DOCUMENT_ROOT']=str_replace($_SERVER['SCRIPT_NAME'],'', + $_SERVER['SCRIPT_FILENAME']); + apache_setenv("DOCUMENT_ROOT",$_SERVER['DOCUMENT_ROOT']); + } + $_SERVER['DOCUMENT_ROOT']=realpath($_SERVER['DOCUMENT_ROOT']); + $base=''; + if (!$cli) + $base=rtrim($this->fixslashes( + dirname($_SERVER['SCRIPT_NAME'])),'/'); + $uri=parse_url((preg_match('/^\w+:\/\//',$_SERVER['REQUEST_URI'])?'': + $scheme.'://'.$_SERVER['SERVER_NAME']).$_SERVER['REQUEST_URI']); + $_SERVER['REQUEST_URI']=$uri['path']. + (isset($uri['query'])?'?'.$uri['query']:''). + (isset($uri['fragment'])?'#'.$uri['fragment']:''); + $path=preg_replace('/^'.preg_quote($base,'/').'/','',$uri['path']); + $jar=[ + 'expire'=>0, + 'lifetime'=>0, + 'path'=>$base?:'/', + 'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) && + !filter_var($_SERVER['SERVER_NAME'],FILTER_VALIDATE_IP)? + $_SERVER['SERVER_NAME']:'', + 'secure'=>($scheme=='https'), + 'httponly'=>TRUE, + 'samesite'=>'Lax', + ]; + $port=80; + if (isset($headers['X-Forwarded-Port'])) + $port=$headers['X-Forwarded-Port']; + elseif (isset($_SERVER['SERVER_PORT'])) + $port=$_SERVER['SERVER_PORT']; + // Default configuration + $this->hive+=[ + 'AGENT'=>$this->agent(), + 'AJAX'=>$this->ajax(), + 'ALIAS'=>NULL, + 'ALIASES'=>[], + 'AUTOLOAD'=>'./', + 'BASE'=>$base, + 'BITMASK'=>ENT_COMPAT, + 'BODY'=>NULL, + 'CACHE'=>FALSE, + 'CASELESS'=>TRUE, + 'CLI'=>$cli, + 'CORS'=>[ + 'headers'=>'', + 'origin'=>FALSE, + 'credentials'=>FALSE, + 'expose'=>FALSE, + 'ttl'=>0 + ], + 'DEBUG'=>0, + 'DIACRITICS'=>[], + 'DNSBL'=>'', + 'EMOJI'=>[], + 'ENCODING'=>$charset, + 'ERROR'=>NULL, + 'ESCAPE'=>TRUE, + 'EXCEPTION'=>NULL, + 'EXEMPT'=>NULL, + 'FALLBACK'=>$this->fallback, + 'FORMATS'=>[], + 'FRAGMENT'=>isset($uri['fragment'])?$uri['fragment']:'', + 'HALT'=>TRUE, + 'HIGHLIGHT'=>FALSE, + 'HOST'=>$_SERVER['SERVER_NAME'], + 'IP'=>$this->ip(), + 'JAR'=>$jar, + 'LANGUAGE'=>isset($headers['Accept-Language'])? + $this->language($headers['Accept-Language']): + $this->fallback, + 'LOCALES'=>'./', + 'LOCK'=>LOCK_EX, + 'LOGGABLE'=>'*', + 'LOGS'=>'./', + 'MB'=>extension_loaded('mbstring'), + 'ONERROR'=>NULL, + 'ONREROUTE'=>NULL, + 'PACKAGE'=>self::PACKAGE, + 'PARAMS'=>[], + 'PATH'=>$path, + 'PATTERN'=>NULL, + 'PLUGINS'=>$this->fixslashes(__DIR__).'/', + 'PORT'=>$port, + 'PREFIX'=>NULL, + 'PREMAP'=>'', + 'QUERY'=>isset($uri['query'])?$uri['query']:'', + 'QUIET'=>FALSE, + 'RAW'=>FALSE, + 'REALM'=>$scheme.'://'.$_SERVER['SERVER_NAME']. + ($port && !in_array($port,[80,443])?(':'.$port):''). + $_SERVER['REQUEST_URI'], + 'RESPONSE'=>'', + 'ROOT'=>$_SERVER['DOCUMENT_ROOT'], + 'ROUTES'=>[], + 'SCHEME'=>$scheme, + 'SEED'=>$this->hash($_SERVER['SERVER_NAME'].$base), + 'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php', + 'TEMP'=>'tmp/', + 'TIME'=>&$_SERVER['REQUEST_TIME_FLOAT'], + 'TZ'=>@date_default_timezone_get(), + 'UI'=>'./', + 'UNLOAD'=>NULL, + 'UPLOADS'=>'./', + 'URI'=>&$_SERVER['REQUEST_URI'], + 'VERB'=>&$_SERVER['REQUEST_METHOD'], + 'VERSION'=>self::VERSION, + 'XFRAME'=>'SAMEORIGIN' + ]; + if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) { + unset($jar['expire']); + session_cache_limiter(''); + if (version_compare(PHP_VERSION, '7.3.0') >= 0) + session_set_cookie_params($jar); + else { + unset($jar['samesite']); + call_user_func_array('session_set_cookie_params',$jar); + } + } + if (PHP_SAPI=='cli-server' && + preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI'])) + $this->reroute('/'); + if (ini_get('auto_globals_jit')) + // Override setting + $GLOBALS+=['_ENV'=>$_ENV,'_REQUEST'=>$_REQUEST]; + // Sync PHP globals with corresponding hive keys + $this->init=$this->hive; + foreach (explode('|',self::GLOBALS) as $global) { + $sync=$this->sync($global); + $this->init+=[ + $global=>preg_match('/SERVER|ENV/',$global)?$sync:[] + ]; + } + if ($check && $error=error_get_last()) + // Error detected + $this->error(500, + sprintf(self::E_Fatal,$error['message']),[$error]); + date_default_timezone_set($this->hive['TZ']); + // Register framework autoloader + spl_autoload_register([$this,'autoload']); + // Register shutdown handler + register_shutdown_function([$this,'unload'],getcwd()); + } + +} + +//! Cache engine +class Cache extends Prefab { + + protected + //! Cache DSN + $dsn, + //! Prefix for cache entries + $prefix, + //! MemCache or Redis object + $ref; + + /** + * Return timestamp and TTL of cache entry or FALSE if not found + * @return array|FALSE + * @param $key string + * @param $val mixed + **/ + function exists($key,&$val=NULL) { + $fw=Base::instance(); + if (!$this->dsn) + return FALSE; + $ndx=$this->prefix.'.'.$key; + $parts=explode('=',$this->dsn,2); + switch ($parts[0]) { + case 'apc': + case 'apcu': + $raw=call_user_func($parts[0].'_fetch',$ndx); + break; + case 'redis': + $raw=$this->ref->get($ndx); + break; + case 'memcache': + $raw=memcache_get($this->ref,$ndx); + break; + case 'memcached': + $raw=$this->ref->get($ndx); + break; + case 'wincache': + $raw=wincache_ucache_get($ndx); + break; + case 'xcache': + $raw=xcache_get($ndx); + break; + case 'folder': + $raw=$fw->read($parts[1].$ndx); + break; + } + if (!empty($raw)) { + list($val,$time,$ttl)=(array)$fw->unserialize($raw); + if ($ttl===0 || $time+$ttl>microtime(TRUE)) + return [$time,$ttl]; + $val=null; + $this->clear($key); + } + return FALSE; + } + + /** + * Store value in cache + * @return mixed|FALSE + * @param $key string + * @param $val mixed + * @param $ttl int + **/ + function set($key,$val,$ttl=0) { + $fw=Base::instance(); + if (!$this->dsn) + return TRUE; + $ndx=$this->prefix.'.'.$key; + if ($cached=$this->exists($key)) + $ttl=$cached[1]; + $data=$fw->serialize([$val,microtime(TRUE),$ttl]); + $parts=explode('=',$this->dsn,2); + switch ($parts[0]) { + case 'apc': + case 'apcu': + return call_user_func($parts[0].'_store',$ndx,$data,$ttl); + case 'redis': + return $this->ref->set($ndx,$data,$ttl?['ex'=>$ttl]:[]); + case 'memcache': + return memcache_set($this->ref,$ndx,$data,0,$ttl); + case 'memcached': + return $this->ref->set($ndx,$data,$ttl); + case 'wincache': + return wincache_ucache_set($ndx,$data,$ttl); + case 'xcache': + return xcache_set($ndx,$data,$ttl); + case 'folder': + return $fw->write($parts[1]. + str_replace(['/','\\'],'',$ndx),$data); + } + return FALSE; + } + + /** + * Retrieve value of cache entry + * @return mixed|FALSE + * @param $key string + **/ + function get($key) { + return $this->dsn && $this->exists($key,$data)?$data:FALSE; + } + + /** + * Delete cache entry + * @return bool + * @param $key string + **/ + function clear($key) { + if (!$this->dsn) + return; + $ndx=$this->prefix.'.'.$key; + $parts=explode('=',$this->dsn,2); + switch ($parts[0]) { + case 'apc': + case 'apcu': + return call_user_func($parts[0].'_delete',$ndx); + case 'redis': + return $this->ref->del($ndx); + case 'memcache': + return memcache_delete($this->ref,$ndx); + case 'memcached': + return $this->ref->delete($ndx); + case 'wincache': + return wincache_ucache_delete($ndx); + case 'xcache': + return xcache_unset($ndx); + case 'folder': + return @unlink($parts[1].$ndx); + } + return FALSE; + } + + /** + * Clear contents of cache backend + * @return bool + * @param $suffix string + **/ + function reset($suffix=NULL) { + if (!$this->dsn) + return TRUE; + $regex='/'.preg_quote($this->prefix.'.','/').'.*'. + preg_quote($suffix,'/').'/'; + $parts=explode('=',$this->dsn,2); + switch ($parts[0]) { + case 'apc': + case 'apcu': + $info=call_user_func($parts[0].'_cache_info', + $parts[0]=='apcu'?FALSE:'user'); + if (!empty($info['cache_list'])) { + $key=array_key_exists('info', + $info['cache_list'][0])?'info':'key'; + foreach ($info['cache_list'] as $item) + if (preg_match($regex,$item[$key])) + call_user_func($parts[0].'_delete',$item[$key]); + } + return TRUE; + case 'redis': + $keys=$this->ref->keys($this->prefix.'.*'.$suffix); + foreach($keys as $key) + $this->ref->del($key); + return TRUE; + case 'memcache': + foreach (memcache_get_extended_stats( + $this->ref,'slabs') as $slabs) + foreach (array_filter(array_keys($slabs),'is_numeric') + as $id) + foreach (memcache_get_extended_stats( + $this->ref,'cachedump',$id) as $data) + if (is_array($data)) + foreach (array_keys($data) as $key) + if (preg_match($regex,$key)) + memcache_delete($this->ref,$key); + return TRUE; + case 'memcached': + foreach ($this->ref->getallkeys()?:[] as $key) + if (preg_match($regex,$key)) + $this->ref->delete($key); + return TRUE; + case 'wincache': + $info=wincache_ucache_info(); + foreach ($info['ucache_entries'] as $item) + if (preg_match($regex,$item['key_name'])) + wincache_ucache_delete($item['key_name']); + return TRUE; + case 'xcache': + if ($suffix && !ini_get('xcache.admin.enable_auth')) { + $cnt=xcache_count(XC_TYPE_VAR); + for ($i=0;$i<$cnt;$i++) { + $list=xcache_list(XC_TYPE_VAR,$i); + foreach ($list['cache_list'] as $item) + if (preg_match($regex,$item['name'])) + xcache_unset($item['name']); + } + } else + xcache_unset_by_prefix($this->prefix.'.'); + return TRUE; + case 'folder': + if ($glob=@glob($parts[1].'*')) + foreach ($glob as $file) + if (preg_match($regex,basename($file))) + @unlink($file); + return TRUE; + } + return FALSE; + } + + /** + * Load/auto-detect cache backend + * @return string + * @param $dsn bool|string + * @param $seed bool|string + **/ + function load($dsn,$seed=NULL) { + $fw=Base::instance(); + if ($dsn=trim($dsn)) { + if (preg_match('/^redis=(.+)/',$dsn,$parts) && + extension_loaded('redis')) { + list($host,$port,$db,$password)=explode(':',$parts[1])+[1=>6379,2=>NULL,3=>NULL]; + $this->ref=new Redis; + if(!$this->ref->connect($host,$port,2)) + $this->ref=NULL; + if(!empty($password)) + $this->ref->auth($password); + if(isset($db)) + $this->ref->select($db); + } + elseif (preg_match('/^memcache=(.+)/',$dsn,$parts) && + extension_loaded('memcache')) + foreach ($fw->split($parts[1]) as $server) { + list($host,$port)=explode(':',$server)+[1=>11211]; + if (empty($this->ref)) + $this->ref=@memcache_connect($host,$port)?:NULL; + else + memcache_add_server($this->ref,$host,$port); + } + elseif (preg_match('/^memcached=(.+)/',$dsn,$parts) && + extension_loaded('memcached')) + foreach ($fw->split($parts[1]) as $server) { + list($host,$port)=explode(':',$server)+[1=>11211]; + if (empty($this->ref)) + $this->ref=new Memcached(); + $this->ref->addServer($host,$port); + } + if (empty($this->ref) && !preg_match('/^folder\h*=/',$dsn)) + $dsn=($grep=preg_grep('/^(apc|wincache|xcache)/', + array_map('strtolower',get_loaded_extensions())))? + // Auto-detect + current($grep): + // Use filesystem as fallback + ('folder='.$fw->TEMP.'cache/'); + if (preg_match('/^folder\h*=\h*(.+)/',$dsn,$parts) && + !is_dir($parts[1])) + mkdir($parts[1],Base::MODE,TRUE); + } + $this->prefix=$seed?:$fw->SEED; + return $this->dsn=$dsn; + } + + /** + * Class constructor + * @param $dsn bool|string + **/ + function __construct($dsn=FALSE) { + if ($dsn) + $this->load($dsn); + } + +} + +//! View handler +class View extends Prefab { + + private + //! Temporary hive + $temp; + + protected + //! Template file + $file, + //! Post-rendering handler + $trigger, + //! Nesting level + $level=0; + + /** @var \Base Framework instance */ + protected $fw; + + function __construct() { + $this->fw=\Base::instance(); + } + + /** + * Encode characters to equivalent HTML entities + * @return string + * @param $arg mixed + **/ + function esc($arg) { + return $this->fw->recursive($arg, + function($val) { + return is_string($val)?$this->fw->encode($val):$val; + } + ); + } + + /** + * Decode HTML entities to equivalent characters + * @return string + * @param $arg mixed + **/ + function raw($arg) { + return $this->fw->recursive($arg, + function($val) { + return is_string($val)?$this->fw->decode($val):$val; + } + ); + } + + /** + * Create sandbox for template execution + * @return string + * @param $hive array + * @param $mime string + **/ + protected function sandbox(array $hive=NULL,$mime=NULL) { + $fw=$this->fw; + $implicit=FALSE; + if (is_null($hive)) { + $implicit=TRUE; + $hive=$fw->hive(); + } + if ($this->level<1 || $implicit) { + if (!$fw->CLI && $mime && !headers_sent() && + !preg_grep ('/^Content-Type:/',headers_list())) + header('Content-Type: '.$mime.'; '. + 'charset='.$fw->ENCODING); + if ($fw->ESCAPE && (!$mime || + preg_match('/^(text\/html|(application|text)\/(.+\+)?xml)$/i',$mime))) + $hive=$this->esc($hive); + if (isset($hive['ALIASES'])) + $hive['ALIASES']=$fw->build($hive['ALIASES']); + } + $this->temp=$hive; + unset($fw,$hive,$implicit,$mime); + extract($this->temp); + $this->temp=NULL; + $this->level++; + ob_start(); + require($this->file); + $this->level--; + return ob_get_clean(); + } + + /** + * Render template + * @return string + * @param $file string + * @param $mime string + * @param $hive array + * @param $ttl int + **/ + function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { + $fw=$this->fw; + $cache=Cache::instance(); + foreach ($fw->split($fw->UI) as $dir) { + if ($cache->exists($hash=$fw->hash($dir.$file),$data)) + return $data; + if (is_file($this->file=$fw->fixslashes($dir.$file))) { + if (isset($_COOKIE[session_name()]) && + !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); + $fw->sync('SESSION'); + $data=$this->sandbox($hive,$mime); + if (isset($this->trigger['afterrender'])) + foreach($this->trigger['afterrender'] as $func) + $data=$fw->call($func,[$data, $dir.$file]); + if ($ttl) + $cache->set($hash,$data,$ttl); + return $data; + } + } + user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); + } + + /** + * post rendering handler + * @param $func callback + */ + function afterrender($func) { + $this->trigger['afterrender'][]=$func; + } + +} + +//! Lightweight template engine +class Preview extends View { + + protected + //! token filter + $filter=[ + 'c'=>'$this->c', + 'esc'=>'$this->esc', + 'raw'=>'$this->raw', + 'export'=>'Base::instance()->export', + 'alias'=>'Base::instance()->alias', + 'format'=>'Base::instance()->format' + ]; + + protected + //! newline interpolation + $interpolation=true; + + /** + * Enable/disable markup parsing interpolation + * mainly used for adding appropriate newlines + * @param $bool bool + */ + function interpolation($bool) { + $this->interpolation=$bool; + } + + /** + * Return C-locale equivalent of number + * @return string + * @param $val int|float + **/ + function c($val) { + $locale=setlocale(LC_NUMERIC,0); + setlocale(LC_NUMERIC,'C'); + $out=(string)(float)$val; + $locale=setlocale(LC_NUMERIC,$locale); + return $out; + } + + /** + * Convert token to variable + * @return string + * @param $str string + **/ + function token($str) { + $str=trim(preg_replace('/\{\{(.+?)\}\}/s','\1',$this->fw->compile($str))); + if (preg_match('/^(.+)(?fw->split(trim($parts[2],"\xC2\xA0")) as $func) + $str=((empty($this->filter[$cmd=$func]) && + function_exists($cmd)) || + is_string($cmd=$this->filter($func)))? + $cmd.'('.$str.')': + 'Base::instance()->'. + 'call($this->filter(\''.$func.'\'),['.$str.'])'; + } + return $str; + } + + /** + * Register or get (one specific or all) token filters + * @param string $key + * @param string|closure $func + * @return array|closure|string + */ + function filter($key=NULL,$func=NULL) { + if (!$key) + return array_keys($this->filter); + $key=strtolower($key); + if (!$func) + return $this->filter[$key]; + $this->filter[$key]=$func; + } + + /** + * Assemble markup + * @return string + * @param $node string + **/ + protected function build($node) { + return preg_replace_callback( + '/\{~(.+?)~\}|\{\*(.+?)\*\}|\{\-(.+?)\-\}|'. + '\{\{(.+?)\}\}((\r?\n)*)/s', + function($expr) { + if ($expr[1]) + $str='token($expr[1]).' ?>'; + elseif ($expr[2]) + return ''; + elseif ($expr[3]) + $str=$expr[3]; + else { + $str='token($expr[4])).')'. + ($this->interpolation? + (!empty($expr[6])?'."'.$expr[6].'"':''):'').' ?>'; + if (isset($expr[5])) + $str.=$expr[5]; + } + return $str; + }, + $node + ); + } + + /** + * Render template string + * @return string + * @param $node string|array + * @param $hive array + * @param $ttl int + * @param $persist bool + * @param $escape bool + **/ + function resolve($node,array $hive=NULL,$ttl=0,$persist=FALSE,$escape=NULL) { + $fw=$this->fw; + $cache=Cache::instance(); + if ($escape!==NULL) { + $esc=$fw->ESCAPE; + $fw->ESCAPE=$escape; + } + if ($ttl || $persist) + $hash=$fw->hash($fw->serialize($node)); + if ($ttl && $cache->exists($hash,$data)) + return $data; + if ($persist) { + if (!is_dir($tmp=$fw->TEMP)) + mkdir($tmp,Base::MODE,TRUE); + if (!is_file($this->file=($tmp. + $fw->SEED.'.'.$hash.'.php'))) + $fw->write($this->file,$this->build($node)); + if (isset($_COOKIE[session_name()]) && + !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); + $fw->sync('SESSION'); + $data=$this->sandbox($hive); + } + else { + if (!$hive) + $hive=$fw->hive(); + if ($fw->ESCAPE) + $hive=$this->esc($hive); + extract($hive); + unset($hive); + ob_start(); + eval(' ?>'.$this->build($node).'set($hash,$data,$ttl); + if ($escape!==NULL) + $fw->ESCAPE=$esc; + return $data; + } + + /** + * Parse template string + * @return string + * @param $text string + **/ + function parse($text) { + // Remove PHP code and comments + return preg_replace( + '/\h*<\?(?!xml)(?:php|\s*=)?.+?\?>\h*|'. + '\{\*.+?\*\}/is','', $text); + } + + /** + * Render template + * @return string + * @param $file string + * @param $mime string + * @param $hive array + * @param $ttl int + **/ + function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { + $fw=$this->fw; + $cache=Cache::instance(); + if (!is_dir($tmp=$fw->TEMP)) + mkdir($tmp,Base::MODE,TRUE); + foreach ($fw->split($fw->UI) as $dir) { + if ($cache->exists($hash=$fw->hash($dir.$file),$data)) + return $data; + if (is_file($view=$fw->fixslashes($dir.$file))) { + if (!is_file($this->file=($tmp. + $fw->SEED.'.'.$fw->hash($view).'.php')) || + filemtime($this->file)read($view); + if (isset($this->trigger['beforerender'])) + foreach ($this->trigger['beforerender'] as $func) + $contents=$fw->call($func, [$contents, $view]); + $text=$this->parse($contents); + $fw->write($this->file,$this->build($text)); + } + if (isset($_COOKIE[session_name()]) && + !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) + session_start(); + $fw->sync('SESSION'); + $data=$this->sandbox($hive,$mime); + if(isset($this->trigger['afterrender'])) + foreach ($this->trigger['afterrender'] as $func) + $data=$fw->call($func, [$data, $view]); + if ($ttl) + $cache->set($hash,$data,$ttl); + return $data; + } + } + user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); + } + + /** + * post rendering handler + * @param $func callback + */ + function beforerender($func) { + $this->trigger['beforerender'][]=$func; + } + +} + +//! ISO language/country codes +class ISO extends Prefab { + + //@{ ISO 3166-1 country codes + const + CC_af='Afghanistan', + CC_ax='Åland Islands', + CC_al='Albania', + CC_dz='Algeria', + CC_as='American Samoa', + CC_ad='Andorra', + CC_ao='Angola', + CC_ai='Anguilla', + CC_aq='Antarctica', + CC_ag='Antigua and Barbuda', + CC_ar='Argentina', + CC_am='Armenia', + CC_aw='Aruba', + CC_au='Australia', + CC_at='Austria', + CC_az='Azerbaijan', + CC_bs='Bahamas', + CC_bh='Bahrain', + CC_bd='Bangladesh', + CC_bb='Barbados', + CC_by='Belarus', + CC_be='Belgium', + CC_bz='Belize', + CC_bj='Benin', + CC_bm='Bermuda', + CC_bt='Bhutan', + CC_bo='Bolivia', + CC_bq='Bonaire, Sint Eustatius and Saba', + CC_ba='Bosnia and Herzegovina', + CC_bw='Botswana', + CC_bv='Bouvet Island', + CC_br='Brazil', + CC_io='British Indian Ocean Territory', + CC_bn='Brunei Darussalam', + CC_bg='Bulgaria', + CC_bf='Burkina Faso', + CC_bi='Burundi', + CC_kh='Cambodia', + CC_cm='Cameroon', + CC_ca='Canada', + CC_cv='Cape Verde', + CC_ky='Cayman Islands', + CC_cf='Central African Republic', + CC_td='Chad', + CC_cl='Chile', + CC_cn='China', + CC_cx='Christmas Island', + CC_cc='Cocos (Keeling) Islands', + CC_co='Colombia', + CC_km='Comoros', + CC_cg='Congo', + CC_cd='Congo, The Democratic Republic of', + CC_ck='Cook Islands', + CC_cr='Costa Rica', + CC_ci='Côte d\'ivoire', + CC_hr='Croatia', + CC_cu='Cuba', + CC_cw='Curaçao', + CC_cy='Cyprus', + CC_cz='Czech Republic', + CC_dk='Denmark', + CC_dj='Djibouti', + CC_dm='Dominica', + CC_do='Dominican Republic', + CC_ec='Ecuador', + CC_eg='Egypt', + CC_sv='El Salvador', + CC_gq='Equatorial Guinea', + CC_er='Eritrea', + CC_ee='Estonia', + CC_et='Ethiopia', + CC_fk='Falkland Islands (Malvinas)', + CC_fo='Faroe Islands', + CC_fj='Fiji', + CC_fi='Finland', + CC_fr='France', + CC_gf='French Guiana', + CC_pf='French Polynesia', + CC_tf='French Southern Territories', + CC_ga='Gabon', + CC_gm='Gambia', + CC_ge='Georgia', + CC_de='Germany', + CC_gh='Ghana', + CC_gi='Gibraltar', + CC_gr='Greece', + CC_gl='Greenland', + CC_gd='Grenada', + CC_gp='Guadeloupe', + CC_gu='Guam', + CC_gt='Guatemala', + CC_gg='Guernsey', + CC_gn='Guinea', + CC_gw='Guinea-Bissau', + CC_gy='Guyana', + CC_ht='Haiti', + CC_hm='Heard Island and McDonald Islands', + CC_va='Holy See (Vatican City State)', + CC_hn='Honduras', + CC_hk='Hong Kong', + CC_hu='Hungary', + CC_is='Iceland', + CC_in='India', + CC_id='Indonesia', + CC_ir='Iran, Islamic Republic of', + CC_iq='Iraq', + CC_ie='Ireland', + CC_im='Isle of Man', + CC_il='Israel', + CC_it='Italy', + CC_jm='Jamaica', + CC_jp='Japan', + CC_je='Jersey', + CC_jo='Jordan', + CC_kz='Kazakhstan', + CC_ke='Kenya', + CC_ki='Kiribati', + CC_kp='Korea, Democratic People\'s Republic of', + CC_kr='Korea, Republic of', + CC_kw='Kuwait', + CC_kg='Kyrgyzstan', + CC_la='Lao People\'s Democratic Republic', + CC_lv='Latvia', + CC_lb='Lebanon', + CC_ls='Lesotho', + CC_lr='Liberia', + CC_ly='Libya', + CC_li='Liechtenstein', + CC_lt='Lithuania', + CC_lu='Luxembourg', + CC_mo='Macao', + CC_mk='Macedonia, The Former Yugoslav Republic of', + CC_mg='Madagascar', + CC_mw='Malawi', + CC_my='Malaysia', + CC_mv='Maldives', + CC_ml='Mali', + CC_mt='Malta', + CC_mh='Marshall Islands', + CC_mq='Martinique', + CC_mr='Mauritania', + CC_mu='Mauritius', + CC_yt='Mayotte', + CC_mx='Mexico', + CC_fm='Micronesia, Federated States of', + CC_md='Moldova, Republic of', + CC_mc='Monaco', + CC_mn='Mongolia', + CC_me='Montenegro', + CC_ms='Montserrat', + CC_ma='Morocco', + CC_mz='Mozambique', + CC_mm='Myanmar', + CC_na='Namibia', + CC_nr='Nauru', + CC_np='Nepal', + CC_nl='Netherlands', + CC_nc='New Caledonia', + CC_nz='New Zealand', + CC_ni='Nicaragua', + CC_ne='Niger', + CC_ng='Nigeria', + CC_nu='Niue', + CC_nf='Norfolk Island', + CC_mp='Northern Mariana Islands', + CC_no='Norway', + CC_om='Oman', + CC_pk='Pakistan', + CC_pw='Palau', + CC_ps='Palestinian Territory, Occupied', + CC_pa='Panama', + CC_pg='Papua New Guinea', + CC_py='Paraguay', + CC_pe='Peru', + CC_ph='Philippines', + CC_pn='Pitcairn', + CC_pl='Poland', + CC_pt='Portugal', + CC_pr='Puerto Rico', + CC_qa='Qatar', + CC_re='Réunion', + CC_ro='Romania', + CC_ru='Russian Federation', + CC_rw='Rwanda', + CC_bl='Saint Barthélemy', + CC_sh='Saint Helena, Ascension and Tristan da Cunha', + CC_kn='Saint Kitts and Nevis', + CC_lc='Saint Lucia', + CC_mf='Saint Martin (French Part)', + CC_pm='Saint Pierre and Miquelon', + CC_vc='Saint Vincent and The Grenadines', + CC_ws='Samoa', + CC_sm='San Marino', + CC_st='Sao Tome and Principe', + CC_sa='Saudi Arabia', + CC_sn='Senegal', + CC_rs='Serbia', + CC_sc='Seychelles', + CC_sl='Sierra Leone', + CC_sg='Singapore', + CC_sk='Slovakia', + CC_sx='Sint Maarten (Dutch Part)', + CC_si='Slovenia', + CC_sb='Solomon Islands', + CC_so='Somalia', + CC_za='South Africa', + CC_gs='South Georgia and The South Sandwich Islands', + CC_ss='South Sudan', + CC_es='Spain', + CC_lk='Sri Lanka', + CC_sd='Sudan', + CC_sr='Suriname', + CC_sj='Svalbard and Jan Mayen', + CC_sz='Swaziland', + CC_se='Sweden', + CC_ch='Switzerland', + CC_sy='Syrian Arab Republic', + CC_tw='Taiwan, Province of China', + CC_tj='Tajikistan', + CC_tz='Tanzania, United Republic of', + CC_th='Thailand', + CC_tl='Timor-Leste', + CC_tg='Togo', + CC_tk='Tokelau', + CC_to='Tonga', + CC_tt='Trinidad and Tobago', + CC_tn='Tunisia', + CC_tr='Turkey', + CC_tm='Turkmenistan', + CC_tc='Turks and Caicos Islands', + CC_tv='Tuvalu', + CC_ug='Uganda', + CC_ua='Ukraine', + CC_ae='United Arab Emirates', + CC_gb='United Kingdom', + CC_us='United States', + CC_um='United States Minor Outlying Islands', + CC_uy='Uruguay', + CC_uz='Uzbekistan', + CC_vu='Vanuatu', + CC_ve='Venezuela', + CC_vn='Viet Nam', + CC_vg='Virgin Islands, British', + CC_vi='Virgin Islands, U.S.', + CC_wf='Wallis and Futuna', + CC_eh='Western Sahara', + CC_ye='Yemen', + CC_zm='Zambia', + CC_zw='Zimbabwe'; + //@} + + //@{ ISO 639-1 language codes (Windows-compatibility subset) + const + LC_af='Afrikaans', + LC_am='Amharic', + LC_ar='Arabic', + LC_as='Assamese', + LC_ba='Bashkir', + LC_be='Belarusian', + LC_bg='Bulgarian', + LC_bn='Bengali', + LC_bo='Tibetan', + LC_br='Breton', + LC_ca='Catalan', + LC_co='Corsican', + LC_cs='Czech', + LC_cy='Welsh', + LC_da='Danish', + LC_de='German', + LC_dv='Divehi', + LC_el='Greek', + LC_en='English', + LC_es='Spanish', + LC_et='Estonian', + LC_eu='Basque', + LC_fa='Persian', + LC_fi='Finnish', + LC_fo='Faroese', + LC_fr='French', + LC_gd='Scottish Gaelic', + LC_gl='Galician', + LC_gu='Gujarati', + LC_he='Hebrew', + LC_hi='Hindi', + LC_hr='Croatian', + LC_hu='Hungarian', + LC_hy='Armenian', + LC_id='Indonesian', + LC_ig='Igbo', + LC_is='Icelandic', + LC_it='Italian', + LC_ja='Japanese', + LC_ka='Georgian', + LC_kk='Kazakh', + LC_km='Khmer', + LC_kn='Kannada', + LC_ko='Korean', + LC_lb='Luxembourgish', + LC_lo='Lao', + LC_lt='Lithuanian', + LC_lv='Latvian', + LC_mi='Maori', + LC_ml='Malayalam', + LC_mr='Marathi', + LC_ms='Malay', + LC_mt='Maltese', + LC_ne='Nepali', + LC_nl='Dutch', + LC_no='Norwegian', + LC_oc='Occitan', + LC_or='Oriya', + LC_pl='Polish', + LC_ps='Pashto', + LC_pt='Portuguese', + LC_qu='Quechua', + LC_ro='Romanian', + LC_ru='Russian', + LC_rw='Kinyarwanda', + LC_sa='Sanskrit', + LC_si='Sinhala', + LC_sk='Slovak', + LC_sl='Slovenian', + LC_sq='Albanian', + LC_sv='Swedish', + LC_ta='Tamil', + LC_te='Telugu', + LC_th='Thai', + LC_tk='Turkmen', + LC_tr='Turkish', + LC_tt='Tatar', + LC_uk='Ukrainian', + LC_ur='Urdu', + LC_vi='Vietnamese', + LC_wo='Wolof', + LC_yo='Yoruba', + LC_zh='Chinese'; + //@} + + /** + * Return list of languages indexed by ISO 639-1 language code + * @return array + **/ + function languages() { + return \Base::instance()->constants($this,'LC_'); + } + + /** + * Return list of countries indexed by ISO 3166-1 country code + * @return array + **/ + function countries() { + return \Base::instance()->constants($this,'CC_'); + } + +} + +//! Container for singular object instances +final class Registry { + + private static + //! Object catalog + $table; + + /** + * Return TRUE if object exists in catalog + * @return bool + * @param $key string + **/ + static function exists($key) { + return isset(self::$table[$key]); + } + + /** + * Add object to catalog + * @return object + * @param $key string + * @param $obj object + **/ + static function set($key,$obj) { + return self::$table[$key]=$obj; + } + + /** + * Retrieve object from catalog + * @return object + * @param $key string + **/ + static function get($key) { + return self::$table[$key]; + } + + /** + * Delete object from catalog + * @param $key string + **/ + static function clear($key) { + self::$table[$key]=NULL; + unset(self::$table[$key]); + } + + //! Prohibit cloning + private function __clone() { + } + + //! Prohibit instantiation + private function __construct() { + } + +} + +return Base::instance(); diff --git a/lib/basket.php b/lib/basket.php new file mode 100644 index 0000000..70cacee --- /dev/null +++ b/lib/basket.php @@ -0,0 +1,239 @@ +. + +*/ + +//! 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(); + } + +} diff --git a/lib/bcrypt.php b/lib/bcrypt.php new file mode 100644 index 0000000..f044ff1 --- /dev/null +++ b/lib/bcrypt.php @@ -0,0 +1,96 @@ +. +* +**/ + +/** +* 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; + } + +} diff --git a/lib/cli/ws.php b/lib/cli/ws.php new file mode 100644 index 0000000..f1573c0 --- /dev/null +++ b/lib/cli/ws.php @@ -0,0 +1,487 @@ +. + +*/ + +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;$ievents['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); + } + +} diff --git a/lib/code.css b/lib/code.css new file mode 100755 index 0000000..618703f --- /dev/null +++ b/lib/code.css @@ -0,0 +1 @@ +code{word-wrap:break-word;color:black}.comment,.doc_comment,.ml_comment{color:dimgray;font-style:italic}.variable{color:blueviolet}.const,.constant_encapsed_string,.class_c,.dir,.file,.func_c,.halt_compiler,.line,.method_c,.lnumber,.dnumber{color:crimson}.string,.and_equal,.boolean_and,.boolean_or,.concat_equal,.dec,.div_equal,.inc,.is_equal,.is_greater_or_equal,.is_identical,.is_not_equal,.is_not_identical,.is_smaller_or_equal,.logical_and,.logical_or,.logical_xor,.minus_equal,.mod_equal,.mul_equal,.ns_c,.ns_separator,.or_equal,.plus_equal,.sl,.sl_equal,.sr,.sr_equal,.xor_equal,.start_heredoc,.end_heredoc,.object_operator,.paamayim_nekudotayim{color:black}.abstract,.array,.array_cast,.as,.break,.case,.catch,.class,.clone,.continue,.declare,.default,.do,.echo,.else,.elseif,.empty.enddeclare,.endfor,.endforach,.endif,.endswitch,.endwhile,.eval,.exit,.extends,.final,.for,.foreach,.function,.global,.goto,.if,.implements,.include,.include_once,.instanceof,.interface,.isset,.list,.namespace,.new,.print,.private,.public,.protected,.require,.require_once,.return,.static,.switch,.throw,.try,.unset,.use,.var,.while{color:royalblue}.open_tag,.open_tag_with_echo,.close_tag{color:orange}.ini_section{color:black}.ini_key{color:royalblue}.ini_value{color:crimson}.xml_tag{color:dodgerblue}.xml_attr{color:blueviolet}.xml_data{color:red}.section{color:black}.directive{color:blue}.data{color:dimgray} diff --git a/lib/db/cursor.php b/lib/db/cursor.php new file mode 100644 index 0000000..2fd324a --- /dev/null +++ b/lib/db/cursor.php @@ -0,0 +1,388 @@ +. + +*/ + +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->ptrquery)? + $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; + } + +} diff --git a/lib/db/jig.php b/lib/db/jig.php new file mode 100644 index 0000000..bcc29ca --- /dev/null +++ b/lib/db/jig.php @@ -0,0 +1,175 @@ +. + +*/ + +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); + } + } + +} diff --git a/lib/db/jig/mapper.php b/lib/db/jig/mapper.php new file mode 100644 index 0000000..784b2a8 --- /dev/null +++ b/lib/db/jig/mapper.php @@ -0,0 +1,541 @@ +. + +*/ + +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( + '/(?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]+$ttlread($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('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(); + } + +} diff --git a/lib/db/jig/session.php b/lib/db/jig/session.php new file mode 100644 index 0000000..eee1339 --- /dev/null +++ b/lib/db/jig/session.php @@ -0,0 +1,194 @@ +. + +*/ + +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+?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; + } + +} diff --git a/lib/db/mongo.php b/lib/db/mongo.php new file mode 100644 index 0000000..46d00be --- /dev/null +++ b/lib/db/mongo.php @@ -0,0 +1,145 @@ +. + +*/ + +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]); + } + } + +} diff --git a/lib/db/mongo/mapper.php b/lib/db/mongo/mapper.php new file mode 100644 index 0000000..0246f6d --- /dev/null +++ b/lib/db/mongo/mapper.php @@ -0,0 +1,405 @@ +. + +*/ + +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]+$ttlcollection->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]+$ttlcollection->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(); + } + +} diff --git a/lib/db/mongo/session.php b/lib/db/mongo/session.php new file mode 100644 index 0000000..013f171 --- /dev/null +++ b/lib/db/mongo/session.php @@ -0,0 +1,194 @@ +. + +*/ + +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; + } + +} diff --git a/lib/db/sql.php b/lib/db/sql.php new file mode 100644 index 0000000..4e464d7 --- /dev/null +++ b/lib/db/sql.php @@ -0,0 +1,523 @@ +. + +*/ + +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); + } + +} diff --git a/lib/db/sql/mapper.php b/lib/db/sql/mapper.php new file mode 100644 index 0000000..1eb45c6 --- /dev/null +++ b/lib/db/sql/mapper.php @@ -0,0 +1,759 @@ +. + +*/ + +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(); + } + +} diff --git a/lib/db/sql/session.php b/lib/db/sql/session.php new file mode 100644 index 0000000..8defbf4 --- /dev/null +++ b/lib/db/sql/session.php @@ -0,0 +1,222 @@ +. + +*/ + +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+?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; + } + +} diff --git a/lib/f3.php b/lib/f3.php new file mode 100644 index 0000000..ae95942 --- /dev/null +++ b/lib/f3.php @@ -0,0 +1,42 @@ +. + +*/ + +//! 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); + } + +} diff --git a/lib/image.php b/lib/image.php new file mode 100644 index 0000000..59c165e --- /dev/null +++ b/lib/image.php @@ -0,0 +1,616 @@ +. + +*/ + +//! 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); + } + } + +} diff --git a/lib/log.php b/lib/log.php new file mode 100644 index 0000000..5b7341d --- /dev/null +++ b/lib/log.php @@ -0,0 +1,71 @@ +. + +*/ + +//! 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; + } + +} diff --git a/lib/magic.php b/lib/magic.php new file mode 100644 index 0000000..f676506 --- /dev/null +++ b/lib/magic.php @@ -0,0 +1,139 @@ +. + +*/ + +//! 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); + } + +} diff --git a/lib/markdown.php b/lib/markdown.php new file mode 100644 index 0000000..885f215 --- /dev/null +++ b/lib/markdown.php @@ -0,0 +1,569 @@ +. + +*/ + +//! 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)? + ('
'.$this->build($str).'
'."\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)? + ('
'.
+				$this->esc($this->snip($str)).
+			'
'."\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]? + (''. + $this->esc($match[2]).$match[3]. + ''. + ($match[4]? + (''. + $this->esc($match[4]). + ''): + ''). + ''. + $this->esc($match[5]). + ''): + (''. + $match[6]. + ''. + ''. + $this->esc($match[7]). + '')). + $match[8]; + $str=''.$out.''; + 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.='<'. + $match[1].$match[2].''; + 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])? + '': + (''. + $part[1].'=')). + ''. + $part[2].''): + (''. + $part[3].'')); + } + $out.=''. + $match[4].'>'; + } + else + $out.=$this->esc($match[5]); + } + $str=''.$out.''; + 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.=''.$match[1]. + ''; + elseif ($match[2]) + $out.='['.$match[2].']'. + ''; + elseif ($match[3]) + $out.=''.$match[3]. + ''.$match[4]. + ($match[5]? + (''. + $match[5].''):''); + else + $out.=$match[0]; + if (isset($match[6])) + $out.=$match[6]; + } + $str=''.$out.''; + break; + default: + $str=''.$this->esc($str).''; + break; + } + } + else + $str=''.$this->esc($str).''; + return '
'.$str.'
'."\n\n"; + } + + /** + * Process horizontal rule + * @return string + **/ + protected function _hr() { + return '
'."\n\n"; + } + + /** + * Process atx-style heading + * @return string + * @param $type string + * @param $str string + **/ + protected function _atx($type,$str) { + $level=strlen($type); + return ''. + $this->scan($str).''."\n\n"; + } + + /** + * Process setext-style heading + * @return string + * @param $str string + * @param $type string + **/ + protected function _setext($str,$type) { + $level=strpos('=-',$type)+1; + return ''. + $this->scan($str).''."\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.''."\n\n"):''). + '
'."\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.='
  • '.$this->scan(trim($tmp)).'
  • '."\n"; + } + } + return strlen($dst)? + ('<'.$type.'>'."\n".$dst.''."\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/','
    ',$str); + return '

    '.$this->scan($str).'

    '."\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|^)(?'.$expr[4].'
    '; + if ($expr[2]) + return ''.$expr[4].''; + return ''.$expr[4].''; + }, + preg_replace( + '/(?\1', + $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 ''.$this->esc($expr[1]).''; + }, + $str + ); + } + + /** + * Process anchor span + * @return string + * @param $str string + **/ + protected function _a($str) { + return preg_replace_callback( + '/(??(?:\h*"(.*?)"\h*)?\)/', + function($expr) { + return ''.$this->scan($expr[1]).''; + }, + $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 ''.$expr[2].''; + } + return $expr[0]; + }, + $str + ); + } + + /** + * Process code span + * @return string + * @param $str string + **/ + protected function _code($str) { + return preg_replace_callback( + '/`` (.+?) ``|(?'. + $this->esc(empty($expr[1])?$expr[2]:$expr[1]).''; + }, + $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( + '/(?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] + ).''): + // Image + (''.
+										$this->esc($expr[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>|'. + '<[^>\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); + } + +} diff --git a/lib/matrix.php b/lib/matrix.php new file mode 100644 index 0000000..d1f0087 --- /dev/null +++ b/lib/matrix.php @@ -0,0 +1,139 @@ +. + +*/ + +//! 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; + } + +} diff --git a/lib/session.php b/lib/session.php new file mode 100644 index 0000000..168e5b6 --- /dev/null +++ b/lib/session.php @@ -0,0 +1,196 @@ +. + +*/ + +//! 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; + } + +} diff --git a/lib/smtp.php b/lib/smtp.php new file mode 100644 index 0000000..415a139 --- /dev/null +++ b/lib/smtp.php @@ -0,0 +1,360 @@ +. + +*/ + +//! 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); + } + +} diff --git a/lib/template.php b/lib/template.php new file mode 100644 index 0000000..6d920c5 --- /dev/null +++ b/lib/template.php @@ -0,0 +1,353 @@ +. + +*/ + +//! 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 ''; + } + + /** + * 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 + '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 + 'token($attrib['from']).';'. + $this->token($attrib['to']).';'. + $this->token($attrib['step']).'): ?>'. + $this->build($node). + ''; + } + + /** + * Template -repeat- tag handler + * @return string + * @param $node array + **/ + protected function _repeat(array $node) { + $attrib=$node['@attrib']; + unset($node['@attrib']); + return + '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). + ''; + } + + /** + * Template -check- tag handler + * @return string + * @param $node array + **/ + protected function _check(array $node) { + $attrib=$node['@attrib']; + unset($node['@attrib']); + // Grab and 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 and blocks + list($node[$true[0]],$node[$false[0]])=[$false[1],$true[1]]; + return + 'token($attrib['if']).'): ?>'. + $this->build($node). + ''; + } + + /** + * 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 ''.$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 + 'token($attrib['expr']).'): ?>'. + $this->build($node). + ''; + } + + /** + * Template -case- tag handler + * @return string + * @param $node array + **/ + protected function _case(array $node) { + $attrib=$node['@attrib']; + unset($node['@attrib']); + return + 'token($attrib['value']): + Base::instance()->stringify($attrib['value'])).': ?>'. + $this->build($node). + 'token($attrib['break']).') ':''). + 'break; ?>'; + } + + /** + * Template -default- tag handler + * @return string + * @param $node array + **/ + protected function _default(array $node) { + return + ''. + $this->build($node). + ''; + } + + /** + * 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(); + } + +} diff --git a/lib/test.php b/lib/test.php new file mode 100644 index 0000000..d45bb18 --- /dev/null +++ b/lib/test.php @@ -0,0 +1,98 @@ +. + +*/ + +//! 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; + } + +} diff --git a/lib/utf.php b/lib/utf.php new file mode 100644 index 0000000..34b8230 --- /dev/null +++ b/lib/utf.php @@ -0,0 +1,199 @@ +. + +*/ + +//! 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)); + } + +} diff --git a/lib/web.php b/lib/web.php new file mode 100644 index 0000000..05aecac --- /dev/null +++ b/lib/web.php @@ -0,0 +1,1017 @@ +. + +*/ + +//! Wrapper for various HTTP utilities +class Web extends Prefab { + + //@{ Error messages + const + E_Request='No suitable HTTP request engine found'; + //@} + + protected + //! HTTP request engine + $wrapper; + + /** + * Detect MIME type using file extension or file inspection + * @return string + * @param $file string + * @param $inspect bool + **/ + function mime($file, $inspect=FALSE) { + if ($inspect) { + if (is_file($file) && is_readable($file)) { + // physical files + if (extension_loaded('fileinfo')) + $mime=mime_content_type($file); + elseif (preg_match('/Darwin/i',PHP_OS)) + $mime=trim(exec('file -bI '.escapeshellarg($file))); + elseif (!preg_match('/^win/i',PHP_OS)) + $mime=trim(exec('file -bi '.escapeshellarg($file))); + if (isset($mime) && !empty($mime)){ + // cut charset information if any + $exp=explode(';',$mime,2); + $mime=$exp[0]; + } + } + else { + // remote and stream files + if (ini_get('allow_url_fopen') && ($fhandle=fopen($file,'rb'))) { + // only get head bytes instead of whole file + $bytes=fread($fhandle,20); + fclose($fhandle); + } + elseif (($response=$this->request($file,['method' => 'HEAD'])) + && preg_grep('/HTTP\/[\d.]{1,3} 200/',$response['headers']) + && ($type = preg_grep('/^Content-Type:/i',$response['headers']))) { + // get mime type directly from response header + return preg_replace('/^Content-Type:\s*/i','',array_pop($type)); + } + else // load whole file + $bytes=file_get_contents($file); + if (extension_loaded('fileinfo')) { + // get mime from fileinfo + $finfo=finfo_open(FILEINFO_MIME_TYPE); + $mime=finfo_buffer($finfo,$bytes); + } + elseif ($bytes) { + // magic number header fallback + $map=[ + '\x64\x6E\x73\x2E'=>'audio/basic', + '\x52\x49\x46\x46.{4}\x41\x56\x49\x20\x4C\x49\x53\x54'=>'video/avi', + '\x42\x4d'=>'image/bmp', + '\x42\x5A\x68'=>'application/x-bzip2', + '\x07\x64\x74\x32\x64\x64\x74\x64'=>'application/xml-dtd', + '\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1'=>'application/msword', + '\x50\x4B\x03\x04\x14\x00\x06\x00'=>'application/msword', + '\x0D\x44\x4F\x43'=>'application/msword', + 'GIF\d+a'=>'image/gif', + '\x1F\x8B'=>'application/x-gzip', + '\xff\xd8\xff'=>'image/jpeg', + '\x49\x46\x00'=>'image/jpeg', + '\xFF\xFB'=>'audio/mpeg', + '\x49\x44\x33'=>'audio/mpeg', + '\x00\x00\x01\xBA'=>'video/mpeg', + '\x4F\x67\x67\x53\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00'=>'audio/vorbis', + '\x25\x50\x44\x46'=>'application/pdf', + '\x89PNG\x0d\x0a'=>'image/png', + '.{4}\x6D\x6F\x6F\x76\x'=>'video/quicktime', + '\x53\x49\x54\x21\x00'=>'application/x-stuffit', + '\x43\x57\x53'=>'application/x-shockwave-flash', + '\x1F\x8B\x08'=>'application/x-tar', + '\x49\x20\x49'=>'image/tiff', + '\x52\x49\x46\x46.{4}\x57\x41\x56\x45\x66\x6D\x74\x20'=>'audio/wav', + '\xFD\xFF\xFF\xFF\x20\x00\x00\x00'=>'application/vnd.ms-excel', + '\x50\x4B\x03\x04'=>'application/x-zip-compressed', + '[ -~]+$'=>'text/plain', + ]; + foreach ($map as $key=>$val) + if (preg_match('/^'.$key.'/',substr($bytes,0,128))) + return $val; + } + } + if (isset($mime) && !empty($mime)) + return $mime; + // Fallback to file extension-based check if no mime was found yet + } + if (preg_match('/\w+$/',$file,$ext)) { + $map=[ + 'au'=>'audio/basic', + 'avi'=>'video/avi', + 'bmp'=>'image/bmp', + 'bz2'=>'application/x-bzip2', + 'css'=>'text/css', + 'dtd'=>'application/xml-dtd', + 'doc'=>'application/msword', + 'gif'=>'image/gif', + 'gz'=>'application/x-gzip', + 'hqx'=>'application/mac-binhex40', + 'html?'=>'text/html', + 'jar'=>'application/java-archive', + 'jpe?g|jfif?'=>'image/jpeg', + 'js'=>'application/x-javascript', + 'midi'=>'audio/x-midi', + 'mp3'=>'audio/mpeg', + 'mpe?g'=>'video/mpeg', + 'ogg'=>'audio/vorbis', + 'pdf'=>'application/pdf', + 'png'=>'image/png', + 'ppt'=>'application/vnd.ms-powerpoint', + 'ps'=>'application/postscript', + 'qt'=>'video/quicktime', + 'ram?'=>'audio/x-pn-realaudio', + 'rdf'=>'application/rdf', + 'rtf'=>'application/rtf', + 'sgml?'=>'text/sgml', + 'sit'=>'application/x-stuffit', + 'svg'=>'image/svg+xml', + 'swf'=>'application/x-shockwave-flash', + 'tgz'=>'application/x-tar', + 'tiff'=>'image/tiff', + 'txt'=>'text/plain', + 'wav'=>'audio/wav', + 'xls'=>'application/vnd.ms-excel', + 'xml'=>'application/xml', + 'zip'=>'application/x-zip-compressed' + ]; + foreach ($map as $key=>$val) + if (preg_match('/'.$key.'/',strtolower($ext[0]))) + return $val; + } + return 'application/octet-stream'; + } + + /** + * Return the MIME types stated in the HTTP Accept header as an array; + * If a list of MIME types is specified, return the best match; or + * FALSE if none found + * @return array|string|FALSE + * @param $list string|array + **/ + function acceptable($list=NULL) { + $accept=[]; + foreach (explode(',',str_replace(' ','',@$_SERVER['HTTP_ACCEPT'])) + as $mime) + if (preg_match('/(.+?)(?:;q=([\d\.]+)|$)/',$mime,$parts)) + $accept[$parts[1]]=isset($parts[2])?$parts[2]:1; + if (!$accept) + $accept['*/*']=1; + else { + krsort($accept); + arsort($accept); + } + if ($list) { + if (is_string($list)) + $list=explode(',',$list); + foreach ($accept as $mime=>$q) + if ($q && $out=preg_grep('/'. + str_replace('\*','.*',preg_quote($mime,'/')).'/',$list)) + return current($out); + return FALSE; + } + return $accept; + } + + /** + * Transmit file to HTTP client; Return file size if successful, + * FALSE otherwise + * @return int|FALSE + * @param $file string + * @param $mime string + * @param $kbps int + * @param $force bool + * @param $name string + * @param $flush bool + **/ + function send($file,$mime=NULL,$kbps=0,$force=TRUE,$name=NULL,$flush=TRUE) { + if (!is_file($file)) + return FALSE; + $size=filesize($file); + if (PHP_SAPI!='cli') { + header('Content-Type: '.($mime?:$this->mime($file))); + if ($force) + header('Content-Disposition: attachment; '. + 'filename="'.($name!==NULL?$name:basename($file)).'"'); + header('Accept-Ranges: bytes'); + header('Content-Length: '.$size); + header('X-Powered-By: '.Base::instance()->PACKAGE); + } + if (!$kbps && $flush) { + while (ob_get_level()) + ob_end_clean(); + readfile($file); + } + else { + $ctr=0; + $handle=fopen($file,'rb'); + $start=microtime(TRUE); + while (!feof($handle) && + ($info=stream_get_meta_data($handle)) && + !$info['timed_out'] && !connection_aborted()) { + if ($kbps) { + // Throttle output + $ctr++; + if ($ctr/$kbps>$elapsed=microtime(TRUE)-$start) + usleep(1e6*($ctr/$kbps-$elapsed)); + } + // Send 1KiB and reset timer + echo fread($handle,1024); + if ($flush) { + ob_flush(); + flush(); + } + } + fclose($handle); + } + return $size; + } + + /** + * Receive file(s) from HTTP client + * @return array|bool + * @param $func callback + * @param $overwrite bool + * @param $slug callback|bool + **/ + function receive($func=NULL,$overwrite=FALSE,$slug=TRUE) { + $fw=Base::instance(); + $dir=$fw->UPLOADS; + if (!is_dir($dir)) + mkdir($dir,Base::MODE,TRUE); + if ($fw->VERB=='PUT') { + $tmp=$fw->TEMP.$fw->SEED.'.'.$fw->hash(uniqid()); + if (!$fw->RAW) + $fw->write($tmp,$fw->BODY); + else { + $src=@fopen('php://input','r'); + $dst=@fopen($tmp,'w'); + if (!$src || !$dst) + return FALSE; + while (!feof($src) && + ($info=stream_get_meta_data($src)) && + !$info['timed_out'] && $str=fgets($src,4096)) + fputs($dst,$str,strlen($str)); + fclose($dst); + fclose($src); + } + $base=basename($fw->URI); + $file=[ + 'name'=>$dir. + ($slug && preg_match('/(.+?)(\.\w+)?$/',$base,$parts)? + (is_callable($slug)? + $slug($base): + ($this->slug($parts[1]). + (isset($parts[2])?$parts[2]:''))): + $base), + 'tmp_name'=>$tmp, + 'type'=>$this->mime($base), + 'size'=>filesize($tmp) + ]; + return (!file_exists($file['name']) || $overwrite) && + (!$func || $fw->call($func,[$file])!==FALSE) && + rename($tmp,$file['name']); + } + $fetch=function($arr) use(&$fetch) { + if (!is_array($arr)) + return [$arr]; + $data=[]; + foreach($arr as $k=>$sub) + $data=array_merge($data,$fetch($sub)); + return $data; + }; + $out=[]; + foreach ($_FILES as $name=>$item) { + $files=[]; + foreach ($item as $k=>$mix) + foreach ($fetch($mix) as $i=>$val) + $files[$i][$k]=$val; + foreach ($files as $file) { + if (empty($file['name'])) + continue; + $base=basename($file['name']); + $file['name']=$dir. + ($slug && preg_match('/(.+?)(\.\w+)?$/',$base,$parts)? + (is_callable($slug)? + $slug($base,$name): + ($this->slug($parts[1]). + (isset($parts[2])?$parts[2]:''))): + $base); + $out[$file['name']]=!$file['error'] && + (!file_exists($file['name']) || $overwrite) && + (!$func || $fw->call($func,[$file,$name])!==FALSE) && + move_uploaded_file($file['tmp_name'],$file['name']); + } + } + return $out; + } + + /** + * Return upload progress in bytes, FALSE on failure + * @return int|FALSE + * @param $id string + **/ + function progress($id) { + // ID returned by session.upload_progress.name + return ini_get('session.upload_progress.enabled') && + isset($_SESSION[$id]['bytes_processed'])? + $_SESSION[$id]['bytes_processed']:FALSE; + } + + /** + * HTTP request via cURL + * @return array + * @param $url string + * @param $options array + **/ + protected function _curl($url,$options) { + $curl=curl_init($url); + if (!$open_basedir=ini_get('open_basedir')) + curl_setopt($curl,CURLOPT_FOLLOWLOCATION, + $options['follow_location']); + curl_setopt($curl,CURLOPT_MAXREDIRS, + $options['max_redirects']); + curl_setopt($curl,CURLOPT_PROTOCOLS,CURLPROTO_HTTP|CURLPROTO_HTTPS); + curl_setopt($curl,CURLOPT_REDIR_PROTOCOLS,CURLPROTO_HTTP|CURLPROTO_HTTPS); + curl_setopt($curl,CURLOPT_CUSTOMREQUEST,$options['method']); + if (isset($options['header'])) + curl_setopt($curl,CURLOPT_HTTPHEADER,$options['header']); + if (isset($options['content'])) + curl_setopt($curl,CURLOPT_POSTFIELDS,$options['content']); + if (isset($options['proxy'])) + curl_setopt($curl,CURLOPT_PROXY,$options['proxy']); + curl_setopt($curl,CURLOPT_ENCODING,'gzip,deflate'); + $timeout=isset($options['timeout'])? + $options['timeout']: + ini_get('default_socket_timeout'); + curl_setopt($curl,CURLOPT_CONNECTTIMEOUT,$timeout); + curl_setopt($curl,CURLOPT_TIMEOUT,$timeout); + $headers=[]; + curl_setopt($curl,CURLOPT_HEADERFUNCTION, + // Callback for response headers + function($curl,$line) use(&$headers) { + if ($trim=trim($line)) + $headers[]=$trim; + return strlen($line); + } + ); + curl_setopt($curl,CURLOPT_SSL_VERIFYHOST,2); + curl_setopt($curl,CURLOPT_SSL_VERIFYPEER,FALSE); + ob_start(); + curl_exec($curl); + $err=curl_error($curl); + curl_close($curl); + $body=ob_get_clean(); + if (!$err && + $options['follow_location'] && $open_basedir && + preg_grep('/HTTP\/[\d.]{1,3} 3\d{2}/',$headers) && + preg_match('/^Location: (.+)$/m',implode(PHP_EOL,$headers),$loc)) { + $options['max_redirects']--; + if($loc[1][0] == '/') { + $parts=parse_url($url); + $loc[1]=$parts['scheme'].'://'.$parts['host']. + ((isset($parts['port']) && !in_array($parts['port'],[80,443])) + ?':'.$parts['port']:'').$loc[1]; + } + return $this->request($loc[1],$options); + } + return [ + 'body'=>$body, + 'headers'=>$headers, + 'engine'=>'cURL', + 'cached'=>FALSE, + 'error'=>$err + ]; + } + + /** + * HTTP request via PHP stream wrapper + * @return array + * @param $url string + * @param $options array + **/ + protected function _stream($url,$options) { + $eol="\r\n"; + if (isset($options['proxy'])) { + $options['proxy']=preg_replace('/https?/i','tcp',$options['proxy']); + $options['request_fulluri']=true; + if (preg_match('/socks4?/i',$options['proxy'])) + return $this->_socket($url,$options); + } + $options['header']=implode($eol,$options['header']); + $body=@file_get_contents($url,FALSE, + stream_context_create(['http'=>$options])); + $headers=isset($http_response_header)? + $http_response_header:[]; + $err=''; + if (is_string($body)) { + $match=NULL; + foreach ($headers as $header) + if (preg_match('/Content-Encoding: (.+)/i',$header,$match)) + break; + if ($match) + switch ($match[1]) { + case 'gzip': + $body=gzdecode($body); + break; + case 'deflate': + $body=gzuncompress($body); + break; + } + } + else { + $tmp=error_get_last(); + $err=$tmp['message']; + } + return [ + 'body'=>$body, + 'headers'=>$headers, + 'engine'=>'stream', + 'cached'=>FALSE, + 'error'=>$err + ]; + } + + /** + * HTTP request via low-level TCP/IP socket + * @return array + * @param $url string + * @param $options array + **/ + protected function _socket($url,$options) { + $eol="\r\n"; + $headers=[]; + $body=''; + $parts=parse_url($url); + $hostname=$parts['host']; + $proxy=false; + if ($parts['scheme']=='https') + $parts['host']='ssl://'.$parts['host']; + if (empty($parts['port'])) + $parts['port']=$parts['scheme']=='https'?443:80; + if (empty($parts['path'])) + $parts['path']='/'; + if (empty($parts['query'])) + $parts['query']=''; + if (isset($options['proxy'])) { + $req=$url; + $pp=parse_url($options['proxy']); + $proxy=$pp['scheme']; + if ($pp['scheme']=='https') + $pp['host']='ssl://'.$pp['host']; + if (empty($pp['port'])) + $pp['port']=$pp['scheme']=='https'?443:80; + $socket=@fsockopen($pp['host'],$pp['port'],$code,$err); + } else { + $req=$parts['path'].($parts['query']?('?'.$parts['query']):''); + $socket=@fsockopen($parts['host'],$parts['port'],$code,$err); + } + if ($socket) { + stream_set_blocking($socket,TRUE); + stream_set_timeout($socket,isset($options['timeout'])? + $options['timeout']:ini_get('default_socket_timeout')); + if ($proxy=='socks4') { + // SOCKS4; http://en.wikipedia.org/wiki/SOCKS#Protocol + $packet="\x04\x01".pack("n", $parts['port']). + pack("H*",dechex(ip2long(gethostbyname($hostname))))."\0"; + fputs($socket, $packet, strlen($packet)); + $response=fread($socket, 9); + if (strlen($response)==8 && (ord($response[0])==0 || ord($response[0])==4) + && ord($response[1])==90) { + $options['header'][]='Host: '.$hostname; + } else + $err='Socket Status '.ord($response[1]); + } + fputs($socket,$options['method'].' '.$req.' HTTP/1.0'.$eol); + fputs($socket,implode($eol,$options['header']).$eol.$eol); + if (isset($options['content'])) + fputs($socket,$options['content'].$eol); + // Get response + $content=''; + while (!feof($socket) && + ($info=stream_get_meta_data($socket)) && + !$info['timed_out'] && !connection_aborted() && + $str=fgets($socket,4096)) + $content.=$str; + fclose($socket); + $html=explode($eol.$eol,$content,2); + $body=isset($html[1])?$html[1]:''; + $headers=array_merge($headers,$current=explode($eol,$html[0])); + $match=NULL; + foreach ($current as $header) + if (preg_match('/Content-Encoding: (.+)/i',$header,$match)) + break; + if ($match) + switch ($match[1]) { + case 'gzip': + $body=gzdecode($body); + break; + case 'deflate': + $body=gzuncompress($body); + break; + } + if ($options['follow_location'] && + preg_grep('/HTTP\/[\d.]{1,3} 3\d{2}/',$headers) && + preg_match('/Location: (.+?)'.preg_quote($eol).'/', + $html[0],$loc)) { + $options['max_redirects']--; + return $this->request($loc[1],$options); + } + } + return [ + 'body'=>$body, + 'headers'=>$headers, + 'engine'=>'socket', + 'cached'=>FALSE, + 'error'=>$err + ]; + } + + /** + * Specify the HTTP request engine to use; If not available, + * fall back to an applicable substitute + * @return string + * @param $arg string + **/ + function engine($arg='curl') { + $arg=strtolower($arg); + $flags=[ + 'curl'=>extension_loaded('curl'), + 'stream'=>ini_get('allow_url_fopen'), + 'socket'=>function_exists('fsockopen') + ]; + if ($flags[$arg]) + return $this->wrapper=$arg; + foreach ($flags as $key=>$val) + if ($val) + return $this->wrapper=$key; + user_error(self::E_Request,E_USER_ERROR); + } + + /** + * Replace old headers with new elements + * @return NULL + * @param $old array + * @param $new string|array + **/ + function subst(array &$old,$new) { + if (is_string($new)) + $new=[$new]; + foreach ($new as $hdr) { + $old=preg_grep('/'.preg_quote(strstr($hdr,':',TRUE),'/').':.+/', + $old,PREG_GREP_INVERT); + array_push($old,$hdr); + } + } + + /** + * Submit HTTP request; Use HTTP context options (described in + * http://www.php.net/manual/en/context.http.php) if specified; + * Cache the page as instructed by remote server + * @return array|FALSE + * @param $url string + * @param $options array + **/ + function request($url,array $options=NULL) { + $fw=Base::instance(); + $parts=parse_url($url); + if (empty($parts['scheme'])) { + // Local URL + $url=$fw->SCHEME.'://'.$fw->HOST. + (in_array($fw->PORT,[80,443])?'':(':'.$fw->PORT)). + ($url[0]!='/'?($fw->BASE.'/'):'').$url; + $parts=parse_url($url); + } + elseif (!preg_match('/https?/',$parts['scheme'])) + return FALSE; + if (!is_array($options)) + $options=[]; + if (empty($options['header'])) + $options['header']=[]; + elseif (is_string($options['header'])) + $options['header']=[$options['header']]; + if (!$this->wrapper) + $this->engine(); + if ($this->wrapper!='stream') { + // PHP streams can't cope with redirects when Host header is set + $this->subst($options['header'],'Host: '.$parts['host']); + } + $this->subst($options['header'], + [ + 'Accept-Encoding: gzip,deflate', + 'User-Agent: '.(isset($options['user_agent'])? + $options['user_agent']: + 'Mozilla/5.0 (compatible; '.php_uname('s').')'), + 'Connection: close' + ] + ); + if (isset($options['content']) && is_string($options['content'])) { + if ($options['method']=='POST' && + !preg_grep('/^Content-Type:/i',$options['header'])) + $this->subst($options['header'], + 'Content-Type: application/x-www-form-urlencoded'); + $this->subst($options['header'], + 'Content-Length: '.strlen($options['content'])); + } + if (isset($parts['user'],$parts['pass'])) + $this->subst($options['header'], + 'Authorization: Basic '. + base64_encode($parts['user'].':'.$parts['pass']) + ); + $options+=[ + 'method'=>'GET', + 'header'=>$options['header'], + 'follow_location'=>TRUE, + 'max_redirects'=>20, + 'ignore_errors'=>FALSE + ]; + $eol="\r\n"; + if ($fw->CACHE && + preg_match('/GET|HEAD/',$options['method'])) { + $cache=Cache::instance(); + if ($cache->exists( + $hash=$fw->hash($options['method'].' '.$url).'.url',$data)) { + if (preg_match('/Last-Modified: (.+?)'.preg_quote($eol).'/', + implode($eol,$data['headers']),$mod)) + $this->subst($options['header'], + 'If-Modified-Since: '.$mod[1]); + } + } + $result=$this->{'_'.$this->wrapper}($url,$options); + if ($result && isset($cache)) { + if (preg_match('/HTTP\/[\d.]{1,3} 304/', + implode($eol,$result['headers']))) { + $result=$cache->get($hash); + $result['cached']=TRUE; + } + elseif (preg_match('/Cache-Control:(?:.*)max-age=(\d+)(?:,?.*'. + preg_quote($eol).')/i',implode($eol,$result['headers']),$exp)) + $cache->set($hash,$result,$exp[1]); + } + $req=[$options['method'].' '.$url]; + foreach ($options['header'] as $header) + array_push($req,$header); + return array_merge(['request'=>$req],$result); + } + + /** + * Strip Javascript/CSS files of extraneous whitespaces and comments; + * Return combined output as a minified string + * @return string + * @param $files string|array + * @param $mime string + * @param $header bool + * @param $path string + **/ + function minify($files,$mime=NULL,$header=TRUE,$path=NULL) { + $fw=Base::instance(); + if (is_string($files)) + $files=$fw->split($files); + if (!$mime) + $mime=$this->mime($files[0]); + preg_match('/\w+$/',$files[0],$ext); + $cache=Cache::instance(); + $dst=''; + if (!isset($path)) + $path=$fw->UI.';./'; + foreach (array_unique($fw->split($path,FALSE)) as $dir) + foreach ($files as $i=>$file) + if (is_file($save=$fw->fixslashes($dir.$file)) && + is_bool(strpos($save,'../')) && + preg_match('/\.(css|js)$/i',$file)) { + unset($files[$i]); + if ($fw->CACHE && + ($cached=$cache->exists( + $hash=$fw->hash($save).'.'.$ext[0],$data)) && + $cached[0]>filemtime($save)) + $dst.=$data; + else { + $data=''; + $src=$fw->read($save); + for ($ptr=0,$len=strlen($src);$ptr<$len;) { + if (preg_match('/^@import\h+url'. + '\(\h*([\'"])((?!(?:https?:)?\/\/).+?)\1\h*\)[^;]*;/', + substr($src,$ptr),$parts)) { + $path=dirname($file); + $data.=$this->minify( + ($path?($path.'/'):'').$parts[2], + $mime,$header + ); + $ptr+=strlen($parts[0]); + continue; + } + if ($ext[0]=='css'&&preg_match('/^url\(([^\'"].*?[^\'"])\)/i', + substr($src,$ptr),$parts)) { + $data.=$parts[0]; + $ptr+=strlen($parts[0]); + continue; + } + if ($src[$ptr]=='/') { + if ($src[$ptr+1]=='*') { + // Multiline comment + $str=strstr( + substr($src,$ptr+2),'*/',TRUE); + $ptr+=strlen($str)+4; + } + elseif ($src[$ptr+1]=='/') { + // Single-line comment + $str=strstr( + substr($src,$ptr+2),"\n",TRUE); + $ptr+=(empty($str))? + strlen(substr($src,$ptr)):strlen($str)+2; + } + else { + // Presume it's a regex pattern + $regex=TRUE; + // Backtrack and validate + for ($ofs=$ptr;$ofs;$ofs--) { + // Pattern should be preceded by + // open parenthesis, colon, + // object property or operator + if (preg_match( + '/(return|[(:=!+\-*&|])$/', + substr($src,0,$ofs))) { + $data.='/'; + $ptr++; + while ($ptr<$len) { + $data.=$src[$ptr]; + $ptr++; + if ($src[$ptr-1]=='\\') { + $data.=$src[$ptr]; + $ptr++; + } + elseif ($src[$ptr-1]=='/') + break; + } + break; + } + elseif (!ctype_space($src[$ofs-1])) { + // Not a regex pattern + $regex=FALSE; + break; + } + } + if (!$regex) { + // Division operator + $data.=$src[$ptr]; + $ptr++; + } + } + continue; + } + if (in_array($src[$ptr],['\'','"','`'])) { + $match=$src[$ptr]; + $data.=$match; + $ptr++; + // String literal + while ($ptr<$len) { + $data.=$src[$ptr]; + $ptr++; + if ($src[$ptr-1]=='\\') { + $data.=$src[$ptr]; + $ptr++; + } + elseif ($src[$ptr-1]==$match) + break; + } + continue; + } + if (ctype_space($src[$ptr])) { + if ($ptr+1CACHE) + $cache->set($hash,$data); + $dst.=$data; + } + } + if (PHP_SAPI!='cli' && $header) + header('Content-Type: '.$mime.'; charset='.$fw->ENCODING); + return $dst; + } + + /** + * Retrieve RSS feed and return as an array + * @return array|FALSE + * @param $url string + * @param $max int + * @param $tags string + **/ + function rss($url,$max=10,$tags=NULL) { + if (!$data=$this->request($url)) + return FALSE; + // Suppress errors caused by invalid XML structures + libxml_use_internal_errors(TRUE); + $xml=simplexml_load_string($data['body'], + NULL,LIBXML_NOBLANKS|LIBXML_NOERROR); + if (!is_object($xml)) + return FALSE; + $out=[]; + if (isset($xml->channel)) { + $out['source']=(string)$xml->channel->title; + $max=min($max,count($xml->channel->item)); + for ($i=0;$i<$max;$i++) { + $item=$xml->channel->item[$i]; + $list=[''=>NULL]+$item->getnamespaces(TRUE); + $fields=[]; + foreach ($list as $ns=>$uri) + foreach ($item->children($uri) as $key=>$val) + $fields[$ns.($ns?':':'').$key]=(string)$val; + $out['feed'][]=$fields; + } + } + else + return FALSE; + Base::instance()->scrub($out,$tags); + return $out; + } + + /** + * Retrieve information from whois server + * @return string|FALSE + * @param $addr string + * @param $server string + **/ + function whois($addr,$server='whois.internic.net') { + $socket=@fsockopen($server,43,$errno,$errstr); + if (!$socket) + // Can't establish connection + return FALSE; + // Set connection timeout parameters + stream_set_blocking($socket,FALSE); + stream_set_timeout($socket,ini_get('default_socket_timeout')); + // Send request + fputs($socket,$addr."\r\n"); + $info=stream_get_meta_data($socket); + // Get response + $response=''; + while (!feof($socket) && !$info['timed_out']) { + $response.=fgets($socket,4096); // MDFK97 + $info=stream_get_meta_data($socket); + } + fclose($socket); + return $info['timed_out']?FALSE:trim($response); + } + + /** + * Return preset diacritics translation table + * @return array + **/ + function diacritics() { + return [ + 'Ǎ'=>'A','А'=>'A','Ā'=>'A','Ă'=>'A','Ą'=>'A','Å'=>'A', + 'Ǻ'=>'A','Ä'=>'Ae','Á'=>'A','À'=>'A','Ã'=>'A','Â'=>'A', + 'Æ'=>'AE','Ǽ'=>'AE','Б'=>'B','Ç'=>'C','Ć'=>'C','Ĉ'=>'C', + 'Č'=>'C','Ċ'=>'C','Ц'=>'C','Ч'=>'Ch','Ð'=>'Dj','Đ'=>'Dj', + 'Ď'=>'Dj','Д'=>'Dj','É'=>'E','Ę'=>'E','Ё'=>'E','Ė'=>'E', + 'Ê'=>'E','Ě'=>'E','Ē'=>'E','È'=>'E','Е'=>'E','Э'=>'E', + 'Ë'=>'E','Ĕ'=>'E','Ф'=>'F','Г'=>'G','Ģ'=>'G','Ġ'=>'G', + 'Ĝ'=>'G','Ğ'=>'G','Х'=>'H','Ĥ'=>'H','Ħ'=>'H','Ï'=>'I', + 'Ĭ'=>'I','İ'=>'I','Į'=>'I','Ī'=>'I','Í'=>'I','Ì'=>'I', + 'И'=>'I','Ǐ'=>'I','Ĩ'=>'I','Î'=>'I','IJ'=>'IJ','Ĵ'=>'J', + 'Й'=>'J','Я'=>'Ja','Ю'=>'Ju','К'=>'K','Ķ'=>'K','Ĺ'=>'L', + 'Л'=>'L','Ł'=>'L','Ŀ'=>'L','Ļ'=>'L','Ľ'=>'L','М'=>'M', + 'Н'=>'N','Ń'=>'N','Ñ'=>'N','Ņ'=>'N','Ň'=>'N','Ō'=>'O', + 'О'=>'O','Ǿ'=>'O','Ǒ'=>'O','Ơ'=>'O','Ŏ'=>'O','Ő'=>'O', + 'Ø'=>'O','Ö'=>'Oe','Õ'=>'O','Ó'=>'O','Ò'=>'O','Ô'=>'O', + 'Œ'=>'OE','П'=>'P','Ŗ'=>'R','Р'=>'R','Ř'=>'R','Ŕ'=>'R', + 'Ŝ'=>'S','Ş'=>'S','Š'=>'S','Ș'=>'S','Ś'=>'S','С'=>'S', + 'Ш'=>'Sh','Щ'=>'Shch','Ť'=>'T','Ŧ'=>'T','Ţ'=>'T','Ț'=>'T', + 'Т'=>'T','Ů'=>'U','Ű'=>'U','Ŭ'=>'U','Ũ'=>'U','Ų'=>'U', + 'Ū'=>'U','Ǜ'=>'U','Ǚ'=>'U','Ù'=>'U','Ú'=>'U','Ü'=>'Ue', + 'Ǘ'=>'U','Ǖ'=>'U','У'=>'U','Ư'=>'U','Ǔ'=>'U','Û'=>'U', + 'В'=>'V','Ŵ'=>'W','Ы'=>'Y','Ŷ'=>'Y','Ý'=>'Y','Ÿ'=>'Y', + 'Ź'=>'Z','З'=>'Z','Ż'=>'Z','Ž'=>'Z','Ж'=>'Zh','á'=>'a', + 'ă'=>'a','â'=>'a','à'=>'a','ā'=>'a','ǻ'=>'a','å'=>'a', + 'ä'=>'ae','ą'=>'a','ǎ'=>'a','ã'=>'a','а'=>'a','ª'=>'a', + 'æ'=>'ae','ǽ'=>'ae','б'=>'b','č'=>'c','ç'=>'c','ц'=>'c', + 'ċ'=>'c','ĉ'=>'c','ć'=>'c','ч'=>'ch','ð'=>'dj','ď'=>'dj', + 'д'=>'dj','đ'=>'dj','э'=>'e','é'=>'e','ё'=>'e','ë'=>'e', + 'ê'=>'e','е'=>'e','ĕ'=>'e','è'=>'e','ę'=>'e','ě'=>'e', + 'ė'=>'e','ē'=>'e','ƒ'=>'f','ф'=>'f','ġ'=>'g','ĝ'=>'g', + 'ğ'=>'g','г'=>'g','ģ'=>'g','х'=>'h','ĥ'=>'h','ħ'=>'h', + 'ǐ'=>'i','ĭ'=>'i','и'=>'i','ī'=>'i','ĩ'=>'i','į'=>'i', + 'ı'=>'i','ì'=>'i','î'=>'i','í'=>'i','ï'=>'i','ij'=>'ij', + 'ĵ'=>'j','й'=>'j','я'=>'ja','ю'=>'ju','ķ'=>'k','к'=>'k', + 'ľ'=>'l','ł'=>'l','ŀ'=>'l','ĺ'=>'l','ļ'=>'l','л'=>'l', + 'м'=>'m','ņ'=>'n','ñ'=>'n','ń'=>'n','н'=>'n','ň'=>'n', + 'ʼn'=>'n','ó'=>'o','ò'=>'o','ǒ'=>'o','ő'=>'o','о'=>'o', + 'ō'=>'o','º'=>'o','ơ'=>'o','ŏ'=>'o','ô'=>'o','ö'=>'oe', + 'õ'=>'o','ø'=>'o','ǿ'=>'o','œ'=>'oe','п'=>'p','р'=>'r', + 'ř'=>'r','ŕ'=>'r','ŗ'=>'r','ſ'=>'s','ŝ'=>'s','ș'=>'s', + 'š'=>'s','ś'=>'s','с'=>'s','ş'=>'s','ш'=>'sh','щ'=>'shch', + 'ß'=>'ss','ţ'=>'t','т'=>'t','ŧ'=>'t','ť'=>'t','ț'=>'t', + 'у'=>'u','ǘ'=>'u','ŭ'=>'u','û'=>'u','ú'=>'u','ų'=>'u', + 'ù'=>'u','ű'=>'u','ů'=>'u','ư'=>'u','ū'=>'u','ǚ'=>'u', + 'ǜ'=>'u','ǔ'=>'u','ǖ'=>'u','ũ'=>'u','ü'=>'ue','в'=>'v', + 'ŵ'=>'w','ы'=>'y','ÿ'=>'y','ý'=>'y','ŷ'=>'y','ź'=>'z', + 'ž'=>'z','з'=>'z','ż'=>'z','ж'=>'zh','ь'=>'','ъ'=>'', + 'њ'=>'nj','љ'=>'lj','ђ'=>'dj','џ'=>'dz','ћ'=>'c','ј'=>'j', + '\''=>'', + ]; + } + + /** + * Return a URL/filesystem-friendly version of string + * @return string + * @param $text string + **/ + function slug($text) { + return trim(strtolower(preg_replace('/([^\pL\pN])+/u','-', + trim(strtr($text,Base::instance()->DIACRITICS+$this->diacritics())))),'-'); + } + + /** + * Return chunk of text from standard Lorem Ipsum passage + * @return string + * @param $count int + * @param $max int + * @param $std bool + **/ + function filler($count=1,$max=20,$std=TRUE) { + $out=''; + if ($std) + $out='Lorem ipsum dolor sit amet, consectetur adipisicing elit, '. + 'sed do eiusmod tempor incididunt ut labore et dolore magna '. + 'aliqua.'; + $rnd=explode(' ', + 'a ab ad accusamus adipisci alias aliquam amet animi aperiam '. + 'architecto asperiores aspernatur assumenda at atque aut beatae '. + 'blanditiis cillum commodi consequatur corporis corrupti culpa '. + 'cum cupiditate debitis delectus deleniti deserunt dicta '. + 'dignissimos distinctio dolor ducimus duis ea eaque earum eius '. + 'eligendi enim eos error esse est eum eveniet ex excepteur '. + 'exercitationem expedita explicabo facere facilis fugiat harum '. + 'hic id illum impedit in incidunt ipsa iste itaque iure iusto '. + 'laborum laudantium libero magnam maiores maxime minim minus '. + 'modi molestiae mollitia nam natus necessitatibus nemo neque '. + 'nesciunt nihil nisi nobis non nostrum nulla numquam occaecati '. + 'odio officia omnis optio pariatur perferendis perspiciatis '. + 'placeat porro possimus praesentium proident quae quia quibus '. + 'quo ratione recusandae reiciendis rem repellat reprehenderit '. + 'repudiandae rerum saepe sapiente sequi similique sint soluta '. + 'suscipit tempora tenetur totam ut ullam unde vel veniam vero '. + 'vitae voluptas'); + for ($i=0,$add=$count-(int)$std;$i<$add;$i++) { + shuffle($rnd); + $words=array_slice($rnd,0,mt_rand(3,$max)); + $out.=(!$std&&$i==0?'':' ').ucfirst(implode(' ',$words)).'.'; + } + return $out; + } + +} + +if (!function_exists('gzdecode')) { + + /** + * Decode gzip-compressed string + * @param $str string + * @return string + **/ + function gzdecode($str) { + $fw=Base::instance(); + if (!is_dir($tmp=$fw->TEMP)) + mkdir($tmp,Base::MODE,TRUE); + file_put_contents($file=$tmp.'/'.$fw->SEED.'.'. + $fw->hash(uniqid(NULL,TRUE)).'.gz',$str,LOCK_EX); + ob_start(); + readgzfile($file); + $out=ob_get_clean(); + @unlink($file); + return $out; + } + +} diff --git a/lib/web/geo.php b/lib/web/geo.php new file mode 100644 index 0000000..98ba204 --- /dev/null +++ b/lib/web/geo.php @@ -0,0 +1,111 @@ +. + +*/ + +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; + } + +} diff --git a/lib/web/google/recaptcha.php b/lib/web/google/recaptcha.php new file mode 100644 index 0000000..38fd2d0 --- /dev/null +++ b/lib/web/google/recaptcha.php @@ -0,0 +1,58 @@ +. + +*/ + +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']; + } + +} diff --git a/lib/web/google/staticmap.php b/lib/web/google/staticmap.php new file mode 100644 index 0000000..023103d --- /dev/null +++ b/lib/web/google/staticmap.php @@ -0,0 +1,65 @@ +. + +*/ + +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; + } + +} diff --git a/lib/web/oauth2.php b/lib/web/oauth2.php new file mode 100644 index 0000000..7a04602 --- /dev/null +++ b/lib/web/oauth2.php @@ -0,0 +1,163 @@ +. + +*/ + +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=[]; + } + +} + diff --git a/lib/web/openid.php b/lib/web/openid.php new file mode 100644 index 0000000..c940af3 --- /dev/null +++ b/lib/web/openid.php @@ -0,0 +1,248 @@ +. + +*/ + +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( + '/^/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]); + } + +} diff --git a/lib/web/pingback.php b/lib/web/pingback.php new file mode 100644 index 0000000..28c51a5 --- /dev/null +++ b/lib/web/pingback.php @@ -0,0 +1,176 @@ +. + +*/ + +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('//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); + } + +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..29233c0 --- /dev/null +++ b/manifest.json @@ -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" +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..dbaf226 --- /dev/null +++ b/readme.md @@ -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. diff --git a/ui/css/base.css b/ui/css/base.css new file mode 100644 index 0000000..531487c --- /dev/null +++ b/ui/css/base.css @@ -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}} \ No newline at end of file diff --git a/ui/css/font-awesome.min.css b/ui/css/font-awesome.min.css new file mode 100644 index 0000000..540440c --- /dev/null +++ b/ui/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/ui/css/theme.css b/ui/css/theme.css new file mode 100644 index 0000000..e20da57 --- /dev/null +++ b/ui/css/theme.css @@ -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; + } + +} diff --git a/ui/fonts/FontAwesome.otf b/ui/fonts/FontAwesome.otf new file mode 100644 index 0000000..401ec0f Binary files /dev/null and b/ui/fonts/FontAwesome.otf differ diff --git a/ui/fonts/fontawesome-webfont.eot b/ui/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/ui/fonts/fontawesome-webfont.eot differ diff --git a/ui/fonts/fontawesome-webfont.svg b/ui/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/ui/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/fonts/fontawesome-webfont.ttf b/ui/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/ui/fonts/fontawesome-webfont.ttf differ diff --git a/ui/fonts/fontawesome-webfont.woff b/ui/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/ui/fonts/fontawesome-webfont.woff differ diff --git a/ui/fonts/fontawesome-webfont.woff2 b/ui/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/ui/fonts/fontawesome-webfont.woff2 differ diff --git a/ui/home.htm b/ui/home.htm new file mode 100644 index 0000000..117e414 --- /dev/null +++ b/ui/home.htm @@ -0,0 +1,190 @@ +
    +
    {{message.message}}
    +

    Parancs küldése, kérem várjon.

    +

    No equipments, please add one.

    +
    +
    +
    +

    {{item.name}}

    +
    +
    + + +
    + +
    + + +
    + + + +
    + + +
    + +
    + {{addtext}} +
    +
    +
    +
    + +
    + + + + + + +
    + +
    +
    + + +
    +
    + + +
    + Plug it in! + + +
    +
    +
    +
    + + + +
    + + + + \ No newline at end of file diff --git a/ui/images/btn.png b/ui/images/btn.png new file mode 100644 index 0000000..18639cb Binary files /dev/null and b/ui/images/btn.png differ diff --git a/ui/images/homescreen.png b/ui/images/homescreen.png new file mode 100644 index 0000000..8bfbe1c Binary files /dev/null and b/ui/images/homescreen.png differ diff --git a/ui/js/axios.min.js b/ui/js/axios.min.js new file mode 100644 index 0000000..abdb244 --- /dev/null +++ b/ui/js/axios.min.js @@ -0,0 +1,3 @@ +/* axios v0.21.0 | (c) 2020 by Matt Zabriskie */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";function r(e){var t=new i(e),n=s(i.prototype.request,t);return o.extend(n,i.prototype,t),o.extend(n,t),n}var o=n(2),s=n(3),i=n(4),a=n(22),u=n(10),c=r(u);c.Axios=i,c.create=function(e){return r(a(c.defaults,e))},c.Cancel=n(23),c.CancelToken=n(24),c.isCancel=n(9),c.all=function(e){return Promise.all(e)},c.spread=n(25),e.exports=c,e.exports.default=c},function(e,t,n){"use strict";function r(e){return"[object Array]"===R.call(e)}function o(e){return"undefined"==typeof e}function s(e){return null!==e&&!o(e)&&null!==e.constructor&&!o(e.constructor)&&"function"==typeof e.constructor.isBuffer&&e.constructor.isBuffer(e)}function i(e){return"[object ArrayBuffer]"===R.call(e)}function a(e){return"undefined"!=typeof FormData&&e instanceof FormData}function u(e){var t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function c(e){return"string"==typeof e}function f(e){return"number"==typeof e}function p(e){return null!==e&&"object"==typeof e}function d(e){if("[object Object]"!==R.call(e))return!1;var t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}function l(e){return"[object Date]"===R.call(e)}function h(e){return"[object File]"===R.call(e)}function m(e){return"[object Blob]"===R.call(e)}function y(e){return"[object Function]"===R.call(e)}function g(e){return p(e)&&y(e.pipe)}function v(e){return"undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams}function x(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function w(){return("undefined"==typeof navigator||"ReactNative"!==navigator.product&&"NativeScript"!==navigator.product&&"NS"!==navigator.product)&&("undefined"!=typeof window&&"undefined"!=typeof document)}function b(e,t){if(null!==e&&"undefined"!=typeof e)if("object"!=typeof e&&(e=[e]),r(e))for(var n=0,o=e.length;n=200&&e<300}};u.headers={common:{Accept:"application/json, text/plain, */*"}},s.forEach(["delete","get","head"],function(e){u.headers[e]={}}),s.forEach(["post","put","patch"],function(e){u.headers[e]=s.merge(a)}),e.exports=u},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){r.forEach(e,function(n,r){r!==t&&r.toUpperCase()===t.toUpperCase()&&(e[t]=n,delete e[r])})}},function(e,t,n){"use strict";var r=n(2),o=n(13),s=n(16),i=n(5),a=n(17),u=n(20),c=n(21),f=n(14);e.exports=function(e){return new Promise(function(t,n){var p=e.data,d=e.headers;r.isFormData(p)&&delete d["Content-Type"];var l=new XMLHttpRequest;if(e.auth){var h=e.auth.username||"",m=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";d.Authorization="Basic "+btoa(h+":"+m)}var y=a(e.baseURL,e.url);if(l.open(e.method.toUpperCase(),i(y,e.params,e.paramsSerializer),!0),l.timeout=e.timeout,l.onreadystatechange=function(){if(l&&4===l.readyState&&(0!==l.status||l.responseURL&&0===l.responseURL.indexOf("file:"))){var r="getAllResponseHeaders"in l?u(l.getAllResponseHeaders()):null,s=e.responseType&&"text"!==e.responseType?l.response:l.responseText,i={data:s,status:l.status,statusText:l.statusText,headers:r,config:e,request:l};o(t,n,i),l=null}},l.onabort=function(){l&&(n(f("Request aborted",e,"ECONNABORTED",l)),l=null)},l.onerror=function(){n(f("Network Error",e,null,l)),l=null},l.ontimeout=function(){var t="timeout of "+e.timeout+"ms exceeded";e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),n(f(t,e,"ECONNABORTED",l)),l=null},r.isStandardBrowserEnv()){var g=(e.withCredentials||c(y))&&e.xsrfCookieName?s.read(e.xsrfCookieName):void 0;g&&(d[e.xsrfHeaderName]=g)}if("setRequestHeader"in l&&r.forEach(d,function(e,t){"undefined"==typeof p&&"content-type"===t.toLowerCase()?delete d[t]:l.setRequestHeader(t,e)}),r.isUndefined(e.withCredentials)||(l.withCredentials=!!e.withCredentials),e.responseType)try{l.responseType=e.responseType}catch(t){if("json"!==e.responseType)throw t}"function"==typeof e.onDownloadProgress&&l.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&l.upload&&l.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then(function(e){l&&(l.abort(),n(e),l=null)}),p||(p=null),l.send(p)})}},function(e,t,n){"use strict";var r=n(14);e.exports=function(e,t,n){var o=n.config.validateStatus;n.status&&o&&!o(n.status)?t(r("Request failed with status code "+n.status,n.config,null,n.request,n)):e(n)}},function(e,t,n){"use strict";var r=n(15);e.exports=function(e,t,n,o,s){var i=new Error(e);return r(i,t,n,o,s)}},function(e,t){"use strict";e.exports=function(e,t,n,r,o){return e.config=t,n&&(e.code=n),e.request=r,e.response=o,e.isAxiosError=!0,e.toJSON=function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code}},e}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){return{write:function(e,t,n,o,s,i){var a=[];a.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&a.push("expires="+new Date(n).toGMTString()),r.isString(o)&&a.push("path="+o),r.isString(s)&&a.push("domain="+s),i===!0&&a.push("secure"),document.cookie=a.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},function(e,t,n){"use strict";var r=n(18),o=n(19);e.exports=function(e,t){return e&&!r(t)?o(e,t):t}},function(e,t){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},function(e,t){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},function(e,t,n){"use strict";var r=n(2),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,n,s,i={};return e?(r.forEach(e.split("\n"),function(e){if(s=e.indexOf(":"),t=r.trim(e.substr(0,s)).toLowerCase(),n=r.trim(e.substr(s+1)),t){if(i[t]&&o.indexOf(t)>=0)return;"set-cookie"===t?i[t]=(i[t]?i[t]:[]).concat([n]):i[t]=i[t]?i[t]+", "+n:n}}),i):i}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){function e(e){var t=e;return n&&(o.setAttribute("href",t),t=o.href),o.setAttribute("href",t),{href:o.href,protocol:o.protocol?o.protocol.replace(/:$/,""):"",host:o.host,search:o.search?o.search.replace(/^\?/,""):"",hash:o.hash?o.hash.replace(/^#/,""):"",hostname:o.hostname,port:o.port,pathname:"/"===o.pathname.charAt(0)?o.pathname:"/"+o.pathname}}var t,n=/(msie|trident)/i.test(navigator.userAgent),o=document.createElement("a");return t=e(window.location.href),function(n){var o=r.isString(n)?e(n):n;return o.protocol===t.protocol&&o.host===t.host}}():function(){return function(){return!0}}()},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){function n(e,t){return r.isPlainObject(e)&&r.isPlainObject(t)?r.merge(e,t):r.isPlainObject(t)?r.merge({},t):r.isArray(t)?t.slice():t}function o(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(s[o]=n(void 0,e[o])):s[o]=n(e[o],t[o])}t=t||{};var s={},i=["url","method","data"],a=["headers","auth","proxy","params"],u=["baseURL","transformRequest","transformResponse","paramsSerializer","timeout","timeoutMessage","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","decompress","maxContentLength","maxBodyLength","maxRedirects","transport","httpAgent","httpsAgent","cancelToken","socketPath","responseEncoding"],c=["validateStatus"];r.forEach(i,function(e){r.isUndefined(t[e])||(s[e]=n(void 0,t[e]))}),r.forEach(a,o),r.forEach(u,function(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(s[o]=n(void 0,e[o])):s[o]=n(void 0,t[o])}),r.forEach(c,function(r){r in t?s[r]=n(e[r],t[r]):r in e&&(s[r]=n(void 0,e[r]))});var f=i.concat(a).concat(u).concat(c),p=Object.keys(e).concat(Object.keys(t)).filter(function(e){return f.indexOf(e)===-1});return r.forEach(p,o),s}},function(e,t){"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},function(e,t,n){"use strict";function r(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise(function(e){t=e});var n=this;e(function(e){n.reason||(n.reason=new o(e),t(n.reason))})}var o=n(23);r.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},r.source=function(){var e,t=new r(function(t){e=t});return{token:t,cancel:e}},e.exports=r},function(e,t){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}}])}); +//# sourceMappingURL=axios.min.map \ No newline at end of file diff --git a/ui/js/vue.js b/ui/js/vue.js new file mode 100644 index 0000000..919aa12 --- /dev/null +++ b/ui/js/vue.js @@ -0,0 +1,11965 @@ +/*! + * Vue.js v2.6.12 + * (c) 2014-2020 Evan You + * Released under the MIT License. + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.Vue = factory()); +}(this, function () { 'use strict'; + + /* */ + + var emptyObject = Object.freeze({}); + + // These helpers produce better VM code in JS engines due to their + // explicitness and function inlining. + function isUndef (v) { + return v === undefined || v === null + } + + function isDef (v) { + return v !== undefined && v !== null + } + + function isTrue (v) { + return v === true + } + + function isFalse (v) { + return v === false + } + + /** + * Check if value is primitive. + */ + function isPrimitive (value) { + return ( + typeof value === 'string' || + typeof value === 'number' || + // $flow-disable-line + typeof value === 'symbol' || + typeof value === 'boolean' + ) + } + + /** + * Quick object check - this is primarily used to tell + * Objects from primitive values when we know the value + * is a JSON-compliant type. + */ + function isObject (obj) { + return obj !== null && typeof obj === 'object' + } + + /** + * Get the raw type string of a value, e.g., [object Object]. + */ + var _toString = Object.prototype.toString; + + function toRawType (value) { + return _toString.call(value).slice(8, -1) + } + + /** + * Strict object type check. Only returns true + * for plain JavaScript objects. + */ + function isPlainObject (obj) { + return _toString.call(obj) === '[object Object]' + } + + function isRegExp (v) { + return _toString.call(v) === '[object RegExp]' + } + + /** + * Check if val is a valid array index. + */ + function isValidArrayIndex (val) { + var n = parseFloat(String(val)); + return n >= 0 && Math.floor(n) === n && isFinite(val) + } + + function isPromise (val) { + return ( + isDef(val) && + typeof val.then === 'function' && + typeof val.catch === 'function' + ) + } + + /** + * Convert a value to a string that is actually rendered. + */ + function toString (val) { + return val == null + ? '' + : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) + ? JSON.stringify(val, null, 2) + : String(val) + } + + /** + * Convert an input value to a number for persistence. + * If the conversion fails, return original string. + */ + function toNumber (val) { + var n = parseFloat(val); + return isNaN(n) ? val : n + } + + /** + * Make a map and return a function for checking if a key + * is in that map. + */ + function makeMap ( + str, + expectsLowerCase + ) { + var map = Object.create(null); + var list = str.split(','); + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase + ? function (val) { return map[val.toLowerCase()]; } + : function (val) { return map[val]; } + } + + /** + * Check if a tag is a built-in tag. + */ + var isBuiltInTag = makeMap('slot,component', true); + + /** + * Check if an attribute is a reserved attribute. + */ + var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); + + /** + * Remove an item from an array. + */ + function remove (arr, item) { + if (arr.length) { + var index = arr.indexOf(item); + if (index > -1) { + return arr.splice(index, 1) + } + } + } + + /** + * Check whether an object has the property. + */ + var hasOwnProperty = Object.prototype.hasOwnProperty; + function hasOwn (obj, key) { + return hasOwnProperty.call(obj, key) + } + + /** + * Create a cached version of a pure function. + */ + function cached (fn) { + var cache = Object.create(null); + return (function cachedFn (str) { + var hit = cache[str]; + return hit || (cache[str] = fn(str)) + }) + } + + /** + * Camelize a hyphen-delimited string. + */ + var camelizeRE = /-(\w)/g; + var camelize = cached(function (str) { + return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) + }); + + /** + * Capitalize a string. + */ + var capitalize = cached(function (str) { + return str.charAt(0).toUpperCase() + str.slice(1) + }); + + /** + * Hyphenate a camelCase string. + */ + var hyphenateRE = /\B([A-Z])/g; + var hyphenate = cached(function (str) { + return str.replace(hyphenateRE, '-$1').toLowerCase() + }); + + /** + * Simple bind polyfill for environments that do not support it, + * e.g., PhantomJS 1.x. Technically, we don't need this anymore + * since native bind is now performant enough in most browsers. + * But removing it would mean breaking code that was able to run in + * PhantomJS 1.x, so this must be kept for backward compatibility. + */ + + /* istanbul ignore next */ + function polyfillBind (fn, ctx) { + function boundFn (a) { + var l = arguments.length; + return l + ? l > 1 + ? fn.apply(ctx, arguments) + : fn.call(ctx, a) + : fn.call(ctx) + } + + boundFn._length = fn.length; + return boundFn + } + + function nativeBind (fn, ctx) { + return fn.bind(ctx) + } + + var bind = Function.prototype.bind + ? nativeBind + : polyfillBind; + + /** + * Convert an Array-like object to a real Array. + */ + function toArray (list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret + } + + /** + * Mix properties into target object. + */ + function extend (to, _from) { + for (var key in _from) { + to[key] = _from[key]; + } + return to + } + + /** + * Merge an Array of Objects into a single Object. + */ + function toObject (arr) { + var res = {}; + for (var i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]); + } + } + return res + } + + /* eslint-disable no-unused-vars */ + + /** + * Perform no operation. + * Stubbing args to make Flow happy without leaving useless transpiled code + * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/). + */ + function noop (a, b, c) {} + + /** + * Always return false. + */ + var no = function (a, b, c) { return false; }; + + /* eslint-enable no-unused-vars */ + + /** + * Return the same value. + */ + var identity = function (_) { return _; }; + + /** + * Generate a string containing static keys from compiler modules. + */ + function genStaticKeys (modules) { + return modules.reduce(function (keys, m) { + return keys.concat(m.staticKeys || []) + }, []).join(',') + } + + /** + * Check if two values are loosely equal - that is, + * if they are plain objects, do they have the same shape? + */ + function looseEqual (a, b) { + if (a === b) { return true } + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = Array.isArray(a); + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + return a.length === b.length && a.every(function (e, i) { + return looseEqual(e, b[i]) + }) + } else if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime() + } else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function (key) { + return looseEqual(a[key], b[key]) + }) + } else { + /* istanbul ignore next */ + return false + } + } catch (e) { + /* istanbul ignore next */ + return false + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b) + } else { + return false + } + } + + /** + * Return the first index at which a loosely equal value can be + * found in the array (if value is a plain object, the array must + * contain an object of the same shape), or -1 if it is not present. + */ + function looseIndexOf (arr, val) { + for (var i = 0; i < arr.length; i++) { + if (looseEqual(arr[i], val)) { return i } + } + return -1 + } + + /** + * Ensure a function is called only once. + */ + function once (fn) { + var called = false; + return function () { + if (!called) { + called = true; + fn.apply(this, arguments); + } + } + } + + var SSR_ATTR = 'data-server-rendered'; + + var ASSET_TYPES = [ + 'component', + 'directive', + 'filter' + ]; + + var LIFECYCLE_HOOKS = [ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'beforeDestroy', + 'destroyed', + 'activated', + 'deactivated', + 'errorCaptured', + 'serverPrefetch' + ]; + + /* */ + + + + var config = ({ + /** + * Option merge strategies (used in core/util/options) + */ + // $flow-disable-line + optionMergeStrategies: Object.create(null), + + /** + * Whether to suppress warnings. + */ + silent: false, + + /** + * Show production mode tip message on boot? + */ + productionTip: "development" !== 'production', + + /** + * Whether to enable devtools + */ + devtools: "development" !== 'production', + + /** + * Whether to record perf + */ + performance: false, + + /** + * Error handler for watcher errors + */ + errorHandler: null, + + /** + * Warn handler for watcher warns + */ + warnHandler: null, + + /** + * Ignore certain custom elements + */ + ignoredElements: [], + + /** + * Custom user key aliases for v-on + */ + // $flow-disable-line + keyCodes: Object.create(null), + + /** + * Check if a tag is reserved so that it cannot be registered as a + * component. This is platform-dependent and may be overwritten. + */ + isReservedTag: no, + + /** + * Check if an attribute is reserved so that it cannot be used as a component + * prop. This is platform-dependent and may be overwritten. + */ + isReservedAttr: no, + + /** + * Check if a tag is an unknown element. + * Platform-dependent. + */ + isUnknownElement: no, + + /** + * Get the namespace of an element + */ + getTagNamespace: noop, + + /** + * Parse the real tag name for the specific platform. + */ + parsePlatformTagName: identity, + + /** + * Check if an attribute must be bound using property, e.g. value + * Platform-dependent. + */ + mustUseProp: no, + + /** + * Perform updates asynchronously. Intended to be used by Vue Test Utils + * This will significantly reduce performance if set to false. + */ + async: true, + + /** + * Exposed for legacy reasons + */ + _lifecycleHooks: LIFECYCLE_HOOKS + }); + + /* */ + + /** + * unicode letters used for parsing html tags, component names and property paths. + * using https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname + * skipping \u10000-\uEFFFF due to it freezing up PhantomJS + */ + var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; + + /** + * Check if a string starts with $ or _ + */ + function isReserved (str) { + var c = (str + '').charCodeAt(0); + return c === 0x24 || c === 0x5F + } + + /** + * Define a property. + */ + function def (obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); + } + + /** + * Parse simple path. + */ + var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]")); + function parsePath (path) { + if (bailRE.test(path)) { + return + } + var segments = path.split('.'); + return function (obj) { + for (var i = 0; i < segments.length; i++) { + if (!obj) { return } + obj = obj[segments[i]]; + } + return obj + } + } + + /* */ + + // can we use __proto__? + var hasProto = '__proto__' in {}; + + // Browser environment sniffing + var inBrowser = typeof window !== 'undefined'; + var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; + var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); + var UA = inBrowser && window.navigator.userAgent.toLowerCase(); + var isIE = UA && /msie|trident/.test(UA); + var isIE9 = UA && UA.indexOf('msie 9.0') > 0; + var isEdge = UA && UA.indexOf('edge/') > 0; + var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); + var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); + var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; + var isPhantomJS = UA && /phantomjs/.test(UA); + var isFF = UA && UA.match(/firefox\/(\d+)/); + + // Firefox has a "watch" function on Object.prototype... + var nativeWatch = ({}).watch; + + var supportsPassive = false; + if (inBrowser) { + try { + var opts = {}; + Object.defineProperty(opts, 'passive', ({ + get: function get () { + /* istanbul ignore next */ + supportsPassive = true; + } + })); // https://github.com/facebook/flow/issues/285 + window.addEventListener('test-passive', null, opts); + } catch (e) {} + } + + // this needs to be lazy-evaled because vue may be required before + // vue-server-renderer can set VUE_ENV + var _isServer; + var isServerRendering = function () { + if (_isServer === undefined) { + /* istanbul ignore if */ + if (!inBrowser && !inWeex && typeof global !== 'undefined') { + // detect presence of vue-server-renderer and avoid + // Webpack shimming the process + _isServer = global['process'] && global['process'].env.VUE_ENV === 'server'; + } else { + _isServer = false; + } + } + return _isServer + }; + + // detect devtools + var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + + /* istanbul ignore next */ + function isNative (Ctor) { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) + } + + var hasSymbol = + typeof Symbol !== 'undefined' && isNative(Symbol) && + typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); + + var _Set; + /* istanbul ignore if */ // $flow-disable-line + if (typeof Set !== 'undefined' && isNative(Set)) { + // use native Set when available. + _Set = Set; + } else { + // a non-standard Set polyfill that only works with primitive keys. + _Set = /*@__PURE__*/(function () { + function Set () { + this.set = Object.create(null); + } + Set.prototype.has = function has (key) { + return this.set[key] === true + }; + Set.prototype.add = function add (key) { + this.set[key] = true; + }; + Set.prototype.clear = function clear () { + this.set = Object.create(null); + }; + + return Set; + }()); + } + + /* */ + + var warn = noop; + var tip = noop; + var generateComponentTrace = (noop); // work around flow check + var formatComponentName = (noop); + + { + var hasConsole = typeof console !== 'undefined'; + var classifyRE = /(?:^|[-_])(\w)/g; + var classify = function (str) { return str + .replace(classifyRE, function (c) { return c.toUpperCase(); }) + .replace(/[-_]/g, ''); }; + + warn = function (msg, vm) { + var trace = vm ? generateComponentTrace(vm) : ''; + + if (config.warnHandler) { + config.warnHandler.call(null, msg, vm, trace); + } else if (hasConsole && (!config.silent)) { + console.error(("[Vue warn]: " + msg + trace)); + } + }; + + tip = function (msg, vm) { + if (hasConsole && (!config.silent)) { + console.warn("[Vue tip]: " + msg + ( + vm ? generateComponentTrace(vm) : '' + )); + } + }; + + formatComponentName = function (vm, includeFile) { + if (vm.$root === vm) { + return '' + } + var options = typeof vm === 'function' && vm.cid != null + ? vm.options + : vm._isVue + ? vm.$options || vm.constructor.options + : vm; + var name = options.name || options._componentTag; + var file = options.__file; + if (!name && file) { + var match = file.match(/([^/\\]+)\.vue$/); + name = match && match[1]; + } + + return ( + (name ? ("<" + (classify(name)) + ">") : "") + + (file && includeFile !== false ? (" at " + file) : '') + ) + }; + + var repeat = function (str, n) { + var res = ''; + while (n) { + if (n % 2 === 1) { res += str; } + if (n > 1) { str += str; } + n >>= 1; + } + return res + }; + + generateComponentTrace = function (vm) { + if (vm._isVue && vm.$parent) { + var tree = []; + var currentRecursiveSequence = 0; + while (vm) { + if (tree.length > 0) { + var last = tree[tree.length - 1]; + if (last.constructor === vm.constructor) { + currentRecursiveSequence++; + vm = vm.$parent; + continue + } else if (currentRecursiveSequence > 0) { + tree[tree.length - 1] = [last, currentRecursiveSequence]; + currentRecursiveSequence = 0; + } + } + tree.push(vm); + vm = vm.$parent; + } + return '\n\nfound in\n\n' + tree + .map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) + ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") + : formatComponentName(vm))); }) + .join('\n') + } else { + return ("\n\n(found in " + (formatComponentName(vm)) + ")") + } + }; + } + + /* */ + + var uid = 0; + + /** + * A dep is an observable that can have multiple + * directives subscribing to it. + */ + var Dep = function Dep () { + this.id = uid++; + this.subs = []; + }; + + Dep.prototype.addSub = function addSub (sub) { + this.subs.push(sub); + }; + + Dep.prototype.removeSub = function removeSub (sub) { + remove(this.subs, sub); + }; + + Dep.prototype.depend = function depend () { + if (Dep.target) { + Dep.target.addDep(this); + } + }; + + Dep.prototype.notify = function notify () { + // stabilize the subscriber list first + var subs = this.subs.slice(); + if (!config.async) { + // subs aren't sorted in scheduler if not running async + // we need to sort them now to make sure they fire in correct + // order + subs.sort(function (a, b) { return a.id - b.id; }); + } + for (var i = 0, l = subs.length; i < l; i++) { + subs[i].update(); + } + }; + + // The current target watcher being evaluated. + // This is globally unique because only one watcher + // can be evaluated at a time. + Dep.target = null; + var targetStack = []; + + function pushTarget (target) { + targetStack.push(target); + Dep.target = target; + } + + function popTarget () { + targetStack.pop(); + Dep.target = targetStack[targetStack.length - 1]; + } + + /* */ + + var VNode = function VNode ( + tag, + data, + children, + text, + elm, + context, + componentOptions, + asyncFactory + ) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text; + this.elm = elm; + this.ns = undefined; + this.context = context; + this.fnContext = undefined; + this.fnOptions = undefined; + this.fnScopeId = undefined; + this.key = data && data.key; + this.componentOptions = componentOptions; + this.componentInstance = undefined; + this.parent = undefined; + this.raw = false; + this.isStatic = false; + this.isRootInsert = true; + this.isComment = false; + this.isCloned = false; + this.isOnce = false; + this.asyncFactory = asyncFactory; + this.asyncMeta = undefined; + this.isAsyncPlaceholder = false; + }; + + var prototypeAccessors = { child: { configurable: true } }; + + // DEPRECATED: alias for componentInstance for backwards compat. + /* istanbul ignore next */ + prototypeAccessors.child.get = function () { + return this.componentInstance + }; + + Object.defineProperties( VNode.prototype, prototypeAccessors ); + + var createEmptyVNode = function (text) { + if ( text === void 0 ) text = ''; + + var node = new VNode(); + node.text = text; + node.isComment = true; + return node + }; + + function createTextVNode (val) { + return new VNode(undefined, undefined, undefined, String(val)) + } + + // optimized shallow clone + // used for static nodes and slot nodes because they may be reused across + // multiple renders, cloning them avoids errors when DOM manipulations rely + // on their elm reference. + function cloneVNode (vnode) { + var cloned = new VNode( + vnode.tag, + vnode.data, + // #7975 + // clone children array to avoid mutating original in case of cloning + // a child. + vnode.children && vnode.children.slice(), + vnode.text, + vnode.elm, + vnode.context, + vnode.componentOptions, + vnode.asyncFactory + ); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.asyncMeta = vnode.asyncMeta; + cloned.isCloned = true; + return cloned + } + + /* + * not type checking this file because flow doesn't play well with + * dynamically accessing methods on Array prototype + */ + + var arrayProto = Array.prototype; + var arrayMethods = Object.create(arrayProto); + + var methodsToPatch = [ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' + ]; + + /** + * Intercept mutating methods and emit events + */ + methodsToPatch.forEach(function (method) { + // cache original method + var original = arrayProto[method]; + def(arrayMethods, method, function mutator () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + case 'unshift': + inserted = args; + break + case 'splice': + inserted = args.slice(2); + break + } + if (inserted) { ob.observeArray(inserted); } + // notify change + ob.dep.notify(); + return result + }); + }); + + /* */ + + var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + + /** + * In some cases we may want to disable observation inside a component's + * update computation. + */ + var shouldObserve = true; + + function toggleObserving (value) { + shouldObserve = value; + } + + /** + * Observer class that is attached to each observed + * object. Once attached, the observer converts the target + * object's property keys into getter/setters that + * collect dependencies and dispatch updates. + */ + var Observer = function Observer (value) { + this.value = value; + this.dep = new Dep(); + this.vmCount = 0; + def(value, '__ob__', this); + if (Array.isArray(value)) { + if (hasProto) { + protoAugment(value, arrayMethods); + } else { + copyAugment(value, arrayMethods, arrayKeys); + } + this.observeArray(value); + } else { + this.walk(value); + } + }; + + /** + * Walk through all properties and convert them into + * getter/setters. This method should only be called when + * value type is Object. + */ + Observer.prototype.walk = function walk (obj) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + defineReactive$$1(obj, keys[i]); + } + }; + + /** + * Observe a list of Array items. + */ + Observer.prototype.observeArray = function observeArray (items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } + }; + + // helpers + + /** + * Augment a target Object or Array by intercepting + * the prototype chain using __proto__ + */ + function protoAugment (target, src) { + /* eslint-disable no-proto */ + target.__proto__ = src; + /* eslint-enable no-proto */ + } + + /** + * Augment a target Object or Array by defining + * hidden properties. + */ + /* istanbul ignore next */ + function copyAugment (target, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target, key, src[key]); + } + } + + /** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + */ + function observe (value, asRootData) { + if (!isObject(value) || value instanceof VNode) { + return + } + var ob; + if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + ob = value.__ob__; + } else if ( + shouldObserve && + !isServerRendering() && + (Array.isArray(value) || isPlainObject(value)) && + Object.isExtensible(value) && + !value._isVue + ) { + ob = new Observer(value); + } + if (asRootData && ob) { + ob.vmCount++; + } + return ob + } + + /** + * Define a reactive property on an Object. + */ + function defineReactive$$1 ( + obj, + key, + val, + customSetter, + shallow + ) { + var dep = new Dep(); + + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return + } + + // cater for pre-defined getter/setters + var getter = property && property.get; + var setter = property && property.set; + if ((!getter || setter) && arguments.length === 2) { + val = obj[key]; + } + + var childOb = !shallow && observe(val); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter () { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + dep.depend(); + if (childOb) { + childOb.dep.depend(); + if (Array.isArray(value)) { + dependArray(value); + } + } + } + return value + }, + set: function reactiveSetter (newVal) { + var value = getter ? getter.call(obj) : val; + /* eslint-disable no-self-compare */ + if (newVal === value || (newVal !== newVal && value !== value)) { + return + } + /* eslint-enable no-self-compare */ + if (customSetter) { + customSetter(); + } + // #7981: for accessor properties without setter + if (getter && !setter) { return } + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + childOb = !shallow && observe(newVal); + dep.notify(); + } + }); + } + + /** + * Set a property on an object. Adds the new property and + * triggers change notification if the property doesn't + * already exist. + */ + function set (target, key, val) { + if (isUndef(target) || isPrimitive(target) + ) { + warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.length = Math.max(target.length, key); + target.splice(key, 1, val); + return val + } + if (key in target && !(key in Object.prototype)) { + target[key] = val; + return val + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + warn( + 'Avoid adding reactive properties to a Vue instance or its root $data ' + + 'at runtime - declare it upfront in the data option.' + ); + return val + } + if (!ob) { + target[key] = val; + return val + } + defineReactive$$1(ob.value, key, val); + ob.dep.notify(); + return val + } + + /** + * Delete a property and trigger change if necessary. + */ + function del (target, key) { + if (isUndef(target) || isPrimitive(target) + ) { + warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.splice(key, 1); + return + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + warn( + 'Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.' + ); + return + } + if (!hasOwn(target, key)) { + return + } + delete target[key]; + if (!ob) { + return + } + ob.dep.notify(); + } + + /** + * Collect dependencies on array elements when the array is touched, since + * we cannot intercept array element access like property getters. + */ + function dependArray (value) { + for (var e = (void 0), i = 0, l = value.length; i < l; i++) { + e = value[i]; + e && e.__ob__ && e.__ob__.dep.depend(); + if (Array.isArray(e)) { + dependArray(e); + } + } + } + + /* */ + + /** + * Option overwriting strategies are functions that handle + * how to merge a parent option value and a child option + * value into the final value. + */ + var strats = config.optionMergeStrategies; + + /** + * Options with restrictions + */ + { + strats.el = strats.propsData = function (parent, child, vm, key) { + if (!vm) { + warn( + "option \"" + key + "\" can only be used during instance " + + 'creation with the `new` keyword.' + ); + } + return defaultStrat(parent, child) + }; + } + + /** + * Helper that recursively merges two data objects together. + */ + function mergeData (to, from) { + if (!from) { return to } + var key, toVal, fromVal; + + var keys = hasSymbol + ? Reflect.ownKeys(from) + : Object.keys(from); + + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + // in case the object is already observed... + if (key === '__ob__') { continue } + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + set(to, key, fromVal); + } else if ( + toVal !== fromVal && + isPlainObject(toVal) && + isPlainObject(fromVal) + ) { + mergeData(toVal, fromVal); + } + } + return to + } + + /** + * Data + */ + function mergeDataOrFn ( + parentVal, + childVal, + vm + ) { + if (!vm) { + // in a Vue.extend merge, both should be functions + if (!childVal) { + return parentVal + } + if (!parentVal) { + return childVal + } + // when parentVal & childVal are both present, + // we need to return a function that returns the + // merged result of both functions... no need to + // check if parentVal is a function here because + // it has to be a function to pass previous merges. + return function mergedDataFn () { + return mergeData( + typeof childVal === 'function' ? childVal.call(this, this) : childVal, + typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal + ) + } + } else { + return function mergedInstanceDataFn () { + // instance merge + var instanceData = typeof childVal === 'function' + ? childVal.call(vm, vm) + : childVal; + var defaultData = typeof parentVal === 'function' + ? parentVal.call(vm, vm) + : parentVal; + if (instanceData) { + return mergeData(instanceData, defaultData) + } else { + return defaultData + } + } + } + } + + strats.data = function ( + parentVal, + childVal, + vm + ) { + if (!vm) { + if (childVal && typeof childVal !== 'function') { + warn( + 'The "data" option should be a function ' + + 'that returns a per-instance value in component ' + + 'definitions.', + vm + ); + + return parentVal + } + return mergeDataOrFn(parentVal, childVal) + } + + return mergeDataOrFn(parentVal, childVal, vm) + }; + + /** + * Hooks and props are merged as arrays. + */ + function mergeHook ( + parentVal, + childVal + ) { + var res = childVal + ? parentVal + ? parentVal.concat(childVal) + : Array.isArray(childVal) + ? childVal + : [childVal] + : parentVal; + return res + ? dedupeHooks(res) + : res + } + + function dedupeHooks (hooks) { + var res = []; + for (var i = 0; i < hooks.length; i++) { + if (res.indexOf(hooks[i]) === -1) { + res.push(hooks[i]); + } + } + return res + } + + LIFECYCLE_HOOKS.forEach(function (hook) { + strats[hook] = mergeHook; + }); + + /** + * Assets + * + * When a vm is present (instance creation), we need to do + * a three-way merge between constructor options, instance + * options and parent options. + */ + function mergeAssets ( + parentVal, + childVal, + vm, + key + ) { + var res = Object.create(parentVal || null); + if (childVal) { + assertObjectType(key, childVal, vm); + return extend(res, childVal) + } else { + return res + } + } + + ASSET_TYPES.forEach(function (type) { + strats[type + 's'] = mergeAssets; + }); + + /** + * Watchers. + * + * Watchers hashes should not overwrite one + * another, so we merge them as arrays. + */ + strats.watch = function ( + parentVal, + childVal, + vm, + key + ) { + // work around Firefox's Object.prototype.watch... + if (parentVal === nativeWatch) { parentVal = undefined; } + if (childVal === nativeWatch) { childVal = undefined; } + /* istanbul ignore if */ + if (!childVal) { return Object.create(parentVal || null) } + { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = {}; + extend(ret, parentVal); + for (var key$1 in childVal) { + var parent = ret[key$1]; + var child = childVal[key$1]; + if (parent && !Array.isArray(parent)) { + parent = [parent]; + } + ret[key$1] = parent + ? parent.concat(child) + : Array.isArray(child) ? child : [child]; + } + return ret + }; + + /** + * Other object hashes. + */ + strats.props = + strats.methods = + strats.inject = + strats.computed = function ( + parentVal, + childVal, + vm, + key + ) { + if (childVal && "development" !== 'production') { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = Object.create(null); + extend(ret, parentVal); + if (childVal) { extend(ret, childVal); } + return ret + }; + strats.provide = mergeDataOrFn; + + /** + * Default strategy. + */ + var defaultStrat = function (parentVal, childVal) { + return childVal === undefined + ? parentVal + : childVal + }; + + /** + * Validate component names + */ + function checkComponents (options) { + for (var key in options.components) { + validateComponentName(key); + } + } + + function validateComponentName (name) { + if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) { + warn( + 'Invalid component name: "' + name + '". Component names ' + + 'should conform to valid custom element name in html5 specification.' + ); + } + if (isBuiltInTag(name) || config.isReservedTag(name)) { + warn( + 'Do not use built-in or reserved HTML elements as component ' + + 'id: ' + name + ); + } + } + + /** + * Ensure all props option syntax are normalized into the + * Object-based format. + */ + function normalizeProps (options, vm) { + var props = options.props; + if (!props) { return } + var res = {}; + var i, val, name; + if (Array.isArray(props)) { + i = props.length; + while (i--) { + val = props[i]; + if (typeof val === 'string') { + name = camelize(val); + res[name] = { type: null }; + } else { + warn('props must be strings when using array syntax.'); + } + } + } else if (isPlainObject(props)) { + for (var key in props) { + val = props[key]; + name = camelize(key); + res[name] = isPlainObject(val) + ? val + : { type: val }; + } + } else { + warn( + "Invalid value for option \"props\": expected an Array or an Object, " + + "but got " + (toRawType(props)) + ".", + vm + ); + } + options.props = res; + } + + /** + * Normalize all injections into Object-based format + */ + function normalizeInject (options, vm) { + var inject = options.inject; + if (!inject) { return } + var normalized = options.inject = {}; + if (Array.isArray(inject)) { + for (var i = 0; i < inject.length; i++) { + normalized[inject[i]] = { from: inject[i] }; + } + } else if (isPlainObject(inject)) { + for (var key in inject) { + var val = inject[key]; + normalized[key] = isPlainObject(val) + ? extend({ from: key }, val) + : { from: val }; + } + } else { + warn( + "Invalid value for option \"inject\": expected an Array or an Object, " + + "but got " + (toRawType(inject)) + ".", + vm + ); + } + } + + /** + * Normalize raw function directives into object format. + */ + function normalizeDirectives (options) { + var dirs = options.directives; + if (dirs) { + for (var key in dirs) { + var def$$1 = dirs[key]; + if (typeof def$$1 === 'function') { + dirs[key] = { bind: def$$1, update: def$$1 }; + } + } + } + } + + function assertObjectType (name, value, vm) { + if (!isPlainObject(value)) { + warn( + "Invalid value for option \"" + name + "\": expected an Object, " + + "but got " + (toRawType(value)) + ".", + vm + ); + } + } + + /** + * Merge two option objects into a new one. + * Core utility used in both instantiation and inheritance. + */ + function mergeOptions ( + parent, + child, + vm + ) { + { + checkComponents(child); + } + + if (typeof child === 'function') { + child = child.options; + } + + normalizeProps(child, vm); + normalizeInject(child, vm); + normalizeDirectives(child); + + // Apply extends and mixins on the child options, + // but only if it is a raw options object that isn't + // the result of another mergeOptions call. + // Only merged options has the _base property. + if (!child._base) { + if (child.extends) { + parent = mergeOptions(parent, child.extends, vm); + } + if (child.mixins) { + for (var i = 0, l = child.mixins.length; i < l; i++) { + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + } + + var options = {}; + var key; + for (key in parent) { + mergeField(key); + } + for (key in child) { + if (!hasOwn(parent, key)) { + mergeField(key); + } + } + function mergeField (key) { + var strat = strats[key] || defaultStrat; + options[key] = strat(parent[key], child[key], vm, key); + } + return options + } + + /** + * Resolve an asset. + * This function is used because child instances need access + * to assets defined in its ancestor chain. + */ + function resolveAsset ( + options, + type, + id, + warnMissing + ) { + /* istanbul ignore if */ + if (typeof id !== 'string') { + return + } + var assets = options[type]; + // check local registration variations first + if (hasOwn(assets, id)) { return assets[id] } + var camelizedId = camelize(id); + if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } + var PascalCaseId = capitalize(camelizedId); + if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } + // fallback to prototype chain + var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; + if (warnMissing && !res) { + warn( + 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, + options + ); + } + return res + } + + /* */ + + + + function validateProp ( + key, + propOptions, + propsData, + vm + ) { + var prop = propOptions[key]; + var absent = !hasOwn(propsData, key); + var value = propsData[key]; + // boolean casting + var booleanIndex = getTypeIndex(Boolean, prop.type); + if (booleanIndex > -1) { + if (absent && !hasOwn(prop, 'default')) { + value = false; + } else if (value === '' || value === hyphenate(key)) { + // only cast empty string / same name to boolean if + // boolean has higher priority + var stringIndex = getTypeIndex(String, prop.type); + if (stringIndex < 0 || booleanIndex < stringIndex) { + value = true; + } + } + } + // check default value + if (value === undefined) { + value = getPropDefaultValue(vm, prop, key); + // since the default value is a fresh copy, + // make sure to observe it. + var prevShouldObserve = shouldObserve; + toggleObserving(true); + observe(value); + toggleObserving(prevShouldObserve); + } + { + assertProp(prop, key, value, vm, absent); + } + return value + } + + /** + * Get the default value of a prop. + */ + function getPropDefaultValue (vm, prop, key) { + // no default, return undefined + if (!hasOwn(prop, 'default')) { + return undefined + } + var def = prop.default; + // warn against non-factory defaults for Object & Array + if (isObject(def)) { + warn( + 'Invalid default value for prop "' + key + '": ' + + 'Props with type Object/Array must use a factory function ' + + 'to return the default value.', + vm + ); + } + // the raw prop value was also undefined from previous render, + // return previous default value to avoid unnecessary watcher trigger + if (vm && vm.$options.propsData && + vm.$options.propsData[key] === undefined && + vm._props[key] !== undefined + ) { + return vm._props[key] + } + // call factory function for non-Function types + // a value is Function if its prototype is function even across different execution context + return typeof def === 'function' && getType(prop.type) !== 'Function' + ? def.call(vm) + : def + } + + /** + * Assert whether a prop is valid. + */ + function assertProp ( + prop, + name, + value, + vm, + absent + ) { + if (prop.required && absent) { + warn( + 'Missing required prop: "' + name + '"', + vm + ); + return + } + if (value == null && !prop.required) { + return + } + var type = prop.type; + var valid = !type || type === true; + var expectedTypes = []; + if (type) { + if (!Array.isArray(type)) { + type = [type]; + } + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]); + expectedTypes.push(assertedType.expectedType || ''); + valid = assertedType.valid; + } + } + + if (!valid) { + warn( + getInvalidTypeMessage(name, value, expectedTypes), + vm + ); + return + } + var validator = prop.validator; + if (validator) { + if (!validator(value)) { + warn( + 'Invalid prop: custom validator check failed for prop "' + name + '".', + vm + ); + } + } + } + + var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; + + function assertType (value, type) { + var valid; + var expectedType = getType(type); + if (simpleCheckRE.test(expectedType)) { + var t = typeof value; + valid = t === expectedType.toLowerCase(); + // for primitive wrapper objects + if (!valid && t === 'object') { + valid = value instanceof type; + } + } else if (expectedType === 'Object') { + valid = isPlainObject(value); + } else if (expectedType === 'Array') { + valid = Array.isArray(value); + } else { + valid = value instanceof type; + } + return { + valid: valid, + expectedType: expectedType + } + } + + /** + * Use function string name to check built-in types, + * because a simple equality check will fail when running + * across different vms / iframes. + */ + function getType (fn) { + var match = fn && fn.toString().match(/^\s*function (\w+)/); + return match ? match[1] : '' + } + + function isSameType (a, b) { + return getType(a) === getType(b) + } + + function getTypeIndex (type, expectedTypes) { + if (!Array.isArray(expectedTypes)) { + return isSameType(expectedTypes, type) ? 0 : -1 + } + for (var i = 0, len = expectedTypes.length; i < len; i++) { + if (isSameType(expectedTypes[i], type)) { + return i + } + } + return -1 + } + + function getInvalidTypeMessage (name, value, expectedTypes) { + var message = "Invalid prop: type check failed for prop \"" + name + "\"." + + " Expected " + (expectedTypes.map(capitalize).join(', ')); + var expectedType = expectedTypes[0]; + var receivedType = toRawType(value); + var expectedValue = styleValue(value, expectedType); + var receivedValue = styleValue(value, receivedType); + // check if we need to specify expected value + if (expectedTypes.length === 1 && + isExplicable(expectedType) && + !isBoolean(expectedType, receivedType)) { + message += " with value " + expectedValue; + } + message += ", got " + receivedType + " "; + // check if we need to specify received value + if (isExplicable(receivedType)) { + message += "with value " + receivedValue + "."; + } + return message + } + + function styleValue (value, type) { + if (type === 'String') { + return ("\"" + value + "\"") + } else if (type === 'Number') { + return ("" + (Number(value))) + } else { + return ("" + value) + } + } + + function isExplicable (value) { + var explicitTypes = ['string', 'number', 'boolean']; + return explicitTypes.some(function (elem) { return value.toLowerCase() === elem; }) + } + + function isBoolean () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; }) + } + + /* */ + + function handleError (err, vm, info) { + // Deactivate deps tracking while processing error handler to avoid possible infinite rendering. + // See: https://github.com/vuejs/vuex/issues/1505 + pushTarget(); + try { + if (vm) { + var cur = vm; + while ((cur = cur.$parent)) { + var hooks = cur.$options.errorCaptured; + if (hooks) { + for (var i = 0; i < hooks.length; i++) { + try { + var capture = hooks[i].call(cur, err, vm, info) === false; + if (capture) { return } + } catch (e) { + globalHandleError(e, cur, 'errorCaptured hook'); + } + } + } + } + } + globalHandleError(err, vm, info); + } finally { + popTarget(); + } + } + + function invokeWithErrorHandling ( + handler, + context, + args, + vm, + info + ) { + var res; + try { + res = args ? handler.apply(context, args) : handler.call(context); + if (res && !res._isVue && isPromise(res) && !res._handled) { + res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); }); + // issue #9511 + // avoid catch triggering multiple times when nested calls + res._handled = true; + } + } catch (e) { + handleError(e, vm, info); + } + return res + } + + function globalHandleError (err, vm, info) { + if (config.errorHandler) { + try { + return config.errorHandler.call(null, err, vm, info) + } catch (e) { + // if the user intentionally throws the original error in the handler, + // do not log it twice + if (e !== err) { + logError(e, null, 'config.errorHandler'); + } + } + } + logError(err, vm, info); + } + + function logError (err, vm, info) { + { + warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); + } + /* istanbul ignore else */ + if ((inBrowser || inWeex) && typeof console !== 'undefined') { + console.error(err); + } else { + throw err + } + } + + /* */ + + var isUsingMicroTask = false; + + var callbacks = []; + var pending = false; + + function flushCallbacks () { + pending = false; + var copies = callbacks.slice(0); + callbacks.length = 0; + for (var i = 0; i < copies.length; i++) { + copies[i](); + } + } + + // Here we have async deferring wrappers using microtasks. + // In 2.5 we used (macro) tasks (in combination with microtasks). + // However, it has subtle problems when state is changed right before repaint + // (e.g. #6813, out-in transitions). + // Also, using (macro) tasks in event handler would cause some weird behaviors + // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109). + // So we now use microtasks everywhere, again. + // A major drawback of this tradeoff is that there are some scenarios + // where microtasks have too high a priority and fire in between supposedly + // sequential events (e.g. #4521, #6690, which have workarounds) + // or even between bubbling of the same event (#6566). + var timerFunc; + + // The nextTick behavior leverages the microtask queue, which can be accessed + // via either native Promise.then or MutationObserver. + // MutationObserver has wider support, however it is seriously bugged in + // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It + // completely stops working after triggering a few times... so, if native + // Promise is available, we will use it: + /* istanbul ignore next, $flow-disable-line */ + if (typeof Promise !== 'undefined' && isNative(Promise)) { + var p = Promise.resolve(); + timerFunc = function () { + p.then(flushCallbacks); + // In problematic UIWebViews, Promise.then doesn't completely break, but + // it can get stuck in a weird state where callbacks are pushed into the + // microtask queue but the queue isn't being flushed, until the browser + // needs to do some other work, e.g. handle a timer. Therefore we can + // "force" the microtask queue to be flushed by adding an empty timer. + if (isIOS) { setTimeout(noop); } + }; + isUsingMicroTask = true; + } else if (!isIE && typeof MutationObserver !== 'undefined' && ( + isNative(MutationObserver) || + // PhantomJS and iOS 7.x + MutationObserver.toString() === '[object MutationObserverConstructor]' + )) { + // Use MutationObserver where native Promise is not available, + // e.g. PhantomJS, iOS7, Android 4.4 + // (#6466 MutationObserver is unreliable in IE11) + var counter = 1; + var observer = new MutationObserver(flushCallbacks); + var textNode = document.createTextNode(String(counter)); + observer.observe(textNode, { + characterData: true + }); + timerFunc = function () { + counter = (counter + 1) % 2; + textNode.data = String(counter); + }; + isUsingMicroTask = true; + } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { + // Fallback to setImmediate. + // Technically it leverages the (macro) task queue, + // but it is still a better choice than setTimeout. + timerFunc = function () { + setImmediate(flushCallbacks); + }; + } else { + // Fallback to setTimeout. + timerFunc = function () { + setTimeout(flushCallbacks, 0); + }; + } + + function nextTick (cb, ctx) { + var _resolve; + callbacks.push(function () { + if (cb) { + try { + cb.call(ctx); + } catch (e) { + handleError(e, ctx, 'nextTick'); + } + } else if (_resolve) { + _resolve(ctx); + } + }); + if (!pending) { + pending = true; + timerFunc(); + } + // $flow-disable-line + if (!cb && typeof Promise !== 'undefined') { + return new Promise(function (resolve) { + _resolve = resolve; + }) + } + } + + /* */ + + var mark; + var measure; + + { + var perf = inBrowser && window.performance; + /* istanbul ignore if */ + if ( + perf && + perf.mark && + perf.measure && + perf.clearMarks && + perf.clearMeasures + ) { + mark = function (tag) { return perf.mark(tag); }; + measure = function (name, startTag, endTag) { + perf.measure(name, startTag, endTag); + perf.clearMarks(startTag); + perf.clearMarks(endTag); + // perf.clearMeasures(name) + }; + } + } + + /* not type checking this file because flow doesn't play well with Proxy */ + + var initProxy; + + { + var allowedGlobals = makeMap( + 'Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + + 'require' // for Webpack/Browserify + ); + + var warnNonPresent = function (target, key) { + warn( + "Property or method \"" + key + "\" is not defined on the instance but " + + 'referenced during render. Make sure that this property is reactive, ' + + 'either in the data option, or for class-based components, by ' + + 'initializing the property. ' + + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', + target + ); + }; + + var warnReservedPrefix = function (target, key) { + warn( + "Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " + + 'properties starting with "$" or "_" are not proxied in the Vue instance to ' + + 'prevent conflicts with Vue internals. ' + + 'See: https://vuejs.org/v2/api/#data', + target + ); + }; + + var hasProxy = + typeof Proxy !== 'undefined' && isNative(Proxy); + + if (hasProxy) { + var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); + config.keyCodes = new Proxy(config.keyCodes, { + set: function set (target, key, value) { + if (isBuiltInModifier(key)) { + warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); + return false + } else { + target[key] = value; + return true + } + } + }); + } + + var hasHandler = { + has: function has (target, key) { + var has = key in target; + var isAllowed = allowedGlobals(key) || + (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)); + if (!has && !isAllowed) { + if (key in target.$data) { warnReservedPrefix(target, key); } + else { warnNonPresent(target, key); } + } + return has || !isAllowed + } + }; + + var getHandler = { + get: function get (target, key) { + if (typeof key === 'string' && !(key in target)) { + if (key in target.$data) { warnReservedPrefix(target, key); } + else { warnNonPresent(target, key); } + } + return target[key] + } + }; + + initProxy = function initProxy (vm) { + if (hasProxy) { + // determine which proxy handler to use + var options = vm.$options; + var handlers = options.render && options.render._withStripped + ? getHandler + : hasHandler; + vm._renderProxy = new Proxy(vm, handlers); + } else { + vm._renderProxy = vm; + } + }; + } + + /* */ + + var seenObjects = new _Set(); + + /** + * Recursively traverse an object to evoke all converted + * getters, so that every nested property inside the object + * is collected as a "deep" dependency. + */ + function traverse (val) { + _traverse(val, seenObjects); + seenObjects.clear(); + } + + function _traverse (val, seen) { + var i, keys; + var isA = Array.isArray(val); + if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { + return + } + if (val.__ob__) { + var depId = val.__ob__.dep.id; + if (seen.has(depId)) { + return + } + seen.add(depId); + } + if (isA) { + i = val.length; + while (i--) { _traverse(val[i], seen); } + } else { + keys = Object.keys(val); + i = keys.length; + while (i--) { _traverse(val[keys[i]], seen); } + } + } + + /* */ + + var normalizeEvent = cached(function (name) { + var passive = name.charAt(0) === '&'; + name = passive ? name.slice(1) : name; + var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first + name = once$$1 ? name.slice(1) : name; + var capture = name.charAt(0) === '!'; + name = capture ? name.slice(1) : name; + return { + name: name, + once: once$$1, + capture: capture, + passive: passive + } + }); + + function createFnInvoker (fns, vm) { + function invoker () { + var arguments$1 = arguments; + + var fns = invoker.fns; + if (Array.isArray(fns)) { + var cloned = fns.slice(); + for (var i = 0; i < cloned.length; i++) { + invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler"); + } + } else { + // return handler return value for single handlers + return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler") + } + } + invoker.fns = fns; + return invoker + } + + function updateListeners ( + on, + oldOn, + add, + remove$$1, + createOnceHandler, + vm + ) { + var name, def$$1, cur, old, event; + for (name in on) { + def$$1 = cur = on[name]; + old = oldOn[name]; + event = normalizeEvent(name); + if (isUndef(cur)) { + warn( + "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), + vm + ); + } else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on[name] = createFnInvoker(cur, vm); + } + if (isTrue(event.once)) { + cur = on[name] = createOnceHandler(event.name, cur, event.capture); + } + add(event.name, cur, event.capture, event.passive, event.params); + } else if (cur !== old) { + old.fns = cur; + on[name] = old; + } + } + for (name in oldOn) { + if (isUndef(on[name])) { + event = normalizeEvent(name); + remove$$1(event.name, oldOn[name], event.capture); + } + } + } + + /* */ + + function mergeVNodeHook (def, hookKey, hook) { + if (def instanceof VNode) { + def = def.data.hook || (def.data.hook = {}); + } + var invoker; + var oldHook = def[hookKey]; + + function wrappedHook () { + hook.apply(this, arguments); + // important: remove merged hook to ensure it's called only once + // and prevent memory leak + remove(invoker.fns, wrappedHook); + } + + if (isUndef(oldHook)) { + // no existing hook + invoker = createFnInvoker([wrappedHook]); + } else { + /* istanbul ignore if */ + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + // already a merged invoker + invoker = oldHook; + invoker.fns.push(wrappedHook); + } else { + // existing plain hook + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + + invoker.merged = true; + def[hookKey] = invoker; + } + + /* */ + + function extractPropsFromVNodeData ( + data, + Ctor, + tag + ) { + // we are only extracting raw values here. + // validation and default values are handled in the child + // component itself. + var propOptions = Ctor.options.props; + if (isUndef(propOptions)) { + return + } + var res = {}; + var attrs = data.attrs; + var props = data.props; + if (isDef(attrs) || isDef(props)) { + for (var key in propOptions) { + var altKey = hyphenate(key); + { + var keyInLowerCase = key.toLowerCase(); + if ( + key !== keyInLowerCase && + attrs && hasOwn(attrs, keyInLowerCase) + ) { + tip( + "Prop \"" + keyInLowerCase + "\" is passed to component " + + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + + " \"" + key + "\". " + + "Note that HTML attributes are case-insensitive and camelCased " + + "props need to use their kebab-case equivalents when using in-DOM " + + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." + ); + } + } + checkProp(res, props, key, altKey, true) || + checkProp(res, attrs, key, altKey, false); + } + } + return res + } + + function checkProp ( + res, + hash, + key, + altKey, + preserve + ) { + if (isDef(hash)) { + if (hasOwn(hash, key)) { + res[key] = hash[key]; + if (!preserve) { + delete hash[key]; + } + return true + } else if (hasOwn(hash, altKey)) { + res[key] = hash[altKey]; + if (!preserve) { + delete hash[altKey]; + } + return true + } + } + return false + } + + /* */ + + // The template compiler attempts to minimize the need for normalization by + // statically analyzing the template at compile time. + // + // For plain HTML markup, normalization can be completely skipped because the + // generated render function is guaranteed to return Array. There are + // two cases where extra normalization is needed: + + // 1. When the children contains components - because a functional component + // may return an Array instead of a single root. In this case, just a simple + // normalization is needed - if any child is an Array, we flatten the whole + // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep + // because functional components already normalize their own children. + function simpleNormalizeChildren (children) { + for (var i = 0; i < children.length; i++) { + if (Array.isArray(children[i])) { + return Array.prototype.concat.apply([], children) + } + } + return children + } + + // 2. When the children contains constructs that always generated nested Arrays, + // e.g.