commit 55845d27408098aacb53a0f3ad7e5a7b68efb38f Author: Vincent LAURENT Date: Mon May 3 19:26:47 2021 +0200 initualisation with fatfree framework diff --git a/vendor/fatfree/.gitignore b/vendor/fatfree/.gitignore new file mode 100644 index 0000000..7c9d10d --- /dev/null +++ b/vendor/fatfree/.gitignore @@ -0,0 +1,2 @@ +/tmp/ +/.idea/ diff --git a/vendor/fatfree/.htaccess b/vendor/fatfree/.htaccess new file mode 100644 index 0000000..2be86c5 --- /dev/null +++ b/vendor/fatfree/.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/vendor/fatfree/composer.json b/vendor/fatfree/composer.json new file mode 100644 index 0000000..84c1afc --- /dev/null +++ b/vendor/fatfree/composer.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/bcosca/fatfree" + } + ], + "autoload": { + "files": ["lib/base.php"] + } +} diff --git a/vendor/fatfree/config.ini b/vendor/fatfree/config.ini new file mode 100644 index 0000000..8911dfc --- /dev/null +++ b/vendor/fatfree/config.ini @@ -0,0 +1,4 @@ +[globals] + +DEBUG=3 +UI=ui/ diff --git a/vendor/fatfree/index.php b/vendor/fatfree/index.php new file mode 100644 index 0000000..1b3381f --- /dev/null +++ b/vendor/fatfree/index.php @@ -0,0 +1,89 @@ +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) { + $classes=array( + 'Base'=> + array( + 'hash', + 'json', + 'session', + 'mbstring' + ), + 'Cache'=> + array( + 'apc', + 'apcu', + 'memcache', + 'memcached', + 'redis', + 'wincache', + 'xcache' + ), + 'DB\SQL'=> + array( + 'pdo', + 'pdo_dblib', + 'pdo_mssql', + 'pdo_mysql', + 'pdo_odbc', + 'pdo_pgsql', + 'pdo_sqlite', + 'pdo_sqlsrv' + ), + 'DB\Jig'=> + array('json'), + 'DB\Mongo'=> + array( + 'json', + 'mongo' + ), + 'Auth'=> + array('ldap','pdo'), + 'Bcrypt'=> + array( + 'openssl' + ), + 'Image'=> + array('gd'), + 'Lexicon'=> + array('iconv'), + 'SMTP'=> + array('openssl'), + 'Web'=> + array('curl','openssl','simplexml'), + 'Web\Geo'=> + array('geoip','json'), + 'Web\OpenID'=> + array('json','simplexml'), + 'Web\OAuth2'=> + array('json'), + 'Web\Pingback'=> + array('dom','xmlrpc'), + 'CLI\WS'=> + array('pcntl') + ); + $f3->set('classes',$classes); + $f3->set('content','welcome.htm'); + echo View::instance()->render('layout.htm'); + } +); + +$f3->route('GET /userref', + function($f3) { + $f3->set('content','userref.htm'); + echo View::instance()->render('layout.htm'); + } +); + +$f3->run(); diff --git a/vendor/fatfree/lib/CHANGELOG.md b/vendor/fatfree/lib/CHANGELOG.md new file mode 100644 index 0000000..b98111f --- /dev/null +++ b/vendor/fatfree/lib/CHANGELOG.md @@ -0,0 +1,986 @@ +CHANGELOG + +3.7.3 +* NEW: added auto_increment detection, [bcosca/fatfree#1192](https://github.com/bcosca/fatfree/issues/1192), [bcosca/fatfree#1093](https://github.com/bcosca/fatfree/issues/1093), [bcosca/fatfree#1175](https://github.com/bcosca/fatfree/issues/1175), [#290](https://github.com/bcosca/fatfree-core/issues/290) +* added SMTP dialog error handling, [#317](https://github.com/bcosca/fatfree-core/issues/317) +* Fix: Check active transaction before rollback/commit (PHP8 issue) +* refactored increment/decrement operator to preceed variables +* added error output in CLI mode, [bcosca/fatfree#1185](https://github.com/bcosca/fatfree/issues/1185) +* Set PORT to 80 when SERVER_PORT is an empty string +* Fix: unescape dbname when extracting from dsn, [#316](https://github.com/bcosca/fatfree-core/issues/316) +* Fix: handling of PDO prepare() errors +* Fix: edge case in DB\SQL->schema(): PK not detected in PgSQL when the column is also a FK [bcosca/fatfree#1207](https://github.com/bcosca/fatfree/issues/1207) +* Fix: Escape literal hyphens in regex character classes, [bcosca/fatfree#1206](https://github.com/bcosca/fatfree/issues/1206) +* Fix: error highlighting +* Fix: pagination with order by on virtual fields +* Fixed a couple PHPDOC issues + +3.7.2 (28 May 2020) +* CHANGED, View->sandbox: disable escaping when rendering as text/plain, [bcosca/fatfree#654](https://github.com/bcosca/fatfree/issues/654) +* update HTTP protocol checks, [bcosca/fatfree#1190](https://github.com/bcosca/fatfree/issues/1190) +* Base->clear: close vulnerability on variable compilation, [bcosca/fatfree#1191](https://github.com/bcosca/fatfree/issues/1191) +* DB\SQL\Mapper: fix empty ID after insert, [bcosca/fatfree#1175](https://github.com/bcosca/fatfree/issues/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](https://github.com/bcosca/fatfree/issues/1187) +* Bug fix, Web->minify: fix minification of ES6 template literals, [bcosca/fatfree#1178](https://github.com/bcosca/fatfree/issues/1178) +* Bug fix, config: refactoring custom section parser regex, [bcosca/fatfree#1149](https://github.com/bcosca/fatfree/issues/1149) +* Bug fix: token resolve on non-alias reroute paths, [ref. 221f0c9](https://github.com/bcosca/fatfree-core/commit/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, [bcosca/fatfree#1174](https://github.com/bcosca/fatfree/issues/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/vendor/fatfree/lib/COPYING b/vendor/fatfree/lib/COPYING new file mode 100755 index 0000000..3c7236c --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/audit.php b/vendor/fatfree/lib/audit.php new file mode 100644 index 0000000..a0d4338 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/auth.php b/vendor/fatfree/lib/auth.php new file mode 100644 index 0000000..a150ce4 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/base.php b/vendor/fatfree/lib/base.php new file mode 100644 index 0000000..dfb59c1 --- /dev/null +++ b/vendor/fatfree/lib/base.php @@ -0,0 +1,3589 @@ +. + +*/ + +//! 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.3-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['QUIET']) { + $error=array_diff_key( + $this->hive['ERROR'], + $this->hive['DEBUG']? + []: + ['trace'=>1] + ); + if ($this->hive['CLI']) + echo PHP_EOL.'==================================='.PHP_EOL. + 'ERROR '.$error['code'].' - '.$error['status'].PHP_EOL. + $error['text'].PHP_EOL.PHP_EOL.$error['trace']; + else + echo $this->hive['AJAX']? + json_encode($error): + (''.$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 (!empty($headers['X-Forwarded-Port'])) + $port=$headers['X-Forwarded-Port']; + elseif (!empty($_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']. + (!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/vendor/fatfree/lib/basket.php b/vendor/fatfree/lib/basket.php new file mode 100644 index 0000000..70cacee --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/bcrypt.php b/vendor/fatfree/lib/bcrypt.php new file mode 100644 index 0000000..414daa7 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/cli/ws.php b/vendor/fatfree/lib/cli/ws.php new file mode 100644 index 0000000..4545e9b --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/code.css b/vendor/fatfree/lib/code.css new file mode 100755 index 0000000..618703f --- /dev/null +++ b/vendor/fatfree/lib/code.css @@ -0,0 +1 @@ +code{word-wrap:break-word;color:black}.comment,.doc_comment,.ml_comment{color:dimgray;font-style:italic}.variable{color:blueviolet}.const,.constant_encapsed_string,.class_c,.dir,.file,.func_c,.halt_compiler,.line,.method_c,.lnumber,.dnumber{color:crimson}.string,.and_equal,.boolean_and,.boolean_or,.concat_equal,.dec,.div_equal,.inc,.is_equal,.is_greater_or_equal,.is_identical,.is_not_equal,.is_not_identical,.is_smaller_or_equal,.logical_and,.logical_or,.logical_xor,.minus_equal,.mod_equal,.mul_equal,.ns_c,.ns_separator,.or_equal,.plus_equal,.sl,.sl_equal,.sr,.sr_equal,.xor_equal,.start_heredoc,.end_heredoc,.object_operator,.paamayim_nekudotayim{color:black}.abstract,.array,.array_cast,.as,.break,.case,.catch,.class,.clone,.continue,.declare,.default,.do,.echo,.else,.elseif,.empty.enddeclare,.endfor,.endforach,.endif,.endswitch,.endwhile,.eval,.exit,.extends,.final,.for,.foreach,.function,.global,.goto,.if,.implements,.include,.include_once,.instanceof,.interface,.isset,.list,.namespace,.new,.print,.private,.public,.protected,.require,.require_once,.return,.static,.switch,.throw,.try,.unset,.use,.var,.while{color:royalblue}.open_tag,.open_tag_with_echo,.close_tag{color:orange}.ini_section{color:black}.ini_key{color:royalblue}.ini_value{color:crimson}.xml_tag{color:dodgerblue}.xml_attr{color:blueviolet}.xml_data{color:red}.section{color:black}.directive{color:blue}.data{color:dimgray} diff --git a/vendor/fatfree/lib/db/cursor.php b/vendor/fatfree/lib/db/cursor.php new file mode 100644 index 0000000..2fd324a --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/db/jig.php b/vendor/fatfree/lib/db/jig.php new file mode 100644 index 0000000..bcc29ca --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/db/jig/mapper.php b/vendor/fatfree/lib/db/jig/mapper.php new file mode 100644 index 0000000..5d26427 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/db/jig/session.php b/vendor/fatfree/lib/db/jig/session.php new file mode 100644 index 0000000..eee1339 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/db/mongo.php b/vendor/fatfree/lib/db/mongo.php new file mode 100644 index 0000000..46d00be --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/db/mongo/mapper.php b/vendor/fatfree/lib/db/mongo/mapper.php new file mode 100644 index 0000000..0246f6d --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/db/mongo/session.php b/vendor/fatfree/lib/db/mongo/session.php new file mode 100644 index 0000000..013f171 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/db/sql.php b/vendor/fatfree/lib/db/sql.php new file mode 100644 index 0000000..566b94a --- /dev/null +++ b/vendor/fatfree/lib/db/sql.php @@ -0,0 +1,552 @@ +. + +*/ + +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=FALSE; + if ($this->pdo->inTransaction()) + $out=$this->pdo->rollback(); + $this->trans=FALSE; + return $out; + } + + /** + * Commit SQL transaction + * @return bool + **/ + function commit() { + $out=FALSE; + if ($this->pdo->inTransaction()) + $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->pdo->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 + // format: engine_name => array of: + // 0: query + // 1: field name of column name + // 2: field name of column type + // 3: field name of default value + // 4: field name of nullable value + // 5: expected field value to be nullable + // 6: field name of primary key flag + // 7: expected field value to be a primary key + // 8: field name of auto increment check (optional) + // 9: expected field value to be an auto-incremented identifier + $cmd=[ + 'sqlite2?'=>[ + 'SELECT * FROM pragma_table_info('.$this->quote($table).') JOIN ('. + 'SELECT sql FROM sqlite_master WHERE type=\'table\' AND '. + 'name='.$this->quote($table).')', + 'name','type','dflt_value','notnull',0,'pk',TRUE,'sql', + '/\W(%s)\W+[^,]+?AUTOINCREMENT\W/i'], + 'mysql'=>[ + 'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`', + 'Field','Type','Default','Null','YES','Key','PRI','Extra','auto_increment'], + '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,'. + ($this->engine=='pgsql' + ?'COALESCE(POSITION(\'nextval\' IN C.COLUMN_DEFAULT),0) AS autoinc,' + :'columnproperty(object_id(C.TABLE_NAME),C.COLUMN_NAME,\'IsIdentity\')' + .' AS autoinc,'). + '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','autoinc',1], + '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; + if (!isset($rows[$row[$val[1]]])) // handle duplicate rows in PgSQL + $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], + 'auto_inc'=>isset($val[8]) && isset($row[$val[8]]) + ? ($this->engine=='sqlite'? + (bool) preg_match(sprintf($val[9],$row[$val[1]]), + $row[$val[8]]): + ($row[$val[8]]==$val[9]) + ) : NULL, + ]; + } + 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=str_replace('\\ ',' ',$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/vendor/fatfree/lib/db/sql/mapper.php b/vendor/fatfree/lib/db/sql/mapper.php new file mode 100644 index 0000000..574cc9f --- /dev/null +++ b/vendor/fatfree/lib/db/sql/mapper.php @@ -0,0 +1,765 @@ +. + +*/ + +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'); + } + // no need to order for a count query as that could include virtual + // field references that are not present here + unset($options['order']); + 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['auto_inc'] === TRUE || + ($field['auto_inc'] === NULL && !$field['nullable'] + && $field['pdo_type']==\PDO::PARAM_INT) + )) + $inc=$key; + unset($field); + } + foreach ($this->fields as $key=>&$field) { + if ($field['pkey']) { + $field['previous']=$field['value']; + if (!$inc && empty($field['value']) && + ($field['auto_inc'] === TRUE || ($field['auto_inc'] === NULL + && $field['pdo_type']==\PDO::PARAM_INT && !$field['nullable'])) + ) + $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/vendor/fatfree/lib/db/sql/session.php b/vendor/fatfree/lib/db/sql/session.php new file mode 100644 index 0000000..8defbf4 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/f3.php b/vendor/fatfree/lib/f3.php new file mode 100644 index 0000000..ae95942 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/image.php b/vendor/fatfree/lib/image.php new file mode 100644 index 0000000..b7f149c --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/log.php b/vendor/fatfree/lib/log.php new file mode 100644 index 0000000..5b7341d --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/magic.php b/vendor/fatfree/lib/magic.php new file mode 100644 index 0000000..f676506 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/markdown.php b/vendor/fatfree/lib/markdown.php new file mode 100644 index 0000000..4be4c56 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/matrix.php b/vendor/fatfree/lib/matrix.php new file mode 100644 index 0000000..6c22bae --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/session.php b/vendor/fatfree/lib/session.php new file mode 100644 index 0000000..168e5b6 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/smtp.php b/vendor/fatfree/lib/smtp.php new file mode 100644 index 0000000..8cef939 --- /dev/null +++ b/vendor/fatfree/lib/smtp.php @@ -0,0 +1,363 @@ +. + +*/ + +//! 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', + E_DIALOG='SMTP dialog error: %s'; + //@} + + 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); + } + if (preg_match('/^(4|5)\d{2}\s.*$/', $reply)) + user_error(sprintf(self::E_DIALOG,$reply),E_USER_ERROR); + 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/vendor/fatfree/lib/template.php b/vendor/fatfree/lib/template.php new file mode 100644 index 0000000..fb6f21d --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/test.php b/vendor/fatfree/lib/test.php new file mode 100644 index 0000000..d45bb18 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/utf.php b/vendor/fatfree/lib/utf.php new file mode 100644 index 0000000..34b8230 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/web.php b/vendor/fatfree/lib/web.php new file mode 100644 index 0000000..7a69051 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/web/geo.php b/vendor/fatfree/lib/web/geo.php new file mode 100644 index 0000000..98ba204 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/web/google/recaptcha.php b/vendor/fatfree/lib/web/google/recaptcha.php new file mode 100644 index 0000000..38fd2d0 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/web/google/staticmap.php b/vendor/fatfree/lib/web/google/staticmap.php new file mode 100644 index 0000000..023103d --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/web/oauth2.php b/vendor/fatfree/lib/web/oauth2.php new file mode 100644 index 0000000..eda6e45 --- /dev/null +++ b/vendor/fatfree/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|array|FALSE + * @param $uri string + * @param $method string + * @param $token string + **/ + 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/vendor/fatfree/lib/web/openid.php b/vendor/fatfree/lib/web/openid.php new file mode 100644 index 0000000..6e84b61 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/lib/web/pingback.php b/vendor/fatfree/lib/web/pingback.php new file mode 100644 index 0000000..28c51a5 --- /dev/null +++ b/vendor/fatfree/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/vendor/fatfree/readme.md b/vendor/fatfree/readme.md new file mode 100644 index 0000000..41ca3be --- /dev/null +++ b/vendor/fatfree/readme.md @@ -0,0 +1,2616 @@ +[![Fat-Free Framework](ui/images/logo.png)](http://fatfree.sf.net/) + +**A powerful yet easy-to-use PHP micro-framework designed to help you build dynamic and robust Web applications - fast!** + +[![Flattr this project](https://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=phpfatfree&url=https://github.com/bcosca/fatfree) + +Condensed in a single ~65KB file, F3 (as we fondly call it) gives you solid foundation, a mature code base, and a no-nonsense approach to writing Web applications. Under the hood is an easy-to-use Web development tool kit, a high-performance URL routing and cache engine, built-in code highlighting, and support for multilingual applications. It's lightweight, easy-to-use, and fast. Most of all, it doesn't get in your way. + +Whether you're a novice or an expert PHP programmer, F3 will get you up and running in no time. No unnecessary and painstaking installation procedures. No complex configuration required. No convoluted directory structures. There's no better time to start developing Web applications the easy way than right now! + +F3 supports both SQL and NoSQL databases off-the-shelf: MySQL, SQLite, MSSQL/Sybase, PostgreSQL, DB2, and MongoDB. It also comes with powerful object-relational mappers for data abstraction and modeling that are just as lightweight as the framework. No configuration needed. + +That's not all. F3 is packaged with other optional plug-ins that extend its capabilities:- + +* Fast and clean template engine, +* Unit testing toolkit, +* Database-managed sessions with automatic CSRF protection, +* Markdown-to-HTML converter, +* Atom/RSS feed reader, +* Image processor, +* Geodata handler, +* Google static maps, +* On-the-fly Javascript/CSS compressor, +* OpenID (consumer), +* Custom logger, +* Basket/Shopping cart, +* Pingback server/consumer, +* Unicode-aware string functions, +* SMTP over SSL/TLS, +* Tools for communicating with other servers, +* And more in a tiny supercharged package! + +Unlike other frameworks, F3 aims to be usable - not usual. + +[![Flattr this project](https://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=phpfatfree&url=https://github.com/bcosca/fatfree) + +The philosophy behind the framework and its approach to software architecture is towards minimalism in structural components, avoiding application complexity and striking a balance between code elegance, application performance and programmer productivity. + +[![Paypal](ui/images/paypal.png)](https://www.paypal.me/fatfree) + +## Table of Contents + +* [Getting Started](#getting-started) +* [Routing Engine](#routing-engine) +* [Framework Variables](#framework-variables) +* [Views and Templates](#views-and-templates) +* [Databases](#databases) +* [Plug-Ins](#plug-ins) +* [Optimization](#optimization) +* [Unit Testing](#unit-testing) +* [Quick Reference](#quick-reference) +* [Support and Licensing](#support-and-licensing) + +[![Twitter](ui/images/twitter.png)](https://twitter.com/phpfatfree) + +### Get the latest release! + +F3 has a stable enterprise-class architecture. Unbeatable performance, user-friendly features and a lightweight footprint. What more can you ask for? +To get this package, simply download this package or visit the [fatfree-core](https://github.com/bcosca/fatfree-core) repository to find the latest edge-version. + +For all composer users out there: + +* start a new project using `composer create-project bcosca/fatfree:dev-init` +* add fatfree to your existing project with `composer require bcosca/fatfree-core` + +It is highly recommended that experienced users develop new applications with the latest version to take advantage of an updated code base and ongoing improvements. + +## Please visit FatFreeFramework.com + +**The most up-to-date user-guide and detailed API documentation with lots of code examples and a graphic guide can be found at [fatfreeframework.com/](http://fatfreeframework.com/).** + +Of course this handy online reference is powered by F3! It showcases the framework's capability and performance. Check it out now. If you'd like to read it at github directly, you can find the websites content at [github.com/F3Community/F3com-data](https://github.com/F3Community/F3com-data) + +## Getting Started + +> *A designer knows he has achieved perfection not when there is nothing left to add, but when there is nothing left to take away. -- Antoine de Saint-Exupéry* + +Fat-Free Framework makes it easy to build entire Web sites in a jiffy. With the same power and brevity as modern Javascript toolkits and libraries, F3 helps you write better-looking and more reliable PHP programs. One glance at your PHP source code and anyone will find it easy to understand, how much you can accomplish in so few lines of code, and how powerful the results are. + +F3 is one of the best documented frameworks around. Learning it costs next to nothing. No strict set of difficult-to-navigate directory structures and obtrusive programming steps. No truck load of configuration options just to display `'Hello, World'` in your browser. Fat-Free gives you a lot of freedom - and style - to get more work done with ease and in less time. + +F3's declarative approach to programming makes it easy for novices and experts alike to understand PHP code. If you're familiar with the programming language Ruby, you'll notice the resemblance between Fat-Free and Sinatra micro-framework because they both employ a simple Domain-Specific Language for ReSTful Web services. But unlike Sinatra and its PHP incarnations (Fitzgerald, Limonade, Glue - to name a few), Fat-Free goes beyond just handling routes and requests. Views can be in any form, such as plain text, HTML, XML or an e-mail message. The framework comes with a fast and easy-to-use template engine. F3 also works seamlessly with other template engines, including Twig, Smarty, and PHP itself. Models communicate with F3's data mappers and the SQL helper for more complex interactions with various database engines. Other plug-ins extend the base functionality even more. It's a total Web development framework - with a lot of muscle! + +### Enough Said - See For Yourself + +Unzip the contents of the distribution package anywhere in your hard drive. By default, the framework file and optional plug-ins are located in the `lib/` path. Organize your directory structures any way you want. You may move the default folders to a path that's not Web-accessible for better security. Delete the plug-ins that you don't need. You can always restore them later and F3 will detect their presence automatically. + +**Important:** If your application uses APC, Memcached, WinCache, XCache, or a filesystem cache, clear all cache entries first before overwriting an older version of the framework with a new one. + +Make sure you're running the right version of PHP. F3 does not support versions earlier than PHP 5.4. You'll be getting syntax errors (false positives) all over the place because new language constructs and closures/anonymous functions are not supported by outdated PHP versions. To find out, open your console (`bash` shell on GNU/Linux, or `cmd.exe` on Windows):- + +``` +/path/to/php -v +``` + +PHP will let you know which particular version you're running and you should get something that looks similar to this:- + +``` +PHP 5.4.30 (cli) (built: Jul 22 2014 21:34:41) +Copyright (c) 1997-2014 The PHP Group +Zend Engine v2.4.0, Copyright (c) 1998-2014 Zend Technologies + with Xdebug v2.2.5, Copyright (c) 2002-2014, by Derick Rethans +``` + +Upgrade if necessary and come back here if you've made the jump to PHP 5.4 or a later release. If you need a PHP 5.4+ hosting service provider, try one of these services: + +* [A2 Hosting](http://www.a2hosting.com/2461-15-1-72.html) +* [DreamHost](http://www.dreamhost.com/r.cgi?665472) +* [Hostek](http://hostek.com/aff.php?aff=364&plat=L) +* [SiteGround](http://www.siteground.com/index.htm?referrerid=155694) + +### Hello, World: The Less-Than-A-Minute Fat-Free Recipe + +Time to start writing our first application:- + +``` php +$f3 = require('path/to/base.php'); +$f3->route('GET /', + function() { + echo 'Hello, world!'; + } +); +$f3->run(); +``` + +Prepend `base.php` on the first line with the appropriate path. Save the above code fragment as `index.php` in your Web root folder. We've written our first Web page. + +Using composer? Then just run `composer require bcosca/fatfree` and use the following: + +``` php +require 'vendor/autoload.php'; +$f3 = \Base::instance(); +$f3->route('GET /', + function() { + echo 'Hello, world!'; + } +); +$f3->run(); +``` + +The first command tells the PHP interpreter that you want the framework's functions and features available to your application. The `$f3->route()` method informs Fat-Free that a Web page is available at the relative URL indicated by the slash (`/`). Anyone visiting your site located at `http://www.example.com/` will see the `'Hello, world!'` message because the URL `/` is equivalent to the root page. To create a route that branches out from the root page, like `http://www.example.com/inside/`, you can define another route with a simple `GET /inside` string. + +The route described above tells the framework to render the page only when it receives a URL request using the HTTP `GET` method. More complex Web sites containing forms use other HTTP methods like `POST`, and you can also implement that as part of a `$f3->route()` specification. + +If the framework sees an incoming request for your Web page located at the root URL `/`, it will automatically route the request to the callback function, which contains the code necessary to process the request and render the appropriate HTML stuff. In this example, we just send the string `'Hello, world!'` to the user's Web browser. + +So we've established our first route. But that won't do much, except to let F3 know that there's a process that will handle it and there's some text to display on the user's Web browser. If you have a lot more pages on your site, you need to set up different routes for each group. For now, let's keep it simple. To instruct the framework to start waiting for requests, we issue the `$f3->run()` command. + +**Can't Get the Example Running?** If you're having trouble getting this simple program to run on your server, you may have to tweak your Web server settings a bit. Take a look at the sample Apache configuration in the following section (along with the Nginx and Lighttpd equivalents). + +**Still having trouble?** Make sure the `$f3 = require('path/to/base.php');` assignment comes before any output in your script. `base.php` modifies the HTTP headers, so any character that is output to the browser before this assignment will cause errors. + +## Routing Engine + +### Overview + +Our first example wasn't too hard to swallow, was it? If you like a little more flavor in your Fat-Free soup, insert another route before the `$f3->run()` command:- + +``` php +$f3->route('GET /about', + function() { + echo 'Donations go to a local charity... us!'; + } +); +``` + +You don't want to clutter the global namespace with function names? Fat-Free recognizes different ways of mapping route handlers to OOP classes and methods:- + +``` php +class WebPage { + function display() { + echo 'I cannot object to an object'; + } +} + +$f3->route('GET /about','WebPage->display'); +``` + +HTTP requests can also be routed to static class methods:- + +``` php +$f3->route('GET /login','Auth::login'); +``` + +Passed arguments are always provided as the second parameter: + +``` php +$f3->route('GET /hello/@name','User::greet'); + +class User { + public static function greet($f3, $args) { //$args is type of Array + echo "Hello " . $args['name']; + } +} +``` +If the provided name argument would be **foo** (/hello/foo), the following output would be shown: + +``` +Hello foo +``` + +### Routes and Tokens + +As a demonstration of Fat-Free's powerful domain-specific language (DSL), you can specify a single route to handle different possibilities:- + +``` php +$f3->route('GET /brew/@count', + function($f3) { + echo $f3->get('PARAMS.count').' bottles of beer on the wall.'; + } +); +``` + +This example shows how we can specify a token `@count` to represent part of a URL. The framework will serve any request URL that matches the `/brew/` prefix, like `/brew/99`, `/brew/98`, etc. This will display `'99 bottles of beer on the wall'` and `'98 bottles of beer on the wall'`, respectively. Fat-Free will also accept a page request for `/brew/unbreakable`. (Expect this to display `'unbreakable bottles of beer on the wall'`.) When such a dynamic route is specified, Fat-Free automagically populates the global `PARAMS` array variable with the value of the captured strings in the URL. The `$f3->get()` call inside the callback function retrieves the value of a framework variable. You can certainly apply this method in your code as part of the presentation or business logic. But we'll discuss that in greater detail later. + +Notice that Fat-Free understands array dot-notation. You can use `PARAMS['count']` regular notation instead in code, which is prone to typo errors and unbalanced braces. In views and templates, the framework permits `@PARAMS.count` notation which is somewhat similar to Javascript. (We'll cover views and templates later.) + +Here's another way to access tokens in a request pattern:- + +``` php +$f3->route('GET /brew/@count', + function($f3,$params) { + echo $params['count'].' bottles of beer on the wall.'; + } +); +``` + +You can use the asterisk (`*`) to accept any URL after the `/brew` route - if you don't really care about the rest of the path:- + +``` php +$f3->route('GET /brew/*', + function() { + echo 'Enough beer! We always end up here.'; + } +); +``` + +An important point to consider: You will get Fat-Free (and yourself) confused if you have both `GET /brew/@count` and `GET /brew/*` together in the same application. Use one or the other. Another thing: Fat-Free sees `GET /brew` as separate and distinct from the route `GET /brew/@count`. Each can have different route handlers. + + +### Dynamic Web Sites + +Wait a second - in all the previous examples, we never really created any directory in our hard drive to store these routes. The short answer: we don't have to. All F3 routes are virtual. They don't mirror our hard disk folder structure. If you have programs or static files (images, CSS, etc.) that do not use the framework - as long as the paths to these files do not conflict with any route defined in your application - your Web server software will deliver them to the user's browser, provided the server is configured properly. + +### Named Routes + +When you define a route, you can assign it a name. Use the route name in your code and templates instead of a typed url. Then if you need to change your urls to please the marketing overlords, you only need to make the change where the route was defined. The route names must follow php variable naming rules (no dots, dashes nor hyphens). + +Let's name a route:- + +``` php +$f3->route('GET @beer_list: /beer', 'Beer->list'); +``` + +The name is inserted after the route VERB (`GET` in this example) preceded by an `@` symbol, and separated from the URL portion by a colon `:` symbol. You can insert a space after the colon if that makes it easier to read your code (as shown here). + +To access the named route in a template, get the value of the named route as the key of the `ALIASES` hive array:- + +``` html +View beer list +``` + +To redirect the visitor to a new URL, call the named route inside the `reroute()` method like:- + +``` php +// a named route is a string value +$f3->reroute('@beer_list'); // note the single quotes +``` + +If you use tokens in your route, F3 will replace those tokens with their current value. If you want to change the token's value before calling reroute, pass it as the 2nd argument.:- + +``` php +$f3->route('GET @beer_list: /beer/@country', 'Beer->bycountry'); +$f3->route('GET @beer_list: /beer/@country/@village', 'Beer->byvillage'); + +// a set of key-value pairs is passed as argument to named route +$f3->reroute('@beer_list(@country=Germany)'); + +// if more than one token needed +$f3->reroute('@beer_list(@country=Germany,@village=Rhine)'); +``` + +Remember to `urlencode()` your arguments if you have characters that do not comply with RFC 1738 guidelines for well-formed URLs. + +### PHP 5.4's Built-In Web Server + +PHP's latest stable version has its own built-in Web server. Start it up using the following configuration:- + +``` +php -S localhost:80 -t /var/www/ +``` + +The above command will start routing all requests to the Web root `/var/www`. If an incoming HTTP request for a file or folder is received, PHP will look for it inside the Web root and send it over to the browser if found. Otherwise, PHP will load the default `index.php` (containing your F3-enabled code). + +### Sample Apache Configuration + +If you're using Apache, make sure you activate the URL rewriting module (mod_rewrite) in your apache.conf (or httpd.conf) file. You should also create a .htaccess file containing the following:- + +``` apache +# 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 ^(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] +``` + +The script tells Apache that whenever an HTTP request arrives and if no physical file (`!-f`) or path (`!-d`) or symbolic link (`!-l`) can be found, it should transfer control to `index.php`, which contains our main/front controller, and which in turn, invokes the framework. + +The `.htaccess file` containing the Apache directives stated above should always be in the same folder as `index.php`. + +You also need to set up Apache so it knows the physical location of `index.php` in your hard drive. A typical configuration is:- + +``` apache +DocumentRoot "/var/www/html" + + Options -Indexes +FollowSymLinks +Includes + AllowOverride All + Order allow,deny + Allow from All + +``` + +If you're developing several applications simultaneously, a virtual host configuration is easier to manage:- + +``` apache +NameVirtualHost * + + ServerName site1.com + DocumentRoot "/var/www/site1" + + Options -Indexes +FollowSymLinks +Includes + AllowOverride All + Order allow,deny + Allow from All + + + + ServerName site2.com + DocumentRoot "/var/www/site2" + + Options -Indexes +FollowSymLinks +Includes + AllowOverride All + Order allow,deny + Allow from All + + +``` + +Each `ServerName` (`site1.com` and `site2.com` in our example) must be listed in your `/etc/hosts` file. On Windows, you should edit `C:/WINDOWS/system32/drivers/etc/hosts`. A reboot might be necessary to effect the changes. You can then point your Web browser to the address `http://site1.com` or `http://site2.com`. Virtual hosts make your applications a lot easier to deploy. + +### Sample Nginx Configuration + +For Nginx servers, here's the recommended configuration (replace ip_address:port with your environment's FastCGI PHP settings):- + +``` nginx +server { + root /var/www/html; + location / { + index index.php index.html index.htm; + try_files $uri $uri/ /index.php?$query_string; + } + location ~ \.php$ { + fastcgi_pass ip_address:port; + include fastcgi_params; + } +} +``` + +### Sample Lighttpd Configuration + +Lighttpd servers are configured in a similar manner:- + +``` +$HTTP["host"] =~ "www\.example\.com$" { + url.rewrite-once = ( "^/(.*?)(\?.+)?$"=>"/index.php/$1?$2" ) + server.error-handler-404 = "/index.php" +} +``` + +### Sample IIS Configuration + +Install the [URL rewrite module](http://www.iis.net/downloads/microsoft/url-rewrite) and the appropriate .NET framework corresponding to your Windows version. Then create a file named `web.config` in your application root with the following contents: + +``` + + + + + + + + + + + + + + + + + +``` + +### Rerouting + +So let's get back to coding. You can declare a page obsolete and redirect your visitors to another site/page:- + +``` php +$f3->route('GET|HEAD /obsoletepage', + function($f3) { + $f3->reroute('/newpage'); + } +); +``` + +If someone tries to access the URL `http://www.example.com/obsoletepage` using either HTTP GET or HEAD request, the framework redirects the user to the URL: `http://www.example.com/newpage` as shown in the above example. You can also redirect the user to another site, like `$f3->reroute('http://www.anotherexample.org/');`. + +Rerouting can be particularly useful when you need to do some maintenance work on your site. You can have a route handler that informs your visitors that your site is offline for a short period. + +HTTP redirects are indispensable but they can also be expensive. As much as possible, refrain from using `$f3->reroute()` to send a user to another page on the same Web site if you can direct the flow of your application by invoking the function or method that handles the target route. However, this approach will not change the URL on the address bar of the user's Web browser. If this is not the behavior you want and you really need to send a user to another page, in instances like successful submission of a form or after a user has been authenticated, Fat-Free sends an `HTTP 302 Found` header. For all other attempts to reroute to another page or site, the framework sends an `HTTP 301 Moved Permanently` header. + +### Triggering a 404 + +At runtime, Fat-Free automatically generates an HTTP 404 error whenever it sees that an incoming HTTP request does not match any of the routes defined in your application. However, there are instances when you need to trigger it yourself. + +Take for instance a route defined as `GET /dogs/@breed`. Your application logic may involve searching a database and attempting to retrieve the record corresponding to the value of `@breed` in the incoming HTTP request. Since Fat-Free will accept any value after the `/dogs/` prefix because of the presence of the `@breed` token, displaying an `HTTP 404 Not Found` message programmatically becomes necessary when the program doesn't find any match in our database. To do that, use the following command:- + +``` php +$f3->error(404); +``` + +### Representational State Transfer (ReST) + +Fat-Free's architecture is based on the concept that HTTP URIs represent abstract Web resources (not limited to HTML) and each resource can move from one application state to another. For this reason, F3 does not have any restrictions on the way you structure your application. If you prefer to use the [Model-View-Controller](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) pattern, F3 can help you compartmentalize your application components to stick to this paradigm. On the other hand, the framework also supports the [Resource-Method-Representation](http://www.peej.co.uk/articles/rmr-architecture.html) pattern, and implementing it is more straightforward. + +Here's an example of a ReST interface:- + +``` php +class Item { + function get() {} + function post() {} + function put() {} + function delete() {} +} + +$f3=require('lib/base.php'); +$f3->map('/cart/@item','Item'); +$f3->run(); +``` + +Fat-Free's `$f3->map()` method provides a ReST interface by mapping HTTP methods in routes to the equivalent methods of an object or a PHP class. If your application receives an incoming HTTP request like `GET /cart/123`, Fat-Free will automatically transfer control to the object's or class' `get()` method. On the other hand, a `POST /cart/123` request will be routed to the `Item` class' `post()` method. + +**Note:** Browsers do not implement the HTTP `PUT` and `DELETE` methods in regular HTML forms. These and other ReST methods (`HEAD`, and `CONNECT`) are accessible only via AJAX calls to the server. + +If the framework receives an HTTP request for a route that maps to a method that is not implemented by a class (perhaps you've made an error in the route mapping, or the method is not written yet), it generates an `HTTP 405 Method Not Allowed` error. + +If a client requests HTTP `OPTIONS` for a URL resource, F3 responds with the appropriate HTTP headers that indicate which methods are allowed for the resource (HEAD, GET, PUT, etc). The framework will not map the `OPTIONS` request to a class. + +### The F3 Autoloader + +Fat-Free has a way of loading classes only at the time you need them, so they don't gobble up more memory than a particular segment of your application needs. And you don't have to write a long list of `include` or `require` statements just to load PHP classes saved in different files and different locations. The framework can do this automatically for you. Just save your files (one class per file) in a folder and tell the framework to automatically load the appropriate file once you invoke a method in the class:- + +``` php +$f3->set('AUTOLOAD','autoload/'); +``` + +You can assign a different location for your autoloaded classes by changing the value of the `AUTOLOAD` global variable. You can also have multiple autoload paths. If you have your classes organized and in different folders, you can instruct the framework to autoload the appropriate class when a static method is called or when an object is instantiated. Modify the `AUTOLOAD` variable this way:- + +``` php +$f3->set('AUTOLOAD','admin/autoload/; user/autoload/; default/'); +``` + +**Important:** Except for the .php extension, the class name and file name must be identical, for the framework to autoload your class properly. The basename of this file must be identical to your class invocation, e.g. F3 will look for either `Foo/BarBaz.php` or `foo/barbaz.php` when it detects a `new Foo\BarBaz` statement in your application. + +### Working with Namespaces + +`AUTOLOAD` allows class hierarchies to reside in similarly-named subfolders, so if you want the framework to autoload a PHP 5.4 namespaced class that's invoked in the following manner:- + +``` php +$f3->set('AUTOLOAD','autoload/'); +$obj=new Gadgets\iPad; +``` + +You can create a folder hierarchy that follows the same structure. Assuming `/var/www/html/` is your Web root, then F3 will look for the class in `/var/www/html/autoload/gadgets/ipad.php`. The file `ipad.php` should have the following minimum code:- + +``` php +namespace Gadgets; +class iPad {} +``` + +Remember: All directory names in Fat-Free must end with a slash. You can assign a search path for the autoloader as follows:- + +``` php +$f3->set('AUTOLOAD','main/;aux/'); +``` + +### Routing to a Namespaced Class + +F3, being a namespace-aware framework, allows you to use a method in namespaced class as a route handler, and there are several ways of doing it. To call a static method:- + +``` php +$f3->set('AUTOLOAD','classes/'); +$f3->route('GET|POST /','Main\Home::show'); +``` + +The above code will invoke the static `show()` method of the class `Home` within the `Main` namespace. The `Home` class must be saved in the folder `classes/main/home.php` for it to be loaded automatically. + +If you prefer to work with objects:- + +``` php +$f3->route('GET|POST /','Main\Home->show'); +``` + +will instantiate the `Home` class at runtime and call the `show()` method thereafter. + +### Event Handlers + +F3 has a couple of routing event listeners that might help you improve the flow and structure of controller classes. Say you have a route defined as follows:- + +``` php +$f3->route('GET /','Main->home'); +``` + +If the application receives an HTTP request matching the above route, F3 instantiates `Main`, but before executing the `home()` method, the framework looks for a method in this class named `beforeRoute()`. In case it's found, F3 runs the code contained in the `beforeRoute()` event handler before transferring control to the `home()` method. Once this is accomplished, the framework looks for an `afterRoute()` event handler. Like `beforeRoute()`, the method gets executed if it's defined. + +### Dynamic Route Handlers + +Here's another F3 goodie:- + +``` php +$f3->route('GET /products/@action','Products->@action'); +``` + +If your application receives a request for, say, `/products/itemize`, F3 will extract the `'itemize'` string from the URL and pass it on to the `@action` token in the route handler. F3 will then look for a class named `Products` and execute the `itemize()` method. + +Dynamic route handlers may have various forms:- + +``` php +// static method +$f3->route('GET /public/@genre','Main::@genre'); +// object mode +$f3->route('GET /public/@controller/@action','@controller->@action'); +``` + +F3 triggers an `HTTP 404 Not Found` error at runtime if it cannot transfer control to the class or method associated with the current route, i.e. an undefined class or method. + +### AJAX and Synchronous Requests + +Routing patterns may contain modifiers that direct the framework to base its routing decision on the type of HTTP request:- + +``` php +$f3->route('GET /example [ajax]','Page->getFragment'); +$f3->route('GET /example [sync]','Page->getFull'); +``` + +The first statement will route the HTTP request to the `Page->getFragment()` callback only if an `X-Requested-With: XMLHttpRequest` header (AJAX object) is received by the server. If an ordinary (synchronous) request is detected, F3 will simply drop down to the next matching pattern, and in this case it executes the `Page->getFull()` callback. + +If no modifiers are defined in a routing pattern, then both AJAX and synchronous request types are routed to the specified handler. + +Route pattern modifiers are also recognized by `$f3->map()`. + +## Framework Variables + +### Basic Use + +Variables defined in Fat-Free are global, i.e. they can be accessed by any MVC component. Framework globals are not identical to PHP globals. An F3 variable named `content` is not identical to PHP's `$content`. F3 is a domain-specific language in its own right and maintains its own separate symbol table for system and application variables. The framework, like every well-designed object-oriented program, does not pollute the PHP global namespace with constants, variables, functions or classes that might conflict with any application. Unlike other frameworks, F3 does not use PHP's `define()` statement. All framework constants are confined to classes. + +To assign a value to a Fat-Free variable: + +``` php +$f3->set('var',value); // or +$f3->var=value; + +$f3->set('hello.world','good morning'); // translates to: 'hello' == array('world'=>'good morning') +$f3->{'hello.world'}='good morning'; // same as prior statement +``` + +**Note:** Fat-Free variables accept all PHP data types, including objects and anonymous functions. + +To set several variables at once: + +``` php +$f3->mset( + [ + 'foo'=>'bar', + 'baz'=>123 + ] +); +``` + +To retrieve the value of a framework variable named `var`:- + +``` php +echo $f3->get('var'); // or +echo $f3->var; +``` + +To remove a Fat-Free variable from memory if you no longer need it (discard it so it doesn't interfere with your other functions/methods), use the method:- + +``` php +$f3->clear('var'); // or +unset($f3->var); +``` + +To find out if a variable has been previously defined:- + +``` php +$f3->exists('var') // +isset($f3->var) +``` + +### Globals + +F3 maintains its own symbol table for framework and application variables, which are independent of PHP's. Some variables are mapped to PHP globals. Fat-Free's `SESSION` is equivalent to `$_SESSION`, and `REQUEST` maps to `$_REQUEST`. Use of framework variables is recommended, instead of PHP's, to help you with data transfer across different functions, classes and methods. They also have other advantages:- + +* You can use framework variables directly in your templates. +* You don't have to instruct PHP to reference a variable outside the current scope using a global keyword inside each function or method. All F3 variables are global to your application. +* Setting the Fat-Free equivalent of a PHP global like `SESSION` also changes PHP's underlying `$_SESSION`. Altering the latter also alters the framework counterpart. + +Fat-Free does not maintain just a dumb storage for variables and their values. It can also automate session management and other things. Assigning or retrieving a value through F3's `SESSION` variable auto-starts the session. If you use `$_SESSION` (or session-related functions) directly, instead of the framework variable `SESSION`, your application becomes responsible for managing sessions. + +As a rule, framework variables do not persist between HTTP requests. Only `SESSION` and `COOKIE` (and their elements) which are mapped to PHP's `$_SESSION` and `$_COOKIE` global variables are exempt from the stateless nature of HTTP. + +There are several predefined global variables used internally by Fat-Free, and you can certainly utilize them in your application. Be sure you know what you're doing. Altering some Fat-Free global variables may result in unexpected framework behavior. + +The framework has several variables to help you keep your files and directory structures organized. We've seen how we can automate class loading by using the `AUTOLOAD`. There's a `UI` global variable, which contains the path pointing to the location of your HTML views/templates. `DEBUG` is another variable you'll be using quite often during application development and it's used for setting the verbosity of error traces. + +Refer to the [Quick Reference](#quick-reference) if you need a comprehensive list of built-in framework variables. + +### Naming Rules + +A framework variable may contain any number of letters, digits and underscores. It must start with an alpha character and should have no spaces. Variable names are case-sensitive. + +F3 uses all-caps for internal predefined global variables. Nothing stops you from using variable names consisting of all-caps in your own program, but as a general rule, stick to lowercase (or camelCase) when you set up your own variables so you can avoid any possible conflict with current and future framework releases. + +You should not use PHP reserved words like `if`, `for`, `class`, `default`, etc. as framework variable names. These may cause unpredictable results. + +### Working with String and Array Variables + +F3 also provides a number of tools to help you with framework variables. + +``` php +$f3->set('a','fire'); +$f3->concat('a','cracker'); +echo $f3->get('a'); // returns the string 'firecracker' + +$f3->copy('a','b'); +echo $f3->get('b'); // returns the same string: 'firecracker' +``` + +F3 also provides some primitive methods for working with array variables:- + +``` php +$f3->set('colors',['red','blue','yellow']); +$f3->push('colors','green'); // works like PHP's array_push() +echo $f3->pop('colors'); // returns 'green' + +$f3->unshift('colors','purple'); // similar to array_unshift() +echo $f3->shift('colors'); // returns 'purple' + +$f3->set('grays',['light','dark']); +$result=$f3->merge('colors','grays'); // merges the two arrays +``` + +### Do-It-Yourself Directory Structures + +Unlike other frameworks that have rigid folder structures, F3 gives you a lot of flexibility. You can have a folder structure that looks like this (parenthesized words in all-caps represent the F3 framework variables that need tweaking):- + +``` +/ (your Web root, where index.php is located) +app/ (application files) + dict/ (LOCALES, optional) + controllers/ + logs/ (LOGS, optional) + models/ + views/ (UI) +css/ +js/ +lib/ (you can store base.php here) +tmp/ (TEMP, used by the framework) + cache/ (CACHE) +``` + +Feel free to organize your files and directories any way you want. Just set the appropriate F3 global variables. If you want a really secure site, Fat-Free even allows you to store all your files in a non-Web-accessible directory. The only requirement is that you leave `index.php`, `.htaccess` and your public files, like CSS, JavaScript, images, etc. in a path visible to your browser. + +### About the F3 Error Handler + +Fat-Free generates its own HTML error pages, with stack traces to help you with debugging. Here's an example:- + +> --- +> ### Internal Server Error +> strpos() expects at least 2 parameters, 0 given +> +> • var/html/dev/main.php:96 strpos() +> • var/html/dev/index.php:16 Base->run() +> --- + +If you feel it's a bit too plain or wish to do other things when the error occurs, you may create your own custom error handler:- + +``` php +$f3->set('ONERROR', + function($f3) { + // custom error handler code goes here + // use this if you want to display errors in a + // format consistent with your site's theme + echo $f3->get('ERROR.status'); + } +); +``` + +F3 maintains a global variable containing the details of the latest error that occurred in your application. The `ERROR` variable is an array structured as follows:- + +``` +ERROR.code - displays the error code (404, 500, etc.) +ERROR.status - header and page title +ERROR.text - error context +ERROR.trace - stack trace +``` + +While developing your application, it's best to set the debug level to maximum so you can trace all errors to their root cause:- + +``` php +$f3->set('DEBUG',3); +``` + +Just insert the command in your application's bootstrap sequence. + +Once your application is ready for release, simply remove the statement from your application, or replace it with:- + +``` php +$f3->set('DEBUG',0); +``` + +This will suppress the stack trace output in any system-generated HTML error page (because it's not meant to be seen by your site visitors). + +`DEBUG` can have values ranging from 0 (stack trace suppressed) to 3 (most verbose). + +**Don't forget!** Stack traces may contain paths, file names, database commands, user names and passwords. You might expose your Web site to unnecessary security risks if you fail to set the `DEBUG` global variable to 0 in a production environment. + +### Configuration Files + +If your application needs to be user-configurable, F3 provides a handy method for reading configuration files to set up your application. This way, you and your users can tweak the application without altering any PHP code. + +Instead of creating a PHP script that contains the following sample code:- + +``` php +$f3->set('num',123); +$f3->set('str','abc'); +$f3->set('hash',['x'=>1,'y'=>2,'z'=>3]); +$f3->set('items',[7,8,9]); +$f3->set('mix',['this',123.45,FALSE]); +``` + +You can construct a configuration file that does the same thing:- + +``` ini +[globals] +num=123 +; this is a regular string +str=abc +; another way of assigning strings +str="abc" +; this is an array +hash[x]=1 +hash[y]=2 +hash[z]=3 +; dot-notation is recognized too +hash.x=1 +hash.y=2 +hash.z=3 +; this is also an array +items=7,8,9 +; array with mixed elements +mix="this",123.45,FALSE +``` + +Instead of lengthy `$f3->set()` statements in your code, you can instruct the framework to load a configuration file as code substitute. Let's save the above text as setup.cfg. We can then call it with a simple:- + +``` php +$f3->config('setup.cfg'); +``` + +String values need not be quoted, unless you want leading or trailing spaces included. If a comma should be treated as part of a string, enclose the string using double-quotes - otherwise, the value will be treated as an array (the comma is used as an array element separator). Strings can span multiple lines:- + +``` ini +[globals] +str="this is a \ +very long \ +string" +``` + +F3 also gives you the ability to define HTTP routes in configuration files:- + +``` ini +[routes] +GET /=home +GET /404=App->page404 +GET /page/@num=Page->@controller +``` + +Route maps can be defined in configuration files too:- + +``` ini +[maps] +/blog=Blog\Login +/blog/@controller=Blog\@controller +``` + +The `[globals]`, `[routes]`, and `[maps]` section headers are required. You can combine both sections in a single configuration file - although having `[routes]` and `[maps]` in a separate file is recommended. This way you can allow end-users to modify some application-specific flags, and at the same time restrict them from meddling with your routing logic. + +## Views and Templates + +### Separation of Concerns + +A user interface like an HTML page should be independent of the underlying PHP code related to routing and business logic. This is fundamental to the MVC paradigm. A basic revision like converting `

    ` to `

    ` should not demand a change in your application code. In the same manner, transforming a simple route like `GET /about` to `GET /about-us` should not have any effect on your user interface and business logic, (the view and model in MVC, or representation and method in RMR). + +Mixing programming constructs and user interface components in a single file, like spaghetti coding, makes future application maintenance a nightmare. + +### PHP as a Template Engine + +F3 supports PHP as a template engine. Take a look at this HTML fragment saved as `template.htm`:-. + +``` html +

    Hello, !

    +``` + +If short tags are enabled on your server, this should work too:- + +``` html +

    Hello,

    +``` + +To display this template, you can have PHP code that looks like this (stored in a file separate from the template):- + +``` php +$f3=require('lib/base.php'); +$f3->route('GET /', + function($f3) { + $f3->set('name','world'); + $view=new View; + echo $view->render('template.htm'); + // Previous two lines can be shortened to:- + // echo View::instance()->render('template.htm'); + } +); +$f3->run(); +``` + +The only issue with using PHP as a template engine, due to the embedded PHP code in these files, is the conscious effort needed to stick to the guidelines on separation of concerns and resist the temptation of mixing business logic with your user interface. + +### A Quick Look at the F3 Template Language + +As an alternative to PHP, you can use F3's own template engine. The above HTML fragment can be rewritten as:- + +``` html +

    Hello, {{ @name }}!

    +``` + +and the code needed to view this template:- + +``` php +$f3=require('lib/base.php'); +$f3->route('GET /', + function($f3) { + $f3->set('name','world'); + $template=new Template; + echo $template->render('template.htm'); + // Above lines can be written as:- + // echo Template::instance()->render('template.htm'); + } +); +$f3->run(); +``` + +Like routing tokens used for catching variables in URLs (still remember the `GET /brew/@count` example in the previous section?), F3 template tokens begin with the `@` symbol followed by a series of letters and digits enclosed in curly braces. The first character must be alpha. Template tokens have a one-to-one correspondence with framework variables. The framework automatically replaces a token with the value stored in a variable of the same name. + +In our example, F3 replaces the `@name` token in our template with the value we assigned to the name variable. At runtime, the output of the above code will be:- + +``` html +

    Hello, world

    +``` + +Worried about performance of F3 templates? At runtime, the framework parses and compiles/converts an F3 template to PHP code the first time it's displayed via `$template->render()`. The framework then uses this compiled code in all subsequent calls. Hence, performance should be the same as PHP templates, if not better due to code optimization done by the template compiler when more complex templates are involved. + +Whether you use PHP's template engine or F3's own, template rendering can be significantly faster if you have APC, WinCache or XCache available on your server. + +As mentioned earlier, framework variables can hold any PHP data type. However, usage of non-scalar data types in F3 templates may produce strange results if you're not careful. Expressions in curly braces will always be evaluated and converted to string. You should limit your user interface variables to simple scalars:- `string`, `integer`, `boolean` or `float` data types. + +But what about arrays? Fat-Free recognizes arrays and you can employ them in your templates. You can have something like:- + +``` html +

    {{ @buddy[0] }}, {{ @buddy[1] }}, and {{ @buddy[2] }}

    +``` + +And populate the `@buddy` array in your PHP code before serving the template:- + +``` php +$f3->set('buddy',['Tom','Dick','Harry']); +``` + +However, if you simply insert `{{ @buddy }}` in your template, PHP 5.4 will replace it with `'Array'` because it converts the token to a string. PHP 5.4, on the other hand, will generate an `Array to string conversion` notice at runtime. + +F3 allows you to embed expressions in templates. These expressions may take on various forms, like arithmetic calculations, boolean expressions, PHP constants, etc. Here are a few examples:- + +``` html +{{ 2*(@page-1) }} +{{ (int)765.29+1.2e3 }} + +{{ var_dump(@xyz) }} +

    That is {{ preg_match('/Yes/i',@response)?'correct':'wrong' }}!

    +{{ @obj->property }} +``` + +An additional note about array expressions: Take note that `@foo.@bar` is a string concatenation `$foo.$bar`), whereas `@foo.bar` translates to `$foo['bar']`. If `$foo[$bar]` is what you intended, use the `@foo[@bar]` regular notation. + +Framework variables may also contain anonymous functions: + +``` php +$f3->set('func', + function($a,$b) { + return $a.', '.$b; + } +); +``` + +The F3 template engine will interpret the token as expected, if you specify the following expression: + +``` html +{{ @func('hello','world') }} +``` + +### Templates Within Templates + +Simple variable substitution is one thing all template engines have. Fat-Free has more up its sleeves:- + +``` html + +``` + +The directive will embed the contents of the header.htm template at the exact position where the directive is stated. You can also have dynamic content in the form of:- + +``` html + +``` + +A practical use for such template directive is when you have several pages with a common HTML layout but with different content. Instructing the framework to insert a sub-template into your main template is as simple as writing the following PHP code:- + +``` php +// switch content to your blog sub-template +$f3->set('content','blog.htm'); +// in another route, switch content to the wiki sub-template +$f3->set('content','wiki.htm'); +``` + +A sub-template may in turn contain any number of directives. F3 allows unlimited nested templates. + +You can specify filenames with something other than .htm or .html file extensions, but it's easier to preview them in your Web browser during the development and debugging phase. The template engine is not limited to rendering HTML files. In fact you can use the template engine to render other kinds of files. + +The `` directive also has an optional `if` attribute so you can specify a condition that needs to be satisfied before the sub-template is inserted:- + +``` html + +``` + +### Exclusion of Segments + +During the course of writing/debugging F3-powered programs and designing templates, there may be instances when disabling the display of a block of HTML may be handy. You can use the `` directive for this purpose:- + +``` html + +

    A chunk of HTML we don't want displayed at the moment

    +
    +``` + +That's like the `` HTML comment tag, but the `` directive makes the HTML block totally invisible once the template is rendered. + +Here's another way of excluding template content or adding comments:- + +``` html +{*

    A chunk of HTML we don't want displayed at the moment

    *} +``` + +### Conditional Segments + +Another useful template feature is the `` directive. It allows you to embed an HTML fragment depending on the evaluation of a certain condition. Here are a few examples:- + +``` html + + Inserted if condition is false + + + +
    Appears when condition is true
    +
    + +
    Appears when condition is false
    +
    +
    +``` + +You can have as many nested `` directives as you need. + +An F3 expression inside an if attribute that equates to `NULL`, an empty string, a boolean `FALSE`, an empty array or zero, automatically invokes ``. If your template has no `` block, then the `` opening and closing tags are optional:- + +``` html + +

    HTML chunk to be included if condition is true

    +
    +``` + +### Repeating Segments + +Fat-Free can also handle repetitive HTML blocks:- + +``` html + +

    {{ trim(@fruit) }}

    +
    +``` + +The `group` attribute `@fruits` inside the `` directive must be an array and should be set in your PHP code accordingly:- + +``` php +$f3->set('fruits',['apple','orange ',' banana']); +``` + +Nothing is gained by assigning a value to `@fruit` in your application code. Fat-Free ignores any preset value it may have because it uses the variable to represent the current item during iteration over the group. The output of the above HTML template fragment and the corresponding PHP code becomes:- + +``` html +

    apple

    +

    orange

    +

    banana

    +``` + +The framework allows unlimited nesting of `` blocks:- + +``` html + +
    +

    {{ @ikey }}

    +

    + + {{ @ispan }} + +

    +
    +
    +``` + +Apply the following F3 command:- + +``` php +$f3->set('div', + [ + 'coffee'=>['arabica','barako','liberica','kopiluwak'], + 'tea'=>['darjeeling','pekoe','samovar'] + ] +); +``` + +As a result, you get the following HTML fragment:- + +``` html +
    +

    coffee

    +

    + arabica + barako + liberica + kopiluwak +

    +

    +
    +

    tea

    +

    + darjeeling + pekoe + samovar +

    +
    +``` + +Amazing, isn't it? And the only thing you had to do in PHP was to define the contents of a single F3 variable `div` to replace the `@div` token. Fat-Free makes both programming and Web template design really easy. + +The `` template directive's `value` attribute returns the value of the current element in the iteration. If you need to get the array key of the current element, use the `key` attribute instead. The `key` attribute is optional. + +`` also has an optional counter attribute that can be used as follows:- + +``` html + +

    {{ trim(@fruit) }}

    +
    +``` + +Internally, F3's template engine records the number of loop iterations and saves that value in the variable/token `@ctr`, which is used in our example to determine the odd/even classification. + +### Embedding Javascript and CSS + +If you have to insert F3 tokens inside a ` +``` + +Embedding template directives inside your ` +``` + +### Document Encoding + +By default, Fat-Free uses the UTF-8 character set unless changed. You can override this behavior by issuing something like:- + +``` php +$f3->set('ENCODING','ISO-8859-1'); +``` + +Once you inform the framework of the desired character set, F3 will use it in all HTML and XML templates until altered again. + +### All Kinds of Templates + +As mentioned earlier in this section, the framework isn't limited to HTML templates. You can process XML templates just as well. The mechanics are pretty much similar. You still have the same `{{ @variable }}` and `{{ expression }}` tokens, ``, ``, ``, and `` directives at your disposal. Just tell F3 that you're passing an XML file instead of HTML:- + +``` php +echo Template::instance()->render('template.xml','application/xml'); +``` + +The second argument represents the MIME type of the document being rendered. + +The View component of MVC covers everything that doesn't fall under the Model and Controller, which means your presentation can and should include all kinds of user interfaces, like RSS, e-mail, RDF, FOAF, text files, etc. The example below shows you how to separate your e-mail presentation from your application's business logic:- + +``` html +MIME-Version: 1.0 +Content-type: text/html; charset={{ @ENCODING }} +From: {{ @from }} +To: {{ @to }} +Subject: {{ @subject }} + +

    Welcome, and thanks for joining {{ @site }}!

    +``` + +Save the above e-mail template as welcome.txt. The associated F3 code would be:- + +``` php +$f3->set('from',''); +$f3->set('to',''); +$f3->set('subject','Welcome'); +ini_set('sendmail_from',$f3->get('from')); +mail( + $f3->get('to'), + $f3->get('subject'), + Template::instance()->render('email.txt','text/html') +); +``` + +Tip: Replace the SMTP mail() function with imap_mail() if your script communicates with an IMAP server. + +Now isn't that something? Of course, if you have a bundle of e-mail recipients, you'd be using a database to populate the firstName, lastName, and email tokens. + +Here's an alternative solution using the F3's SMTP plug-in:- + +``` php +$mail=new SMTP('smtp.gmail.com',465,'SSL','account@gmail.com','secret'); +$mail->set('from',''); +$mail->set('to','"Slasher" '); +$mail->set('subject','Welcome'); +$mail->send(Template::instance()->render('email.txt')); +``` + +### Multilingual Support + +F3 supports multiple languages right out of the box. + +First, create a dictionary file with the following structure (one file per language):- + +``` php +'I love F3', + 'today'=>'Today is {0,date}', + 'pi'=>'{0,number}', + 'money'=>'Amount remaining: {0,number,currency}' +]; +``` + +Save it as `dict/en.php`. Let's create another dictionary, this time for German. Save the file as `dict/de.php`:- + +``` php +'Ich liebe F3', + 'today'=>'Heute ist {0,date}', + 'money'=>'Restbetrag: {0,number,currency}' +]; +``` + +Dictionaries are nothing more than key-value pairs. F3 automatically instantiates framework variables based on the keys in the language files. As such, it's easy to embed these variables as tokens in your templates. Using the F3 template engine:- + +``` html +

    {{ @love }}

    +

    +{{ @today,time() | format }}.
    +{{ @money,365.25 | format }}
    +{{ @pi }} +

    +``` + +And the longer version that utilizes PHP as a template engine:- + +``` php + +

    get('love'); ?>

    +

    + get('today',time()); ?>.
    + get('money',365.25); ?> + get('pi'); ?> +

    +``` + +Next, we instruct F3 to look for dictionaries in the `dict/` folder:- + +``` php +$f3->set('LOCALES','dict/'); +``` + +But how does the framework determine which language to use? F3 will detect it automatically by looking at the HTTP request headers first, specifically the `Accept-Language` header sent by the browser. + +To override this behavior, you can trigger F3 to use a language specified by the user or application:- + +``` php +$f3->set('LANGUAGE','de'); +``` + +**Note:** In the above example, the key pi exists only in the English dictionary. The framework will always use English (`en`) as a fallback to populate keys that are not present in the specified (or detected) language. + +You may also create dictionary files for language variants like `en-US`, `es-AR`, etc. In this case, F3 will use the language variant first (like `es-AR`). If there are keys that do not exist in the variant, the framework will look up the key in the root language (`es`), then use the `en` language file as the final fallback. +Dictionary key-value pairs become F3 variables once referenced. Make sure the keys do not conflict with any framework variable instantiated via `$f3->set()`, `$f3->mset()`, or `$f3->config()`. + +Did you notice the peculiar `'Today is {0,date}'` pattern in our previous example? F3's multilingual capability hinges on string/message formatting rules of the ICU project. The framework uses its own subset of the ICU string formatting implementation. There is no need for PHP's `intl` extension to be activated on the server. + +One more thing: F3 can also load .ini-style formatted files as dictionaries:- + +``` ini +love="I love F3" +today="Today is {0,date}" +pi="{0,number}" +money="Amount remaining: {0,number,currency}" +``` + +Save it as `dict/en.ini` so the framework can load it automatically. + +### Data Sanitation + +By default, both view handler and template engine escapes all rendered variables, i.e. converted to HTML entities to protect you from possible XSS and code injection attacks. On the other hand, if you wish to pass valid HTML fragments from your application code to your template:- + +``` php +$f3->set('ESCAPE',FALSE); +``` + +This may have undesirable effects. You might not want all variables to pass through unescaped. Fat-Free allows you to unescape variables individually. For F3 templates:- + +``` html +{{ @html_content | raw }} +``` + +In the case of PHP templates:- + +``` php +raw($html_content); ?> +``` + +As an addition to auto-escaping of F3 variables, the framework also gives you a free hand at sanitizing user input from HTML forms:- + +``` php +$f3->scrub($_GET,'p; br; span; div; a'); +``` + +This command will strip all tags (except those specified in the second argument) and unsafe characters from the specified variable. If the variable contains an array, each element in the array is sanitized recursively. If an asterisk (*) is passed as the second argument, `$f3->scrub()` permits all HTML tags to pass through untouched and simply remove unsafe control characters. + +## Databases + +### Connecting to a Database Engine + +Fat-Free is designed to make the job of interfacing with SQL databases a breeze. If you're not the type to immerse yourself in details about SQL, but lean more towards object-oriented data handling, you can go directly to the next section of this tutorial. However, if you need to do some complex data-handling and database performance optimization tasks, SQL is the way to go. + +Establishing communication with a SQL engine like MySQL, SQLite, SQL Server, Sybase, and Oracle is done using the familiar `$f3->set()` command. Connecting to a SQLite database would be:- + +``` php +$db=new DB\SQL('sqlite:/absolute/path/to/your/database.sqlite'); +``` + +Another example, this time with MySQL:- + +``` php +$db=new DB\SQL( + 'mysql:host=localhost;port=3306;dbname=mysqldb', + 'admin', + 'p455w0rD' +); +``` + +### Querying the Database + +OK. That was easy, wasn't it? That's pretty much how you would do the same thing in ordinary PHP. You just need to know the DSN format of the database you're connecting to. See the PDO section of the PHP manual. + +Let's continue our PHP code:- + +``` php +$f3->set('result',$db->exec('SELECT brandName FROM wherever')); +echo Template::instance()->render('abc.htm'); +``` + +Huh, what's going on here? Shouldn't we be setting up things like PDOs, statements, cursors, etc.? The simple answer is: you don't have to. F3 simplifies everything by taking care of all the hard work in the backend. + +This time we create an HTML template like `abc.htm` that has at a minimum the following:- + +``` html + + {{ @item.brandName }} + +``` + +In most instances, the SQL command set should be enough to generate a Web-ready result so you can use the `result` array variable in your template directly. Be that as it may, Fat-Free will not stop you from getting into its SQL handler internals. In fact, F3's `DB\SQL` class derives directly from PHP's `PDO` class, so you still have access to the underlying PDO components and primitives involved in each process, if you need some fine-grain control. + +### Transactions + +Here's another example. Instead of a single statement provided as an argument to the `$db->exec()` command, you can also pass an array of SQL statements:- + +``` php +$db->exec( + [ + 'DELETE FROM diet WHERE food="cola"', + 'INSERT INTO diet (food) VALUES ("carrot")', + 'SELECT * FROM diet' + ] +); +``` + +F3 is smart enough to know that if you're passing an array of SQL instructions, this indicates a SQL batch transaction. You don't have to worry about SQL rollbacks and commits because the framework will automatically revert to the initial state of the database if any error occurs during the transaction. If successful, F3 commits all changes made to the database. + +You can also start and end a transaction programmatically:- + +``` php +$db->begin(); +$db->exec('DELETE FROM diet WHERE food="cola"'); +$db->exec('INSERT INTO diet (food) VALUES ("carrot")'); +$db->exec('SELECT * FROM diet'); +$db->commit(); +``` + +A rollback will occur if any of the statements encounter an error. + +To get a list of all database instructions issued:- + +``` php +echo $db->log(); +``` + +### Parameterized Queries + +Passing string arguments to SQL statements is fraught with danger. Consider this:- + +``` php +$db->exec( + 'SELECT * FROM users '. + 'WHERE username="'.$f3->get('POST.userID'.'"') +); +``` + +If the `POST` variable `userID` does not go through any data sanitation process, a malicious user can pass the following string and damage your database irreversibly:- + +``` sql +admin"; DELETE FROM users; SELECT "1 +``` + +Luckily, parameterized queries help you mitigate these risks:- + +``` php +$db->exec( + 'SELECT * FROM users WHERE userID=?', + $f3->get('POST.userID') +); +``` + +If F3 detects that the value of the query parameter/token is a string, the underlying data access layer escapes the string and adds quotes as necessary. + +Our example in the previous section will be a lot safer from SQL injection if written this way:- + +``` php +$db->exec( + [ + 'DELETE FROM diet WHERE food=:name', + 'INSERT INTO diet (food) VALUES (?)', + 'SELECT * FROM diet' + ], + [ + array(':name'=>'cola'), + array(1=>'carrot'), + NULL + ] +); +``` + +### CRUD (But With a Lot of Style) + +F3 is packed with easy-to-use object-relational mappers (ORMs) that sit between your application and your data - making it a lot easier and faster for you to write programs that handle common data operations - like creating, retrieving, updating, and deleting (CRUD) information from SQL and NoSQL databases. Data mappers do most of the work by mapping PHP object interactions to the corresponding backend queries. + +Suppose you have an existing MySQL database containing a table of users of your application. (SQLite, PostgreSQL, SQL Server, Sybase will do just as well.) It would have been created using the following SQL command:- + +``` sql +CREATE TABLE users ( + userID VARCHAR(30), + password VARCHAR(30), + visits INT, + PRIMARY KEY(userID) +); +``` + +**Note:** MongoDB is a NoSQL database engine and inherently schema-less. F3 has its own fast and lightweight NoSQL implementation called Jig, which uses PHP-serialized or JSON-encoded flat files. These abstraction layers require no rigid data structures. Fields may vary from one record to another. They can also be defined or dropped on the fly. + +Now back to SQL. First, we establish communication with our database. + +``` php +$db=new DB\SQL( + 'mysql:host=localhost;port=3306;dbname=mysqldb', + 'admin', + 'wh4t3v3r' +); +``` + +To retrieve a record from our table:- + +``` php +$user=new DB\SQL\Mapper($db,'users'); +$user->load(['userID=?','tarzan']); +``` + +The first line instantiates a data mapper object that interacts with the `users` table in our database. Behind the scene, F3 retrieves the structure of the `users` table and determines which field(s) are defined as primary key(s). At this point, the mapper object contains no data yet (dry state) so `$user` is nothing more than a structured object - but it contains the methods it needs to perform the basic CRUD operations and some extras. To retrieve a record from our users table with a `userID` field containing the string value `tarzan`, we use the `load() method`. This process is called "auto-hydrating" the data mapper object. + +Easy, wasn't it? F3 understands that a SQL table already has a structural definition existing within the database engine itself. Unlike other frameworks, F3 requires no extra class declarations (unless you want to extend the data mappers to fit complex objects), no redundant PHP array/object property-to-field mappings (duplication of efforts), no code generators (which require code regeneration if the database structure changes), no stupid XML/YAML files to configure your models, no superfluous commands just to retrieve a single record. With F3, a simple resizing of a `varchar` field in MySQL does not demand a change in your application code. Consistent with MVC and "separation of concerns", the database admin has as much control over the data (and the structures) as a template designer has over HTML/XML templates. + +If you prefer working with NoSQL databases, the similarities in query syntax are superficial. In the case of the MongoDB data mapper, the equivalent code would be:- + +``` php +$db=new DB\Mongo('mongodb://localhost:27017','testdb'); +$user=new DB\Mongo\Mapper($db,'users'); +$user->load(['userID'=>'tarzan']); +``` + +With Jig, the syntax is similar to F3's template engine:- + +``` php +$db=new DB\Jig('db/data/',DB\Jig::FORMAT_JSON); +$user=new DB\Jig\Mapper($db,'users'); +$user->load(['@userID=?','tarzan']); +``` + +### The Smart SQL ORM + +The framework automatically maps the field `visits` in our table to a data mapper property during object instantiation, i.e. `$user=new DB\SQL\Mapper($db,'users');`. Once the object is created, `$user->password` and `$user->userID` would map to the `password` and `userID` fields in our table, respectively. + +You can't add or delete a mapped field, or change a table's structure using the ORM. You must do this in MySQL, or whatever database engine you're using. After you make the changes in your database engine, Fat-Free will automatically synchronize the new table structure with your data mapper object when you run your application. + +F3 derives the data mapper structure directly from the database schema. No guesswork involved. It understands the differences between MySQL, SQLite, MSSQL, Sybase, and PostgreSQL database engines. + +SQL identifiers should not use reserved words, and should be limited to alphanumeric characters `A-Z`, `0-9`, and the underscore symbol (`_`). Column names containing spaces (or special characters) and surrounded by quotes in the data definition are not compatible with the ORM. They cannot be represented properly as PHP object properties. + +Let's say we want to increment the user's number of visits and update the corresponding record in our users table, we can add the following code:- + +``` php +$user->visits++; +$user->save(); +``` + +If we wanted to insert a record, we follow this process:- + +``` php +$user=new DB\SQL\Mapper($db,'users'); +// or $user=new DB\Mongo\Mapper($db,'users'); +// or $user=new DB\Jig\Mapper($db,'users'); +$user->userID='jane'; +$user->password=password_hash('secret', PASSWORD_BCRYPT, [ 'cost' => 12 ]); +$user->visits=0; +$user->save(); +``` + +We still use the same `save()` method. But how does F3 know when a record should be inserted or updated? At the time a data mapper object is auto-hydrated by a record retrieval, the framework keeps track of the record's primary keys (or `_id`, in the case of MongoDB and Jig) - so it knows which record should be updated or deleted - even when the values of the primary keys are changed. A programmatically-hydrated data mapper - the values of which were not retrieved from the database, but populated by the application - will not have any memory of previous values in its primary keys. The same applies to MongoDB and Jig, but using object `_id` as reference. So, when we instantiated the `$user` object above and populated its properties with values from our program - without at all retrieving a record from the user table, F3 knows that it should insert this record. + +A mapper object will not be empty after a `save()`. If you wish to add a new record to your database, you must first dehydrate the mapper:- + +``` php +$user->reset(); +$user->userID='cheetah'; +$user->password=password_hash('unknown', PASSWORD_BCRYPT, [ 'cost' => 12 ]); +$user->save(); +``` + +Calling `save()` a second time without invoking `reset()` will simply update the record currently pointed to by the mapper. + +### Caveat for SQL Tables + +Although the issue of having primary keys in all tables in your database is argumentative, F3 does not stop you from creating a data mapper object that communicates with a table containing no primary keys. The only drawback is: you can't delete or update a mapped record because there's absolutely no way for F3 to determine which record you're referring to plus the fact that positional references are not reliable. Row IDs are not portable across different SQL engines and may not be returned by the PHP database driver. + +To remove a mapped record from our table, invoke the `erase()` method on an auto-hydrated data mapper. For example:- + +``` php +$user=new DB\SQL\Mapper($db,'users'); +$user->load(['userID=? AND password=?','cheetah','ch1mp']); +$user->erase(); +``` + +Jig's query syntax would be slightly similar:- + +``` php +$user=new DB\Jig\Mapper($db,'users'); +$user->load(['@userID=? AND @password=?','cheetah','chimp']); +$user->erase(); +``` + +And the MongoDB equivalent would be:- + +``` php +$user=new DB\Mongo\Mapper($db,'users'); +$user->load(['userID'=>'cheetah','password'=>'chimp']); +$user->erase(); +``` + +### The Weather Report + +To find out whether our data mapper was hydrated or not:- + +``` php +if ($user->dry()) + echo 'No record matching criteria'; +``` + +### Beyond CRUD + +We've covered the CRUD handlers. There are some extra methods that you might find useful:- + +``` php +$f3->set('user',new DB\SQL\Mapper($db,'users')); +$f3->get('user')->copyFrom('POST'); +$f3->get('user')->save(); +``` + +Notice that we can also use Fat-Free variables as containers for mapper objects. +The `copyFrom()` method hydrates the mapper object with elements from a framework array variable, the array keys of which must have names identical to the mapper object properties, which in turn correspond to the record's field names. So, when a Web form is submitted (assuming the HTML name attribute is set to `userID`), the contents of that input field is transferred to `$_POST['userID']`, duplicated by F3 in its `POST.userID` variable, and saved to the mapped field `$user->userID` in the database. The process becomes very simple if they all have identically-named elements. Consistency in array keys, i.e. template token names, framework variable names and field names is key :) + +On the other hand, if we wanted to retrieve a record and copy the field values to a framework variable for later use, like template rendering:- + +``` php +$f3->set('user',new DB\SQL\Mapper($db,'users')); +$f3->get('user')->load(['userID=?','jane']); +$f3->get('user')->copyTo('POST'); +``` + +We can then assign {{ @POST.userID }} to the same input field's value attribute. To sum up, the HTML input field will look like this:- + +``` html + +``` + +The `save()`, `update()`, `copyFrom()` data mapper methods and the parameterized variants of `load()` and `erase()` are safe from SQL injection. + +### Navigation and Pagination + +By default, a data mapper's `load()` method retrieves only the first record that matches the specified criteria. If you have more than one that meets the same condition as the first record loaded, you can use the `skip()` method for navigation:- + +``` php +$user=new DB\SQL\Mapper($db,'users'); +$user->load('visits>3'); +// Rewritten as a parameterized query +$user->load(['visits>?',3]); + +// For MongoDB users:- +// $user=new DB\Mongo\Mapper($db,'users'); +// $user->load(['visits'=>['$gt'=>3]]); + +// If you prefer Jig:- +// $user=new DB\Jig\Mapper($db,'users'); +// $user->load('@visits>?',3); + +// Display the userID of the first record that matches the criteria +echo $user->userID; +// Go to the next record that matches the same criteria +$user->skip(); // Same as $user->skip(1); +// Back to the first record +$user->skip(-1); +// Move three records forward +$user->skip(3); +``` + +You may use `$user->next()` as a substitute for `$user->skip()`, and `$user->prev()` if you think it gives more meaning to `$user->skip(-1)`. + +Use the `dry()` method to check if you've maneuvered beyond the limits of the result set. `dry()` will return TRUE if you try `skip(-1)` on the first record. It will also return TRUE if you `skip(1)` on the last record that meets the retrieval criteria. + +The `load()` method accepts a second argument: an array of options containing key-value pairs such as:- + +``` php +$user->load( + ['visits>?',3], + [ + 'order'=>'userID DESC' + 'offset'=>5, + 'limit'=>3 + ] +); +``` + +If you're using MySQL, the query translates to:- + +``` mysql +SELECT * FROM users +WHERE visits>3 +ORDER BY userID DESC +LIMIT 3 OFFSET 5; +``` + +This is one way of presenting data in small chunks. Here's another way of paginating results:- + +``` php +$page=$user->paginate(2,5,['visits>?',3]); +``` + +In the above scenario, F3 will retrieve records that match the criteria `'visits>3'`. It will then limit the results to 5 records (per page) starting at page offset 2 (0-based). The framework will return an array consisting of the following elements:- + +``` +[subset] array of mapper objects that match the criteria +[count] number of subsets available +[pos] actual subset position +``` + +The actual subset position returned will be NULL if the first argument of `paginate()` is a negative number or exceeds the number of subsets found. + +### Virtual Fields + +There are instances when you need to retrieve a computed value of a field, or a cross-referenced value from another table. Enter virtual fields. The SQL mini-ORM allows you to work on data derived from existing fields. + +Suppose we have the following table defined as:- + +``` sql +CREATE TABLE products + productID VARCHAR(30), + description VARCHAR(255), + supplierID VARCHAR(30), + unitprice DECIMAL(10,2), + quantity INT, + PRIMARY KEY(productID) +); +``` + +No `totalprice` field exists, so we can tell the framework to request from the database engine the arithmetic product of the two fields:- + +``` php +$item=new DB\SQL\Mapper($db,'products'); +$item->totalprice='unitprice*quantity'; +$item->load(['productID=:pid',':pid'=>'apple']); +echo $item->totalprice; +``` + +The above code snippet defines a virtual field called `totalprice` which is computed by multiplying `unitprice` by the `quantity`. The SQL mapper saves that rule/formula, so when the time comes to retrieve the record from the database, we can use the virtual field like a regular mapped field. + +You can have more complex virtual fields:- + +``` php +$item->mostNumber='MAX(quantity)'; +$item->load(); +echo $item->mostNumber; +``` + +This time the framework retrieves the product with the highest quantity (notice the `load()` method does not define any criteria, so all records in the table will be processed). Of course, the virtual field `mostNumber` will still give you the right figure if you wish to limit the expression to a specific group of records that match a specified criteria. + +You can also derive a value from another table:- + +``` php +$item->supplierName= + 'SELECT name FROM suppliers '. + 'WHERE products.supplierID=suppliers.supplierID'; +$item->load(); +echo $item->supplierName; +``` + +Every time you load a record from the products table, the ORM cross-references the `supplerID` in the `products` table with the `supplierID` in the `suppliers` table. + +To destroy a virtual field, use `unset($item->totalPrice);`. The `isset($item->totalPrice)` expression returns TRUE if the `totalPrice` virtual field was defined, or FALSE if otherwise. + +Remember that a virtual field must be defined prior to data retrieval. The ORM does not perform the actual computation, nor the derivation of results from another table. It is the database engine that does all the hard work. + +### Seek and You Shall Find + +If you have no need for record-by-record navigation, you can retrieve an entire batch of records in one shot:- + +``` php +$frequentUsers=$user->find(['visits>?',3],['order'=>'userID']); +``` + +Jig mapper's query syntax has a slight resemblance:- + +``` php +$frequentUsers=$user->find(['@visits>?',3],['order'=>'userID']); +``` + +The equivalent code using the MongoDB mapper:- + +``` php +$frequentUsers=$user->find(['visits'=>['$gt'=>3]],['userID'=>1]); +``` + +The `find()` method searches the `users` table for records that match the criteria, sorts the result by `userID` and returns the result as an array of mapper objects. `find('visits>3')` is different from `load('visits>3')`. The latter refers to the current `$user` object. `find()` does not have any effect on `skip()`. + +**Important:** Declaring an empty condition, NULL, or a zero-length string as the first argument of `find()` or `load()` will retrieve all records. Be sure you know what you're doing - you might exceed PHP's memory_limit on large tables or collections. + +The `find()` method has the following syntax:- + +``` php +find( + $criteria, + [ + 'group'=>'foo', + 'order'=>'foo,bar', + 'limit'=>5, + 'offset'=>0 + ] +); +``` + +find() returns an array of objects. Each object is a mapper to a record that matches the specified criteria.:- + +``` php +$place=new DB\SQL\Mapper($db,'places'); +$list=$place->find('state="New York"'); +foreach ($list as $obj) + echo $obj->city.', '.$obj->country; +``` + +If you need to convert a mapper object to an associative array, use the `cast()` method:- + +``` php +$array=$place->cast(); +echo $array['city'].', '.$array['country']; +``` + +To retrieve the number of records in a table that match a certain condition, use the `count()` method. + +``` php +if (!$user->count(['visits>?',10])) + echo 'We need a better ad campaign!'; +``` + +There's also a `select()` method that's similar to `find()` but provides more fine-grained control over fields returned. It has a SQL-like syntax:- + +``` php +select( + 'foo, bar, MIN(baz) AS lowest', + 'foo > ?', + [ + 'group'=>'foo, bar', + 'order'=>'baz ASC', + 'limit'=>5, + 'offset'=>3 + ] +); +``` + +Much like the `find()` method, `select()` does not alter the mapper object's contents. It only serves as a convenience method for querying a mapped table. The return value of both methods is an array of mapper objects. Using `dry()` to determine whether a record was found by an of these methods is inappropriate. If no records match the `find()` or `select()` criteria, the return value is an empty array. + +### Profiling + +If you ever want to find out which SQL statements issued directly by your application (or indirectly thru mapper objects) are causing performance bottlenecks, you can do so with a simple:- + +``` php +echo $db->log(); +``` + +F3 keeps track of all commands issued to the underlying SQL database driver, as well as the time it takes for each statement to complete - just the right information you need to tweak application performance. + +### Sometimes It Just Ain't Enough + +In most cases, you can live by the comforts given by the data mapper methods we've discussed so far. If you need the framework to do some heavy-duty work, you can extend the SQL mapper by declaring your own classes with custom methods - but you can't avoid getting your hands greasy on some hardcore SQL:- + +``` php +class Vendor extends DB\SQL\Mapper { + + // Instantiate mapper + function __construct(DB\SQL $db) { + // This is where the mapper and DB structure synchronization occurs + parent::__construct($db,'vendors'); + } + + // Specialized query + function listByCity() { + return $this->select( + 'vendorID,name,city',['order'=>'city DESC']); + /* + We could have done the the same thing with plain vanilla SQL:- + return $this->db->exec( + 'SELECT vendorID,name,city FROM vendors '. + 'ORDER BY city DESC;' + ); + */ + } + +} + +$vendor=new Vendor; +$vendor->listByCity(); +``` + +Extending the data mappers in this fashion is an easy way to construct your application's DB-related models. + +### Pros and Cons + +If you're handy with SQL, you'd probably say: everything in the ORM can be handled with old-school SQL queries. Indeed. We can do without the additional event listeners by using database triggers and stored procedures. We can accomplish relational queries with joined tables. The ORM is just unnecessary overhead. But the point is - data mappers give you the added functionality of using objects to represent database entities. As a developer, you can write code faster and be more productive. The resulting program will be cleaner, if not shorter. But you'll have to weigh the benefits against the compromise in speed - specially when handling large and complex data stores. Remember, all ORMS - no matter how thin they are - will always be just another abstraction layer. They still have to pass the work to the underlying SQL engines. + +By design, F3's ORMs do not provide methods for directly connecting objects to each other, i.e. SQL joins - because this opens up a can of worms. It makes your application more complex than it should be, and there's the tendency of objects thru eager or lazy fetching techniques to be deadlocked and even out of sync due to object inheritance and polymorphism (impedance mismatch) with the database entities they're mapped to. There are indirect ways of doing it in the SQL mapper, using virtual fields - but you'll have to do this programmatically and at your own risk. + +If you are tempted to apply "pure" OOP concepts in your application to represent all your data (because "everything is an object"), keep in mind that data almost always lives longer than the application. Your program may already be outdated long before the data has lost its value. Don't add another layer of complexity in your program by using intertwined objects and classes that deviate too much from the schema and physical structure of the data. + +Before you weave multiple objects together in your application to manipulate the underlying tables in your database, think about this: creating views to represent relationships and triggers to define object behavior in the database engine are more efficient. Relational database engines are designed to handle views, joined tables and triggers. They are not dumb data stores. Tables joined in a view will appear as a single table, and Fat-Free can auto-map a view just as well as a regular table. Replicating JOINs as relational objects in PHP is slower compared to the database engine's machine code, relational algebra and optimization logic. Besides, joining tables repeatedly in our application is a sure sign that the database design needs to be audited, and views considered an integral part of data retrieval. If a table cross-references data from another table frequently, consider normalizing your structures or creating a view instead. Then create a mapper object to auto-map that view. It's faster and requires less effort. + +Consider this SQL view created inside your database engine:- + +``` sql +CREATE VIEW combined AS + SELECT + projects.project_id AS project, + users.name AS name + FROM projects + LEFT OUTER JOIN users ON + projects.project_id=users.project_id AND + projects.user_id=users.user_id; +``` + +Your application code becomes simple because it does not have to maintain two mapper objects (one for the projects table and another for users) just to retrieve data from two joined tables:- + +``` php +$combined=new DB\SQL\Mapper($db,'combined'); +$combined->load(['project=?',123]); +echo $combined->name; +``` + +Tip:Use the tools as they're designed for. Fat-Free already has an easy-to-use SQL helper. Use it if you need a bigger hammer :) Try to seek a balance between convenience and performance. SQL will always be your fallback if you're working on complex and legacy data structures. + +## Plug-Ins + +### About F3 Plug-ins + +Plug-ins are nothing more than autoloaded classes that use framework built-ins to extend F3's features and functionality. If you'd like to contribute, leave a note at the Fat-Free Discussion Area hosted by Google Groups or tell us about it in the FreeNode `#fatfree` IRC channel. Someone else might be involved in a similar project. The framework community will appreciate it a lot if we unify our efforts. + +### CAPTCHA Images + +There might be instances when you want to make your forms more secure against spam bots and malicious automated scripts. F3 provides a `captcha()` method to generate images with random text that are designed to be recognizable only by humans. + +``` php +$img = new Image(); +$img->captcha('fonts/CoolFont.ttf',16,5,'SESSION.captcha_code'); +$img->render(); +``` + +This example generates an random image based on your desired TrueType font. The `fonts/` folder is a subfolder within application's `UI` path. The second parameter indicates the font size, and the third argument defines the number of hexadecimal characters to generate. + +The last argument represents an F3 variable name. This is where F3 will store the string equivalent of the CAPTCHA image. To make the string reload-safe, we specified a session variable:- `SESSION.captcha_code` which maps to `$_SESSION['captcha_code']`, which you can use later to verify whether the input element in the form submitted matches this string. + +### Grabbing Data from Another Site + +We've covered almost every feature available in the framework to run a stand-alone Web server. For most applications, these features will serve you quite well. But what do you do if your application needs data from another Web server on the network? F3 has the Web plugin to help you in this situation:- + +``` php +$web=new Web; +$request=$web->request('http://www.google.com/'); +// another way to do it:- +$request=Web::instance()->request('http://www.google.com/'); +``` + +This simple example sends an HTTP request to the page located at www.google.com and stores it in the `$request` PHP variable. The `request()` method returns an array containing the HTTP response such that `$request['headers']` and `$request['body']` represent the response headers and body, respectively. We could have saved the contents using the F3::set command, or echo'ed the output directly to our browser. Retrieving another HTML page on the net may not have any practical purpose. But it can be particularly useful in ReSTful applications, like querying a CouchDB server. + +``` php +$host='localhost:5984'; +$web->request($host.'/_all_dbs'), +$web->request($host.'/testdb/',['method'=>'PUT']); +``` + +You may have noticed that you can pass an array of additional options to the `request()` method:- + +``` php +$web->request( + 'https://www.example.com:443?'. + http_build_query( + [ + 'key1'=>'value1', + 'key2'=>'value2' + ] + ), + [ + 'headers'=>[ + 'Accept: text/html,application/xhtml+xml,application/xml', + 'Accept-Language: en-us' + ], + 'follow_location'=>FALSE, + 'max_redirects'=>30, + 'ignore_errors'=>TRUE + ] +); +``` + +If the framework variable `CACHE` is enabled, and if the remote server instructs your application to cache the response to the HTTP request, F3 will comply with the request and retrieve the cached response each time the framework receives a similar request from your application, thus behaving like a browser. + +Fat-Free will use whatever means are available on your Web server for the `request()` method to run: PHP stream wrappers (`allow_url_fopen`), cURL module, or low-level sockets. + +### Handling File Downloads + +F3 has a utility for sending files to an HTTP client, i.e. fulfilling download requests. You can use it to hide the real path to your download files. This adds some layer of security because users won't be able to download files if they don't know the file names and their locations. Here's how it's done:- + +``` php +$f3->route('GET /downloads/@filename', + function($f3,$args) { + // send() method returns FALSE if file doesn't exist + if (!Web::instance()->send('/real/path/'.$args['filename'])) + // Generate an HTTP 404 + $f3->error(404); + } +); +``` + +### Remoting and Distributed Applications + +The `request()` method can also be used in complex SOAP or XML-RPC applications, if you find the need for another Web server to process data on your computer's behalf - thus harnessing the power of distributing computing. W3Schools.com has an excellent tutorial on SOAP. On the other hand, TutorialsPoint.com gives a nice overview of XML-RPC. + +## Optimization + +### Cache Engine + +Caching static Web pages - so the code in some route handlers can be skipped and templates don't have to be reprocessed - is one way of reducing your Web server's work load so it can focus on other tasks. You can activate the framework's cache engine by providing a third argument to the `$f3->route()` method. Just specify the number of seconds before a cached Web page expires:- + +``` php +$f3->route('GET /my_page','App->method',60); +``` + +Here's how it works. In this example, when F3 detects that the URL `/my_page` is accessed for the first time, it executes the route handler represented by the second argument and saves all browser output to the framework's built-in cache (server-side). A similar instruction is automatically sent to the user's Web browser (client-side), so that instead of sending an identical request to the server within the 60-second period, the browser can just retrieve the page locally. The framework uses the cache for an entirely different purpose - serving framework-cached data to other users asking for the same Web page within the 60-second time frame. It skips execution of the route handler and serves the previously-saved page directly from disk. When someone tries to access the same URL after the 60-second timer has lapsed, F3 will refresh the cache with a new copy. + +Web pages with static data are the most likely candidates for caching. Fat-Free will not cache a Web page at a specified URL if the third argument in the `$f3->route()` method is zero or unspecified. F3 conforms to the HTTP specifications: only GET and HEAD requests can be cached. + +Here's an important point to consider when designing your application. Don't cache Web pages unless you understand the possible unwanted side-effects of the cache at the client-side. Make sure that you activate caching on Web pages that have nothing to do with the user's session state. + +For example, you designed your site in such a way that all your Web pages have the menu options: `"Home"`, `"About Us"`, and `"Login"`, displayed when a user is not logged into your application. You also want the menu options to change to: `"Home"`, `"About Us"`, and `"Logout"`, once the user has logged in. If you instructed Fat-Free to cache the contents of `"About Us"` page (which includes the menu options), it does so and also sends the same instruction to the HTTP client. Regardless of the user's session state, i.e. logged in or logged out, the user's browser will take a snapshot of the page at the session state it was in. Future requests by the user for the `"About Us"` page before the cache timeout expires will display the same menu options available at that time the page was initially saved. Now, a user may have already logged in, but the menu options are still the same as if no such event occurred. That's not the kind of behavior we want from our application. + +Some pointers:- + +* Don't cache dynamic pages. It's quite obvious you don't want to cache data that changes frequently. You can, however, activate caching on pages that contain data updated on an hourly, daily or even yearly basis.For security reasons, the framework restricts cache engine usage to HTTP `GET` routes only. It will not cache submitted forms!Don't activate the cache on Web pages that at first glance look static. In our example, the "About Us" content may be static, but the menu isn't. +* Activate caching on pages that are available only in ONE session state. If you want to cache the `"About Us"` page, make sure it's available only when a user is not logged in. +* If you have a RAMdisk or fast solid-state drive, configure the `CACHE` global variable so it points to that drive. This will make your application run like a Formula 1 race car. + +**Note:** Don't set the timeout value to a very long period until you're ready to roll out your application, i.e. the release or production state. Changes you make to any of your PHP scripts may not have the expected effect on the displayed output if the page exists in the framework cache and the expiration period has not lapsed. If you do alter a program that generates a page affected by the cache timer and you want these changes to take effect immediately, you should clear the cache by erasing the files in the cache/ directory (or whatever path the `CACHE` global variable points to). F3 will automatically refresh the cache if necessary. At the client-side, there's little you can do but instruct the user to clear the browser's cache or wait for the cache period to expire. + +PHP needs to be set up correctly for the F3 cache engine to work properly. Your operating system timezone should be synchronized with the date.timezone setting in the `php.ini` file. + +Similar to routes, Fat-Free also allows you to cache database queries. Speed gains can be quite significant, specially when used on complex SQL statements that involve look-up of static data or database content that rarely changes. Activating the database query cache so the framework doesn't have to re-execute the SQL statements every time is as simple as adding a 3rd argument to the F3::sql command - the cache timeout. For example:- + +``` php +$db->exec('SELECT * from sizes;',NULL,86400); +``` + +If we expect the result of this database query to always be `Small`, `Medium`, and `Large` within a 24-hour period, we specify `86400` seconds as the 2nd argument so Fat-Free doesn't have to execute the query more than once a day. Instead, the framework will store the result in the cache, retrieve it from the cache every time a request comes in during the specified 24-hour time frame, and re-execute the query when the timer lapses. + +The SQL data mapper also uses the cache engine to optimize synchronization of table structures with the objects that represent them. The default is `60` seconds. If you make any changes to a table's structure in your database engine, you'll have to wait for the cache timer to expire before seeing the effect in your application. You can change this behavior by specifying a third argument to the data mapper constructor. Set it to a high value if you don't expect to make any further changes to your table structure. + +``` php +$user=new DB\SQL\Mapper($db,'users',86400); +``` + +By default, Fat-Free's cache engine is disabled. You can enable it and allow it to auto-detect APC, WinCache or XCache. If it cannot find an appropriate backend, F3 will use the filesystem, i.e. the `tmp/cache/` folder:- + +``` php +$f3->set('CACHE',TRUE); +``` + +Disabling the cache is as simple as:- + +``` php +$f3->set('CACHE',FALSE); +``` + +If you wish to override the auto-detection feature, you can do so - as in the case of a Memcached back-end which F3 also supports:- + +``` php +$f3->set('CACHE','memcache=localhost:11211'); +``` + +You can also use the cache engine to store your own variables. These variables will persist between HTTP requests and remain in cache until the engine receives instructions to delete them. To save a value in the cache:- + +``` php +$f3->set('var','I want this value saved',90); +``` + +`$f3->set()` method's third argument instructs the framework to save the variable in the cache for a 90-second duration. If your application issues a `$f3->get('var')` within this period, F3 will automatically retrieve the value from cache. In like manner, `$f3->clear('var')` will purge the value from both cache and RAM. If you want to determine if a variable exists in cache, `$f3->exists('var')); returns one of two possible values: FALSE if the framework variable passed does not exist in cache, or an integer representing the time the variable was saved (Un*x time in seconds, with microsecond precision). + +### Keeping Javascript and CSS on a Healthy Diet + +Fat-Free also has a Javascript and CSS compressor available in the Web plug-in. It can combine all your CSS files into one stylesheet (or Javascript files into a single script) so the number of components on a Web page are decreased. Reducing the number of HTTP requests to your Web server results in faster page loading. First you need to prepare your HTML template so it can take advantage of this feature. Something like:- + +``` html + +``` + +Do the same with your Javascript files:- + +``` html + +``` + +Of course we need to set up a route so your application can handle the necessary call to the Fat-Free CSS/Javascript compressor:- + +``` php +$f3->route('GET /minify/@type', + function($f3,$args) { + $f3->set('UI',$args['type'].'/'); + echo Web::instance()->minify($_GET['files']); + }, + 3600 +); +``` + +And that's all there is to it! `minify()` reads each file (`typo.css` and `grid.css` in our CSS example, `underscore.js` in our Javascript example), strips off all unnecessary whitespaces and comments, combines all of the related items as a single Web page component, and attaches a far-future expiry date so the user's Web browser can cache the data. It's important that the `PARAMS.type` variable base points to the correct path. Otherwise, the URL rewriting mechanism inside the compressor won't find the CSS/Javascript files. + +### Client-Side Caching + +In our examples, the framework sends a far-future expiry date to the client's Web browser so any request for the same CSS or Javascript block will come from the user's hard drive. On the server side, F3 will check each request and see if the CSS or Javascript blocks have already been cached. The route we specified has a cache refresh period of `3600` seconds. Additionally, if the Web browser sends an `If-Modified-Since` request header and the framework sees the cache hasn't changed, F3 just sends an `HTTP 304 Not Modified` response so no content is actually delivered. Without the `If-Modified-Since` header, Fat-Free renders the output from the cached file if available. Otherwise, the relevant code is executed. + +Tip: If you're not modifying your Javascript/CSS files frequently (as it would be if you're using a Javascript library like jQuery, MooTools, Dojo, etc.), consider adding a cache timer to the route leading to your Javascript/CSS minify handler (3rd argument of F3::route()) so Fat-Free doesn't have compress and combine these files each time such a request is received. + +### PHP Code Acceleration + +Want to make your site run even faster? Fat-Free works best with either Alternative PHP Cache (APC), XCache, or WinCache. These PHP extensions boost performance of your application by optimizing your PHP scripts (including the framework code). + +### Bandwidth Throttling + +A fast application that processes all HTTP requests and responds to them at the shortest time possible is not always a good idea - specially if your bandwidth is limited or traffic on your Web site is particularly heavy. Serving pages ASAP also makes your application vulnerable to Denial-of-Service (DOS) attacks. F3 has a bandwidth throttling feature that allows you to control how fast your Web pages are served. You can specify how much time it should take to process a request:- + +``` php +$f3->route('/throttledpage','MyApp->handler',0,128); +``` + +In this example, the framework will serve the Web page at a rate of 128KiBps. + +Bandwidth throttling at the application level can be particularly useful for login pages. Slow responses to dictionary attacks is a good way of mitigating this kind of security risk. + +## Unit Testing + +### Bullet-Proof Code + +Robust applications are the result of comprehensive testing. Verifying that each part of your program conforms to the specifications and lives up to the expectations of the end-user means finding bugs and fixing them as early as possible in the application development cycle. + +If you know little or nothing about unit testing methodologies, you're probably embedding pieces of code directly in your existing program to help you with debugging. That of course means you have to remove them once the program is running. Leftover code fragments, poor design and faulty implementation can creep up as bugs when you roll out your application later. + +F3 makes it easy for you to debug programs - without getting in the way of your regular thought processes. The framework does not require you to build complex OOP classes, heavy test structures, and obtrusive procedures. + +A unit (or test fixture) can be a function/method or a class. Let's have a simple example:- + +``` php +function hello() { + return 'Hello, World'; +} +``` + +Save it in a file called `hello.php`. Now how do we know it really runs as expected? Let's create our test procedure:- + +``` php +$f3=require('lib/base.php'); + +// Set up +$test=new Test; +include('hello.php'); + +// This is where the tests begin +$test->expect( + is_callable('hello'), + 'hello() is a function' +); + +// Another test +$hello=hello(); +$test->expect( + !empty($hello), + 'Something was returned' +); + +// This test should succeed +$test->expect + is_string($hello), + 'Return value is a string' +); + +// This test is bound to fail +$test->expect( + strlen($hello)==13, + 'String length is 13' +); + +// Display the results; not MVC but let's keep it simple +foreach ($test->results() as $result) { + echo $result['text'].'
    '; + if ($result['status']) + echo 'Pass'; + else + echo 'Fail ('.$result['source'].')'; + echo '
    '; +} +``` + +Save it in a file called `test.php`. This way we can preserve the integrity of `hello.php`. + +Now here's the meat of our unit testing process. + +F3's built-in `Test` class keeps track of the result of each `expect()` call. The output of `$test->results()` is an array of arrays with the keys `text` (mirroring argument 2 of `expect()`), `status` (boolean representing the result of a test), and `source` (file name/line number of the specific test) to aid in debugging. + +Fat-Free gives you the freedom to display test results in any way you want. You can have the output in plain text or even a nice-looking HTML template. So how do we run our unit test? If you saved `test.php` in the document root folder, you can just open your browser and specify the address `http://localhost/test.php`. That's all there is to it. + +### Mocking HTTP Requests + +F3 gives you the ability to simulate HTTP requests from within your PHP program so you can test the behavior of a particular route. Here's a simple mock request:- + +``` php +$f3->mock('GET /test?foo=bar'); +``` + +To mock a POST request and submit a simulated HTML form:- + +``` php +$f3->mock('POST /test',['foo'=>'bar']); +``` + +### Expecting the Worst that can Happen + +Once you get the hang of testing the smallest units of your application, you can then move on to the bigger components, modules, and subsystems - checking along the way if the parts are correctly communicating with each other. Testing manageable chunks of code leads to more reliable programs that work as you expect, and weaves the testing process into the fabric of your development cycle. The question to ask yourself is:- Have I tested all possible scenarios? More often than not, those situations that have not been taken into consideration are the likely causes of bugs. Unit testing helps a lot in minimizing these occurrences. Even a few tests on each fixture can greatly reduce headaches. On the other hand, writing applications without unit testing at all invites trouble. + +## Quick Reference + +### System Variables + +`string AGENT` + +* Auto-detected HTTP user agent, e.g. `Mozilla/5.0 (Linux; Android 4.2.2; Nexus 7) AppleWebKit/537.31`. + +`bool AJAX` + +* `TRUE` if an XML HTTP request is detected, `FALSE` otherwise. + +`string AUTOLOAD` + +* Search path for user-defined PHP classes that the framework will attempt to autoload at runtime. Accepts a pipe (`|`), comma (`,`), or semi-colon (`;`) as path separator. + +`string BASE` + +* Path to the `index.php` main/front controller. + +`string BODY` + +* HTTP request body for ReSTful post-processing. + +`bool/string CACHE` + +* Cache backend. Unless assigned a value like `'memcache=localhost'` (and the PHP memcache module is present), F3 auto-detects the presence of APC, WinCache and XCache and uses the first available PHP module if set to TRUE. If none of these PHP modules are available, a filesystem-based backend is used (default directory: `tmp/cache`). The framework disables the cache engine if assigned a `FALSE` value. + +`bool CASELESS` + +* Pattern matching of routes against incoming URIs is case-insensitive by default. Set to `FALSE` to make it case-sensitive. + +`array COOKIE, GET, POST, REQUEST, SESSION, FILES, SERVER, ENV` + +* Framework equivalents of PHP globals. Variables may be used throughout an application. However, direct use in templates is not advised due to security risks. + +`integer DEBUG` + +* Stack trace verbosity. Assign values 1 to 3 for increasing verbosity levels. Zero (0) suppresses the stack trace. This is the default value and it should be the assigned setting on a production server. + +`string DNSBL` + +* Comma-separated list of [DNS blacklist servers](http://whatismyipaddress.com/blacklist-check). Framework generates a `403 Forbidden` error if the user's IPv4 address is listed on the specified server(s). + +`array DIACRITICS` + +* Key-value pairs for foreign-to-ASCII character translations. + +`string ENCODING` + +* Character set used for document encoding. Default value is `UTF-8`. + +`array ERROR` + +* Information about the last HTTP error that occurred. `ERROR.code` is the HTTP status code. `ERROR.status` contains a brief description of the error. `ERROR.text` provides more detail. For HTTP 500 errors, use `ERROR.trace` to retrieve the stack trace. + +`bool ESCAPE` + +* Used to enable/disable auto-escaping. + +`string EXEMPT` + +* Comma-separated list of IPv4 addresses exempt from DNSBL lookups. + +`string FALLBACK` + +* Language (and dictionary) to use if no translation is available. + +`bool HALT` + +* If TRUE (default), framework stops execution after a non-fatal error is detected. + +`array HEADERS` + +* HTTP request headers received by the server. + +`bool HIGHLIGHT` + +* Enable/disable syntax highlighting of stack traces. Default value: `TRUE` (requires `code.css` stylesheet). + +`string HOST` + +* Server host name. If `$_SERVER['SERVER_NAME']` is not available, return value of `gethostname()` is used. + +`string IP` + +* Remote IP address. The framework derives the address from headers if HTTP client is behind a proxy server. + +`array JAR` + +* Default cookie parameters. + +`string LANGUAGE` + +* Current active language. Value is used to load the appropriate language translation file in the folder pointed to by `LOCALES`. If set to `NULL`, language is auto-detected from the HTTP `Accept-Language` request header. + +`string LOCALES` + +* Location of the language dictionaries. + +`string LOGS` + +* Location of custom logs. + +`mixed ONERROR` + +* Callback function to use as custom error handler. + +`string PACKAGE` + +* Framework name. + +`array PARAMS` + +* Captured values of tokens defined in a `route()` pattern. `PARAMS.0` contains the captured URL relative to the Web root. + +`string PATTERN` + +* Contains the routing pattern that matches the current request URI. + +`string PLUGINS` + +* Location of F3 plugins. Default value is the folder where the framework code resides, i.e. the path to `base.php`. + +`int PORT` + +* TCP/IP listening port used by the Web server. + +`string PREFIX` + +* String prepended to language dictionary terms. + +`bool QUIET` + +* Toggle switch for suppressing or enabling standard output and error messages. Particularly useful in unit testing. + +`bool RAW` + +* Disable automatic storage of HTTP request body into `BODY`. Should be TRUE when processing large data coming from `php://input` which will not fit in memory. Default value: `FALSE` + +`string REALM` + +* Full canonical URL. + +`string RESPONSE` + +* The body of the last HTTP response. F3 populates this variable regardless of the `QUIET` setting. + +`string ROOT` + +* Absolute path to document root folder. + +`array ROUTES` + +* Contains the defined application routes. + +`string SCHEME` + +* Server protocol, i.e. `http` or `https`. + +`string SERIALIZER` + +* Default serializer. Normally set to `php`, unless PHP `igbinary` extension is auto-detected. Assign `json` if desired. + +`string TEMP` + +* Temporary folder for cache, filesystem locks, compiled F3 templates, etc. Default is the `tmp/` folder inside the Web root. Adjust accordingly to conform to your site's security policies. + +`string TZ` + +* Default timezone. Changing this value automatically calls the underlying `date_default_timezone_set()` function. + +`string UI` + +* Search path for user interface files used by the `View` and `Template` classes' `render()` method. Default value is the Web root. Accepts a pipe (`|`), comma (`,`), or semi-colon (`;`) as separator for multiple paths. + +`callback UNLOAD` + +* Executed by framework on script shutdown. + +`string UPLOADS` + +* Directory where file uploads are saved. + +`string URI` + +* Current HTTP request URI. + +`string VERB` + +* Current HTTP request method. + +`string VERSION` + +* Framework version. + +### Template Directives + +``` +@token +``` +* Replace `@token` with value of equivalent F3 variable. + +``` +{{ mixed expr }} +``` +* Evaluate. `expr` may include template tokens, constants, operators (unary, arithmetic, ternary and relational), parentheses, data type converters, and functions. If not an attribute of a template directive, result is echoed. + +``` +{{ string expr | raw }} +``` +* Render unescaped `expr`. F3 auto-escapes strings by default. + +``` +{{ string expr | esc }} +``` +* Render escaped `expr`. This is the default framework behavior. The `| esc` suffix is only necessary if `ESCAPE` global variable is set to `FALSE`. + +``` +{{ string expr, arg1, ..., argN | format }} +``` +* Render an ICU-formatted `expr` and pass the comma-separated arguments, where `arg1, ..., argn` is one of:- `'date'`, `'time'`, `'number, integer'`, `'number, currency'`, or `'number, percent'`. + +``` + +``` +* Get contents of `subtemplate` and insert at current position in template if optional condition is `TRUE`. + +``` +text-block +``` +* Remove `text-block` at runtime. Used for embedding comments in templates. + +``` +text-block +``` +* Display `text-block` as-is, without interpretation/modification by the template engine. + +``` + + true-block + false-block + +``` +* Evaluate condition. If `TRUE`, then `true-block` is rendered. Otherwise, `false-block` is used. + +``` + + text-block + +``` +* Evaluate `from` statement once. Check if the expression in the `to` attribute is `TRUE`, render `text-block` and evaluate `step` statement. Repeat iteration until `to` expression is `FALSE`. + +``` + + text-block + +``` +* Repeat `text-block` as many times as there are elements in the array variable `@group` or the expression `expr`. `@key` and `@value` function in the same manner as the key-value pair in the equivalent PHP `foreach()` statement. Variable represented by `key` in `counter` attribute increments by `1` with every iteration. + +``` + + + text-block + + . + . + . + +``` +* Equivalent of the PHP switch-case jump table structure. + +``` +{* text-block *} +``` +* Alias for ``. + +### API Documentation + +The most up-to-date documentation is located at [http://fatfreeframework.com/](http://fatfreeframework.com/). It contains examples of usage of the various framework components. + +## Support and Licensing + +Technical support is available at the official discussion forum: [`https://groups.google.com/forum/#!forum/f3-framework`](https://groups.google.com/forum/#!forum/f3-framework). If you need live support, you can talk to the development team and other members of the F3 community via [Slack](https://fatfreeframework-slack.herokuapp.com/) or IRC. We're on the FreeNode `#fatfree` channel (`chat.freenode.net`). Visit [`http://webchat.freenode.net/`](http://webchat.freenode.net/) to join the conversation. You can also download the [Firefox Chatzilla](https://addons.mozilla.org/en-US/firefox/addon/chatzilla/) add-on or [Pidgin](http://www.pidgin.im/) if you don't have an IRC client so you can participate in the live chat. +You can also find help at [Stack Overflow](http://stackoverflow.com/questions/tagged/fat-free-framework) + +### Nightly Builds + +F3 uses Git for version control. To clone the latest code repository on GitHub: + +``` bash +git clone git://github.com/bcosca/fatfree-core.git +``` + +If all you want is a zipball of our test bench with all unit tests, grab it [**here**](https://github.com/bcosca/fatfree/archive/dev.zip). + +To file a bug report, visit [`https://github.com/bcosca/fatfree-core/issues`](https://github.com/bcosca/fatfree-core/issues). + +### Fair Licensing + +**Fat-Free Framework is free and released as open source software covered by the terms of the [GNU Public License](http://www.gnu.org/licenses/gpl-3.0.html) (GPL v3).** You may not use the software, documentation, and samples except in compliance with the license. If the terms and conditions of this license are too restrictive for your use, alternative licensing is available for a very reasonable fee. + +If you feel that this software is one great weapon to have in your programming arsenal, it saves you a lot of time and money, use it for commercial gain or in your business organization, please consider making a donation to the project. A significant amount of time, effort, and money has been spent on this project. Your donations help keep this project alive and the development team motivated. Donors and sponsors get priority support (24-hour response time on business days). + +### Credits + +The Fat-Free Framework is community-driven software. It can't be what it is today without the help and support from the following people and organizations: + +* GitHub +* Stehlik & Company +* bodalgo.com +* Square Lines, LLC +* Mirosystems +* Talis Group, Ltd. +* Tecnilógica +* G Holdings, LLC +* S2 Development, Ltd. +* Store Machine +* PHP Experts, Inc. +* Meins und Vogel GmbH +* Online Prepaid Services +* Frugal Photographer +* Christian Knuth +* Florent Racineux +* Sascha Ohms +* Lars Brandi Jensen +* Eyðun Lamhauge +* Jermaine Maree +* Sergey Zaretsky +* Daniel Kloke +* Brian Nelson +* Roberts Lapins +* Boris Gurevich +* Jose Maria Garrido Diaz +* Dawn Comfort +* Johan Viberg +* Povilas Musteikis +* Andrew Snook +* Jafar Amjad +* Taylor McCall +* Raymond Kirkland +* Yuriy Gerassimenko +* William Stam +* Sam George +* Steve Wasiura +* Andreas Ljunggren +* Sashank Tadepalli +* Chad Bishop +* Bradley Slavik +* Lee Blue +* Alexander Shatilo +* Justin Noel +* Ivan Kovac +* Tony's Internet Solutions +* Charles Stigler +* Attila van der Velde +* Indoblo Commerce Ltd. +* Jens Níemeyer +* Raghu Veer Dendukuri +* NovelLead B.V. +* Emir Alp +* Dominic Schwarz +* Sven Zahrend +* LucidStorm +* Nevatech +* Matt Wielgos +* Maximilian Summe +* Caspar Frey +* FocusHeart +* Philip Lawrence +* Peter Beverwyk +* Judith Grass +* Randal Hintz +* Franz Josef +* Biswajit Nayak +* R Mohan +* Michael Messner +* Jason Borseth +* Dmitrij Chernov +* Marek Toman +* Simone Cociancich +* Alan Holding +* Philipp Hirsch +* Aurélien Botermans +* Christian Treptow +* Кубарев Дмитрий (Dmitry Kubarev) +* Alexandru Catalin Trandafir +* Leigh Harrison +* Дмитриев Иван (Ivan Dmitriev) +* IT_GAP +* Sergeev Andrey +* Steven J Mixon +* Roland Fath +* Justin Parker +* Costas Menico +* Mathieu-Philippe Bourgeois +* Ryan McKillop +* Chris Clarke +* Ngan Ting On +* Eli Argon +* Seregin Andrew +* Marek Toman +* Diji Enterprises +* uonick +* Kamil Kiblis +* Mars Yau +* Martin Latinov +* Malikov Evgene +* Andres Espinoza Arce +* Matthew Williamson +* Andrew Brookes +* Steve Cove +* Steven Witten +* Silvan Seeholzer +* Toni Schönbuchner +* Marek Toman +* Dexter Freivald +* Chad West +* Bond Akinmade +* AlpiSol - Ernaldo Pisati +* Adam Wilkins +* Mihai Flaviu Molnar +* Carolina R Molla +* Andres Espinoza Arce +* Jan Kremlacek +* Eric Schultz +* Ricardo Andrade +* Derek Loewen +* Michael Nelson +* Denis Bach +* Lenard Osmani + +Special thanks to the selfless others who expressed their desire to remain anonymous, yet share their time, contribute code, send donations, promote the framework to a wider audience, as well as provide encouragement and regular financial assistance. Their generosity is F3's prime motivation. + +[![Paypal](ui/images/paypal.png)](https://www.paypal.me/fatfree) + +### Legal notice + +By making a donation to this project you signify that you acknowledged, understood, accepted, and agreed to the terms and conditions contained in this notice. Your donation to the Fat-Free Framework project is voluntary and is not a fee for any services, goods, or advantages, and making a donation to the project does not entitle you to any services, goods, or advantages. We have the right to use the money you donate to the Fat-Free Framework project in any lawful way and for any lawful purpose we see fit and we are not obligated to disclose the way and purpose to any party unless required by applicable law. Although Fat-Free Framework is free software, to our best knowledge this project does not have any tax-exempt status. The Fat-Free Framework project is neither a registered non-profit corporation nor a registered charity in any country. Your donation may or may not be tax-deductible; please consult this with your tax advisor. We will not publish/disclose your name and e-mail address without your consent, unless required by applicable law. Your donation is non-refundable. + +**Copyright (c) 2009-2019 F3::Factory/Bong Cosca <bong.cosca@yahoo.com>** + +## Support on Beerpay +Hey dude! Help me out for a couple of :beers:! + +[![Beerpay](https://beerpay.io/bcosca/fatfree/badge.svg?style=beer-square)](https://beerpay.io/bcosca/fatfree) [![Beerpay](https://beerpay.io/bcosca/fatfree/make-wish.svg?style=flat-square)](https://beerpay.io/bcosca/fatfree?focus=wish) diff --git a/vendor/fatfree/ui/css/base.css b/vendor/fatfree/ui/css/base.css new file mode 100644 index 0000000..1cfee5a --- /dev/null +++ b/vendor/fatfree/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: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 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/vendor/fatfree/ui/css/theme.css b/vendor/fatfree/ui/css/theme.css new file mode 100644 index 0000000..aee0fe9 --- /dev/null +++ b/vendor/fatfree/ui/css/theme.css @@ -0,0 +1,165 @@ +body { + font-family:Ubuntu,sans-serif; + font-size:1.2em; +} + +h1 { + color:#faa; +} + +h2 { + color:#e88; +} + +h3 { + color:#b66; +} + +h4 { + color:#944; +} + +h5 { + color:#722; +} + +h6 { + color:#500; +} + +small { + font-size:0.75em +} + +a { + text-decoration:none; + color:#66f; +} + +a:hover { + color:#666; +} + +img { + max-width:100% +} + +table { + font-size:.8em; + color:#666 !important; + background:#eee; + width:100%; + border-radius:.5em; +} + +table code { + background:transparent; + padding:0 +} + +tr { + border-bottom:1px solid #fff; +} + +tr:last-child { + border-bottom:none; +} + +th,td { + font-size:1em; + line-height:1.25em; + margin:0; + padding:1em; + white-space:nowrap; +} + +th { + font-weight:bold; + text-align:left; + text-transform:uppercase; + color:#fff; + background:#999; +} + +th a { + color:#fff; +} + +th:first-child, +td:first-child { + width:50%; +} + +pre { + background:#efefef; + padding:0.75em; + border-radius:0.75em; +} + +ul,p { + color:#666; + line-height:1.5em; +} + +p code,ul code { + padding:.25em .75em; + border-radius:.75em; + white-space:nowrap +} + +blockquote pre,blockquote code { + color:#666; + background:#fff; +} + +code { + background:#eee; +} + +.center { + text-align:center; +} + +.right { + text-align:right; +} + +.content { + padding:0 20px; + max-width:768px; + margin:0 auto; +} + +.header { + background:#eee; +} + +.header img { + width:90%; + max-width:768px; + padding:0 5%; +} + +.footer { + font-size:0.9em; + background:#333; +} + +.footer p { + color:#eee; + padding:20px; + max-width:768px; + margin:0 auto; +} + +.footer .stats { + font-size:.9em; +} + +@media screen and (max-width:48em) { + + body { + font-size:1em; + } + +} diff --git a/vendor/fatfree/ui/images/logo.png b/vendor/fatfree/ui/images/logo.png new file mode 100644 index 0000000..0667487 Binary files /dev/null and b/vendor/fatfree/ui/images/logo.png differ diff --git a/vendor/fatfree/ui/images/paypal.png b/vendor/fatfree/ui/images/paypal.png new file mode 100644 index 0000000..80e89f2 Binary files /dev/null and b/vendor/fatfree/ui/images/paypal.png differ diff --git a/vendor/fatfree/ui/images/twitter.png b/vendor/fatfree/ui/images/twitter.png new file mode 100644 index 0000000..c1b1a8d Binary files /dev/null and b/vendor/fatfree/ui/images/twitter.png differ diff --git a/vendor/fatfree/ui/layout.htm b/vendor/fatfree/ui/layout.htm new file mode 100644 index 0000000..fb76133 --- /dev/null +++ b/vendor/fatfree/ui/layout.htm @@ -0,0 +1,14 @@ + + + + + Powered by <?php echo $PACKAGE; ?> + + + + + + + render(Base::instance()->get('content')); ?> + + diff --git a/vendor/fatfree/ui/userref.htm b/vendor/fatfree/ui/userref.htm new file mode 100644 index 0000000..5b740ad --- /dev/null +++ b/vendor/fatfree/ui/userref.htm @@ -0,0 +1,4 @@ +
    + + convert(Base::instance()->read('readme.md')); ?> +
    diff --git a/vendor/fatfree/ui/welcome.htm b/vendor/fatfree/ui/welcome.htm new file mode 100644 index 0000000..31f69f9 --- /dev/null +++ b/vendor/fatfree/ui/welcome.htm @@ -0,0 +1,62 @@ +
    +

    +
    +
    +

    Version

    +

    The first thing you might want to do is visualize your directory structures. Fat-Free gives you total control over your Web site. Organize your folders in any way that pleases you (or your development team if you're part of a group). Decide where you want to store the following:

    +
      +
    • Application and code libraries
    • +
    • HTML templates
    • +
    • Graphics and media files
    • +
    • Javascript and CSS files
    • +
    • Database (if you plan to use an embedded DB like SQLite)
    • +
    • Configuration files
    • +
    • Uploads/Downloads
    • +
    +

    For security reasons, consider relocating the lib/ folder to a path that's not Web-accessible. If you decide to move this folder, just change the line in index.php containing require 'lib/base.php'; so it points to the new location. The lib/ folder also contains framework plug-ins that extend F3's capabilities. You can change the default location of all plug-ins by moving the files to your desired subdirectory. Then, it's just a matter of pointing the PLUGINS global variable to the new location. You may delete the plug-ins that you don't need. You can reinstate them later as you find necessary.

    +

    F3 can autoload OOP classes for you. Just add the path to the AUTOLOAD variable.

    +

    When you're ready to write your F3-enabled site, you can start editing the rest of the code contained in the index.php file that displayed this Web page. Developing PHP applications will never be the same!

    +

    PHP Dependencies

    +

    Some framework features in this version will not be available if PHP is not configured with the modules needed by your application.

    + + + + + + $modules): ?> + + + + + +
    Class/Plug-inPHP Module
    + + onclick="return false">
    + +
    +
      +
    • The Base class requires all listed PHP modules enabled to function properly.
    • +
    • The Cache class will use any available module in the list. If none can be found, it will use the filesystem as fallback.
    • +
    • The DB\SQL class requires the pdo module and a PDO driver relevant to your application.
    • +
    • The Bcrypt class will use the mcrypt or openssl module for entropy generation. Otherwise, it employs a custom random function.
    • +
    • The Web class will use the curl module for HTTP requests to another server. If this is not detected, it will use other transports available, such as the HTTP stream wrapper or native sockets.
    • +
    • The geoip module listed in the Web\Geo class is optional; the class will use an alternative Web service for geo-location.
    • +
    • Other framework classes in the list need all its listed modules enabled.
    • +
    +

    Need Help?

    +

    If you have any questions regarding the framework, technical support is available at https://groups.google.com/forum/?fromgroups#!forum/f3-framework

    +

    You can also join our Slack Channel to get support

    +

    Need live support? You can talk to the development team and the rest of the Fat-Free community via IRC. We're on the FreeNode (chat.freenode.net) #fatfree channel. If the channel appears quiet, the development team might just be busy with the next great release, or it's probably due to time zone differences. Just hang around.

    +

    The User Reference is designed to serve as a handbook and programming guide. However, the online documentation at https://github.com/bcosca/fatfree provides the latest and most comprehensive information about the framework.

    +

    Fair Licensing

    +

    Fat-Free Framework is free software covered by the terms of the GNU Public License (GPL v3). You may not use the software, documentation, and samples except in compliance with the license. If the terms and conditions of this license are too restrictive for your use, alternative licensing is available for a very reasonable fee.

    +

    If you feel that this software is one great weapon to have in your programming arsenal, it saves you a lot of time and money, use it for commercial gain or in your business organization, please consider making a donation to the project. A significant amount of time, effort, and money has been spent on this project. Your donations help keep this project alive and the development team motivated. Donors and sponsors get priority support commensurate to your contribution (24-hour response time on business days).

    +

    Support F3

    +

    F3 is community-driven software. Support the development of the Fat-Free Framework. Your contributions help keep this project alive.

    +

    +
    +