[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: ikkez\nbuy_me_a_coffee: ikkez\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "CHANGELOG\n\n3.9.2 (02.12.2025)\n---\nfeat, Audit: Added bot/AI detection, #401\nfix: php 8.5 compatibility\n\n3.9.1 (09.08.2025)\n---\n*\tfix: return type hint for unserialize, closes [#391](https://github.com/f3-factory/fatfree-core/issues/391)\n*\tfeat: Web->slug, specify the separator, [#397](https://github.com/f3-factory/fatfree-core/issues/397)\n*\tfeat: added hive pull method, [#395](https://github.com/f3-factory/fatfree-core/issues/395)\n*\tfix: throw exceptions instead of deprecated USER_ERROR [#393](https://github.com/f3-factory/fatfree-core/issues/393)\n*\tfix: error thrown by `read()` for cache, [#392](https://github.com/f3-factory/fatfree-core/issues/392)\n*\tfix: failsafe when passing null to explode parameter\n\n3.9.0 (29.12.2024)\n---\n*\tfeat [#377], DB\\SQL: check TABLE_SCHEMA in fields introspection query\n*\tfeat [#379]: Added `REROUTE_TRAILING_SLASH` to optionally disable rerouting\n*\tfeat [#380], Audit: Adding new mac address validation\n*\tfix [#372], Audit: `isprivate()`, `FILTER_FLAG_NO_PRIV_RANGE` usage\n*\tfix [#378]: websocket handlers call\n*\tfix: php 8.4 session handler compatibility\n*\tfix: various php 8.4 warnings\n*\tfix [#382]: PHP 8.4 `E_STRICT` deprecation fix\n* \tfix [#371]: trim string based routes handler\n*\tfix [bcosca/fatfree#1285]: multi-line template tags \n* \tfix [#375]: PHP 8.4 Fixes for implicit nullability deprecation\n*\tfix [#374]: http_build_query usage deprecation fix\n* \tfix [#369]: unify key string replacement for cache getter/setter\n\n3.8.2 (24.07.2023)\n---\n*\tfeat, Base->format: optimize international date formatting for php8.1+\n*\tfix, Base->format: keep php7 compatibility in date formatter [#360](https://github.com/f3-factory/fatfree-core/issues/360)\n*\tfix, Markdown: unicode chars not captured correctly for headline slugs [#363](https://github.com/f3-factory/fatfree-core/issues/363)\n*\tfix, Preview->resolve: PHP8+ error, undefined variable $hash [#359](https://github.com/f3-factory/fatfree-core/issues/359)\n*\tfix, Base->clean: PHP8+ error, strip_tags with null value usage\n\n3.8.1 (05.11.2022)\n---\n*   fix: CORS preflight request fails to find route for ajax-only definitions [bcosca/fatfree#1242](https://github.com/bcosca/fatfree/issues/1242)\n*   fix: add realpath to captcha font filepath, [#314](https://github.com/bcosca/fatfree-core/issues/314)\n*   fix: case-insensitive custom tag matching [#353](https://github.com/bcosca/fatfree-core/issues/353)\n*   fix: php8 error suppression on invalid locale constant access [bcosca/fatfree#1259](https://github.com/bcosca/fatfree/issues/1259)\n*   fix: iteration over inaccessible object properties, fixes [#350](https://github.com/bcosca/fatfree-core/issues/350)\n*   feat: let jig handle utf8 issues more gracefully [#352](https://github.com/bcosca/fatfree-core/issues/352)\n*   fix: BC issue for pre php74\n*   fix: ensure template->parse does render zero text-node, [#354](https://github.com/bcosca/fatfree-core/issues/354)\n*   fix: DB\\SQL\\Mapper: allow to pass an empty array as $filter, fixes [bcosca/fatfree#1257](https://github.com/bcosca/fatfree/issues/1257)\n*   fix: adhoc null fields in Twig are executed as callable, [#310](https://github.com/bcosca/fatfree-core/issues/310)\n*   fix: ensure merged default PARAMS are properly encoded when building alias, [#345](https://github.com/bcosca/fatfree-core/issues/345)\n*   fix: Added CORS defaults that are not initialized\n*   fix: SQL cache schema for $fields\n*   fix: adhoc field with null value php81 issue, [#339](https://github.com/bcosca/fatfree-core/issues/339)\n*   fix: check against correct identity flags when using IDENTITY_INSERT for sql server\n*   added missing file location in error handler [bcosca/fatfree#1255](https://github.com/bcosca/fatfree/issues/1255)\n*   Web->request, add option to adjust accept-encoding in curl engine [#355](https://github.com/bcosca/fatfree-core/issues/355)\n\n3.8.0 (15 Feb 2022)\n---\n*   Feat: allow access to previous session data in cache-based session handler\n*   Feat: pass session information to onSuspect Session handler\n*   Fix: PHP 8.1 compatibility fixes [#332](https://github.com/bcosca/fatfree-core/issues/332) [#333](https://github.com/bcosca/fatfree-core/issues/333)\n*   Fix: check for critical schemes in url validation\n*   Fix: plural format syntax with empty param, [#325](https://github.com/bcosca/fatfree-core/issues/325)\n*   Fix: DB mapper not able to fetch field scheme in sqlite views\n*   Fix: capitalization of array key X-Http-Method-Override in headers [#327](https://github.com/bcosca/fatfree-core/issues/327)\n*   Fix SMTP: allow RFC2047 encoded words in From/To/Cc/Bcc headers\n*   Fix: use correct ternary value, [#323](https://github.com/bcosca/fatfree-core/issues/323)\n*   Fix: trace not present in error handler when in CLI mode and !DEBUG, [#323](https://github.com/bcosca/fatfree-core/issues/323)\n\n3.7.3 (13 Dec 2020)\n---\n*   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)\n*   added SMTP dialog error handling, [#317](https://github.com/bcosca/fatfree-core/issues/317)\n*   Fix: Check active transaction before rollback/commit (PHP8 issue)\n*   refactored increment/decrement operator to preceed variables\n*   added error output in CLI mode, [bcosca/fatfree#1185](https://github.com/bcosca/fatfree/issues/1185)\n*   Set PORT to 80 when SERVER_PORT is an empty string\n*   Fix: unescape dbname when extracting from dsn, [#316](https://github.com/bcosca/fatfree-core/issues/316)\n*   Fix: handling of PDO prepare() errors\n*   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)\n*   Fix: Escape literal hyphens in regex character classes, [bcosca/fatfree#1206](https://github.com/bcosca/fatfree/issues/1206)\n*   Fix: error highlighting\n*   Fix: pagination with order by on virtual fields\n*   Fixed a couple PHPDOC issues\n \n3.7.2 (28 May 2020)\n---\n*   CHANGED, View->sandbox: disable escaping when rendering as text/plain, [bcosca/fatfree#654](https://github.com/bcosca/fatfree/issues/654)\n*   update HTTP protocol checks, [bcosca/fatfree#1190](https://github.com/bcosca/fatfree/issues/1190)\n*   Base->clear: close vulnerability on variable compilation, [bcosca/fatfree#1191](https://github.com/bcosca/fatfree/issues/1191)\n*   DB\\SQL\\Mapper: fix empty ID after insert, [bcosca/fatfree#1175](https://github.com/bcosca/fatfree/issues/1175)\n*   DB\\SQL\\Mapper: fix using correct key variable for grouped sql pagination sets\n*   Fix return type of 'count' in Cursor->paginate(), [bcosca/fatfree#1187](https://github.com/bcosca/fatfree/issues/1187)\n*   Bug fix, Web->minify: fix minification of ES6 template literals, [bcosca/fatfree#1178](https://github.com/bcosca/fatfree/issues/1178)\n*   Bug fix, config: refactoring custom section parser regex, [bcosca/fatfree#1149](https://github.com/bcosca/fatfree/issues/1149)\n*   Bug fix: token resolve on non-alias reroute paths, [ref. 221f0c9](https://github.com/bcosca/fatfree-core/commit/221f0c930f8664565c9825faeb9ed9af0f7a01c8)\n*   Websocket: Improved event handler usage\n*   optimized internal get calls\n*   only use cached lexicon when a $ttl was given\n*   only use money_format up until php7.4, [bcosca/fatfree#1174](https://github.com/bcosca/fatfree/issues/1174)\n\n3.7.1 (30. December 2019)\n---\n*   Base->build: Add support for brace-enclosed route tokens\n*   Base->reroute, fix duplicate fragment issue on non-alias routes\n*   DB\\SQL\\Mapper: fix empty check for pkey when reloading after insert\n*   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)\n\n3.7.0 (26. November 2019)\n---\n*   NEW: Matrix, added select and walk methods for array processing and validation tools\n*   NEW: Added configurable file locking via LOCK var\n*   NEW: json support for dictionary files\n*   NEW: $die parameter on ONREROUTE hook\n*   NEW: Added SameSite cookie support for php7.3+ (JAR.samesite), [bcosca/fatfree#1165](https://github.com/bcosca/fatfree/issues/1165)\n*   NEW, DB\\SQL\\Mapper: added updateAll method to batch-update multiple records at once\n*   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)\n*   Cache, Redis: Added ability to set a Redis password, [#287](https://github.com/bcosca/fatfree-core/issues/287)\n*   DB\\SQL\\Session: make datatype of data column configurable, [bcosca/fatfree#1130](https://github.com/bcosca/fatfree/issues/1130)\n*   DB\\SQL\\Mapper: only add adhoc fields in count queries that are used for grouping\n*   DB\\SQL\\Mapper: fixed inserting an already loaded record again (duplicating), [bcosca/fatfree#1093](https://github.com/bcosca/fatfree/issues/1093)\n*   Magic (Mappers): fix isset check on existing properties\n*   SMTP: added support for Bounce mail recipient (\"Sender\" header)\n*   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)\n*   Web: Added more cyrillic letters to diacritics, [bcosca/fatfree#1158](https://github.com/bcosca/fatfree/issues/1158)\n*   Web: Fixed url string falsely detected as comment section [9ac8e615](https://github.com/bcosca/fatfree-core/commit/9ac8e615ccaf750b49497a3c86161331b24e637f)\n*   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)\n*   WS: Fixed processing all queued data frames inside the buffer, [#277](https://github.com/bcosca/fatfree-core/issues/277)\n*   WS: Allow packet size override\n*   Markdown: Support mixed `strong` and `italic` elements, [#276](https://github.com/bcosca/fatfree-core/issues/276)\n*   Markdown: Keep spaces around `=` sign in ini code blocks \n*   Added route alias key name validation, [#243](https://github.com/bcosca/fatfree-core/issues/243)\n*   Added fragment argument to alias method, [#282](https://github.com/bcosca/fatfree-core/issues/282)\n*   Allow adding fragment to reroute, [#1156](https://github.com/bcosca/fatfree/issues/1156)\n*   Added additional HTTP status codes, [#283](https://github.com/bcosca/fatfree-core/issues/283)\n*   Added X-Forwarded-For IP to log entries, [bcosca/fatfree#1042](https://github.com/bcosca/fatfree/issues/1042)\n*   Bug fix: broken custom date/time formatting, [bcosca/fatfree#1147](https://github.com/bcosca/fatfree/issues/1147)\n*   Bug fix: duplicate UI path rendering edge-case in Views and minify, [bcosca/fatfree#1152](https://github.com/bcosca/fatfree/issues/1152)\n*   Bug fix: unicode chars in custom config section keys, [bcosca/fatfree#1149](https://github.com/bcosca/fatfree/issues/1149)\n*   Bug fix: ensure valid reroute path in location header, [bcosca/fatfree#1140](https://github.com/bcosca/fatfree/issues/1140)\n*   Bug fix: use dictionary path for lexicon caching-hash\n*   Bug fix, php7.3: number format ternary, [bcosca/fatfree#1142](https://github.com/bcosca/fatfree/issues/1142)\n*   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)\n\n3.6.5 (24 December 2018)\n---\n*\tNEW: Log, added timestamp to each line\n*\tNEW: Auth, added support for custom compare method, [#116](https://github.com/bcosca/fatfree-core/issues/116)\n*\tNEW: cache tag support for mongo & jig mapper, ref [#166](https://github.com/bcosca/fatfree-core/issues/116)\n*\tNEW: Allow PHP functions as template token filters\n*\tWeb: Fix double redirect bug when running cURL with open_basedir disabled\n*\tWeb: Cope with responses from HTTP/2 servers\n*\tWeb->filler: remove very first space, when $std is false\n*\tWeb\\OAuth2: Cope with HTTP/2 responses\n*\tWeb\\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)\n*\tWeb\\OAuth2: fixed empty results on some endpoints [#250](https://github.com/bcosca/fatfree-core/issues/250)\n*\tDB\\SQL\\Mapper: optimize mapper->count memory usage\n*\tDB\\SQL\\Mapper: New table alias operator\n*\tDB\\SQL\\Mapper: fix count() performance on non-grouped result sets, [bcosca/fatfree#1114](https://github.com/bcosca/fatfree/issues/1114)\n*\tDB\\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)\n*\tDB\\SQL->log: Remove extraneous whitespace\n*\tDB\\SQL: Added ability to add inline comments per SQL query\n*\tCLI\\WS, Refactoring: Streamline socket server\n*\tCLI\\WS: Add option for dropping query in OAuth2 URI \n*\tCLI\\WS: Add URL-safe base64 encoding\n*\tCLI\\WS: Detect errors in returned JSON values\n*\tCLI\\WS: Added support for Sec-WebSocket-Protocol header\n*\tMatrix->calendar: Allow unix timestamp as date argument\n*\tBasket: Access basket item by _id [#260](https://github.com/bcosca/fatfree-core/issues/260)\n*\tSMTP: Added TLS 1.2 support [bcosca/fatfree#1115](https://github.com/bcosca/fatfree/issues/1115)\n*\tSMTP->send: Respect $log argument\n*\tBase->cast: recognize binary and octal numbers in config\n*\tBase->cast: add awareness of hexadecimal literals\n*\tBase->abort: Remove unnecessary Content-Encoding header\n*\tBase->abort: Ensure headers have not been flushed\n*\tBase->format: Differentiate between long- and full-date (with localized weekday) formats\n*\tBase->format: Conform with intl extension's number output\n*\tEnable route handler to override Access-Control headers in response to OPTIONS request, [#257](https://github.com/bcosca/fatfree-core/issues/257)\n*\tAugment filters with a var_export function\n*\tBug fix php7.3: Fix template parse regex to be compatible with strict PCRE2 rules for hyphen placement in a character class\n*\tBug fix, Cache->set: update creation time when updating existing cache entries \n*\tBug fix: incorrect ICU date/time formatting\n*\tBug fix, Jig: lazy write on empty data\n*\tBug fix: Method uppercase to avoid route failure [#252](https://github.com/bcosca/fatfree-core/issues/252)\n*\tFixed error description when (PSR-11) `CONTAINER` fails to resolve a class [#253](https://github.com/bcosca/fatfree-core/issues/253)\n*\tMitigate CSRF predictability/vulnerability\n*\tExpose Mapper->factory() method\n\n3.6.4 (19 April 2018)\n---\n*\tNEW: Added Dependency Injection support with CONTAINER variable [#221](https://github.com/bcosca/fatfree-core/issues/221)\n*\tNEW: configurable LOGGABLE error codes [#1091](https://github.com/bcosca/fatfree/issues/1091#issuecomment-364674701)\n*\tNEW: JAR.lifetime option, [#178](https://github.com/bcosca/fatfree-core/issues/178)\n*\tTemplate: reduced Prefab calls\n*\tTemplate: optimized reflection for better derivative support, [bcosca/fatfree#1088](https://github.com/bcosca/fatfree/issues/1088)\n*\tTemplate: optimized parsing for template attributes and tokens\n*\tDB\\Mongo: fixed logging with mongodb extention\n*\tDB\\Jig: added lazy-loading [#7e1cd9b9b89](https://github.com/bcosca/fatfree-core/commit/7e1cd9b9b89c4175d0f6b86ced9d9bd49c04ac39)\n*\tDB\\Jig\\Mapper: Added group feature, bcosca/fatfree#616\n*\tDB\\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)\n*\tDB\\SQL\\Mapper: disable order clause auto-quoting when it's already been quoted\n*\tWeb->location: add failsafe for geoip_region_name_by_code() [#GB:Bxyn9xn9AgAJ](https://groups.google.com/d/msg/f3-framework/APau4wnwNzE/Bxyn9xn9AgAJ)\n*\tWeb->request: Added proxy support [#e936361b](https://github.com/bcosca/fatfree-core/commit/e936361bc03010c4c7c38a396562e5e96a8a100d)\n*\tWeb->mime: Added JFIF format\n*\tMarkdown: handle line breaks in paragraph blocks, [bcosca/fatfree#1100](https://github.com/bcosca/fatfree/issues/1100)\n*\tconfig: reduced cast calls on parsing config sections\n*\tPatch empty SERVER_NAME [bcosca/fatfree#1084](https://github.com/bcosca/fatfree/issues/1084)\n*\tBugfix: unreliable request headers in Web->request() response [bcosca/fatfree#1092](https://github.com/bcosca/fatfree/issues/1092)\n*\tFixed, View->render: utilizing multiple UI paths, [bcosca/fatfree#1083](https://github.com/bcosca/fatfree/issues/1083)\n*\tFixed URL parsing with PHP 5.4 [#247](https://github.com/bcosca/fatfree-core/issues/247)\n*\tFixed PHP 7.2 warnings when session is active prematurely, [#238](https://github.com/bcosca/fatfree-core/issues/238)\n*\tFixed setcookie $expire variable type [#240](https://github.com/bcosca/fatfree-core/issues/240)\n*\tFixed expiration time when updating an existing cookie\n\n3.6.3 (31 December 2017)\n---\n*\tPHP7 fix: remove deprecated (unset) cast\n*\tWeb->request: restricted follow_location to 3XX responses only\n*\tCLI mode: refactored arguments parsing\n*\tCLI mode: fixed query string encoding\n*\tSMTP: Refactor parsing of attachments\n*\tSMTP: clean-up mail headers for multipart messages, [#1065](https://github.com/bcosca/fatfree/issues/1065)\n*\tconfig: fixed performance issues on parsing config files\n*\tconfig: cast command parameters in config entries to php type & constant, [#1030](https://github.com/bcosca/fatfree/issues/1030)\n*\tconfig: reduced registry calls\n*\tconfig: skip hive escaping when resolving dynamic config vars, [#1030](https://github.com/bcosca/fatfree/issues/1030)\n*\tBug fix: Incorrect cookie lifetime computation, [#1070](https://github.com/bcosca/fatfree/issues/1070), [#1016](https://github.com/bcosca/fatfree/issues/1016)\n*\tDB\\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)\n*\tDB\\SQL\\Session: check if _agent is too long for SQL based sessions [#236](https://github.com/bcosca/fatfree-core/issues/236)\n*\tDB\\SQL\\Session: fix Session handler table creation issue on SQL Server, [#899](https://github.com/bcosca/fatfree/issues/899)\n*\tDB\\SQL: fix oracle db issue with empty error variable, [#1072](https://github.com/bcosca/fatfree/issues/1072)\n*\tDB\\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)\n*\tPrevent directory traversal attacks on filesystem based cache [#1073](https://github.com/bcosca/fatfree/issues/1073)\n*\tBug fix, Template: PHP constants used in include with attribute, [#983](https://github.com/bcosca/fatfree/issues/983)\n*\tBug fix, Template: Numeric value in expression alters PHP_EOL context\n*\tTemplate: use existing linefeed instead of PHP_EOL, [#1048](https://github.com/bcosca/fatfree/issues/1048)\n*\tTemplate: make newline interpolation handling configurable [#223](https://github.com/bcosca/fatfree-core/issues/223)\n*\tTemplate: add beforerender to Preview\n*\tfix custom FORMATS without modifiers\n*\tCache: Refactor Cache->reset for XCache\n*\tCache: loosen reset cache key pattern, [#1041](https://github.com/bcosca/fatfree/issues/1041)\n*\tXCache: suffix reset only works if xcache.admin.enable_auth is disabled\n*\tAdded HTTP 103 as recently approved by the IETF\n*\tLDAP changes to for AD flexibility [#227](https://github.com/bcosca/fatfree-core/issues/227)\n*\tHide debug trace from ajax errors when DEBUG=0 [#1071](https://github.com/bcosca/fatfree/issues/1071)\n*\tfix View->render using potentially wrong cache entry\n\n3.6.2 (26 June 2017)\n---\n*   Return a status code > 0 when dying on error [#220](https://github.com/bcosca/fatfree-core/issues/220)\n*   fix SMTP line width [#215](https://github.com/bcosca/fatfree-core/issues/215)\n*   Allow using a custom field for ldap user id checking [#217](https://github.com/bcosca/fatfree-core/issues/217)\n*   NEW: DB\\SQL->exists: generic method to check if SQL table exists\n*   Pass handler to route handler and hooks [#1035](https://github.com/bcosca/fatfree/issues/1035)\n*   pass carriage return of multiline dictionary keys\n*   Better Web->slug customization\n*   fix incorrect header issue [#211](https://github.com/bcosca/fatfree-core/issues/211)\n*   fix schema issue on databases with case-sensitive collation, fixes [#209](https://github.com/bcosca/fatfree-core/issues/209)\n*   Add filter for deriving C-locale equivalent of a number\n*   Bug fix: @LANGUAGE remains unchanged after override\n*   abort: added Header pre-check\n*   Assemble URL after ONREROUTE\n*   Add reroute argument to skip script termination\n*   Invoke ONREROUTE after headers are sent\n*   SQLite switch to backtick as quote\n*   Bug fix: Incorrect timing in SQL query logs\n*   DB\\SQL\\Mapper: Cast return value of count to integer\n*   Patched $_SERVER['REQUEST_URI'] to ensure it contains a relative URI\n*   Tweak debug verbosity\n*   fix php carriage return issue in preview->build [#205](https://github.com/bcosca/fatfree-core/pull/205)\n*   fixed template string resolution [#205](https://github.com/bcosca/fatfree-core/pull/205)\n*   Fixed unexpected default seed on CACHE set [#1028](https://github.com/bcosca/fatfree/issues/1028)\n*   DB\\SQL\\Mapper: Optimized field escaping on options\n*   Optimize template conversion to PHP file\n\n3.6.1 (2 April 2017)\n---\n*\tNEW: Recaptcha plugin [#194](https://github.com/bcosca/fatfree-core/pull/194)\n*\tNEW: MB variable for detecting multibyte support\n*\tNEW: DB\\SQL: Cache parsed schema for the TTL duration\n*\tNEW: quick erase flag on Jig/Mongo/SQL mappers [#193](https://github.com/bcosca/fatfree-core/pull/193)\n*\tNEW: Allow OPTIONS method to return a response body [#171](https://github.com/bcosca/fatfree-core/pull/171)\n*\tNEW: Add support for Memcached (bcosca/fatfree#997)\n*\tNEW: Rudimentary preload resource (HTTP2 server) support via template push()\n*\tNEW: Add support for new MongoDB driver [#177](https://github.com/bcosca/fatfree-core/pull/177)\n*\tChanged: template filter are all lowercase now\n*\tChanged: Fix template lookup inconsistency: removed base dir from UI on render\n*\tChanged: count() method now has an options argument [#192](https://github.com/bcosca/fatfree-core/pull/192)\n*\tChanged: SMTP, Spit out error message if any\n*\t\\DB\\SQL\\Mapper: refactored row count strategy\n*\tDB\\SQL\\Mapper: Allow non-scalar values to be assigned as mapper property\n*\tDB\\SQL::PARAM_FLOAT: remove cast to float (#106 and bcosca/fatfree#984) (#191)\n*\tDB\\SQL\\mapper->erase: allow empty string\n*\tDB\\SQL\\mapper->insert: fields reset after successful INSERT\n*\tAdd option to debounce Cursor->paginate subset [#195](https://github.com/bcosca/fatfree-core/pull/195)\n*\tView: Don't delete sandboxed variables (#198)\n*\tPreview: Optimize compilation of template expressions\n*\tPreview: Use shorthand tag for direct rendering\n*\tPreview->resolve(): new tweak to allow template persistence as option\n*\tWeb: Expose diacritics translation table\n*\tSMTP: Enable logging of message body only when $log argument is 'verbose'\n*\tSMTP: Convert headers to camelcase for consistency\n*\tmake cache seed more flexible, #164\n*\tImprove trace details for DEBUG>2\n*\tEnable config() to read from an array of input files\n*\tImproved alias and reroute regex\n*\tMake camelCase and snakeCase Unicode-aware\n*\tformat: Provision for optional whitespaces\n*\tBreak APCu-BC dependence\n*\tOld PHP 5.3 cleanup\n*\tDebug log must include HTTP query\n*\tRecognize X-Forwarded-Port header (bcosca/fatfree#1002)\n*\tAvoid use of deprecated mcrypt module\n*\tReturn only the client's IP when using the `X-Forwarded-For` header to deduce an IP address\n*\tRemove orphan mutex locks on termination (#157)\n*\tUse 80 as default port number to avoid issues when `$_SERVER['SERVER_PORT']` is not existing\n*\tfread replaced with readfile() for simple send() usecase\n*\tBug fix: request URI with multiple leading slashes, #203\n*\tBug fix: Query generates wrong adhoc field value\n*\tBug fix: SMTP stream context issue #200\n*\tBug fix: child pseudo class selector in minify, bcosca/fatfree#1008\n*\tBug fix: \"Undefined index: CLI\" error (#197)\n*\tBug fix: cast Cache-Control expire time to int, bcosca/fatfree#1004\n*\tBug fix: Avoid issuance of multiple Content-Type headers for nested templates\n*\tBug fix: wildcard token issue with digits (bcosca/fatfree#996)\n*\tBug fix: afterupdate ignored when row does not change\n*\tBug fix: session handler read() method for PHP7 (need strict string) #184 #185\n*\tBug fix: reroute mocking in CLI mode (#183)\n*\tBug fix: Reroute authoritative relative references (#181)\n*\tBug fix: locales order and charset hyphen\n*\tBug fix: base stripped twice in router (#176)\n\n3.6.0 (19 November 2016)\n---\n*\tNEW: [cli] request type\n*\tNEW: console-friendly CLI mode\n*\tNEW: lexicon caching\n*\tNEW: Silent operator skips startup error check (#125)\n*\tNEW: DB\\SQL->trans()\n*\tNEW: custom config section parser, i.e. [conf > Foo::bar]\n*\tNEW: support for cache tags in SQL\n*\tNEW: custom FORMATS\n*\tNEW: Mongo mapper fields whitelist\n*\tNEW: WebSocket server\n*\tNEW: Base->extend method (#158)\n*\tNEW: Implement framework variable caching via config, i.e. FOO = \"bar\" | 3600\n*\tNEW: Lightweight OAuth2 client\n*\tNEW: SEED variable, configurable app-specific hashing prefix (#149, bcosca/fatfree#951, bcosca/fatfree#884, bcosca/fatfree#629)\n*\tNEW: CLI variable\n*\tNEW: Web->send, specify custom filename (#124)\n*\tNEW: Web->send, added flushing flag (#131)\n*\tNEW: Indexed route wildcards, now exposed in PARAMS['*']\n*\tChanged: PHP 5.4 is now the minimum version requirement\n*\tChanged: Prevent database wrappers from being cloned\n*\tChanged: Router works on PATH instead of URI (#126) NB: PARAMS.0 no longer contains the query string\n*\tChanged: Removed ALIASES autobuilding (#118)\n*\tChanged: Route wildcards match empty strings (#119)\n*\tChanged: Disable default debug highlighting, HIGHLIGHT is false now\n*\tGeneral PHP 5.4 optimizations\n*\tOptimized config parsing\n*\tOptimized Base->recursive\n*\tOptimized header extraction\n*\tOptimized cache/expire headers\n*\tOptimized session_start behaviour (bcosca/fatfree#673)\n*\tOptimized reroute regex\n*\tTweaked cookie removal\n*\tBetter route precedence order\n*\tPerformance tweak: reduced cache calls\n*\tRefactored lexicon (LOCALES) build-up, much faster now\n*\tAdded turkish locale bug workaround\n*\tGeo->tzinfo Update to UTC\n*\tAdded Xcache reset (bcosca/fatfree#928)\n*\tRedis cache: allow db name in dsn\n*\tSMTP: Improve server emulation responses\n*\tSMTP: Optimize transmission envelope\n*\tSMTP: Implement mock transmission\n*\tSMTP: Various bug fixes and feature improvements\n*\tSMTP: quit on failed authentication\n*\tGeo->weather: force metric units\n*\tBase->until: Implement CLI interoperability\n*\tBase->format: looser plural syntax\n*\tBase->format: Force decimal as default number format\n*\tBase->merge: Added $keep flag to save result to the hive key\n*\tBase->reroute: Allow array as URL argument for aliasing\n*\tBase->alias: Allow query string (or array) to be appended to alias\n*\tPermit reroute to named routes with URL query segment\n*\tSync COOKIE global on set()\n*\tPermit non-hive variables to use JS dot notation\n*\tRFC2616: Use absolute URIs for Location header\n*\tMatrix->calendar: Check if calendar extension is loaded\n*\tMarkdown: require start of line/whitespace for text processing (#136)\n*\tDB\\[SQL|Jig|Mongo]->log(FALSE) disables logging\n*\tDB\\SQL->exec: Added timestamp toggle to db log\n*\tDB\\SQL->schema: Remove unnecessary line terminators\n*\tDB\\SQL\\Mapper: allow array filter with empty string\n*\tDB\\SQL\\Mapper: optimized handling for key-less tables\n*\tDB\\SQL\\Mapper: added float support (#106)\n*\tDB\\SQL\\Session: increased default column sizes (#148, bcosca/fatfree#931, bcosca/fatfree#950)\n*\tWeb: Catch cURL errors\n*\tOptimize Web->receive (bcosca/fatfree#930)\n*\tWeb->minify: fix arbitrary file download vulnerability\n*\tWeb->request: fix cache control max-age detection (bcosca/fatfree#908)\n*\tWeb->request: Add request headers & error message to return value (bcosca/fatfree#737)\n*\tWeb->request: Refactored response to HTTP request\n*\tWeb->send flush while sending big files\n*\tImage->rgb: allow hex strings\n*\tImage->captcha: Check if GD module supports TrueType\n*\tImage->load: Return FALSE on load failure\n*\tImage->resize: keep aspect ratio when only width or height was given\n*\tUpdated OpenID lib (bcosca/fatfree#965)\n*\tAudit->card: add new mastercard \"2\" BIN range (bcosca/fatfree#954)\n*\tDeprecated: Bcrypt class\n*\tPreview->render: optimized detection to remove short open PHP tags and allow xml tags (#133)\n*\tDisplay file and line number in exception handler (bcosca/fatfree#967)\n*\tAdded error reporting level to Base->error and ERROR.level (bcosca/fatfree#957)\n*\tAdded optional custom cache instance to Session (#141)\n*\tCLI-aware mock()\n*\tXFRAME and PACKAGE can be switched off now (#128)\n*\tBug fix: wrong time calculation on memcache reset (#170)\n*\tBug fix: encode CLI parameters\n*\tBug fix: Close connection on abort explicitly (#162)\n*\tBug fix: Image->identicon, Avoid double-size sprite rotation (and possible segfault)\n*\tBug fix: Image->render and Image->dump, removed unnecessary 2nd argument (#146)\n*\tBug fix: Magic->offsetset, access property as array element (#147)\n*\tBug fix: multi-line custom template tag parsing (bcosca/fatfree#935)\n*\tBug fix: cache headers on errors (bcosca/fatfree#885)\n*\tBug fix: Web, deprecated CURLOPT_SSL_VERIFYHOST in curl\n*\tBug fix: Web, Invalid user error constant (bcosca/fatfree#962)\n*\tBug fix: Web->request, redirections for domain-less location (#135)\n*\tBug fix: DB\\SQL\\Mapper, reset changed flag after update (#142, #152)\n*\tBug fix: DB\\SQL\\Mapper, fix changed flag when using assignment operator #143 #150 #151\n*\tBug fix: DB\\SQL\\Mapper, revival of the HAVING clause\n*\tBug fix: DB\\SQL\\Mapper, pgsql with non-integer primary keys (bcosca/fatfree#916)\n*\tBug fix: DB\\SQL\\Session, quote table name (bcosca/fatfree#977)\n*\tBug fix: snakeCase returns word starting with underscore (bcosca/fatfree#927)\n*\tBug fix: mock does not populate PATH variable\n*\tBug fix: Geo->weather API key (#129)\n*\tBug fix: Incorrect compilation of array element with zero index\n*\tBug fix: Compilation of array construct is incorrect\n*\tBug fix: Trailing slash redirection on UTF-8 paths (#121)\n\n3.5.1 (31 December 2015)\n---\n*\tNEW: ttl attribute in <include> template tag\n*\tNEW: allow anonymous function for template filter\n*\tNEW: format modifier for international and custom currency symbol\n*\tNEW: Image->data() returns image resource\n*\tNEW: extract() get prefixed array keys from an assoc array\n*\tNEW: Optimized and faster Template parser with full support for HTML5 empty tags\n*\tNEW: Added support for {@token} encapsulation syntax in routes definition\n*\tNEW: DB\\SQL->exec(), automatically shift to 1-based query arguments\n*\tNEW: abort() flush output\n*\tAdded referenced value to devoid()\n*\tTemplate token filters are now resolved within Preview->token()\n*\tWeb->_curl: restrict redirections to HTTP\n*\tWeb->minify(), skip importing of external files\n*\tImproved session and error handling in until()\n*\tGet the error trace array with the new $format parameter\n*\tBetter support for unicode URLs\n*\tOptimized TZ detection with date_default_timezone_get()\n*\tformat() Provide default decimal places\n*\tOptimize code: remove redundant TTL checks\n*\tOptimized timeout handling in Web->request()\n*\tImproved PHPDoc hints\n*\tAdded missing russian DIACRITICS letters\n*\tDB\\Cursor: allow child implementation of reset()\n*\tDB\\Cursor: Copyfrom now does an internal call to set()\n*\tDB\\SQL: Provide the ability to disable SQL logging\n*\tDB\\SQL: improved query analysis to trigger fetchAll\n*\tDB\\SQL\\Mapper: added support for binary table columns\n*\tSQL,JIG,MONGO,CACHE Session handlers refactored and optimized\n*\tSMTP Refactoring and optimization\n*\tBug fix: SMTP, Align quoted_printable_encode() with SMTP specs (dot-stuffing)\n*\tBug fix: SMTP, Send buffered optional headers to output\n*\tBug fix: SMTP, Content-Transfer-Encoding for non-TLS connections\n*\tBug fix: SMTP, Single attachment error\n*\tBug fix: Cursor->load not always mapping to first record\n*\tBug fix: dry SQL mapper should not trigger 'load'\n*\tBug fix: Code highlighting on empty text\n*\tBug fix: Image->resize, round dimensions instead of cast\n*\tBug fix: whitespace handling in $f3->compile()\n*\tBug fix: TTL of `View` and `Preview` (`Template`)\n*\tBug fix: token filter regex\n*\tBug fix: Template, empty attributes\n*\tBug fix: Preview->build() greedy regex\n*\tBug fix: Web->minify() single-line comment on last line\n*\tBug fix: Web->request(), follow_location with cURL and open_basedir\n*\tBug fix: Web->send() Single quotes around filename not interpreted correctly by some browsers\n\n3.5.0 (2 June 2015)\n---\n*\tNEW: until() method for long polling\n*\tNEW: abort() to disconnect HTTP client (and continue execution)\n*\tNEW: SQL Mapper->required() returns TRUE if field is not nullable\n*\tNEW: PREMAP variable for allowing prefixes to handlers named after HTTP verbs\n*\tNEW: [configs] section to allow config includes\n*\tNEW: Test->passed() returns TRUE if no test failed\n*\tNEW: SQL mapper changed() function\n*\tNEW: fatfree-core composer support\n*\tNEW: constants() method to expose constants\n*\tNEW: Preview->filter() for configurable token filters\n*\tNEW: CORS variable for Cross-Origin Resource Sharing support, #731\n*\tChange in behavior: Switch to htmlspecialchars for escaping\n*\tChange in behavior: No movement in cursor position after erase(), #797\n*\tChange in behavior: ERROR.trace is a multiline string now\n*\tChange in behavior: Strict token recognition in <include> href attribute\n*\tRouter fix: loose method search\n*\tBetter route precedence order, #12\n*\tPreserve contents of ROUTES, #723\n*\tAlias: allow array of parameters\n*\tImprovements on reroute method\n*\tFix for custom Jig session files\n*\tAudit: better mobile detection\n*\tAudit: add argument to test string as browser agent\n*\tDB mappers: abort insert/update/erase from hooks, #684\n*\tDB mappers: Allow array inputs in copyfrom()\n*\tCache,SQL,Jig,Mongo Session: custom callback for suspect sessions\n*\tFix for unexpected HIVE values when defining an empty HIVE array\n*\tSQL mapper: check for results from CALL and EXEC queries, #771\n*\tSQL mapper: consider SQL schema prefix, #820\n*\tSQL mapper: write to log before execution to\n\tenable tracking of PDOStatement error\n*\tAdd SQL Mapper->table() to return table name\n*\tAllow override of the schema in SQL Mapper->schema()\n*\tImprovement: Keep JIG table as reference, #758\n*\tExpand regex to include whitespaces in SQL DB dsn, #817\n*\tView: Removed reserved variables $fw and $implicit\n*\tAdd missing newlines after template expansion\n*\tWeb->receive: fix for complex field names, #806\n*\tWeb: Improvements in socket engine\n*\tWeb: customizable user_agent for all engines, #822\n*\tSMTP: Provision for Content-ID in attachments\n*\tImage + minify: allow absolute paths\n*\tPromote framework error to E_USER_ERROR\n*\tGeo->weather switch to OpenWeather\n*\tExpose mask() and grab() methods for routing\n*\tExpose trace() method to expose the debug backtrace\n*\tImplement recursion strategy using IteratorAggregate, #714\n*\tExempt whitespace between % and succeeding operator from being minified, #773\n*\tOptimized error detection and ONERROR handler, fatfree-core#18\n*\tTweak error log output\n*\tOptimized If-Modified-Since cache header usage\n*\tImproved APCu compatibility, #724\n*\tBug fix: Web::send fails on filename with spaces, #810\n*\tBug fix: overwrite limit in findone()\n*\tBug fix: locale-specific edge cases affecting SQL schema, #772\n*\tBug fix: Newline stripping in config()\n*\tBug fix: bracket delimited identifier for sybase and dblib driver\n*\tBug fix: Mongo mapper collection->count driver compatibility\n*\tBug fix: SQL Mapper->set() forces adhoc value if already defined\n*\tBug fix: Mapper ignores HAVING clause\n*\tBug fix: Constructor invocation in call()\n*\tBug fix: Wrong element returned by ajax/sync request\n*\tBug fix: handling of non-consecutive compound key members\n*\tBug fix: Virtual fields not retrieved when group option is present, #757\n*\tBug fix: group option generates incorrect SQL query, #757\n*\tBug fix: ONERROR does not receive PARAMS on fatal error\n\n3.4.0 (1 January 2015)\n---\n*\tNEW: [redirects] section\n*\tNEW: Custom config sections\n*\tNEW: User-defined AUTOLOAD function\n*\tNEW: ONREROUTE variable\n*\tNEW: Provision for in-memory Jig database (#727)\n*\tReturn run() result (#687)\n*\tPass result of run() to mock() (#687)\n*\tAdd port suffix to REALM variable\n*\tNew attribute in <include> tag to extend hive\n*\tAdjust unit tests and clean up templates\n*\tExpose header-related methods\n*\tWeb->request: allow content array\n*\tPreserve contents of ROUTES (#723)\n*\tSmart detection of PHP functions in template expressions\n*\tAdd afterrender() hook to View class\n*\tImplement ArrayAccess and magic properties on hive\n*\tImprovement on mocking of superglobals and request body\n*\tFix table creation for pgsql handled sessions\n*\tAdd QUERY to hive\n*\tExempt E_NOTICE from default error_reporting()\n*\tAdd method to build alias routes from template, fixes #693\n*\tFix dangerous caching of cookie values\n*\tFix multiple encoding in nested templates\n*\tFix node attribute parsing for empty/zero values\n*\tApply URL encoding on BASE to emulate v2 behavior (#123)\n*\tImprove Base->map performance (#595)\n*\tAdd simple backtrace for fatal errors\n*\tCount Cursor->load() results (#581)\n*\tAdd form field name to Web->receive() callback arguments\n*\tFix missing newlines after template expansion\n*\tFix overwrite of ENCODING variable\n*\tlimit & offset workaround for SQL Server, fixes #671\n*\tSQL Mapper->find: GROUP BY SQL compliant statement\n*\tBug fix: Missing abstract method fields()\n*\tBug fix: Auto escaping does not work with mapper objects (#710)\n*\tBug fix: 'with' attribute in <include> tag raise error when no token\n\tinside\n*\tView rendering: optional Content-Type header\n*\tBug fix: Undefined variable: cache (#705)\n*\tBug fix: Routing does not work if project base path includes valid\n\tspecial URI character (#704)\n*\tBug fix: Template hash collision (#702)\n*\tBug fix: Property visibility is incorrect (#697)\n*\tBug fix: Missing Allow header on HTTP 405 response\n*\tBug fix: Double quotes in lexicon files (#681)\n*\tBug fix: Space should not be mandatory in ICU pluralization format string\n*\tBug fix: Incorrect log entry when SQL query contains a question mark\n*\tBug fix: Error stack trace\n*\tBug fix: Cookie expiration (#665)\n*\tBug fix: OR operator (||) parsed incorrectly\n*\tBug fix: Routing treatment of * wildcard character\n*\tBug fix:  Mapper copyfrom() method doesn't allow class/object callbacks\n\t(#590)\n*\tBug fix: exists() creates elements/properties (#591)\n*\tBug fix: Wildcard in routing pattern consumes entire query string (#592)\n*\tBug fix: Workaround bug in latest MongoDB driver\n*\tBug fix: Default error handler silently fails for AJAX request with\n\tDEBUG>0 (#599)\n*\tBug fix: Mocked BODY overwritten (#601)\n*\tBug fix: Undefined pkey (#607)\n\n3.3.0 (8 August 2014)\n---\n*\tNEW: Attribute in <include> tag to extend hive\n*\tNEW: Image overlay with transparency and alignment control\n*\tNEW: Allow redirection of specified route patterns to a URL\n*\tBug fix: Missing AND operator in SQL Server schema query (Issue #576)\n*\tCount Cursor->load() results (Feature request #581)\n*\tMapper copyfrom() method doesn't allow class/object callbacks (Issue #590)\n*\tBug fix: exists() creates elements/properties (Issue #591)\n*\tBug fix: Wildcard in routing pattern consumes entire query string\n\t(Issue #592)\n*\tTweak Base->map performance (Issue #595)\n*\tBug fix: Default error handler silently fails for AJAX request with\n\tDEBUG>0 (Issue #599)\n*\tBug fix: Mocked BODY overwritten (Issue #601)\n*\tBug fix: Undefined pkey (Issue #607)\n*\tBug fix: beforeupdate() position (Issue #633)\n*\tBug fix: exists() return value for cached keys\n*\tBug fix: Missing error code in UNLOAD handler\n*\tBug fix: OR operator (||) parsed incorrectly\n*\tAdd input name parameter to custom slug function\n*\tApply URL encoding on BASE to emulate v2 behavior (Issue #123)\n*\tReduce mapper update() iterations\n*\tBug fix: Routing treatment of * wildcard character\n*\tSQL Mapper->find: GROUP BY SQL compliant statement\n*\tWork around bug in latest MongoDB driver\n*\tWork around probable race condition and optimize cache access\n*\tView rendering: Optional Content-Type header\n*\tFix missing newlines after template expansion\n*\tAdd form field name to Web->receive() callback arguments\n*\tQuick reference: add RAW variable\n\n3.2.2 (19 March 2014)\n---\n*\tNEW: Locales set automatically (Feature request #522)\n*\tNEW: Mapper dbtype()\n*\tNEW: before- and after- triggers for all mappers\n*\tNEW: Decode HTML5 entities if PHP>5.3 detected (Feature request #552)\n*\tNEW: Send credentials only if AUTH is present in the SMTP extension\n\tresponse (Feature request #545)\n*\tNEW: BITMASK variable to allow ENT_COMPAT override\n*\tNEW: Redis support for caching\n*\tEnable SMTP feature detection\n*\tEnable extended ICU custom date format (Feature request #555)\n*\tEnable custom time ICU format\n*\tAdd option to turn off session table creation (Feature request #557)\n*\tEnhanced template token rendering and custom filters (Feature request\n\t#550)\n*\tAvert multiple loads in DB-managed sessions (Feature request #558)\n*\tAdd EXEC to associative fetch\n*\tBug fix: Building template tokens breaks on inline OR condition (Issue\n\t#573)\n*\tBug fix: SMTP->send does not use the $log parameter (Issue #571)\n*\tBug fix: Allow setting sqlsrv primary keys on insert (Issue #570)\n*\tBug fix: Generated query for obtaining table schema in sqlsrv incorrect\n\t(Bug #565)\n*\tBug fix: SQL mapper flag set even when value has not changed (Bug #562)\n*\tBug fix: Add XFRAME config option (Feature request #546)\n*\tBug fix: Incorrect parsing of comments (Issue #541)\n*\tBug fix: Multiple Set-Cookie headers (Issue #533)\n*\tBug fix: Mapper is dry after save()\n*\tBug fix: Prevent infinite loop when error handler is triggered\n\t(Issue #361)\n*\tBug fix: Mapper tweaks not passing primary keys as arguments\n*\tBug fix: Zero indexes in dot-notated arrays fail to compile\n*\tBug fix: Prevent GROUP clause double-escaping\n*\tBug fix: Regression of zlib compression bug\n*\tBug fix: Method copyto() does not include ad hoc fields\n*\tCheck existence of OpenID mode (Issue #529)\n*\tGenerate a 404 when a tokenized class doesn't exist\n*\tFix SQLite quotes (Issue #521)\n*\tBug fix: BASE is incorrect on Windows\n\n3.2.1 (7 January 2014)\n---\n*\tNEW: EMOJI variable, UTF->translate(), UTF->emojify(), and UTF->strrev()\n*\tAllow empty strings in config()\n*\tAdd support for turning off php://input buffering via RAW\n\t(FALSE by default)\n*\tAdd Cursor->load() and Cursor->find() TTL support\n*\tSupport Web->receive() large file downloads via PUT\n*\tONERROR safety check\n*\tFix session CSRF cookie detection\n*\tFramework object now passed to route handler contructors\n*\tAllow override of DIACRITICS\n*\tVarious code optimizations\n*\tSupport log disabling (Issue #483)\n*\tImplicit mapper load() on authentication\n*\tDeclare abstract methods for Cursor derivatives\n*\tSupport single-quoted HTML/XML attributes (Feature request #503)\n*\tRelax property visibility of mappers and derivatives\n*\tDeprecated: {{~ ~}} instructions and {{* *}} comments; Use {~ ~} and\n\t{* *} instead\n*\tMinor fix: Audit->ipv4() return value\n*\tBug fix: Backslashes in BASE not converted on Windows\n*\tBug fix: UTF->substr() with negative offset and specified length\n*\tBug fix: Replace named URL tokens on render()\n*\tBug fix: BASE is not empty when run from document root\n*\tBug fix: stringify() recursion\n\n3.2.0 (18 December 2013)\n---\n*\tNEW: Automatic CSRF protection (with IP and User-Agent checks) for\n\tsessions mapped to SQL-, Jig-, Mongo- and Cache-based backends\n*\tNEW: Named routes\n*\tNEW: PATH variable; returns the URL relative to BASE\n*\tNEW: Image->captcha() color parameters\n*\tNEW: Ability to access MongoCuror thru the cursor() method\n*\tNEW: Mapper->fields() method returns array of field names\n*\tNEW: Mapper onload(), oninsert(), onupdate(), and onerase() event\n\tlisteners/triggers\n*\tNEW: Preview class (a lightweight template engine)\n*\tNEW: rel() method derives path from URL relative to BASE; useful for\n\trerouting\n*\tNEW: PREFIX variable for prepending a string to a dictionary term;\n\tEnable support for prefixed dictionary arrays and .ini files (Feature\n\trequest #440)\n*\tNEW: Google static map plugin\n*\tNEW: devoid() method\n*\tIntroduce clean(); similar to scrub(), except that arg is passed by\n\tvalue\n*\tUse $ttl for cookie expiration (Issue #457)\n*\tFix needs_rehash() cost comparison\n*\tAdd pass-by-reference argument to exists() so if method returns TRUE,\n\ta subsequent get() is unnecessary\n*\tImprove MySQL support\n*\tMove esc(), raw(), and dupe() to View class where they more\n\tappropriately belong\n*\tAllow user-defined fields in SQL mapper constructor (Feature request\n\t#450)\n*\tRe-implement the pre-3.0 template resolve() feature\n*\tRemove redundant instances of session_commit()\n*\tAdd support for input filtering in Mapper->copyfrom()\n*\tPrevent intrusive behavior of Mapper->copyfrom()\n*\tSupport multiple SQL primary keys\n*\tSupport custom tag attributes/inline tokens defined at runtime\n\t(Feature request #438)\n*\tBroader support for HTTP basic auth\n*\tProhibit Jig _id clear()\n*\tAdd support for detailed stringify() output\n*\tAdd base directory to UI path as fallback\n*\tSupport Test->expect() chaining\n*\tSupport __tostring() in stringify()\n*\tTrigger error on invalid CAPTCHA length (Issue #458)\n*\tBug fix: exists() pass-by-reference argument returns incorrect value\n*\tBug fix: DB Exec does not return affected row if query contains a\n\tsub-SELECT (Issue #437)\n*\tImprove seed generator and add code for detecting of acceptable\n\tlimits in Image->captcha() (Feature request #460)\n*\tAdd decimal format ICU extension\n*\tBug fix: 404-reported URI contains HTTP query\n*\tBug fix: Data type detection in DB->schema()\n*\tBug fix: TZ initialization\n*\tBug fix: paginate() passes incorrect argument to count()\n*\tBug fix: Incorrect query when reloading after insert()\n*\tBug fix: SQL preg_match error in pdo_type matching (Issue #447)\n*\tBug fix: Missing merge() function (Issue #444)\n*\tBug fix: BASE misdefined in command line mode\n*\tBug fix: Stringifying hive may run infinite (Issue #436)\n*\tBug fix: Incomplete stringify() when DEBUG<3 (Issue #432)\n*\tBug fix: Redirection of basic auth (Issue #430)\n*\tBug fix: Filter only PHP code (including short tags) in templates\n*\tBug fix: Markdown paragraph parser does not convert PHP code blocks\n\tproperly\n*\tBug fix: identicon() colors on same keys are randomized\n*\tBug fix: quotekey() fails on aliased keys\n*\tBug fix: Missing _id in Jig->find() return value\n*\tBug fix: LANGUAGE/LOCALES handling\n*\tBug fix: Loose comparison in stringify()\n\n3.1.2 (5 November 2013)\n---\n*\tAbandon .chm help format; Package API documentation in plain HTML;\n\t(Launch lib/api/index.html in your browser)\n*\tDeprecate BAIL in favor of HALT (default: TRUE)\n*\tRevert to 3.1.0 autoload behavior; Add support for lowercase folder\n\tnames\n*\tAllow Spring-style HTTP method overrides\n*\tAdd support for SQL Server-based sessions\n*\tCapture full X-Forwarded-For header\n*\tAdd protection against malicious scripts; Extra check if file was really\n\tuploaded\n*\tPass-thru page limit in return value of Cursor->paginate()\n*\tOptimize code: Implement single-pass escaping\n*\tShort circuit Jig->find() if source file is empty\n*\tBug fix: PHP globals passed by reference in hive() result (Issue #424)\n*\tBug fix: ZIP mime type incorrect behavior\n*\tBug fix: Jig->erase() filter malfunction\n*\tBug fix: Mongo->select() group\n*\tBug fix: Unknown bcrypt constant\n\n3.1.1 (13 October 2013)\n---\n*\tNEW: Support OpenID attribute exchange\n*\tNEW: BAIL variable enables/disables continuance of execution on non-fatal\n\terrors\n*\tDeprecate BAIL in favor of HALT (default: FALSE)\n*\tAdd support for Oracle\n*\tMark cached queries in log (Feature Request #405)\n*\tImplement Bcrypt->needs_reshash()\n*\tAdd entropy to SQL cache hash; Add uuid() method to DB backends\n*\tFind real document root; Simplify debug paths\n*\tPermit OpenID required fields to be declared as comma-separated string or\n\tarray\n*\tPass modified filename as argument to user-defined function in\n\tWeb->receive()\n*\tQuote keys in optional SQL clauses (Issue #408)\n*\tAllow UNLOAD to override fatal error detection (Issue #404)\n*\tMutex operator precedence error (Issue #406)\n*\tBug fix: exists() malfunction (Issue #401)\n*\tBug fix: Jig mapper triggers error when loading from CACHE (Issue #403)\n*\tBug fix: Array index check\n*\tBug fix: OpenID verified() return value\n*\tBug fix: Basket->find() should return a set of results (Issue #407);\n\tAlso implemented findone() for consistency with mappers\n*\tBug fix: PostgreSQL last insert ID (Issue #410)\n*\tBug fix: $port component URL overwritten by _socket()\n*\tBug fix: Calculation of elapsed time\n\n3.1.0 (20 August 2013)\n---\n*\tNEW: Web->filler() returns a chunk of text from the standard\n\tLorem Ipsum passage\n*\tChange in behavior: Drop support for JSON serialization\n*\tSQL->exec() now returns value of RETURNING clause\n*\tAdd support for $ttl argument in count() (Issue #393)\n*\tAllow UI to be overridden by custom $path\n*\tReturn result of PDO primitives: begintransaction(), rollback(), and\n\tcommit()\n*\tFull support for PHP 5.5\n*\tFlush buffers only when DEBUG=0\n*\tSupport class->method, class::method, and lambda functions as\n\tWeb->basic() arguments\n*\tCommit session on Basket->save()\n*\tOptional enlargement in Image->resize()\n*\tSupport authentication on hosts running PHP-CGI\n*\tChange visibility level of Cache properties\n*\tPrevent ONERROR recursion\n*\tWork around Apache pre-2.4 VirtualDocumentRoot bug\n*\tPrioritize cURL in HTTP engine detection\n*\tBug fix: Minify tricky JS\n*\tBug fix: desktop() detection\n*\tBug fix: Double-slash on TEMP-relative path\n*\tBug fix: Cursor mapping of first() and last() records\n*\tBug fix: Premature end of Web->receive() on multiple files\n*\tBug fix: German umlaute to its corresponding grammatically-correct\n\tequivalent\n\n3.0.9 (12 June 2013)\n---\n*\tNEW: Web->whois()\n*\tNEW: Template <switch> <case> tags\n*\tImprove CACHE consistency\n*\tCase-insensitive MIME type detection\n*\tSupport pre-PHP 5.3.4 in Prefab->instance()\n*\tRefactor isdesktop() and ismobile(); Add isbot()\n*\tAdd support for Markdown strike-through\n*\tWork around ODBC's lack of quote() support\n*\tRemove useless Prefab destructor\n*\tSupport multiple cache instances\n*\tBug fix: Underscores in OpenId keys mangled\n*\tRefactor format()\n*\tNumerous tweaks\n*\tBug fix: MongoId object not preserved\n*\tBug fix: Double-quotes included in lexicon() string (Issue #341)\n*\tBug fix: UTF-8 formatting mangled on Windows (Issue #342)\n*\tBug fix: Cache->load() error when CACHE is FALSE (Issue #344)\n*\tBug fix: send() ternary expression\n*\tBug fix: Country code constants\n\n3.0.8 (17 May 2013)\n---\n*\tNEW: Bcrypt lightweight hashing library\\\n*\tReturn total number of records in superset in Cursor->paginate()\n*\tONERROR short-circuit (Enhancement #334)\n*\tApply quotes/backticks on DB identifiers\n*\tAllow enabling/disabling of SQL log\n*\tNormalize glob() behavior (Issue #330)\n*\tBug fix: mbstring 2-byte text truncation (Issue #325)\n*\tBug fix: Unsupported operand types (Issue #324)\n\n3.0.7 (2 May 2013)\n---\n*\tNEW: route() now allows an array of routing patterns as first argument;\n\tsupport array as first argument of map()\n*\tNEW: entropy() for calculating password strength (NIST 800-63)\n*\tNEW: AGENT variable containing auto-detected HTTP user agent string\n*\tNEW: ismobile() and isdesktop() methods\n*\tNEW: Prefab class and descendants now accept constructor arguments\n*\tChange in behavior: Cache->exists() now returns timestamp and TTL of\n\tcache entry or FALSE if not found (Feature request #315)\n*\tPreserve timestamp and TTL when updating cache entry (Feature request\n\t#316)\n*\tImproved currency formatting with C99 compliance\n*\tSuppress unnecessary program halt at startup caused by misconfigured\n\tserver\n*\tAdd support for dashes in custom attribute names in templates\n*\tBug fix: Routing precedene (Issue #313)\n*\tBug fix: Remove Jig _id element from document property\n*\tBug fix: Web->rss() error when not enough items in the feed (Issue #299)\n*\tBug fix: Web engine fallback (Issue #300)\n*\tBug fix: <strong> and <em> formatting\n*\tBug fix: Text rendering of text with trailing punctuation (Issue #303)\n*\tBug fix: Incorrect regex in SMTP\n\n3.0.6 (31 Mar 2013)\n---\n*\tNEW: Image->crop()\n*\tModify documentation blocks for PHPDoc interoperability\n*\tAllow user to control whether Base->rerouet() uses a permanent or\n\ttemporary redirect\n*\tAllow JAR elements to be set individually\n*\tRefactor DB\\SQL\\Mapper->insert() to cope with autoincrement fields\n*\tTrigger error when captcha() font is missing\n*\tRemove unnecessary markdown regex recursion\n*\tCheck for scalars instead of DB\\SQL strings\n*\tImplement more comprehensive diacritics table\n*\tAdd option for disabling 401 errors when basic auth() fails\n*\tAdd markdown syntax highlighting for Apache configuration\n*\tMarkdown->render() deprecated to remove dependency on UI variable;\n\tFeature replaced by Markdown->convert() to enable translation from\n\tmarkdown string to HTML\n*\tOptimize factory() code of all data mappers\n*\tApply backticks on MySQL table names\n*\tBug fix: Routing failure when directory path contains a tilde (Issue #291)\n*\tBug fix: Incorrect markdown parsing of strong/em sequences and inline HTML\n*\tBug fix: Cached page not echoed (Issue #278)\n*\tBug fix: Object properties not escaped when rendering\n*\tBug fix: OpenID error response ignored\n*\tBug fix: memcache_get_extended_stats() timeout\n*\tBug fix: Base->set() doesn't pass TTL to Cache->set()\n*\tBug fix: Base->scrub() ignores pass-thru * argument (Issue #274)\n\n3.0.5 (16 Feb 2013)\n---\n*\tNEW: Markdown class with PHP, HTML, and .ini syntax highlighting support\n*\tNEW: Options for caching of select() and find() results\n*\tNEW: Web->acceptable()\n*\tAdd send() argument for forcing downloads\n*\tProvide read() option for applying Unix LF as standard line ending\n*\tBypass lexicon() call if LANGUAGE is undefined\n*\tLoad fallback language dictionary if LANGUAGE is undefined\n*\tmap() now checks existence of class/methods for non-tokenized URLs\n*\tImprove error reporting of non-existent Template methods\n*\tAddress output buffer issues on some servers\n*\tBug fix: Setting DEBUG to 0 won't suppress the stack trace when the\n\tcontent type is application/json (Issue #257)\n*\tBug fix: Image dump/render additional arguments shifted\n*\tBug fix: ob_clean() causes buffer issues with zlib compression\n*\tBug fix: minify() fails when commenting CSS @ rules (Issue #251)\n*\tBug fix: Handling of commas inside quoted strings\n*\tBug fix: Glitch in stringify() handling of closures\n*\tBug fix: dry() in mappers returns TRUE despite being hydrated by\n\tfactory() (Issue #265)\n*\tBug fix: expect() not handling flags correctly\n*\tBug fix: weather() fails when server is unreachable\n\n3.0.4 (29 Jan 2013)\n---\n*\tNEW: Support for ICU/CLDR pluralization\n*\tNEW: User-defined FALLBACK language\n*\tNEW: minify() now recognizes CSS @import directives\n*\tNEW: UTF->bom() returns byte order mark for UTF-8 encoding\n*\tExpose SQL\\Mapper->schema()\n*\tChange in behavior: Send error response as JSON string if AJAX request is\n\tdetected\n*\tDeprecated: afind*() methods\n*\tDiscard output buffer in favor of debug output\n*\tMake _id available to Jig queries\n*\tMagic class now implements ArrayAccess\n*\tAbort execution on startup errors\n*\tSuppress stack trace on DEBUG level 0\n*\tAllow single = as equality operator in Jig query expressions\n*\tAbort OpenID discovery if Web->request() fails\n*\tMimic PHP *RECURSION* in stringify()\n*\tModify Jig parser to allow wildcard-search using preg_match()\n*\tAbort execution after error() execution\n*\tConcatenate cached/uncached minify() iterations; Prevent spillover\n\tcaching of previous minify() result\n*\tWork around obscure PHP session id regeneration bug\n*\tRevise algorithm for Jig filter involving undefined fields (Issue #230)\n*\tUse checkdnsrr() instead of gethostbyname() in DNSBL check\n*\tAuto-adjust pagination to cursor boundaries\n*\tAdd Romanian diacritics\n*\tBug fix: Root namespace reference and sorting with undefined Jig fields\n*\tBug fix: Greedy receive() regex\n*\tBug fix: Default LANGUAGE always 'en'\n*\tBug fix: minify() hammers cache backend\n*\tBug fix: Previous values of primary keys not saved during factory()\n\tinstantiation\n*\tBug fix: Jig find() fails when search key is not present in all records\n*\tBug fix: Jig SORT_DESC (Issue #233)\n*\tBug fix: Error reporting (Issue #225)\n*\tBug fix: language() return value\n\n3.0.3 (29 Dec 2013)\n---\n*\tNEW: [ajax] and [sync] routing pattern modifiers\n*\tNEW: Basket class (session-based pseudo-mapper, shopping cart, etc.)\n*\tNEW: Test->message() method\n*\tNEW: DB profiling via DB->log()\n*\tNEW: Matrix->calendar()\n*\tNEW: Audit->card() and Audit->mod10() for credit card verification\n*\tNEW: Geo->weather()\n*\tNEW: Base->relay() accepts comma-separated callbacks; but unlike\n\tBase->chain(), result of previous callback becomes argument of the next\n*\tNumerous performance tweaks\n*\tInteroperability with new MongoClient class\n*\tWeb->request() now recognizes gzip and deflate encoding\n*\tDifferences in behavior of Web->request() engines rectified\n*\tmutex() now uses an ID as argument (instead of filename to make it clear\n\tthat specified file is not the target being locked, but a primitive\n\tcross-platform semaphore)\n*\tDB\\SQL\\Mapper field _id now returned even in the absence of any\n\tauto-increment field\n*\tMagic class spinned off as a separate file\n*\tISO 3166-1 alpha-2 table updated\n*\tApache redirect emulation for PHP 5.4 CLI server mode\n*\tFramework instance now passed as argument to any user-defined shutdown\n\tfunction\n*\tCache engine now used as storage for Web->minify() output\n*\tFlag added for enabling/disabling Image class filter history\n*\tBug fix: Trailing routing token consumes HTTP query\n*\tBug fix: LANGUAGE spills over to LOCALES setting\n*\tBug fix: Inconsistent dry() return value\n*\tBug fix: URL-decoding\n\n3.0.2 (23 Dec 2013)\n---\n*\tNEW: Syntax-highlighted stack traces via Base->highlight(); boolean\n\tHIGHLIGHT global variable can be used to enable/disable this feature\n*\tNEW: Template engine <ignore> tag\n*\tNEW: Image->captcha()\n*\tNEW: DNSBL-based spammer detection (ported from 2.x)\n*\tNEW: paginate(), first(), and last() methods for data mappers\n*\tNEW: X-HTTP-Method-Override header now recognized\n*\tNEW: Base->chain() method for executing callbacks in succession\n*\tNEW: HOST global variable; derived from either $_SERVER['SERVER_NAME'] or\n\tgethostname()\n*\tNEW: REALM global variable representing full canonical URI\n*\tNEW: Auth plug-in\n*\tNEW: Pingback plug-in (implements both Pingback 1.0 protocol client and\n\tserver)\n*\tNEW: DEBUG verbosity can now reach up to level 3; Base->stringify() drills\n\tdown to object properties at this setting\n*\tNEW: HTTP PATCH method added to recognized HTTP ReST methods\n*\tWeb->slug() now trims trailing dashes\n*\tWeb->request() now allows relative local URLs as argument\n*\tUse of PARAMS in route handlers now unnecessary; framework now passes two\n\targuments to route handlers: the framework object instance and an array\n\tcontaining the captured values of tokens in route patterns\n*\tStandardized timeout settings among Web->request() backends\n*\tSession IDs regenerated for additional security\n*\tAutomatic HTTP 404 responses by Base->call() now restricted to route\n\thandlers\n*\tEmpty comments in ini-style files now parsed properly\n*\tUse file_get_contents() in methods that don't involve high concurrency\n\n3.0.1 (14 Dec 2013)\n---\n*\tMajor rewrite of much of the framework's core features\n"
  },
  {
    "path": "COPYING",
    "content": "GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007\n\nCopyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\nEveryone is permitted to copy and distribute verbatim copies\nof this license document, but changing it is not allowed.\n\nPreamble\n\nThe GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\nThe licenses for most software and other practical works are designed\nto take away your freedom to share and change the works. By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users. We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors. You can apply it to\nyour programs, too.\n\nWhen we speak of free software, we are referring to freedom, not\nprice. Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\nTo protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights. Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\nFor example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received. You must make sure that they, too, receive\nor can get the source code. And you must show them these terms so they\nknow their rights.\n\nDevelopers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\nFor the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software. For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\nSome devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so. This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software. The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable. Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts. If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\nFinally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary. To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\nThe precise terms and conditions for copying, distribution and\nmodification follow.\n\nTERMS AND CONDITIONS\n\n0. Definitions.\n\n\"This License\" refers to version 3 of the GNU General Public License.\n\n\"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n\"The Program\" refers to any copyrightable work licensed under this\nLicense. Each licensee is addressed as \"you\". \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\nTo \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy. The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\nA \"covered work\" means either the unmodified Program or a work based\non the Program.\n\nTo \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy. Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\nTo \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies. Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License. If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n1. Source Code.\n\nThe \"source code\" for a work means the preferred form of the work\nfor making modifications to it. \"Object code\" means any non-source\nform of a work.\n\nA \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\nThe \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form. A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\nThe \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities. However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work. For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\nThe Corresponding Source for a work in source code form is that\nsame work.\n\n2. Basic Permissions.\n\nAll rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met. This License explicitly affirms your unlimited\npermission to run the unmodified Program. The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work. This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force. You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright. Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under\nthe conditions stated below. Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\nNo covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\nWhen you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n4. Conveying Verbatim Copies.\n\nYou may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n5. Conveying Modified Source Versions.\n\nYou may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\na) The work must carry prominent notices stating that you modified\nit, and giving a relevant date.\n\nb) The work must carry prominent notices stating that it is\nreleased under this License and any conditions added under section\n7. This requirement modifies the requirement in section 4 to\n\"keep intact all notices\".\n\nc) You must license the entire work, as a whole, under this\nLicense to anyone who comes into possession of a copy. This\nLicense will therefore apply, along with any applicable section 7\nadditional terms, to the whole of the work, and all its parts,\nregardless of how they are packaged. This License gives no\npermission to license the work in any other way, but it does not\ninvalidate such permission if you have separately received it.\n\nd) If the work has interactive user interfaces, each must display\nAppropriate Legal Notices; however, if the Program has interactive\ninterfaces that do not display Appropriate Legal Notices, your\nwork need not make them do so.\n\nA compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit. Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n6. Conveying Non-Source Forms.\n\nYou may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\na) Convey the object code in, or embodied in, a physical product\n(including a physical distribution medium), accompanied by the\nCorresponding Source fixed on a durable physical medium\ncustomarily used for software interchange.\n\nb) Convey the object code in, or embodied in, a physical product\n(including a physical distribution medium), accompanied by a\nwritten offer, valid for at least three years and valid for as\nlong as you offer spare parts or customer support for that product\nmodel, to give anyone who possesses the object code either (1) a\ncopy of the Corresponding Source for all the software in the\nproduct that is covered by this License, on a durable physical\nmedium customarily used for software interchange, for a price no\nmore than your reasonable cost of physically performing this\nconveying of source, or (2) access to copy the\nCorresponding Source from a network server at no charge.\n\nc) Convey individual copies of the object code with a copy of the\nwritten offer to provide the Corresponding Source. This\nalternative is allowed only occasionally and noncommercially, and\nonly if you received the object code with such an offer, in accord\nwith subsection 6b.\n\nd) Convey the object code by offering access from a designated\nplace (gratis or for a charge), and offer equivalent access to the\nCorresponding Source in the same way through the same place at no\nfurther charge. You need not require recipients to copy the\nCorresponding Source along with the object code. If the place to\ncopy the object code is a network server, the Corresponding Source\nmay be on a different server (operated by you or a third party)\nthat supports equivalent copying facilities, provided you maintain\nclear directions next to the object code saying where to find the\nCorresponding Source. Regardless of what server hosts the\nCorresponding Source, you remain obligated to ensure that it is\navailable for as long as needed to satisfy these requirements.\n\ne) Convey the object code using peer-to-peer transmission, provided\nyou inform other peers where the object code and Corresponding\nSource of the work are being offered to the general public at no\ncharge under subsection 6d.\n\nA separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\nA \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling. In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage. For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product. A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n\"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source. The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\nIf you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information. But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\nThe requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed. Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n7. Additional Terms.\n\n\"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law. If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit. (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.) You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\na) Disclaiming warranty or limiting liability differently from the\nterms of sections 15 and 16 of this License; or\n\nb) Requiring preservation of specified reasonable legal notices or\nauthor attributions in that material or in the Appropriate Legal\nNotices displayed by works containing it; or\n\nc) Prohibiting misrepresentation of the origin of that material, or\nrequiring that modified versions of such material be marked in\nreasonable ways as different from the original version; or\n\nd) Limiting the use for publicity purposes of names of licensors or\nauthors of the material; or\n\ne) Declining to grant rights under trademark law for use of some\ntrade names, trademarks, or service marks; or\n\nf) Requiring indemnification of licensors and authors of that\nmaterial by anyone who conveys the material (or modified versions of\nit) with contractual assumptions of liability to the recipient, for\nany liability that these contractual assumptions directly impose on\nthose licensors and authors.\n\nAll other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10. If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term. If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n8. Termination.\n\nYou may not propagate or modify a covered work except as expressly\nprovided under this License. Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\nHowever, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\nMoreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\nTermination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License. If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n9. Acceptance Not Required for Having Copies.\n\nYou are not required to accept this License in order to receive or\nrun a copy of the Program. Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance. However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work. These actions infringe copyright if you do\nnot accept this License. Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n10. Automatic Licensing of Downstream Recipients.\n\nEach time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License. You are not responsible\nfor enforcing compliance by third parties with this License.\n\nAn \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations. If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License. For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n11. Patents.\n\nA \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based. The\nwork thus licensed is called the contributor's \"contributor version\".\n\nA contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version. For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\nIn the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement). To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients. \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\nA patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License. You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n12. No Surrender of Others' Freedom.\n\nIf conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License. If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all. For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n13. Use with the GNU Affero General Public License.\n\nNotwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work. The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n14. Revised Versions of this License.\n\nThe Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time. Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number. If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation. If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\nIf the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\nLater license versions may give you additional or different\npermissions. However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n15. Disclaimer of Warranty.\n\nTHERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n16. Limitation of Liability.\n\nIN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n17. Interpretation of Sections 15 and 16.\n\nIf the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\nEND OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "README.md",
    "content": "# fatfree-core\nFat-Free Framework core library\n\n### Usage:\n\nFirst make sure to add a proper url rewrite configuration to your server, see https://fatfreeframework.com/3.6/routing-engine#DynamicWebSites\n\n**without composer:**\n\n```php\n$f3 = require('lib/base.php');\n```\n\n**with composer:**\n\n```\ncomposer require bcosca/fatfree-core\n```\n\n```php\nrequire(\"vendor/autoload.php\");\n$f3 = \\Base::instance();\n```\n\n---\nFor the main repository (demo package), see https://github.com/bcosca/fatfree  \nFor the test bench and unit tests, see https://github.com/f3-factory/fatfree-dev  \nFor the user guide, see https://fatfreeframework.com/user-guide  \nFor the documentation, see https://fatfreeframework.com/api-reference\n"
  },
  {
    "path": "audit.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Data validator\nclass Audit extends Prefab {\n\n\t//@{ User agents\n\tconst\n\t\tUA_Mobile='android|blackberry|phone|ipod|palm|windows\\s+ce',\n\t\tUA_Desktop='bsd|linux|os\\s+[x9]|solaris|windows',\n\t\tUA_AI='gpt|claude|mistral|oai|google-extended|perplexity|anthropic|cohere|duckassist|amazonbot|bingbot|-ai|ai-',\n\t\tUA_Bot='bot|crawl|slurp|spider|agent|omgili|external';\n\t//@}\n\n\t/**\n\t*\tReturn TRUE if string is a valid URL\n\t*\t@return bool\n\t*\t@param $str string\n\t**/\n\tfunction url($str) {\n\t\treturn is_string(filter_var($str,FILTER_VALIDATE_URL))\n\t\t\t&& !preg_match('/^(javascript|php):\\/\\/.*$/i', $str);\n\t}\n\n\t/**\n\t*\tReturn TRUE if string is a valid e-mail address;\n\t*\tCheck DNS MX records if specified\n\t*\t@return bool\n\t*\t@param $str string\n\t*\t@param $mx boolean\n\t**/\n\tfunction email($str,$mx=TRUE) {\n\t\t$hosts=[];\n\t\treturn is_string(filter_var($str,FILTER_VALIDATE_EMAIL)) &&\n\t\t\t(!$mx || getmxrr(substr($str,strrpos($str,'@')+1),$hosts));\n\t}\n\n\t/**\n\t*\tReturn TRUE if string is a valid IPV4 address\n\t*\t@return bool\n\t*\t@param $addr string\n\t**/\n\tfunction ipv4($addr) {\n\t\treturn (bool)filter_var($addr,FILTER_VALIDATE_IP,FILTER_FLAG_IPV4);\n\t}\n\n\t/**\n\t*\tReturn TRUE if string is a valid IPV6 address\n\t*\t@return bool\n\t*\t@param $addr string\n\t**/\n\tfunction ipv6($addr) {\n\t\treturn (bool)filter_var($addr,FILTER_VALIDATE_IP,FILTER_FLAG_IPV6);\n\t}\n\n\t/**\n\t*\tReturn TRUE if IP address is within private range\n\t*\t@return bool\n\t*\t@param $addr string\n\t**/\n\tfunction isprivate($addr) {\n\t\treturn (bool)filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)\n\t\t\t&& !(bool)filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE);\n\t}\n\n\t/**\n\t*\tReturn TRUE if IP address is within reserved range\n\t*\t@return bool\n\t*\t@param $addr string\n\t**/\n\tfunction isreserved($addr) {\n\t\treturn !(bool)filter_var($addr,FILTER_VALIDATE_IP,\n\t\t\tFILTER_FLAG_IPV4|FILTER_FLAG_IPV6|FILTER_FLAG_NO_RES_RANGE);\n\t}\n\n\t/**\n\t*\tReturn TRUE if IP address is neither private nor reserved\n\t*\t@return bool\n\t*\t@param $addr string\n\t**/\n\tfunction ispublic($addr) {\n\t\treturn (bool)filter_var($addr,FILTER_VALIDATE_IP,\n\t\t\tFILTER_FLAG_IPV4|FILTER_FLAG_IPV6|\n\t\t\tFILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE);\n\t}\n\n\t/**\n\t*\tReturn TRUE if user agent is a desktop browser\n\t*\t@return bool\n\t*\t@param $agent string\n\t**/\n\tfunction isdesktop($agent=NULL) {\n\t\tif (!isset($agent))\n\t\t\t$agent=Base::instance()->AGENT;\n\t\treturn (bool)preg_match('/('.self::UA_Desktop.')/i',$agent) &&\n\t\t\t!$this->ismobile($agent);\n\t}\n\n\t/**\n\t*\tReturn TRUE if user agent is a mobile device\n\t*\t@return bool\n\t*\t@param $agent string\n\t**/\n\tfunction ismobile($agent=NULL) {\n\t\tif (!isset($agent))\n\t\t\t$agent=Base::instance()->AGENT;\n\t\treturn (bool)preg_match('/('.self::UA_Mobile.')/i',$agent);\n\t}\n\n\t/**\n\t*\tReturn TRUE if user agent is a Web bot\n\t*\t@return bool\n\t*\t@param $agent string\n\t**/\n\tfunction isbot($agent=NULL) {\n\t\tif (!isset($agent))\n\t\t\t$agent=Base::instance()->AGENT;\n\t\treturn (bool)preg_match('/('.self::UA_Bot.')/i',$agent);\n\t}\n\n\t/**\n\t*\tReturn TRUE if user agent is an AI\n\t*\t@return bool\n\t*\t@param $agent string\n\t**/\n\tfunction isai($agent=NULL) {\n\t\tif (!isset($agent))\n\t\t\t$agent=Base::instance()->AGENT;\n\t\treturn (bool)preg_match('/('.self::UA_AI.')/i',$agent);\n\t}\n\n\n\t/**\n\t*\tReturn TRUE if user agent is a Web bot or an AI\n\t*\t@return bool\n\t*\t@param $agent string\n\t**/\n\tfunction isbotorai($agent=NULL) {\n\t\treturn $this->isbot($agent) || $this->isai($agent);\n\t}\n\n\t/**\n\t*\tReturn TRUE if specified ID has a valid (Luhn) Mod-10 check digit\n\t*\t@return bool\n\t*\t@param $id string\n\t**/\n\tfunction mod10($id) {\n\t\tif (!ctype_digit($id))\n\t\t\treturn FALSE;\n\t\t$id=strrev($id);\n\t\t$sum=0;\n\t\tfor ($i=0,$l=strlen($id);$i<$l;++$i)\n\t\t\t$sum+=$id[$i]+$i%2*(($id[$i]>4)*-4+$id[$i]%5);\n\t\treturn !($sum%10);\n\t}\n\n\t/**\n\t*\tReturn credit card type if number is valid\n\t*\t@return string|FALSE\n\t*\t@param $id string\n\t**/\n\tfunction card($id) {\n\t\t$id=preg_replace('/[^\\d]/','',$id);\n\t\tif ($this->mod10($id)) {\n\t\t\tif (preg_match('/^3[47][0-9]{13}$/',$id))\n\t\t\t\treturn 'American Express';\n\t\t\tif (preg_match('/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/',$id))\n\t\t\t\treturn 'Diners Club';\n\t\t\tif (preg_match('/^6(?:011|5[0-9][0-9])[0-9]{12}$/',$id))\n\t\t\t\treturn 'Discover';\n\t\t\tif (preg_match('/^(?:2131|1800|35\\d{3})\\d{11}$/',$id))\n\t\t\t\treturn 'JCB';\n\t\t\tif (preg_match('/^5[1-5][0-9]{14}$|'.\n\t\t\t\t'^(222[1-9]|2[3-6]\\d{2}|27[0-1]\\d|2720)\\d{12}$/',$id))\n\t\t\t\treturn 'MasterCard';\n\t\t\tif (preg_match('/^4[0-9]{12}(?:[0-9]{3})?$/',$id))\n\t\t\t\treturn 'Visa';\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tReturn entropy estimate of a password (NIST 800-63)\n\t*\t@return int|float\n\t*\t@param $str string\n\t**/\n\tfunction entropy($str) {\n\t\t$len=strlen($str);\n\t\treturn 4*min($len,1)+($len>1?(2*(min($len,8)-1)):0)+\n\t\t\t($len>8?(1.5*(min($len,20)-8)):0)+($len>20?($len-20):0)+\n\t\t\t6*(bool)(preg_match(\n\t\t\t\t'/[A-Z].*?[0-9[:punct:]]|[0-9[:punct:]].*?[A-Z]/',$str));\n\t}\n\n    /**\n     *\tReturn TRUE if string is a valid MAC address including EUI-64 format\n     *\t@return bool\n     *\t@param $addr string\n     **/\n    function mac($addr) {\n        return (bool)filter_var($addr,FILTER_VALIDATE_MAC)\n            || preg_match('/^([0-9a-f]{2}:){3}ff:fe(:[0-9a-f]{2}){3}$/i', $addr);\n    }\n\n}\n"
  },
  {
    "path": "auth.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Authorization/authentication plug-in\nclass Auth {\n\n\t//@{ Error messages\n\tconst\n\t\tE_LDAP='LDAP connection failure',\n\t\tE_SMTP='SMTP connection failure';\n\t//@}\n\n\tprotected\n\t\t//! Auth storage\n\t\t$storage,\n\t\t//! Mapper object\n\t\t$mapper,\n\t\t//! Storage options\n\t\t$args,\n\t\t//! Custom compare function\n\t\t$func;\n\n\t/**\n\t*\tJig storage handler\n\t*\t@return bool\n\t*\t@param $id string\n\t*\t@param $pw string\n\t*\t@param $realm string\n\t**/\n\tprotected function _jig($id,$pw,$realm) {\n\t\t$success = (bool)\n\t\t\tcall_user_func_array(\n\t\t\t\t[$this->mapper,'load'],\n\t\t\t\t[\n\t\t\t\t\tarray_merge(\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t'@'.$this->args['id'].'==?'.\n\t\t\t\t\t\t\t($this->func?'':' AND @'.$this->args['pw'].'==?').\n\t\t\t\t\t\t\t(isset($this->args['realm'])?\n\t\t\t\t\t\t\t\t(' AND @'.$this->args['realm'].'==?'):''),\n\t\t\t\t\t\t\t$id\n\t\t\t\t\t\t],\n\t\t\t\t\t\t($this->func?[]:[$pw]),\n\t\t\t\t\t\t(isset($this->args['realm'])?[$realm]:[])\n\t\t\t\t\t)\n\t\t\t\t]\n\t\t\t);\n\t\tif ($success && $this->func)\n\t\t\t$success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw']));\n\t\treturn $success;\n\t}\n\n\t/**\n\t*\tMongoDB storage handler\n\t*\t@return bool\n\t*\t@param $id string\n\t*\t@param $pw string\n\t*\t@param $realm string\n\t**/\n\tprotected function _mongo($id,$pw,$realm) {\n\t\t$success = (bool)\n\t\t\t$this->mapper->load(\n\t\t\t\t[$this->args['id']=>$id]+\n\t\t\t\t($this->func?[]:[$this->args['pw']=>$pw])+\n\t\t\t\t(isset($this->args['realm'])?\n\t\t\t\t\t[$this->args['realm']=>$realm]:[])\n\t\t\t);\n\t\tif ($success && $this->func)\n\t\t\t$success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw']));\n\t\treturn $success;\n\t}\n\n\t/**\n\t*\tSQL storage handler\n\t*\t@return bool\n\t*\t@param $id string\n\t*\t@param $pw string\n\t*\t@param $realm string\n\t**/\n\tprotected function _sql($id,$pw,$realm) {\n\t\t$success = (bool)\n\t\t\tcall_user_func_array(\n\t\t\t\t[$this->mapper,'load'],\n\t\t\t\t[\n\t\t\t\t\tarray_merge(\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t$this->args['id'].'=?'.\n\t\t\t\t\t\t\t($this->func?'':' AND '.$this->args['pw'].'=?').\n\t\t\t\t\t\t\t(isset($this->args['realm'])?\n\t\t\t\t\t\t\t\t(' AND '.$this->args['realm'].'=?'):''),\n\t\t\t\t\t\t\t$id\n\t\t\t\t\t\t],\n\t\t\t\t\t\t($this->func?[]:[$pw]),\n\t\t\t\t\t\t(isset($this->args['realm'])?[$realm]:[])\n\t\t\t\t\t)\n\t\t\t\t]\n\t\t\t);\n\t\tif ($success && $this->func)\n\t\t\t$success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw']));\n\t\treturn $success;\n\t}\n\n\t/**\n\t*\tLDAP storage handler\n\t*\t@return bool\n\t*\t@param $id string\n\t*\t@param $pw string\n\t**/\n\tprotected function _ldap($id,$pw) {\n\t\t$port=(int)($this->args['port']?:389);\n\t\t$filter=$this->args['filter']=$this->args['filter']?:\"uid=\".$id;\n\t\t$this->args['attr']=$this->args['attr']?:[\"uid\"];\n\t\tarray_walk($this->args['attr'],\n\t\tfunction($attr)use(&$filter,$id) {\n\t\t\t$filter=str_ireplace($attr.\"=*\",$attr.\"=\".$id,$filter);});\n\t\t$dc=@ldap_connect($this->args['dc'],$port);\n\t\tif ($dc &&\n\t\t\tldap_set_option($dc,LDAP_OPT_PROTOCOL_VERSION,3) &&\n\t\t\tldap_set_option($dc,LDAP_OPT_REFERRALS,0) &&\n\t\t\tldap_bind($dc,$this->args['rdn'],$this->args['pw']) &&\n\t\t\t($result=ldap_search($dc,$this->args['base_dn'],\n\t\t\t\t$filter,$this->args['attr'])) &&\n\t\t\tldap_count_entries($dc,$result) &&\n\t\t\t($info=ldap_get_entries($dc,$result)) &&\n\t\t\t$info['count']==1 &&\n\t\t\t@ldap_bind($dc,$info[0]['dn'],$pw) &&\n\t\t\t@ldap_close($dc)) {\n\t\t\treturn in_array($id,(array_map(function($value){return $value[0];},\n\t\t\t\tarray_intersect_key($info[0],\n\t\t\t\t\tarray_flip($this->args['attr'])))),TRUE);\n\t\t}\n        throw new \\Exception(self::E_LDAP);\n\t}\n\n\t/**\n\t*\tSMTP storage handler\n\t*\t@return bool\n\t*\t@param $id string\n\t*\t@param $pw string\n\t**/\n\tprotected function _smtp($id,$pw) {\n\t\t$socket=@fsockopen(\n\t\t\t(strtolower($this->args['scheme'])=='ssl'?\n\t\t\t\t'ssl://':'').$this->args['host'],\n\t\t\t\t$this->args['port']);\n\t\t$dialog=function($cmd=NULL) use($socket) {\n\t\t\tif (!is_null($cmd))\n\t\t\t\tfputs($socket,$cmd.\"\\r\\n\");\n\t\t\t$reply='';\n\t\t\twhile (!feof($socket) &&\n\t\t\t\t($info=stream_get_meta_data($socket)) &&\n\t\t\t\t!$info['timed_out'] && $str=fgets($socket,4096)) {\n\t\t\t\t$reply.=$str;\n\t\t\t\tif (preg_match('/(?:^|\\n)\\d{3} .+\\r\\n/s',\n\t\t\t\t\t$reply))\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\treturn $reply;\n\t\t};\n\t\tif ($socket) {\n\t\t\tstream_set_blocking($socket,TRUE);\n\t\t\t$dialog();\n\t\t\t$fw=Base::instance();\n\t\t\t$dialog('EHLO '.$fw->HOST);\n\t\t\tif (strtolower($this->args['scheme'])=='tls') {\n\t\t\t\t$dialog('STARTTLS');\n\t\t\t\tstream_socket_enable_crypto(\n\t\t\t\t\t$socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT);\n\t\t\t\t$dialog('EHLO '.$fw->HOST);\n\t\t\t}\n\t\t\t// Authenticate\n\t\t\t$dialog('AUTH LOGIN');\n\t\t\t$dialog(base64_encode($id));\n\t\t\t$reply=$dialog(base64_encode($pw));\n\t\t\t$dialog('QUIT');\n\t\t\tfclose($socket);\n\t\t\treturn (bool)preg_match('/^235 /',$reply);\n\t\t}\n        throw new \\Exception(self::E_SMTP);\n\t}\n\n\t/**\n\t*\tLogin auth mechanism\n\t*\t@return bool\n\t*\t@param $id string\n\t*\t@param $pw string\n\t*\t@param $realm string\n\t**/\n\tfunction login($id,$pw,$realm=NULL) {\n\t\treturn $this->{'_'.$this->storage}($id,$pw,$realm);\n\t}\n\n\t/**\n\t*\tHTTP basic auth mechanism\n\t*\t@return bool\n\t*\t@param $func callback\n\t**/\n\tfunction basic($func=NULL) {\n\t\t$fw=Base::instance();\n\t\t$realm=$fw->REALM;\n\t\t$hdr=NULL;\n\t\tif (isset($_SERVER['HTTP_AUTHORIZATION']))\n\t\t\t$hdr=$_SERVER['HTTP_AUTHORIZATION'];\n\t\telseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']))\n\t\t\t$hdr=$_SERVER['REDIRECT_HTTP_AUTHORIZATION'];\n\t\tif (!empty($hdr))\n\t\t\tlist($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW'])=\n\t\t\t\texplode(':',base64_decode(substr($hdr,6)));\n\t\tif (isset($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']) &&\n\t\t\t$this->login(\n\t\t\t\t$_SERVER['PHP_AUTH_USER'],\n\t\t\t\t$func?\n\t\t\t\t\t$fw->call($func,$_SERVER['PHP_AUTH_PW']):\n\t\t\t\t\t$_SERVER['PHP_AUTH_PW'],\n\t\t\t\t$realm\n\t\t\t))\n\t\t\treturn TRUE;\n\t\tif (PHP_SAPI!='cli')\n\t\t\theader('WWW-Authenticate: Basic realm=\"'.$realm.'\"');\n\t\t$fw->status(401);\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@return object\n\t*\t@param $storage string|object\n\t*\t@param $args array\n\t*\t@param $func callback\n\t**/\n\tfunction __construct($storage,?array $args=NULL,$func=NULL) {\n\t\tif (is_object($storage) && is_a($storage,'DB\\Cursor')) {\n\t\t\t$this->storage=$storage->dbtype();\n\t\t\t$this->mapper=$storage;\n\t\t\tunset($ref);\n\t\t}\n\t\telse\n\t\t\t$this->storage=$storage;\n\t\t$this->args=$args;\n\t\t$this->func=$func;\n\t}\n\n}\n"
  },
  {
    "path": "base.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2023 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Factory class for single-instance objects\nabstract class Prefab {\n\n\t/**\n\t*\tReturn class instance\n\t*\t@return static\n\t**/\n\tstatic function instance() {\n\t\tif (!Registry::exists($class=get_called_class())) {\n\t\t\t$ref=new ReflectionClass($class);\n\t\t\t$args=func_get_args();\n\t\t\tRegistry::set($class,\n\t\t\t\t$args?$ref->newinstanceargs($args):new $class);\n\t\t}\n\t\treturn Registry::get($class);\n\t}\n\n}\n/**\n * @property string ALIAS\n * @property array ALIASES\n * @property string|array AUTOLOAD\n * @property bool|string CACHE\n * @property bool CASELESS\n * @property callable|\\Prefab|\\Psr\\Container\\ContainerInterface CONTAINER\n * @property array COOKIE\n * @property array GET\n * @property array POST\n * @property array REQUEST\n * @property array SESSION\n * @property array FILES\n * @property array SERVER\n * @property array ENV\n * @property array CORS\n * @property int DEBUG\n * @property array DIACRITICS\n * @property string DNSBL\n * @property array EMOJI\n * @property string ENCODING\n * @property bool ESCAPE\n * @property string EXEMPT\n * @property object EXCEPTION\n * @property string FALLBACK\n * @property array FORMATS\n * @property string FRAGMENT\n * @property bool HALT\n * @property bool HIGHLIGHT\n * @property array JAR\n * @property string LANGUAGE\n * @property string LOCALES\n * @property string|array LOGGABLE\n * @property string LOGS\n * @property mixed ONERROR\n * @property mixed ONREROUTE\n * @property string|null PACKAGE\n * @property array PARAMS\n * @property string PLUGINS\n * @property string PREFIX\n * @property string PREMAP\n * @property bool QUIET\n * @property bool RAW\n * @property array ROUTES\n * @property string SEED\n * @property string SERIALIZER\n * @property string TEMP\n * @property float TIME\n * @property bool REROUTE_TRAILING_SLASH\n * @property string TZ\n * @property string UI\n * @property callback UNLOAD\n * @property string UPLOADS\n * @property string URI\n * @property string VERB\n * @property string VERSION\n * @property string|null XFRAME\n * @property-read string AGENT\n * @property-read bool AJAX\n * @property-read string BASE\n * @property-read string BODY\n * @property-read bool CLI\n * @property-read array ERROR\n * @property-read array HEADERS\n * @property-read string HOST\n * @property-read string IP\n * @property-read string PATH\n * @property-read string PATTERN\n * @property-read int PORT\n * @property-read string QUERY\n * @property-read string REALM\n * @property-read string RESPONSE\n * @property-read string ROOT\n * @property-read string SCHEME\n */\n//! Base structure\nfinal class Base extends Prefab implements ArrayAccess {\n\n\t//@{ Framework details\n\tconst\n\t\tPACKAGE='Fat-Free Framework',\n\t\tVERSION='3.9.2-Release';\n\t//@}\n\n\t//@{ HTTP status codes (RFC 2616)\n\tconst\n\t\tHTTP_100='Continue',\n\t\tHTTP_101='Switching Protocols',\n\t\tHTTP_103='Early Hints',\n\t\tHTTP_200='OK',\n\t\tHTTP_201='Created',\n\t\tHTTP_202='Accepted',\n\t\tHTTP_203='Non-Authorative Information',\n\t\tHTTP_204='No Content',\n\t\tHTTP_205='Reset Content',\n\t\tHTTP_206='Partial Content',\n\t\tHTTP_300='Multiple Choices',\n\t\tHTTP_301='Moved Permanently',\n\t\tHTTP_302='Found',\n\t\tHTTP_303='See Other',\n\t\tHTTP_304='Not Modified',\n\t\tHTTP_305='Use Proxy',\n\t\tHTTP_307='Temporary Redirect',\n\t\tHTTP_308='Permanent Redirect',\n\t\tHTTP_400='Bad Request',\n\t\tHTTP_401='Unauthorized',\n\t\tHTTP_402='Payment Required',\n\t\tHTTP_403='Forbidden',\n\t\tHTTP_404='Not Found',\n\t\tHTTP_405='Method Not Allowed',\n\t\tHTTP_406='Not Acceptable',\n\t\tHTTP_407='Proxy Authentication Required',\n\t\tHTTP_408='Request Timeout',\n\t\tHTTP_409='Conflict',\n\t\tHTTP_410='Gone',\n\t\tHTTP_411='Length Required',\n\t\tHTTP_412='Precondition Failed',\n\t\tHTTP_413='Request Entity Too Large',\n\t\tHTTP_414='Request-URI Too Long',\n\t\tHTTP_415='Unsupported Media Type',\n\t\tHTTP_416='Requested Range Not Satisfiable',\n\t\tHTTP_417='Expectation Failed',\n\t\tHTTP_421='Misdirected Request',\n\t\tHTTP_422='Unprocessable Entity',\n\t\tHTTP_423='Locked',\n\t\tHTTP_429='Too Many Requests',\n\t\tHTTP_451='Unavailable For Legal Reasons',\n\t\tHTTP_500='Internal Server Error',\n\t\tHTTP_501='Not Implemented',\n\t\tHTTP_502='Bad Gateway',\n\t\tHTTP_503='Service Unavailable',\n\t\tHTTP_504='Gateway Timeout',\n\t\tHTTP_505='HTTP Version Not Supported',\n\t\tHTTP_507='Insufficient Storage',\n\t\tHTTP_511='Network Authentication Required';\n\t//@}\n\n\tconst\n\t\t//! Mapped PHP globals\n\t\tGLOBALS='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV',\n\t\t//! HTTP verbs\n\t\tVERBS='GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS',\n\t\t//! Default directory permissions\n\t\tMODE=0755,\n\t\t//! Syntax highlighting stylesheet\n\t\tCSS='code.css';\n\n\t//@{ Request types\n\tconst\n\t\tREQ_SYNC=1,\n\t\tREQ_AJAX=2,\n\t\tREQ_CLI=4;\n\t//@}\n\n\t//@{ Error messages\n\tconst\n\t\tE_Pattern='Invalid routing pattern: %s',\n\t\tE_Named='Named route does not exist: %s',\n\t\tE_Alias='Invalid named route alias: %s',\n\t\tE_Fatal='Fatal error: %s',\n\t\tE_Open='Unable to open %s',\n\t\tE_Routes='No routes specified',\n\t\tE_Class='Invalid class %s',\n\t\tE_Method='Invalid method %s',\n\t\tE_Hive='Invalid hive key %s';\n\t//@}\n\n\tprivate\n\t\t//! Globals\n\t\t$hive,\n\t\t//! Initial settings\n\t\t$init,\n\t\t//! Language lookup sequence\n\t\t$languages,\n\t\t//! Mutex locks\n\t\t$locks=[],\n\t\t//! Default fallback language\n\t\t$fallback='en';\n\n\t/**\n\t*\tSync PHP global with corresponding hive key\n\t*\t@return array\n\t*\t@param $key string\n\t**/\n\tfunction sync($key) {\n\t\treturn $this->hive[$key]=&$GLOBALS['_'.$key];\n\t}\n\n\t/**\n\t*\tReturn the parts of specified hive key\n\t*\t@return array\n\t*\t@param $key string\n\t**/\n\tprivate function cut($key) {\n\t\treturn preg_split('/\\[\\h*[\\'\"]?(.+?)[\\'\"]?\\h*\\]|(->)|\\./',\n\t\t\t$key,-1,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);\n\t}\n\n\t/**\n\t*\tReplace tokenized URL with available token values\n\t*\t@return string\n\t*\t@param $url array|string\n\t*\t@param $addParams boolean merge default PARAMS from hive into args\n\t*\t@param $args array\n\t**/\n\tfunction build($url, $args=[], $addParams=TRUE) {\n\t\tif ($addParams)\n\t\t\t$args+=$this->recursive($this->hive['PARAMS'], function($val) {\n\t\t\t\treturn implode('/', array_map('urlencode', explode('/', $val ?? '')));\n\t\t\t});\n\t\tif (is_array($url))\n\t\t\tforeach ($url as &$var) {\n\t\t\t\t$var=$this->build($var,$args, false);\n\t\t\t\tunset($var);\n\t\t\t}\n\t\telse {\n\t\t\t$i=0;\n\t\t\t$url=preg_replace_callback('/(\\{)?@(\\w+)(?(1)\\})|(\\*)/',\n\t\t\t\tfunction($match) use(&$i,$args) {\n\t\t\t\t\tif (isset($match[2]) &&\n\t\t\t\t\t\tarray_key_exists($match[2],$args))\n\t\t\t\t\t\treturn $args[$match[2]];\n\t\t\t\t\tif (isset($match[3]) &&\n\t\t\t\t\t\tarray_key_exists($match[3],$args)) {\n\t\t\t\t\t\tif (!is_array($args[$match[3]]))\n\t\t\t\t\t\t\treturn $args[$match[3]];\n\t\t\t\t\t\t++$i;\n\t\t\t\t\t\treturn $args[$match[3]][$i-1];\n\t\t\t\t\t}\n\t\t\t\t\treturn $match[0];\n\t\t\t\t},$url);\n\t\t}\n\t\treturn $url;\n\t}\n\n\t/**\n\t*\tParse string containing key-value pairs\n\t*\t@return array\n\t*\t@param $str string\n\t**/\n\tfunction parse($str) {\n\t\tpreg_match_all('/(\\w+|\\*)\\h*=\\h*(?:\\[(.+?)\\]|(.+?))(?=,|$)/',\n\t\t\t$str,$pairs,PREG_SET_ORDER);\n\t\t$out=[];\n\t\tforeach ($pairs as $pair)\n\t\t\tif ($pair[2]) {\n\t\t\t\t$out[$pair[1]]=[];\n\t\t\t\tforeach (explode(',',$pair[2]) as $val)\n\t\t\t\t\tarray_push($out[$pair[1]],$val);\n\t\t\t}\n\t\t\telse\n\t\t\t\t$out[$pair[1]]=trim($pair[3]);\n\t\treturn $out;\n\t}\n\n\t/**\n\t * Cast string variable to PHP type or constant\n\t * @param $val\n\t * @return mixed\n\t */\n\tfunction cast($val) {\n\t\tif ($val && preg_match('/^(?:0x[0-9a-f]+|0[0-7]+|0b[01]+)$/i',$val))\n\t\t\treturn intval($val,0);\n\t\tif (is_numeric($val))\n\t\t\treturn $val+0;\n\t\t$val=trim($val?:'');\n\t\tif (preg_match('/^\\w+$/i',$val) && defined($val))\n\t\t\treturn constant($val);\n\t\treturn $val;\n\t}\n\n\t/**\n\t*\tConvert JS-style token to PHP expression\n\t*\t@return string\n\t*\t@param $str string\n\t*\t@param $evaluate bool compile expressions as well or only convert variable access\n\t**/\n\tfunction compile($str, $evaluate=TRUE) {\n\t\treturn (!$evaluate)\n\t\t\t? preg_replace_callback(\n\t\t\t\t'/^@(\\w+)((?:\\..+|\\[(?:(?:[^\\[\\]]*|(?R))*)\\])*)/',\n\t\t\t\tfunction($expr) {\n\t\t\t\t\t$str='$'.$expr[1];\n\t\t\t\t\tif (isset($expr[2]))\n\t\t\t\t\t\t$str.=preg_replace_callback(\n\t\t\t\t\t\t\t'/\\.([^.\\[\\]]+)|\\[((?:[^\\[\\]\\'\"]*|(?R))*)\\]/',\n\t\t\t\t\t\t\tfunction($sub) {\n\t\t\t\t\t\t\t\t$val=isset($sub[2]) ? $sub[2] : $sub[1];\n\t\t\t\t\t\t\t\tif (ctype_digit($val))\n\t\t\t\t\t\t\t\t\t$val=(int)$val;\n\t\t\t\t\t\t\t\t$out='['.$this->export($val).']';\n\t\t\t\t\t\t\t\treturn $out;\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t$expr[2]\n\t\t\t\t\t\t);\n\t\t\t\t\treturn $str;\n\t\t\t\t},\n\t\t\t\t$str\n\t\t\t)\n\t\t\t: preg_replace_callback(\n\t\t\t'/(?<!\\w)@(\\w+(?:(?:\\->|::)\\w+)?)'.\n\t\t\t'((?:\\.\\w+|\\[(?:(?:[^\\[\\]]*|(?R))*)\\]|(?:\\->|::)\\w+|\\()*)/',\n\t\t\tfunction($expr) {\n\t\t\t\t$str='$'.$expr[1];\n\t\t\t\tif (isset($expr[2]))\n\t\t\t\t\t$str.=preg_replace_callback(\n\t\t\t\t\t\t'/\\.(\\w+)(\\()?|\\[((?:[^\\[\\]]*|(?R))*)\\]/',\n\t\t\t\t\t\tfunction($sub) {\n\t\t\t\t\t\t\tif (empty($sub[2])) {\n\t\t\t\t\t\t\t\tif (ctype_digit($sub[1]))\n\t\t\t\t\t\t\t\t\t$sub[1]=(int)$sub[1];\n\t\t\t\t\t\t\t\t$out='['.\n\t\t\t\t\t\t\t\t\t(isset($sub[3])?\n\t\t\t\t\t\t\t\t\t\t$this->compile($sub[3]):\n\t\t\t\t\t\t\t\t\t\t$this->export($sub[1])).\n\t\t\t\t\t\t\t\t']';\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t$out=function_exists($sub[1])?\n\t\t\t\t\t\t\t\t\t$sub[0]:\n\t\t\t\t\t\t\t\t\t('['.$this->export($sub[1]).']'.$sub[2]);\n\t\t\t\t\t\t\treturn $out;\n\t\t\t\t\t\t},\n\t\t\t\t\t\t$expr[2]\n\t\t\t\t\t);\n\t\t\t\treturn $str;\n\t\t\t},\n\t\t\t$str\n\t\t);\n\t}\n\n\t/**\n\t*\tGet hive key reference/contents; Add non-existent hive keys,\n\t*\tarray elements, and object properties by default\n\t*\t@return mixed\n\t*\t@param $key string\n\t*\t@param $add bool\n\t*\t@param $var mixed\n\t**/\n\tfunction &ref($key,$add=TRUE,&$var=NULL) {\n\t\t$null=NULL;\n\t\t$parts=$this->cut($key);\n\t\tif ($parts[0]=='SESSION') {\n\t\t\tif (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)\n\t\t\t\tsession_start();\n\t\t\t$this->sync('SESSION');\n\t\t}\n\t\telseif (!preg_match('/^\\w+$/',$parts[0]))\n            throw new \\Exception(\\sprintf(self::E_Hive,$this->stringify($key)));\n\t\tif (is_null($var)) {\n\t\t\tif ($add)\n\t\t\t\t$var=&$this->hive;\n\t\t\telse\n\t\t\t\t$var=$this->hive;\n\t\t}\n\t\t$obj=FALSE;\n\t\tforeach ($parts as $part)\n\t\t\tif ($part=='->')\n\t\t\t\t$obj=TRUE;\n\t\t\telseif ($obj) {\n\t\t\t\t$obj=FALSE;\n\t\t\t\tif (!is_object($var))\n\t\t\t\t\t$var=new stdClass;\n\t\t\t\tif ($add || property_exists($var,$part))\n\t\t\t\t\t$var=&$var->$part;\n\t\t\t\telse {\n\t\t\t\t\t$var=&$null;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\tif (!is_array($var))\n\t\t\t\t\t$var=[];\n\t\t\t\tif ($add || array_key_exists($part,$var))\n\t\t\t\t\t$var=&$var[$part];\n\t\t\t\telse {\n\t\t\t\t\t$var=&$null;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\treturn $var;\n\t}\n\n\t/**\n\t*\tReturn TRUE if hive key is set\n\t*\t(or return timestamp and TTL if cached)\n\t*\t@return bool\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\tfunction exists($key,&$val=NULL) {\n\t\t$val=$this->ref($key,FALSE);\n\t\treturn isset($val)?\n\t\t\tTRUE:\n\t\t\t(Cache::instance()->exists($this->hash($key).'.var',$val)?:FALSE);\n\t}\n\n\t/**\n\t*\tReturn TRUE if hive key is empty and not cached\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t*\t@return bool\n\t**/\n\tfunction devoid($key,&$val=NULL) {\n\t\t$val=$this->ref($key,FALSE);\n\t\treturn empty($val) &&\n\t\t\t(!Cache::instance()->exists($this->hash($key).'.var',$val) ||\n\t\t\t\t!$val);\n\t}\n\n\t/**\n\t*\tBind value to hive key\n\t*\t@return mixed\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t*\t@param $ttl int\n\t**/\n\tfunction set($key,$val,$ttl=0) {\n\t\t$time=(int)$this->hive['TIME'];\n\t\tif (preg_match('/^(GET|POST|COOKIE)\\b(.+)/',$key,$expr)) {\n\t\t\t$this->set('REQUEST'.$expr[2],$val);\n\t\t\tif ($expr[1]=='COOKIE') {\n\t\t\t\t$parts=$this->cut($key);\n\t\t\t\t$jar=$this->unserialize($this->serialize($this->hive['JAR']));\n\t\t\t\tunset($jar['lifetime']);\n\t\t\t\tif (version_compare(PHP_VERSION, '7.3.0') >= 0) {\n\t\t\t\t\tunset($jar['expire']);\n\t\t\t\t\tif (isset($_COOKIE[$parts[1]]))\n\t\t\t\t\t\tsetcookie($parts[1],'',['expires'=>0]+$jar);\n\t\t\t\t\tif ($ttl)\n\t\t\t\t\t\t$jar['expires']=$time+$ttl;\n\t\t\t\t\tsetcookie($parts[1],$val?:'',$jar);\n\t\t\t\t} else {\n\t\t\t\t\tunset($jar['samesite']);\n\t\t\t\t\tif (isset($_COOKIE[$parts[1]]))\n\t\t\t\t\t\tcall_user_func_array('setcookie',\n\t\t\t\t\t\t\tarray_merge([$parts[1],''],['expire'=>0]+$jar));\n\t\t\t\t\tif ($ttl)\n\t\t\t\t\t\t$jar['expire']=$time+$ttl;\n\t\t\t\t\tcall_user_func_array('setcookie',[$parts[1],$val?:'']+$jar);\n\t\t\t\t}\n\t\t\t\t$_COOKIE[$parts[1]]=$val;\n\t\t\t\treturn $val;\n\t\t\t}\n\t\t}\n\t\telse switch ($key) {\n\t\tcase 'CACHE':\n\t\t\t$val=Cache::instance()->load($val);\n\t\t\tbreak;\n\t\tcase 'ENCODING':\n\t\t\tini_set('default_charset',$val);\n\t\t\tif (extension_loaded('mbstring'))\n\t\t\t\tmb_internal_encoding($val);\n\t\t\tbreak;\n\t\tcase 'FALLBACK':\n\t\t\t$this->fallback=$val;\n\t\t\t$lang=$this->language($this->hive['LANGUAGE']);\n\t\tcase 'LANGUAGE':\n\t\t\tif (!isset($lang))\n\t\t\t\t$val=$this->language($val);\n\t\t\t$lex=$this->lexicon($this->hive['LOCALES'],$ttl);\n\t\tcase 'LOCALES':\n\t\t\tif (isset($lex) || $lex=$this->lexicon($val,$ttl))\n\t\t\t\tforeach ($lex as $dt=>$dd) {\n\t\t\t\t\t$ref=&$this->ref($this->hive['PREFIX'].$dt);\n\t\t\t\t\t$ref=$dd;\n\t\t\t\t\tunset($ref);\n\t\t\t\t}\n\t\t\tbreak;\n\t\tcase 'TZ':\n\t\t\tdate_default_timezone_set($val);\n\t\t\tbreak;\n\t\t}\n\t\t$ref=&$this->ref($key);\n\t\t$ref=$val;\n\t\tif (preg_match('/^JAR\\b/',$key)) {\n\t\t\tif ($key=='JAR.lifetime')\n\t\t\t\t$this->set('JAR.expire',$val==0?0:\n\t\t\t\t\t(is_int($val)?$time+$val:strtotime($val)));\n\t\t\telse {\n\t\t\t\tif ($key=='JAR.expire')\n\t\t\t\t\t$this->hive['JAR']['lifetime']=max(0,$val-$time);\n\t\t\t\t$jar=$this->unserialize($this->serialize($this->hive['JAR']));\n\t\t\t\tunset($jar['expire']);\n\t\t\t\tif (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)\n\t\t\t\t\tif (version_compare(PHP_VERSION, '7.3.0') >= 0)\n\t\t\t\t\t\tsession_set_cookie_params($jar);\n\t\t\t\t\telse {\n\t\t\t\t\t\tunset($jar['samesite']);\n\t\t\t\t\t\tcall_user_func_array('session_set_cookie_params',$jar);\n\t\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif ($ttl)\n\t\t\t// Persist the key-value pair\n\t\t\tCache::instance()->set($this->hash($key).'.var',$val,$ttl);\n\t\treturn $ref;\n\t}\n\n\t/**\n\t*\tRetrieve contents of hive key\n\t*\t@return mixed\n\t*\t@param $key string\n\t*\t@param $args string|array\n\t**/\n\tfunction get($key,$args=NULL) {\n\t\tif (is_string($val=$this->ref($key,FALSE)) && !is_null($args))\n\t\t\treturn call_user_func_array(\n\t\t\t\t[$this,'format'],\n\t\t\t\tarray_merge([$val],is_array($args)?$args:[$args])\n\t\t\t);\n\t\tif (is_null($val)) {\n\t\t\t// Attempt to retrieve from cache\n\t\t\tif (Cache::instance()->exists($this->hash($key).'.var',$data))\n\t\t\t\treturn $data;\n\t\t}\n\t\treturn $val;\n\t}\n\n\t/**\n\t*\tUnset hive key\n\t*\t@param $key string\n\t**/\n\tfunction clear($key) {\n\t\t// Normalize array literal\n\t\t$cache=Cache::instance();\n\t\t$parts=$this->cut($key);\n\t\tif ($key=='CACHE')\n\t\t\t// Clear cache contents\n\t\t\t$cache->reset();\n\t\telseif (preg_match('/^(GET|POST|COOKIE)\\b(.+)/',$key,$expr)) {\n\t\t\t$this->clear('REQUEST'.$expr[2]);\n\t\t\tif ($expr[1]=='COOKIE') {\n\t\t\t\t$parts=$this->cut($key);\n\t\t\t\t$jar=$this->hive['JAR'];\n\t\t\t\tunset($jar['lifetime']);\n\t\t\t\t$jar['expire']=0;\n\t\t\t\tif (version_compare(PHP_VERSION, '7.3.0') >= 0) {\n\t\t\t\t\t$jar['expires']=$jar['expire'];\n\t\t\t\t\tunset($jar['expire']);\n\t\t\t\t\tsetcookie($parts[1],'',$jar);\n\t\t\t\t} else {\n\t\t\t\t\tunset($jar['samesite']);\n\t\t\t\t\tcall_user_func_array('setcookie',\n\t\t\t\t\t\tarray_merge([$parts[1],''],$jar));\n\t\t\t\t}\n\t\t\t\tunset($_COOKIE[$parts[1]]);\n\t\t\t}\n\t\t}\n\t\telseif ($parts[0]=='SESSION') {\n\t\t\tif (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)\n\t\t\t\tsession_start();\n\t\t\tif (empty($parts[1])) {\n\t\t\t\t// End session\n\t\t\t\tsession_unset();\n\t\t\t\tsession_destroy();\n\t\t\t\t$this->clear('COOKIE.'.session_name());\n\t\t\t}\n\t\t\t$this->sync('SESSION');\n\t\t}\n\t\tif (!isset($parts[1]) && array_key_exists($parts[0],$this->init))\n\t\t\t// Reset global to default value\n\t\t\t$this->hive[$parts[0]]=$this->init[$parts[0]];\n\t\telse {\n\t\t\t$val=preg_replace('/^(\\$hive)/','$this->hive',\n\t\t\t\t$this->compile('@hive.'.$key, FALSE));\n\t\t\teval('unset('.$val.');');\n\t\t\tif ($parts[0]=='SESSION') {\n\t\t\t\tsession_commit();\n\t\t\t\tsession_start();\n\t\t\t}\n\t\t\tif ($cache->exists($hash=$this->hash($key).'.var'))\n\t\t\t\t// Remove from cache\n\t\t\t\t$cache->clear($hash);\n\t\t}\n\t}\n\n\t/**\n\t*\tReturn TRUE if hive variable is 'on'\n\t*\t@return bool\n\t*\t@param $key string\n\t**/\n\tfunction checked($key) {\n\t\t$ref=&$this->ref($key);\n\t\treturn $ref=='on';\n\t}\n\n\t/**\n\t*\tReturn TRUE if property has public visibility\n\t*\t@return bool\n\t*\t@param $obj object\n\t*\t@param $key string\n\t**/\n\tfunction visible($obj,$key) {\n\t\tif (property_exists($obj,$key)) {\n\t\t\t$ref=new ReflectionProperty(get_class($obj),$key);\n\t\t\t$out=$ref->ispublic();\n\t\t\tunset($ref);\n\t\t\treturn $out;\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tMulti-variable assignment using associative array\n\t*\t@param $vars array\n\t*\t@param $prefix string\n\t*\t@param $ttl int\n\t**/\n\tfunction mset(array $vars,$prefix='',$ttl=0) {\n\t\tforeach ($vars as $key=>$val)\n\t\t\t$this->set($prefix.$key,$val,$ttl);\n\t}\n\n\t/**\n\t*\tPublish hive contents\n\t*\t@return array\n\t**/\n\tfunction hive() {\n\t\treturn $this->hive;\n\t}\n\n\t/**\n\t*\tCopy contents of hive variable to another\n\t*\t@return mixed\n\t*\t@param $src string\n\t*\t@param $dst string\n\t**/\n\tfunction copy($src,$dst) {\n\t\t$ref=&$this->ref($dst);\n\t\treturn $ref=$this->ref($src,FALSE);\n\t}\n\n\t/**\n\t*\tConcatenate string to hive string variable\n\t*\t@return string\n\t*\t@param $key string\n\t*\t@param $val string\n\t**/\n\tfunction concat($key,$val) {\n\t\t$ref=&$this->ref($key);\n\t\t$ref.=$val;\n\t\treturn $ref;\n\t}\n\n\t/**\n\t*\tSwap keys and values of hive array variable\n\t*\t@return array\n\t*\t@param $key string\n\t*\t@public\n\t**/\n\tfunction flip($key) {\n\t\t$ref=&$this->ref($key);\n\t\treturn $ref=array_combine(array_values($ref),array_keys($ref));\n\t}\n\n\t/**\n\t*\tAdd element to the end of hive array variable\n\t*\t@return mixed\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\tfunction push($key,$val) {\n\t\t$ref=&$this->ref($key);\n\t\t$ref[]=$val;\n\t\treturn $val;\n\t}\n\n\t/**\n\t*\tRemove last element of hive array variable\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\tfunction pop($key) {\n\t\t$ref=&$this->ref($key);\n\t\treturn array_pop($ref);\n\t}\n\n\t/**\n\t*\tAdd element to the beginning of hive array variable\n\t*\t@return mixed\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\tfunction unshift($key,$val) {\n\t\t$ref=&$this->ref($key);\n\t\tarray_unshift($ref,$val);\n\t\treturn $val;\n\t}\n\n\t/**\n\t*\tRemove first element of hive array variable\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\tfunction shift($key) {\n\t\t$ref=&$this->ref($key);\n\t\treturn array_shift($ref);\n\t}\n\n\t/**\n\t*\tMerge array with hive array variable\n\t*\t@return array\n\t*\t@param $key string\n\t*\t@param $src string|array\n\t*\t@param $keep bool\n\t**/\n\tfunction merge($key,$src,$keep=FALSE) {\n\t\t$ref=&$this->ref($key);\n\t\tif (!$ref)\n\t\t\t$ref=[];\n\t\t$out=array_merge($ref,is_string($src)?$this->hive[$src]:$src);\n\t\tif ($keep)\n\t\t\t$ref=$out;\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tExtend hive array variable with default values from $src\n\t*\t@return array\n\t*\t@param $key string\n\t*\t@param $src string|array\n\t*\t@param $keep bool\n\t**/\n\tfunction extend($key,$src,$keep=FALSE) {\n\t\t$ref=&$this->ref($key);\n\t\tif (!$ref)\n\t\t\t$ref=[];\n\t\t$out=array_replace_recursive(\n\t\t\tis_string($src)?$this->hive[$src]:$src,$ref);\n\t\tif ($keep)\n\t\t\t$ref=$out;\n\t\treturn $out;\n\t}\n\n    /**\n     * fetch a hive key result, then clear the key\n     * @param string $key\n     * @return mixed\n     */\n    function pull($key): mixed\n    {\n        $value = $this->get($key);\n        $this->clear($key);\n        return $value;\n    }\n\n\t/**\n\t*\tConvert backslashes to slashes\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction fixslashes($str) {\n\t\treturn $str?strtr($str,'\\\\','/'):$str;\n\t}\n\n\t/**\n\t*\tSplit comma-, semi-colon, or pipe-separated string\n\t*\t@return array\n\t*\t@param $str string\n\t*\t@param $noempty bool\n\t**/\n\tfunction split($str,$noempty=TRUE) {\n\t\treturn array_map('trim',\n\t\t\tpreg_split('/[,;|]/',$str?:'',0,$noempty?PREG_SPLIT_NO_EMPTY:0));\n\t}\n\n\t/**\n\t*\tConvert PHP expression/value to compressed exportable string\n\t*\t@return string\n\t*\t@param $arg mixed\n\t*\t@param $stack array\n\t**/\n\tfunction stringify($arg,?array $stack=NULL) {\n\t\tif ($stack) {\n\t\t\tforeach ($stack as $node)\n\t\t\t\tif ($arg===$node)\n\t\t\t\t\treturn '*RECURSION*';\n\t\t}\n\t\telse\n\t\t\t$stack=[];\n\t\tswitch (gettype($arg)) {\n\t\t\tcase 'object':\n\t\t\t\t$str='';\n\t\t\t\tforeach (get_object_vars($arg) as $key=>$val)\n\t\t\t\t\t$str.=($str?',':'').\n\t\t\t\t\t\t$this->export($key).'=>'.\n\t\t\t\t\t\t$this->stringify($val,\n\t\t\t\t\t\t\tarray_merge($stack,[$arg]));\n\t\t\t\treturn get_class($arg).'::__set_state(['.$str.'])';\n\t\t\tcase 'array':\n\t\t\t\t$str='';\n\t\t\t\t$num=isset($arg[0]) &&\n\t\t\t\t\tctype_digit(implode('',array_keys($arg)));\n\t\t\t\tforeach ($arg as $key=>$val)\n\t\t\t\t\t$str.=($str?',':'').\n\t\t\t\t\t\t($num?'':($this->export($key).'=>')).\n\t\t\t\t\t\t$this->stringify($val,array_merge($stack,[$arg]));\n\t\t\t\treturn '['.$str.']';\n\t\t\tdefault:\n\t\t\t\treturn $this->export($arg);\n\t\t}\n\t}\n\n\t/**\n\t*\tFlatten array values and return as CSV string\n\t*\t@return string\n\t*\t@param $args array\n\t**/\n\tfunction csv(array $args) {\n\t\treturn implode(',',array_map('stripcslashes',\n\t\t\tarray_map([$this,'stringify'],$args)));\n\t}\n\n\t/**\n\t*\tConvert snakecase string to camelcase\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction camelcase($str) {\n\t\treturn preg_replace_callback(\n\t\t\t'/_(\\pL)/u',\n\t\t\tfunction($match) {\n\t\t\t\treturn strtoupper($match[1]);\n\t\t\t},\n\t\t\t$str\n\t\t);\n\t}\n\n\t/**\n\t*\tConvert camelcase string to snakecase\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction snakecase($str) {\n\t\treturn strtolower(preg_replace('/(?!^)\\p{Lu}/u','_\\0',$str));\n\t}\n\n\t/**\n\t*\tReturn -1 if specified number is negative, 0 if zero,\n\t*\tor 1 if the number is positive\n\t*\t@return int\n\t*\t@param $num mixed\n\t**/\n\tfunction sign($num) {\n\t\treturn $num?($num/abs($num)):0;\n\t}\n\n\t/**\n\t*\tExtract values of array whose keys start with the given prefix\n\t*\t@return array\n\t*\t@param $arr array\n\t*\t@param $prefix string\n\t**/\n\tfunction extract($arr,$prefix) {\n\t\t$out=[];\n\t\tforeach (preg_grep('/^'.preg_quote($prefix,'/').'/',array_keys($arr))\n\t\t\tas $key)\n\t\t\t$out[substr($key,strlen($prefix))]=$arr[$key];\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tConvert class constants to array\n\t*\t@return array\n\t*\t@param $class object|string\n\t*\t@param $prefix string\n\t**/\n\tfunction constants($class,$prefix='') {\n\t\t$ref=new ReflectionClass($class);\n\t\treturn $this->extract($ref->getconstants(),$prefix);\n\t}\n\n\t/**\n\t*\tGenerate 64bit/base36 hash\n\t*\t@return string\n\t*\t@param $str\n\t**/\n\tfunction hash($str) {\n\t\treturn str_pad(base_convert(\n\t\t\tsubstr(sha1($str?:''),-16),16,36),11,'0',STR_PAD_LEFT);\n\t}\n\n\t/**\n\t*\tReturn Base64-encoded equivalent\n\t*\t@return string\n\t*\t@param $data string\n\t*\t@param $mime string\n\t**/\n\tfunction base64($data,$mime) {\n\t\treturn 'data:'.$mime.';base64,'.base64_encode($data);\n\t}\n\n\t/**\n\t*\tConvert special characters to HTML entities\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction encode($str) {\n\t\treturn @htmlspecialchars($str,$this->hive['BITMASK'],\n\t\t\t$this->hive['ENCODING'])?:$this->scrub($str);\n\t}\n\n\t/**\n\t*\tConvert HTML entities back to characters\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction decode($str) {\n\t\treturn htmlspecialchars_decode($str,$this->hive['BITMASK']);\n\t}\n\n\t/**\n\t*\tInvoke callback recursively for all data types\n\t*\t@return mixed\n\t*\t@param $arg mixed\n\t*\t@param $func callback\n\t*\t@param $stack array\n\t**/\n\tfunction recursive($arg,$func,$stack=[]) {\n\t\tif ($stack) {\n\t\t\tforeach ($stack as $node)\n\t\t\t\tif ($arg===$node)\n\t\t\t\t\treturn $arg;\n\t\t}\n\t\tswitch (gettype($arg)) {\n\t\t\tcase 'object':\n\t\t\t\t$ref=new ReflectionClass($arg);\n\t\t\t\tif ($ref->iscloneable()) {\n\t\t\t\t\t$arg=clone($arg);\n\t\t\t\t\t$cast=($it=is_a($arg,'IteratorAggregate'))?\n\t\t\t\t\t\titerator_to_array($arg):get_object_vars($arg);\n\t\t\t\t\tforeach ($cast as $key=>$val) {\n\t\t\t\t\t\t// skip inaccessible properties #350\n\t\t\t\t\t\tif (!$it && !isset($arg->$key))\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t$arg->$key=$this->recursive(\n\t\t\t\t\t\t\t$val,$func,array_merge($stack,[$arg]));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn $arg;\n\t\t\tcase 'array':\n\t\t\t\t$copy=[];\n\t\t\t\tforeach ($arg as $key=>$val)\n\t\t\t\t\t$copy[$key]=$this->recursive($val,$func,\n\t\t\t\t\t\tarray_merge($stack,[$arg]));\n\t\t\t\treturn $copy;\n\t\t}\n\t\treturn $func($arg);\n\t}\n\n\t/**\n\t*\tRemove HTML tags (except those enumerated) and non-printable characters\n    *   NB: This method doesn't mitigate XSS/code injection attacks.\n    *   @return mixed\n    *\t@param $arg mixed\n\t*\t@param $tags string\n\t**/\n\tfunction clean($arg,$tags=NULL) {\n\t\treturn $this->recursive($arg,\n\t\t\tfunction($val) use($tags) {\n\t\t\t\tif ($tags!='*')\n\t\t\t\t\t$val=trim(strip_tags($val??'',\n\t\t\t\t\t\t'<'.implode('><',$this->split($tags)).'>'));\n\t\t\t\treturn trim(preg_replace(\n\t\t\t\t\t'/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/','',$val));\n\t\t\t}\n\t\t);\n\t}\n\n\t/**\n\t*\tSimilar to clean(), except that variable is passed by reference\n\t*\t@return mixed\n\t*\t@param $var mixed\n\t*\t@param $tags string\n\t**/\n\tfunction scrub(&$var,$tags=NULL) {\n\t\treturn $var=$this->clean($var,$tags);\n\t}\n\n\t/**\n\t*\tReturn locale-aware formatted string\n\t*\t@return string\n\t**/\n\tfunction format() {\n\t\t$args=func_get_args();\n\t\t$val=array_shift($args);\n\t\t// Get formatting rules\n\t\t$conv=localeconv();\n\t\treturn preg_replace_callback(\n\t\t\t'/\\{\\s*(?P<pos>\\d+)\\s*(?:,\\s*(?P<type>\\w+)\\s*'.\n\t\t\t'(?:,\\s*(?P<mod>(?:\\w+(?:\\s*\\{.+?\\}\\s*,?\\s*)?)*)'.\n\t\t\t'(?:,\\s*(?P<prop>.+?))?)?)?\\s*\\}/',\n\t\t\tfunction($expr) use($args,$conv) {\n\t\t\t\t/**\n\t\t\t\t * @var string $pos\n\t\t\t\t * @var string $mod\n\t\t\t\t * @var string $type\n\t\t\t\t * @var string $prop\n\t\t\t\t */\n\t\t\t\textract($expr);\n\t\t\t\t/**\n\t\t\t\t * @var string $thousands_sep\n\t\t\t\t * @var string $negative_sign\n\t\t\t\t * @var string $positive_sign\n\t\t\t\t * @var string $frac_digits\n\t\t\t\t * @var string $decimal_point\n\t\t\t\t * @var string $int_curr_symbol\n\t\t\t\t * @var string $currency_symbol\n\t\t\t\t */\n\t\t\t\textract($conv);\n\t\t\t\tif (!array_key_exists($pos,$args))\n\t\t\t\t\treturn $expr[0];\n\t\t\t\tif (isset($type)) {\n\t\t\t\t\tif (isset($this->hive['FORMATS'][$type]))\n\t\t\t\t\t\treturn $this->call(\n\t\t\t\t\t\t\t$this->hive['FORMATS'][$type],\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t$args[$pos],\n\t\t\t\t\t\t\t\tisset($mod)?$mod:null,\n\t\t\t\t\t\t\t\tisset($prop)?$prop:null\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t);\n\t\t\t\t\t$php81=version_compare(PHP_VERSION, '8.1.0')>=0;\n\t\t\t\t\tswitch ($type) {\n\t\t\t\t\t\tcase 'plural':\n\t\t\t\t\t\t\tpreg_match_all('/(?<tag>\\w+)'.\n\t\t\t\t\t\t\t\t'(?:\\s*\\{\\s*(?<data>.*?)\\s*\\})/',\n\t\t\t\t\t\t\t\t$mod,$matches,PREG_SET_ORDER);\n\t\t\t\t\t\t\t$ord=['zero','one','two'];\n\t\t\t\t\t\t\tforeach ($matches as $match) {\n\t\t\t\t\t\t\t\t/** @var string $tag */\n\t\t\t\t\t\t\t\t/** @var string $data */\n\t\t\t\t\t\t\t\textract($match);\n\t\t\t\t\t\t\t\tif (isset($ord[$args[$pos]]) &&\n\t\t\t\t\t\t\t\t\t$tag==$ord[$args[$pos]] || $tag=='other')\n\t\t\t\t\t\t\t\t\treturn str_replace('#',$args[$pos],$data);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\tcase 'number':\n\t\t\t\t\t\t\tif (isset($mod))\n\t\t\t\t\t\t\t\tswitch ($mod) {\n\t\t\t\t\t\t\t\t\tcase 'integer':\n\t\t\t\t\t\t\t\t\t\treturn number_format(\n\t\t\t\t\t\t\t\t\t\t\t$args[$pos],0,'',$thousands_sep);\n\t\t\t\t\t\t\t\t\tcase 'currency':\n\t\t\t\t\t\t\t\t\t\t$int=$cstm=FALSE;\n\t\t\t\t\t\t\t\t\t\tif (isset($prop) &&\n\t\t\t\t\t\t\t\t\t\t\t$cstm=!$int=($prop=='int'))\n\t\t\t\t\t\t\t\t\t\t\t$currency_symbol=$prop;\n\t\t\t\t\t\t\t\t\t\tif (!$cstm &&\n\t\t\t\t\t\t\t\t\t\t\tfunction_exists('money_format') &&\n\t\t\t\t\t\t\t\t\t\t\tversion_compare(PHP_VERSION,'7.4.0')<0)\n\t\t\t\t\t\t\t\t\t\t\treturn money_format(\n\t\t\t\t\t\t\t\t\t\t\t\t'%'.($int?'i':'n'),$args[$pos]);\n\t\t\t\t\t\t\t\t\t\t$fmt=[\n\t\t\t\t\t\t\t\t\t\t\t0=>'(nc)',1=>'(n c)',\n\t\t\t\t\t\t\t\t\t\t\t2=>'(nc)',10=>'+nc',\n\t\t\t\t\t\t\t\t\t\t\t11=>'+n c',12=>'+ nc',\n\t\t\t\t\t\t\t\t\t\t\t20=>'nc+',21=>'n c+',\n\t\t\t\t\t\t\t\t\t\t\t22=>'nc +',30=>'n+c',\n\t\t\t\t\t\t\t\t\t\t\t31=>'n +c',32=>'n+ c',\n\t\t\t\t\t\t\t\t\t\t\t40=>'nc+',41=>'n c+',\n\t\t\t\t\t\t\t\t\t\t\t42=>'nc +',100=>'(cn)',\n\t\t\t\t\t\t\t\t\t\t\t101=>'(c n)',102=>'(cn)',\n\t\t\t\t\t\t\t\t\t\t\t110=>'+cn',111=>'+c n',\n\t\t\t\t\t\t\t\t\t\t\t112=>'+ cn',120=>'cn+',\n\t\t\t\t\t\t\t\t\t\t\t121=>'c n+',122=>'cn +',\n\t\t\t\t\t\t\t\t\t\t\t130=>'+cn',131=>'+c n',\n\t\t\t\t\t\t\t\t\t\t\t132=>'+ cn',140=>'c+n',\n\t\t\t\t\t\t\t\t\t\t\t141=>'c+ n',142=>'c +n'\n\t\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t\t\tif ($args[$pos]<0) {\n\t\t\t\t\t\t\t\t\t\t\t$sgn=$negative_sign;\n\t\t\t\t\t\t\t\t\t\t\t$pre='n';\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\t\t\t\t$sgn=$positive_sign;\n\t\t\t\t\t\t\t\t\t\t\t$pre='p';\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\treturn str_replace(\n\t\t\t\t\t\t\t\t\t\t\t['+','n','c'],\n\t\t\t\t\t\t\t\t\t\t\t[$sgn,number_format(\n\t\t\t\t\t\t\t\t\t\t\t\tabs($args[$pos]),\n\t\t\t\t\t\t\t\t\t\t\t\t$frac_digits,\n\t\t\t\t\t\t\t\t\t\t\t\t$decimal_point,\n\t\t\t\t\t\t\t\t\t\t\t\t$thousands_sep),\n\t\t\t\t\t\t\t\t\t\t\t\t$int?$int_curr_symbol\n\t\t\t\t\t\t\t\t\t\t\t\t\t:$currency_symbol],\n\t\t\t\t\t\t\t\t\t\t\t$fmt[(int)(\n\t\t\t\t\t\t\t\t\t\t\t\t(${$pre.'_cs_precedes'}%2).\n\t\t\t\t\t\t\t\t\t\t\t\t(${$pre.'_sign_posn'}%5).\n\t\t\t\t\t\t\t\t\t\t\t\t(${$pre.'_sep_by_space'}%3)\n\t\t\t\t\t\t\t\t\t\t\t)]\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tcase 'percent':\n\t\t\t\t\t\t\t\t\t\treturn number_format(\n\t\t\t\t\t\t\t\t\t\t\t$args[$pos]*100,0,$decimal_point,\n\t\t\t\t\t\t\t\t\t\t\t$thousands_sep).'%';\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t$frac=$args[$pos]-(int)$args[$pos];\n\t\t\t\t\t\t\treturn number_format(\n\t\t\t\t\t\t\t\t$args[$pos],\n\t\t\t\t\t\t\t\tisset($prop)?\n\t\t\t\t\t\t\t\t\t$prop:\n\t\t\t\t\t\t\t\t\t($frac?strlen($frac)-2:0),\n\t\t\t\t\t\t\t\t$decimal_point,$thousands_sep);\n\t\t\t\t\t\tcase 'date':\n\t\t\t\t\t\t\tif ($php81) {\n\t\t\t\t\t\t\t\t$lang = $this->split($this->LANGUAGE);\n\t\t\t\t\t\t\t\t// requires intl extension\n\t\t\t\t\t\t\t\t$dateType=(empty($mod) || $mod=='short') ? IntlDateFormatter::SHORT :\n\t\t\t\t\t\t\t\t\t($mod=='full' ? IntlDateFormatter::FULL : IntlDateFormatter::LONG);\n\t\t\t\t\t\t\t\t$pattern = $dateType === IntlDateFormatter::SHORT\n\t\t\t\t\t\t\t\t\t? (($ptn=IntlDatePatternGenerator::create($lang[0]))\n\t\t\t\t\t\t\t\t\t\t? $ptn->getBestPattern('yyyyMMdd') : null) : null;\n\t\t\t\t\t\t\t\t$formatter = new IntlDateFormatter($lang[0],$dateType,\n\t\t\t\t\t\t\t\t\tIntlDateFormatter::NONE, null,null, $pattern);\n\t\t\t\t\t\t\t\treturn $formatter->format($args[$pos]);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif (empty($mod) || $mod=='short')\n\t\t\t\t\t\t\t\t\t$prop='%x';\n\t\t\t\t\t\t\t\telseif ($mod=='full')\n\t\t\t\t\t\t\t\t\t$prop='%A, %d %B %Y';\n\t\t\t\t\t\t\t\telseif ($mod!='custom')\n\t\t\t\t\t\t\t\t\t$prop='%d %B %Y';\n\t\t\t\t\t\t\t\treturn strftime($prop,$args[$pos]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\tcase 'time':\n\t\t\t\t\t\t\tif ($php81) {\n\t\t\t\t\t\t\t\t$lang = $this->split($this->LANGUAGE);\n\t\t\t\t\t\t\t\t// requires intl extension\n\t\t\t\t\t\t\t\t$formatter = new IntlDateFormatter($lang[0],\n\t\t\t\t\t\t\t\t\tIntlDateFormatter::NONE,\n\t\t\t\t\t\t\t\t\t(empty($mod) || $mod=='short')\n\t\t\t\t\t\t\t\t\t\t? IntlDateFormatter::SHORT :\n\t\t\t\t\t\t\t\t\t\t($mod=='full' ? IntlDateFormatter::LONG : IntlDateFormatter::MEDIUM),\n\t\t\t\t\t\t\t\t\tIntlTimeZone::createTimeZone($this->hive['TZ']));\n\t\t\t\t\t\t\t\treturn $formatter->format($args[$pos]);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif (empty($mod) || $mod=='short')\n\t\t\t\t\t\t\t\t\t$prop='%X';\n\t\t\t\t\t\t\t\telseif ($mod!='custom')\n\t\t\t\t\t\t\t\t\t$prop='%r';\n\t\t\t\t\t\t\t\treturn strftime($prop,$args[$pos]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\treturn $expr[0];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn $args[$pos];\n\t\t\t},\n\t\t\t$val\n\t\t);\n\t}\n\n\t/**\n\t*\tReturn string representation of expression\n\t*\t@return string\n\t*\t@param $expr mixed\n\t**/\n\tfunction export($expr) {\n\t\treturn var_export($expr,TRUE);\n\t}\n\n\t/**\n\t*\tAssign/auto-detect language\n\t*\t@return string\n\t*\t@param $code string\n\t**/\n\tfunction language($code) {\n\t\t$code=preg_replace('/\\h+|;q=[0-9.]+/','',$code?:'');\n\t\t$code.=($code?',':'').$this->fallback;\n\t\t$this->languages=[];\n\t\tforeach (array_reverse(explode(',',$code)) as $lang)\n\t\t\tif (preg_match('/^(\\w{2})(?:-(\\w{2}))?\\b/i',$lang,$parts)) {\n\t\t\t\t// Generic language\n\t\t\t\tarray_unshift($this->languages,$parts[1]);\n\t\t\t\tif (isset($parts[2])) {\n\t\t\t\t\t// Specific language\n\t\t\t\t\t$parts[0]=$parts[1].'-'.($parts[2]=strtoupper($parts[2]));\n\t\t\t\t\tarray_unshift($this->languages,$parts[0]);\n\t\t\t\t}\n\t\t\t}\n\t\t$this->languages=array_unique($this->languages);\n\t\t$locales=[];\n\t\t$windows=preg_match('/^win/i',PHP_OS);\n\t\t// Work around PHP's Turkish locale bug\n\t\tforeach (preg_grep('/^(?!tr)/i',$this->languages) as $locale) {\n\t\t\tif ($windows) {\n\t\t\t\t$parts=explode('-',$locale);\n\t\t\t\tif (!defined('ISO::LC_'.$parts[0]))\n\t\t\t\t\tcontinue;\n\t\t\t\t$locale=constant('ISO::LC_'.$parts[0]);\n\t\t\t\tif (isset($parts[1]) &&\n\t\t\t\t\tdefined($cc='ISO::CC_'.strtolower($parts[1])))\n\t\t\t\t\t$locale.='-'.constant($cc);\n\t\t\t}\n\t\t\t$locale=str_replace('-','_',$locale);\n\t\t\t$locales[]=$locale.'.'.ini_get('default_charset');\n\t\t\t$locales[]=$locale;\n\t\t}\n\t\tsetlocale(LC_ALL,$locales);\n\t\treturn $this->hive['LANGUAGE']=implode(',',$this->languages);\n\t}\n\n\t/**\n\t*\tReturn lexicon entries\n\t*\t@return array\n\t*\t@param $path string\n\t*\t@param $ttl int\n\t**/\n\tfunction lexicon($path,$ttl=0) {\n\t\t$languages=$this->languages?:explode(',',$this->fallback);\n\t\t$cache=Cache::instance();\n\t\tif ($ttl && $cache->exists(\n\t\t\t$hash=$this->hash(implode(',',$languages).$path).'.dic',$lex))\n\t\t\treturn $lex;\n\t\t$lex=[];\n\t\tforeach ($languages as $lang)\n\t\t\tforeach ($this->split($path) as $dir)\n\t\t\t\tif ((is_file($file=($base=$dir.$lang).'.php') ||\n\t\t\t\t\tis_file($file=$base.'.php')) &&\n\t\t\t\t\tis_array($dict=require($file)))\n\t\t\t\t\t$lex+=$dict;\n\t\t\t\telseif (is_file($file=$base.'.json') &&\n\t\t\t\t\tis_array($dict=json_decode(file_get_contents($file), true)))\n\t\t\t\t\t$lex+=$dict;\n\t\t\t\telseif (is_file($file=$base.'.ini')) {\n\t\t\t\t\tpreg_match_all(\n\t\t\t\t\t\t'/(?<=^|\\n)(?:'.\n\t\t\t\t\t\t\t'\\[(?<prefix>.+?)\\]|'.\n\t\t\t\t\t\t\t'(?<lval>[^\\h\\r\\n;].*?)\\h*=\\h*'.\n\t\t\t\t\t\t\t'(?<rval>(?:\\\\\\\\\\h*\\r?\\n|.+?)*)'.\n\t\t\t\t\t\t')(?=\\r?\\n|$)/',\n\t\t\t\t\t\t$this->read($file),$matches,PREG_SET_ORDER);\n\t\t\t\t\tif ($matches) {\n\t\t\t\t\t\t$prefix='';\n\t\t\t\t\t\tforeach ($matches as $match)\n\t\t\t\t\t\t\tif ($match['prefix'])\n\t\t\t\t\t\t\t\t$prefix=$match['prefix'].'.';\n\t\t\t\t\t\t\telseif (!array_key_exists(\n\t\t\t\t\t\t\t\t$key=$prefix.$match['lval'],$lex))\n\t\t\t\t\t\t\t\t$lex[$key]=trim(preg_replace(\n\t\t\t\t\t\t\t\t\t'/\\\\\\\\\\h*\\r?\\n/',\"\\n\",$match['rval']));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\tif ($ttl)\n\t\t\t$cache->set($hash,$lex,$ttl);\n\t\treturn $lex;\n\t}\n\n\t/**\n\t*\tReturn string representation of PHP value\n\t*\t@return string\n\t*\t@param $arg mixed\n\t**/\n\tfunction serialize($arg) {\n\t\tswitch (strtolower($this->hive['SERIALIZER'])) {\n\t\t\tcase 'igbinary':\n\t\t\t\treturn igbinary_serialize($arg);\n\t\t\tdefault:\n\t\t\t\treturn serialize($arg);\n\t\t}\n\t}\n\n\t/**\n\t*\tReturn PHP value derived from string\n\t*\t@return mixed\n\t*\t@param $arg mixed\n\t**/\n\tfunction unserialize($arg) {\n\t\tswitch (strtolower($this->hive['SERIALIZER'])) {\n\t\t\tcase 'igbinary':\n\t\t\t\treturn igbinary_unserialize($arg);\n\t\t\tdefault:\n\t\t\t\treturn unserialize($arg);\n\t\t}\n\t}\n\n\t/**\n\t*\tSend HTTP status header; Return text equivalent of status code\n\t*\t@return string\n\t*\t@param $code int\n\t**/\n\tfunction status($code) {\n\t\t$reason=@constant('self::HTTP_'.$code);\n\t\tif (!$this->hive['CLI'] && !headers_sent())\n\t\t\theader($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason);\n\t\treturn $reason;\n\t}\n\n\t/**\n\t*\tSend cache metadata to HTTP client\n\t*\t@param $secs int\n\t**/\n\tfunction expire($secs=0) {\n\t\tif (!$this->hive['CLI'] && !headers_sent()) {\n\t\t\t$secs=(int)$secs;\n\t\t\tif ($this->hive['PACKAGE'])\n\t\t\t\theader('X-Powered-By: '.$this->hive['PACKAGE']);\n\t\t\tif ($this->hive['XFRAME'])\n\t\t\t\theader('X-Frame-Options: '.$this->hive['XFRAME']);\n\t\t\theader('X-XSS-Protection: 1; mode=block');\n\t\t\theader('X-Content-Type-Options: nosniff');\n\t\t\tif ($this->hive['VERB']=='GET' && $secs) {\n\t\t\t\t$time=microtime(TRUE);\n\t\t\t\theader_remove('Pragma');\n\t\t\t\theader('Cache-Control: max-age='.$secs);\n\t\t\t\theader('Expires: '.gmdate('r',round($time+$secs)));\n\t\t\t\theader('Last-Modified: '.gmdate('r'));\n\t\t\t}\n\t\t\telse {\n\t\t\t\theader('Pragma: no-cache');\n\t\t\t\theader('Cache-Control: no-cache, no-store, must-revalidate');\n\t\t\t\theader('Expires: '.gmdate('r',0));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t*\tReturn HTTP user agent\n\t*\t@return string\n\t**/\n\tfunction agent() {\n\t\t$headers=$this->hive['HEADERS'];\n\t\treturn isset($headers['X-Operamini-Phone-UA'])?\n\t\t\t$headers['X-Operamini-Phone-UA']:\n\t\t\t(isset($headers['X-Skyfire-Phone'])?\n\t\t\t\t$headers['X-Skyfire-Phone']:\n\t\t\t\t(isset($headers['User-Agent'])?\n\t\t\t\t\t$headers['User-Agent']:''));\n\t}\n\n\t/**\n\t*\tReturn TRUE if XMLHttpRequest detected\n\t*\t@return bool\n\t**/\n\tfunction ajax() {\n\t\t$headers=$this->hive['HEADERS'];\n\t\treturn isset($headers['X-Requested-With']) &&\n\t\t\t$headers['X-Requested-With']=='XMLHttpRequest';\n\t}\n\n\t/**\n\t*\tSniff IP address\n\t*\t@return string\n\t**/\n\tfunction ip() {\n\t\t$headers=$this->hive['HEADERS'];\n\t\treturn isset($headers['Client-IP'])?\n\t\t\t$headers['Client-IP']:\n\t\t\t(isset($headers['X-Forwarded-For'])?\n\t\t\t\texplode(',',$headers['X-Forwarded-For'])[0]:\n\t\t\t\t(isset($_SERVER['REMOTE_ADDR'])?\n\t\t\t\t\t$_SERVER['REMOTE_ADDR']:''));\n\t}\n\n\t/**\n\t*\tReturn filtered stack trace as a formatted string (or array)\n\t*\t@return string|array\n\t*\t@param $trace array|NULL\n\t*\t@param $format bool\n\t**/\n\tfunction trace(?array $trace=NULL,$format=TRUE) {\n\t\tif (!$trace) {\n\t\t\t$trace=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);\n\t\t\t$frame=$trace[0];\n\t\t\tif (isset($frame['file']) && $frame['file']==__FILE__)\n\t\t\t\tarray_shift($trace);\n\t\t}\n\t\t$debug=$this->hive['DEBUG'];\n\t\t$trace=array_filter(\n\t\t\t$trace,\n\t\t\tfunction($frame) use($debug) {\n\t\t\t\treturn isset($frame['file']) &&\n\t\t\t\t\t($debug>1 ||\n\t\t\t\t\t(($frame['file']!=__FILE__ || $debug) &&\n\t\t\t\t\t(empty($frame['function']) ||\n\t\t\t\t\t!preg_match('/^(?:(?:trigger|user)_error|'.\n\t\t\t\t\t\t'__call|call_user_func)/',$frame['function']))));\n\t\t\t}\n\t\t);\n\t\tif (!$format)\n\t\t\treturn $trace;\n\t\t$out='';\n\t\t$eol=\"\\n\";\n\t\t// Analyze stack trace\n\t\tforeach ($trace as $frame) {\n\t\t\t$line='';\n\t\t\tif (isset($frame['class']))\n\t\t\t\t$line.=$frame['class'].$frame['type'];\n\t\t\tif (isset($frame['function']))\n\t\t\t\t$line.=$frame['function'].'('.\n\t\t\t\t\t($debug>2 && isset($frame['args'])?\n\t\t\t\t\t\t$this->csv($frame['args']):'').')';\n\t\t\t$src=$this->fixslashes(str_replace($_SERVER['DOCUMENT_ROOT'].\n\t\t\t\t'/','',$frame['file'])).':'.$frame['line'];\n\t\t\t$out.='['.$src.'] '.$line.$eol;\n\t\t}\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tLog error; Execute ONERROR handler if defined, else display\n\t*\tdefault error page (HTML for synchronous requests, JSON string\n\t*\tfor AJAX requests)\n\t*\t@param $code int\n\t*\t@param $text string\n\t*\t@param $trace array\n\t*\t@param $level int\n\t**/\n\tfunction error($code,$text='',?array $trace=NULL,$level=0) {\n\t\t$prior=$this->hive['ERROR'];\n\t\t$header=$this->status($code);\n\t\t$req=$this->hive['VERB'].' '.$this->hive['PATH'];\n\t\tif ($this->hive['QUERY'])\n\t\t\t$req.='?'.$this->hive['QUERY'];\n\t\tif (!$text)\n\t\t\t$text='HTTP '.$code.' ('.$req.')';\n\t\t$trace=$this->trace($trace);\n\t\t$loggable=$this->hive['LOGGABLE'];\n\t\tif (!is_array($loggable))\n\t\t\t$loggable=$this->split($loggable);\n\t\tforeach ($loggable as $status)\n\t\t\tif ($status=='*' ||\n\t\t\t\tpreg_match('/^'.preg_replace('/\\D/','\\d',$status).'$/',(string) $code)) {\n\t\t\t\terror_log($text);\n\t\t\t\tforeach (explode(\"\\n\",$trace) as $nexus)\n\t\t\t\t\tif ($nexus)\n\t\t\t\t\t\terror_log($nexus);\n\t\t\t\tbreak;\n\t\t\t}\n\t\tif ($highlight=(!$this->hive['CLI'] && !$this->hive['AJAX'] &&\n\t\t\t$this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS)))\n\t\t\t$trace=$this->highlight($trace);\n\t\t$this->hive['ERROR']=[\n\t\t\t'status'=>$header,\n\t\t\t'code'=>$code,\n\t\t\t'text'=>$text,\n\t\t\t'trace'=>$trace,\n\t\t\t'level'=>$level\n\t\t];\n\t\t$this->expire(-1);\n\t\t$handler=$this->hive['ONERROR'];\n\t\t$this->hive['ONERROR']=NULL;\n\t\t$eol=\"\\n\";\n\t\tif ((!$handler ||\n\t\t\t$this->call($handler,[$this,$this->hive['PARAMS']],\n\t\t\t\t'beforeroute,afterroute')===FALSE) &&\n\t\t\t!$prior && !$this->hive['QUIET']) {\n\t\t\t$error=array_diff_key(\n\t\t\t\t$this->hive['ERROR'],\n\t\t\t\t$this->hive['DEBUG']?\n\t\t\t\t\t[]:\n\t\t\t\t\t['trace'=>1]\n\t\t\t);\n\t\t\tif ($this->hive['CLI'])\n\t\t\t\techo PHP_EOL.'==================================='.PHP_EOL.\n\t\t\t\t\t'ERROR '.$error['code'].' - '.$error['status'].PHP_EOL.\n\t\t\t\t\t$error['text'].PHP_EOL.PHP_EOL.(isset($error['trace']) ? $error['trace'] : '');\n\t\t\telse\n\t\t\t\techo $this->hive['AJAX']?\n\t\t\t\t\tjson_encode($error):\n\t\t\t\t\t('<!DOCTYPE html>'.$eol.\n\t\t\t\t\t'<html>'.$eol.\n\t\t\t\t\t'<head>'.\n\t\t\t\t\t\t'<title>'.$code.' '.$header.'</title>'.\n\t\t\t\t\t\t($highlight?\n\t\t\t\t\t\t\t('<style>'.$this->read($css).'</style>'):'').\n\t\t\t\t\t'</head>'.$eol.\n\t\t\t\t\t'<body>'.$eol.\n\t\t\t\t\t\t'<h1>'.$header.'</h1>'.$eol.\n\t\t\t\t\t\t'<p>'.$this->encode($text?:$req).'</p>'.$eol.\n\t\t\t\t\t\t($this->hive['DEBUG']?('<pre>'.$trace.'</pre>'.$eol):'').\n\t\t\t\t\t'</body>'.$eol.\n\t\t\t\t\t'</html>');\n\t\t}\n\t\tif ($this->hive['HALT'])\n\t\t\tdie(1);\n\t}\n\n\t/**\n\t*\tMock HTTP request\n\t*\t@return mixed\n\t*\t@param $pattern string\n\t*\t@param $args array\n\t*\t@param $headers array\n\t*\t@param $body string\n\t**/\n\tfunction mock($pattern,\n\t\t?array $args=NULL,?array $headers=NULL,$body=NULL) {\n\t\tif (!$args)\n\t\t\t$args=[];\n\t\t$types=['sync','ajax','cli'];\n\t\tpreg_match('/([\\|\\w]+)\\h+(?:@(\\w+)(?:(\\(.+?)\\))*|([^\\h]+))'.\n\t\t\t'(?:\\h+\\[('.implode('|',$types).')\\])?/',$pattern,$parts);\n\t\t$verb=strtoupper($parts[1]);\n\t\tif ($parts[2]) {\n\t\t\tif (empty($this->hive['ALIASES'][$parts[2]]))\n                throw new \\Exception(sprintf(self::E_Named,$parts[2]));\n\t\t\t$parts[4]=$this->hive['ALIASES'][$parts[2]];\n\t\t\t$parts[4]=$this->build($parts[4],\n\t\t\t\tisset($parts[3])?$this->parse($parts[3]):[]);\n\t\t}\n\t\tif (empty($parts[4]))\n            throw new \\Exception(sprintf(self::E_Pattern,$pattern));\n\t\t$url=parse_url($parts[4]);\n\t\tparse_str(isset($url['query'])?$url['query']:'',$GLOBALS['_GET']);\n\t\tif (preg_match('/GET|HEAD/',$verb))\n\t\t\t$GLOBALS['_GET']=array_merge($GLOBALS['_GET'],$args);\n\t\t$GLOBALS['_POST']=$verb=='POST'?$args:[];\n\t\t$GLOBALS['_REQUEST']=array_merge($GLOBALS['_GET'],$GLOBALS['_POST']);\n\t\tforeach ($headers?:[] as $key=>$val)\n\t\t\t$_SERVER['HTTP_'.strtr(strtoupper($key),'-','_')]=$val;\n\t\t$this->hive['VERB']=$verb;\n\t\t$this->hive['PATH']=$url['path'];\n\t\t$this->hive['URI']=$this->hive['BASE'].$url['path'];\n\t\tif ($GLOBALS['_GET'])\n\t\t\t$this->hive['URI'].='?'.http_build_query($GLOBALS['_GET']);\n\t\t$this->hive['BODY']='';\n\t\tif (!preg_match('/GET|HEAD/',$verb))\n\t\t\t$this->hive['BODY']=$body?:http_build_query($args);\n\t\t$this->hive['AJAX']=isset($parts[5]) &&\n\t\t\tpreg_match('/ajax/i',$parts[5]);\n\t\t$this->hive['CLI']=isset($parts[5]) &&\n\t\t\tpreg_match('/cli/i',$parts[5]);\n\t\treturn $this->run();\n\t}\n\n\t/**\n\t*\tAssemble url from alias name\n\t*\t@return string\n\t*\t@param $name string\n\t*\t@param $params array|string\n\t*\t@param $query string|array\n\t*\t@param $fragment string\n\t**/\n\tfunction alias($name,$params=[],$query=NULL,$fragment=NULL) {\n\t\tif (!is_array($params))\n\t\t\t$params=$this->parse($params);\n\t\tif (empty($this->hive['ALIASES'][$name]))\n            throw new \\Exception(sprintf(self::E_Named,$name));\n\t\t$url=$this->build($this->hive['ALIASES'][$name],$params);\n\t\tif (is_array($query))\n\t\t\t$query=http_build_query($query);\n\t\treturn $url.($query?('?'.$query):'').($fragment?'#'.$fragment:'');\n\t}\n\n\t/**\n\t*\tBind handler to route pattern\n\t*\t@return NULL\n\t*\t@param $pattern string|array\n\t*\t@param $handler callback\n\t*\t@param $ttl int\n\t*\t@param $kbps int\n\t**/\n\tfunction route($pattern,$handler,$ttl=0,$kbps=0) {\n\t\t$types=['sync','ajax','cli'];\n\t\t$alias=null;\n\t\tif (is_array($pattern)) {\n\t\t\tforeach ($pattern as $item)\n\t\t\t\t$this->route($item,$handler,$ttl,$kbps);\n\t\t\treturn;\n\t\t}\n\t\tpreg_match('/([\\|\\w]+)\\h+(?:(?:@?(.+?)\\h*:\\h*)?(@(\\w+)|[^\\h]+))'.\n\t\t\t'(?:\\h+\\[('.implode('|',$types).')\\])?/u',$pattern,$parts);\n\t\tif (isset($parts[2]) && $parts[2]) {\n\t\t\tif (!preg_match('/^\\w+$/',$parts[2]))\n                throw new \\Exception(sprintf(self::E_Alias,$parts[2]));\n\t\t\t$this->hive['ALIASES'][$alias=$parts[2]]=$parts[3];\n\t\t}\n\t\telseif (!empty($parts[4])) {\n\t\t\tif (empty($this->hive['ALIASES'][$parts[4]]))\n                throw new \\Exception(sprintf(self::E_Named,$parts[4]));\n\t\t\t$parts[3]=$this->hive['ALIASES'][$alias=$parts[4]];\n\t\t}\n\t\tif (empty($parts[3]))\n            throw new \\Exception(sprintf(self::E_Pattern,$pattern));\n\t\t$type=empty($parts[5])?0:constant('self::REQ_'.strtoupper($parts[5]));\n\t\tforeach ($this->split($parts[1]) as $verb) {\n\t\t\tif (!preg_match('/'.self::VERBS.'/',$verb))\n\t\t\t\t$this->error(501,$verb.' '.$this->hive['URI']);\n\t\t\t$this->hive['ROUTES'][$parts[3]][$type][strtoupper($verb)]=\n\t\t\t\t[is_string($handler) ? trim($handler) : $handler,$ttl,$kbps,$alias];\n\t\t}\n\t}\n\n\t/**\n\t*\tReroute to specified URI\n\t*\t@return NULL\n\t*\t@param $url array|string\n\t*\t@param $permanent bool\n\t*\t@param $die bool\n\t**/\n\tfunction reroute($url=NULL,$permanent=FALSE,$die=TRUE) {\n\t\tif (!$url)\n\t\t\t$url=$this->hive['REALM'];\n\t\tif (is_array($url))\n\t\t\t$url=call_user_func_array([$this,'alias'],$url);\n\t\telseif (preg_match('/^(?:@([^\\/()?#]+)(?:\\((.+?)\\))*(\\?[^#]+)*(#.+)*)/',\n\t\t\t$url,$parts) && isset($this->hive['ALIASES'][$parts[1]]))\n\t\t\t$url=$this->build($this->hive['ALIASES'][$parts[1]],\n\t\t\t\t\tisset($parts[2])?$this->parse($parts[2]):[]).\n\t\t\t\t(isset($parts[3])?$parts[3]:'').(isset($parts[4])?$parts[4]:'');\n\t\telse\n\t\t\t$url=$this->build($url);\n\t\tif (($handler=$this->hive['ONREROUTE']) &&\n\t\t\t$this->call($handler,[$url,$permanent,$die])!==FALSE)\n\t\t\treturn;\n\t\tif ($url[0]!='/' && !preg_match('/^\\w+:\\/\\//i',$url))\n\t\t\t$url='/'.$url;\n\t\tif ($url[0]=='/' && (empty($url[1]) || $url[1]!='/')) {\n\t\t\t$port=$this->hive['PORT'];\n\t\t\t$port=in_array($port,[80,443])?'':(':'.$port);\n\t\t\t$url=$this->hive['SCHEME'].'://'.\n\t\t\t\t$this->hive['HOST'].$port.$this->hive['BASE'].$url;\n\t\t}\n\t\tif ($this->hive['CLI'])\n\t\t\t$this->mock('GET '.$url.' [cli]');\n\t\telse {\n\t\t\theader('Location: '.$url);\n\t\t\t$this->status($permanent?301:302);\n\t\t\tif ($die)\n\t\t\t\tdie;\n\t\t}\n\t}\n\n\t/**\n\t*\tProvide ReST interface by mapping HTTP verb to class method\n\t*\t@return NULL\n\t*\t@param $url string\n\t*\t@param $class string|object\n\t*\t@param $ttl int\n\t*\t@param $kbps int\n\t**/\n\tfunction map($url,$class,$ttl=0,$kbps=0) {\n\t\tif (is_array($url)) {\n\t\t\tforeach ($url as $item)\n\t\t\t\t$this->map($item,$class,$ttl,$kbps);\n\t\t\treturn;\n\t\t}\n\t\tforeach (explode('|',self::VERBS) as $method)\n\t\t\t$this->route($method.' '.$url,is_string($class)?\n\t\t\t\t$class.'->'.$this->hive['PREMAP'].strtolower($method):\n\t\t\t\t[$class,$this->hive['PREMAP'].strtolower($method)],\n\t\t\t\t$ttl,$kbps);\n\t}\n\n\t/**\n\t*\tRedirect a route to another URL\n\t*\t@return NULL\n\t*\t@param $pattern string|array\n\t*\t@param $url string\n\t*\t@param $permanent bool\n\t*/\n\tfunction redirect($pattern,$url,$permanent=TRUE) {\n\t\tif (is_array($pattern)) {\n\t\t\tforeach ($pattern as $item)\n\t\t\t\t$this->redirect($item,$url,$permanent);\n\t\t\treturn;\n\t\t}\n\t\t$this->route($pattern,function($fw) use($url,$permanent) {\n\t\t\t$fw->reroute($url,$permanent);\n\t\t});\n\t}\n\n\t/**\n\t*\tReturn TRUE if IPv4 address exists in DNSBL\n\t*\t@return bool\n\t*\t@param $ip string\n\t**/\n\tfunction blacklisted($ip) {\n\t\tif ($this->hive['DNSBL'] &&\n\t\t\t!in_array($ip,\n\t\t\t\tis_array($this->hive['EXEMPT'])?\n\t\t\t\t\t$this->hive['EXEMPT']:\n\t\t\t\t\t$this->split($this->hive['EXEMPT']))) {\n\t\t\t// Reverse IPv4 dotted quad\n\t\t\t$rev=implode('.',array_reverse(explode('.',$ip)));\n\t\t\tforeach (is_array($this->hive['DNSBL'])?\n\t\t\t\t$this->hive['DNSBL']:\n\t\t\t\t$this->split($this->hive['DNSBL']) as $server)\n\t\t\t\t// DNSBL lookup\n\t\t\t\tif (checkdnsrr($rev.'.'.$server,'A'))\n\t\t\t\t\treturn TRUE;\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tApplies the specified URL mask and returns parameterized matches\n\t*\t@return $args array\n\t*\t@param $pattern string\n\t*\t@param $url string|NULL\n\t**/\n\tfunction mask($pattern,$url=NULL) {\n\t\tif (!$url)\n\t\t\t$url=$this->rel($this->hive['URI']);\n\t\t$case=$this->hive['CASELESS']?'i':'';\n\t\t$wild=preg_quote($pattern,'/');\n\t\t$i=0;\n\t\twhile (is_int($pos=strpos($wild,'\\*'))) {\n\t\t\t$wild=substr_replace($wild,'(?P<_'.$i.'>[^\\?]*)',$pos,2);\n\t\t\t++$i;\n\t\t}\n\t\tpreg_match('/^'.\n\t\t\tpreg_replace(\n\t\t\t\t'/((\\\\\\{)?@(\\w+\\b)(?(2)\\\\\\}))/',\n\t\t\t\t'(?P<\\3>[^\\/\\?]+)',\n\t\t\t\t$wild).'\\/?$/'.$case.'um',$url,$args);\n\t\tforeach (array_keys($args) as $key) {\n\t\t\tif (preg_match('/^_\\d+$/',$key)) {\n\t\t\t\tif (empty($args['*']))\n\t\t\t\t\t$args['*']=$args[$key];\n\t\t\t\telse {\n\t\t\t\t\tif (is_string($args['*']))\n\t\t\t\t\t\t$args['*']=[$args['*']];\n\t\t\t\t\tarray_push($args['*'],$args[$key]);\n\t\t\t\t}\n\t\t\t\tunset($args[$key]);\n\t\t\t}\n\t\t\telseif (is_numeric($key) && $key)\n\t\t\t\tunset($args[$key]);\n\t\t}\n\t\treturn $args;\n\t}\n\n\t/**\n\t*\tMatch routes against incoming URI\n\t*\t@return mixed\n\t**/\n\tfunction run() {\n\t\tif ($this->blacklisted($this->hive['IP']))\n\t\t\t// Spammer detected\n\t\t\t$this->error(403);\n\t\tif (!$this->hive['ROUTES'])\n\t\t\t// No routes defined\n            throw new \\Exception(self::E_Routes);\n\t\t// Match specific routes first\n\t\t$paths=[];\n\t\tforeach ($keys=array_keys($this->hive['ROUTES']) as $key) {\n\t\t\t$path=preg_replace('/@\\w+/','*@',$key);\n\t\t\tif (substr($path,-1)!='*')\n\t\t\t\t$path.='+';\n\t\t\t$paths[]=$path;\n\t\t}\n\t\t$vals=array_values($this->hive['ROUTES']);\n\t\tarray_multisort($paths,SORT_DESC,$keys,$vals);\n\t\t$this->hive['ROUTES']=array_combine($keys,$vals);\n\t\t// Convert to BASE-relative URL\n\t\t$req=urldecode($this->hive['PATH']);\n\t\t$preflight=FALSE;\n\t\tif ($cors=(isset($this->hive['HEADERS']['Origin']) &&\n\t\t\t$this->hive['CORS']['origin'])) {\n\t\t\t$cors=$this->hive['CORS'];\n\t\t\theader('Access-Control-Allow-Origin: '.$cors['origin']);\n\t\t\theader('Access-Control-Allow-Credentials: '.\n\t\t\t\t$this->export($cors['credentials']));\n\t\t\t$preflight=\n\t\t\t\tisset($this->hive['HEADERS']['Access-Control-Request-Method']);\n\t\t}\n\t\t$allowed=[];\n\t\tforeach ($this->hive['ROUTES'] as $pattern=>$routes) {\n\t\t\tif (!$args=$this->mask($pattern,$req))\n\t\t\t\tcontinue;\n\t\t\tksort($args);\n\t\t\t$route=NULL;\n\t\t\t$ptr=$this->hive['CLI']?self::REQ_CLI:$this->hive['AJAX']+1;\n\t\t\tif (isset($routes[$ptr][$this->hive['VERB']]) ||\n\t\t\t\t($preflight && isset($routes[$ptr])) ||\n\t\t\t\tisset($routes[$ptr=0]))\n\t\t\t\t$route=$routes[$ptr];\n\t\t\tif (!$route)\n\t\t\t\tcontinue;\n\t\t\tif (isset($route[$this->hive['VERB']]) && !$preflight) {\n\t\t\t\tif ($this->hive['REROUTE_TRAILING_SLASH']===TRUE &&\n\t\t\t\t\t$this->hive['VERB']=='GET' &&\n\t\t\t\t\tpreg_match('/.+\\/$/',$this->hive['PATH']))\n\t\t\t\t\t$this->reroute(substr($this->hive['PATH'],0,-1).\n\t\t\t\t\t\t($this->hive['QUERY']?('?'.$this->hive['QUERY']):''));\n\t\t\t\tlist($handler,$ttl,$kbps,$alias)=$route[$this->hive['VERB']];\n\t\t\t\t// Capture values of route pattern tokens\n\t\t\t\t$this->hive['PARAMS']=$args;\n\t\t\t\t// Save matching route\n\t\t\t\t$this->hive['ALIAS']=$alias;\n\t\t\t\t$this->hive['PATTERN']=$pattern;\n\t\t\t\tif ($cors && $cors['expose'])\n\t\t\t\t\theader('Access-Control-Expose-Headers: '.\n\t\t\t\t\t\t(is_array($cors['expose'])?\n\t\t\t\t\t\t\timplode(',',$cors['expose']):$cors['expose']));\n\t\t\t\tif (is_string($handler)) {\n\t\t\t\t\t// Replace route pattern tokens in handler if any\n\t\t\t\t\t$handler=preg_replace_callback('/({)?@(\\w+\\b)(?(1)})/',\n\t\t\t\t\t\tfunction($id) use($args) {\n\t\t\t\t\t\t\t$pid=count($id)>2?2:1;\n\t\t\t\t\t\t\treturn isset($args[$id[$pid]])?\n\t\t\t\t\t\t\t\t$args[$id[$pid]]:\n\t\t\t\t\t\t\t\t$id[0];\n\t\t\t\t\t\t},\n\t\t\t\t\t\t$handler\n\t\t\t\t\t);\n\t\t\t\t\tif (preg_match('/(.+)\\h*(?:->|::)/',$handler,$match) &&\n\t\t\t\t\t\t!class_exists($match[1]))\n\t\t\t\t\t\t$this->error(404);\n\t\t\t\t}\n\t\t\t\t// Process request\n\t\t\t\t$result=NULL;\n\t\t\t\t$body='';\n\t\t\t\t$now=microtime(TRUE);\n\t\t\t\tif (preg_match('/GET|HEAD/',$this->hive['VERB']) && $ttl) {\n\t\t\t\t\t// Only GET and HEAD requests are cacheable\n\t\t\t\t\t$headers=$this->hive['HEADERS'];\n\t\t\t\t\t$cache=Cache::instance();\n\t\t\t\t\t$cached=$cache->exists(\n\t\t\t\t\t\t$hash=$this->hash($this->hive['VERB'].' '.\n\t\t\t\t\t\t\t$this->hive['URI']).'.url',$data);\n\t\t\t\t\tif ($cached) {\n\t\t\t\t\t\tif (isset($headers['If-Modified-Since']) &&\n\t\t\t\t\t\t\tstrtotime($headers['If-Modified-Since'])+\n\t\t\t\t\t\t\t\t$ttl>$now) {\n\t\t\t\t\t\t\t$this->status(304);\n\t\t\t\t\t\t\tdie;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Retrieve from cache backend\n\t\t\t\t\t\tlist($headers,$body,$result)=$data;\n\t\t\t\t\t\tif (!$this->hive['CLI'])\n\t\t\t\t\t\t\tarray_walk($headers,'header');\n\t\t\t\t\t\t$this->expire($cached[0]+$ttl-$now);\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t\t// Expire HTTP client-cached page\n\t\t\t\t\t\t$this->expire($ttl);\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t\t$this->expire(0);\n\t\t\t\tif (!strlen($body)) {\n\t\t\t\t\tif (!$this->hive['RAW'] && !$this->hive['BODY'])\n\t\t\t\t\t\t$this->hive['BODY']=file_get_contents('php://input');\n\t\t\t\t\tob_start();\n\t\t\t\t\t// Call route handler\n\t\t\t\t\t$result=$this->call($handler,[$this,$args,$handler],\n\t\t\t\t\t\t'beforeroute,afterroute');\n\t\t\t\t\t$body=ob_get_clean();\n\t\t\t\t\tif (isset($cache) && !error_get_last()) {\n\t\t\t\t\t\t// Save to cache backend\n\t\t\t\t\t\t$cache->set($hash,[\n\t\t\t\t\t\t\t// Remove cookies\n\t\t\t\t\t\t\tpreg_grep('/Set-Cookie\\:/',headers_list(),\n\t\t\t\t\t\t\t\tPREG_GREP_INVERT),$body,$result],$ttl);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t$this->hive['RESPONSE']=$body;\n\t\t\t\tif (!$this->hive['QUIET']) {\n\t\t\t\t\tif ($kbps) {\n\t\t\t\t\t\t$ctr=0;\n\t\t\t\t\t\tforeach (str_split($body,1024) as $part) {\n\t\t\t\t\t\t\t// Throttle output\n\t\t\t\t\t\t\t++$ctr;\n\t\t\t\t\t\t\tif ($ctr/$kbps>($elapsed=microtime(TRUE)-$now) &&\n\t\t\t\t\t\t\t\t!connection_aborted())\n\t\t\t\t\t\t\t\tusleep(round(1e6*($ctr/$kbps-$elapsed)));\n\t\t\t\t\t\t\techo $part;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t\techo $body;\n\t\t\t\t}\n\t\t\t\tif ($result || $this->hive['VERB']!='OPTIONS')\n\t\t\t\t\treturn $result;\n\t\t\t}\n\t\t\t$allowed=array_merge($allowed,array_keys($route));\n\t\t}\n\t\tif (!$allowed)\n\t\t\t// URL doesn't match any route\n\t\t\t$this->error(404);\n\t\telseif (!$this->hive['CLI']) {\n\t\t\tif (!preg_grep('/Allow:/',$headers_send=headers_list()))\n\t\t\t\t// Unhandled HTTP method\n\t\t\t\theader('Allow: '.implode(',',array_unique($allowed)));\n\t\t\tif ($cors) {\n\t\t\t\tif (!preg_grep('/Access-Control-Allow-Methods:/',$headers_send))\n\t\t\t\t\theader('Access-Control-Allow-Methods: OPTIONS,'.\n\t\t\t\t\t\timplode(',',$allowed));\n\t\t\t\tif ($cors['headers'] &&\n\t\t\t\t\t!preg_grep('/Access-Control-Allow-Headers:/',$headers_send))\n\t\t\t\t\theader('Access-Control-Allow-Headers: '.\n\t\t\t\t\t\t(is_array($cors['headers'])?\n\t\t\t\t\t\t\timplode(',',$cors['headers']):\n\t\t\t\t\t\t\t$cors['headers']));\n\t\t\t\tif ($cors['ttl']>0)\n\t\t\t\t\theader('Access-Control-Max-Age: '.$cors['ttl']);\n\t\t\t}\n\t\t\tif ($this->hive['VERB']!='OPTIONS')\n\t\t\t\t$this->error(405);\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tLoop until callback returns TRUE (for long polling)\n\t*\t@return mixed\n\t*\t@param $func callback\n\t*\t@param $args array\n\t*\t@param $timeout int\n\t**/\n\tfunction until($func,$args=NULL,$timeout=60) {\n\t\tif (!$args)\n\t\t\t$args=[];\n\t\t$time=time();\n\t\t$max=ini_get('max_execution_time');\n\t\t$limit=max(0,($max?min($timeout,$max):$timeout)-1);\n\t\t$out='';\n\t\t// Turn output buffering on\n\t\tob_start();\n\t\t// Not for the weak of heart\n\t\twhile (\n\t\t\t// No error occurred\n\t\t\t!$this->hive['ERROR'] &&\n\t\t\t// Got time left?\n\t\t\ttime()-$time+1<$limit &&\n\t\t\t// Still alive?\n\t\t\t!connection_aborted() &&\n\t\t\t// Restart session\n\t\t\t!headers_sent() &&\n\t\t\t(session_status()==PHP_SESSION_ACTIVE || session_start()) &&\n\t\t\t// CAUTION: Callback will kill host if it never becomes truthy!\n\t\t\t!$out=$this->call($func,$args)) {\n\t\t\tif (!$this->hive['CLI'])\n\t\t\t\tsession_commit();\n\t\t\t// Hush down\n\t\t\tsleep(1);\n\t\t}\n\t\tob_flush();\n\t\tflush();\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tDisconnect HTTP client;\n\t*\tSet FcgidOutputBufferSize to zero if server uses mod_fcgid;\n\t*\tDisable mod_deflate when rendering text/html output\n\t**/\n\tfunction abort() {\n\t\tif (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)\n\t\t\tsession_start();\n\t\t$out='';\n\t\twhile (ob_get_level())\n\t\t\t$out=ob_get_clean().$out;\n\t\tif (!headers_sent()) {\n\t\t\theader('Content-Length: '.strlen($out));\n\t\t\theader('Connection: close');\n\t\t}\n\t\tsession_commit();\n\t\techo $out;\n\t\tflush();\n\t\tif (function_exists('fastcgi_finish_request'))\n\t\t\tfastcgi_finish_request();\n\t}\n\n\t/**\n\t*\tGrab the real route handler behind the string expression\n\t*\t@return string|array\n\t*\t@param $func string\n\t*\t@param $args array\n\t**/\n\tfunction grab($func,$args=NULL) {\n\t\tif (preg_match('/(.+)\\h*(->|::)\\h*(.+)/s',$func,$parts)) {\n\t\t\t// Convert string to executable PHP callback\n\t\t\tif (!class_exists($parts[1]))\n                throw new \\Exception(sprintf(self::E_Class,$parts[1]));\n\t\t\tif ($parts[2]=='->') {\n\t\t\t\tif (is_subclass_of($parts[1],'Prefab'))\n\t\t\t\t\t$parts[1]=call_user_func($parts[1].'::instance');\n\t\t\t\telseif (isset($this->hive['CONTAINER'])) {\n\t\t\t\t\t$container=$this->hive['CONTAINER'];\n\t\t\t\t\tif (is_object($container) && is_callable([$container,'has'])\n\t\t\t\t\t\t&& $container->has($parts[1])) // PSR11\n\t\t\t\t\t\t$parts[1]=call_user_func([$container,'get'],$parts[1]);\n\t\t\t\t\telseif (is_callable($container))\n\t\t\t\t\t\t$parts[1]=call_user_func($container,$parts[1],$args);\n\t\t\t\t\telseif (is_string($container) &&\n\t\t\t\t\t\tis_subclass_of($container,'Prefab'))\n\t\t\t\t\t\t$parts[1]=call_user_func($container.'::instance')->\n\t\t\t\t\t\t\tget($parts[1]);\n\t\t\t\t\telse\n                        throw new \\Exception(sprintf(self::E_Class,\n\t\t\t\t\t\t\t$this->stringify($parts[1])));\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t$ref=new ReflectionClass($parts[1]);\n\t\t\t\t\t$parts[1]=method_exists($parts[1],'__construct') && $args?\n\t\t\t\t\t\t$ref->newinstanceargs($args):\n\t\t\t\t\t\t$ref->newinstance();\n\t\t\t\t}\n\t\t\t}\n\t\t\t$func=[$parts[1],$parts[3]];\n\t\t}\n\t\treturn $func;\n\t}\n\n\t/**\n\t*\tExecute callback/hooks (supports 'class->method' format)\n\t*\t@return mixed|FALSE\n\t*\t@param $func callback\n\t*\t@param $args mixed\n\t*\t@param $hooks string\n\t**/\n\tfunction call($func,$args=NULL,$hooks='') {\n\t\tif (!is_array($args))\n\t\t\t$args=[$args];\n\t\t// Grab the real handler behind the string representation\n\t\tif (is_string($func))\n\t\t\t$func=$this->grab($func,$args);\n\t\t// Execute function; abort if callback/hook returns FALSE\n\t\tif (!is_callable($func))\n\t\t\t// No route handler\n\t\t\tif ($hooks=='beforeroute,afterroute') {\n\t\t\t\t$allowed=[];\n\t\t\t\tif (is_array($func))\n\t\t\t\t\t$allowed=array_intersect(\n\t\t\t\t\t\tarray_map('strtoupper',get_class_methods($func[0])),\n\t\t\t\t\t\texplode('|',self::VERBS)\n\t\t\t\t\t);\n\t\t\t\theader('Allow: '.implode(',',$allowed));\n\t\t\t\t$this->error(405);\n\t\t\t}\n\t\t\telse\n                throw new \\Exception(sprintf(self::E_Method,\n\t\t\t\t\tis_string($func)?$func:$this->stringify($func)));\n\t\t$obj=FALSE;\n\t\tif (is_array($func)) {\n\t\t\t$hooks=$this->split($hooks);\n\t\t\t$obj=TRUE;\n\t\t}\n\t\t// Execute pre-route hook if any\n\t\tif ($obj && $hooks && in_array($hook='beforeroute',$hooks) &&\n\t\t\tmethod_exists($func[0],$hook) &&\n\t\t\tcall_user_func_array([$func[0],$hook],$args)===FALSE)\n\t\t\treturn FALSE;\n\t\t// Execute callback\n\t\t$out=call_user_func_array($func,$args?:[]);\n\t\tif ($out===FALSE)\n\t\t\treturn FALSE;\n\t\t// Execute post-route hook if any\n\t\tif ($obj && $hooks && in_array($hook='afterroute',$hooks) &&\n\t\t\tmethod_exists($func[0],$hook) &&\n\t\t\tcall_user_func_array([$func[0],$hook],$args)===FALSE)\n\t\t\treturn FALSE;\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tExecute specified callbacks in succession; Apply same arguments\n\t*\tto all callbacks\n\t*\t@return array\n\t*\t@param $funcs array|string\n\t*\t@param $args mixed\n\t**/\n\tfunction chain($funcs,$args=NULL) {\n\t\t$out=[];\n\t\tforeach (is_array($funcs)?$funcs:$this->split($funcs) as $func)\n\t\t\t$out[]=$this->call($func,$args);\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tExecute specified callbacks in succession; Relay result of\n\t*\tprevious callback as argument to the next callback\n\t*\t@return array\n\t*\t@param $funcs array|string\n\t*\t@param $args mixed\n\t**/\n\tfunction relay($funcs,$args=NULL) {\n\t\tforeach (is_array($funcs)?$funcs:$this->split($funcs) as $func)\n\t\t\t$args=[$this->call($func,$args)];\n\t\treturn array_shift($args);\n\t}\n\n\t/**\n\t*\tConfigure framework according to .ini-style file settings;\n\t*\tIf optional 2nd arg is provided, template strings are interpreted\n\t*\t@return object\n\t*\t@param $source string|array\n\t*\t@param $allow bool\n\t**/\n\tfunction config($source,$allow=FALSE) {\n\t\tif (is_string($source))\n\t\t\t$source=$this->split($source);\n\t\tif ($allow)\n\t\t\t$preview=Preview::instance();\n\t\tforeach ($source as $file) {\n\t\t\tpreg_match_all(\n\t\t\t\t'/(?<=^|\\n)(?:'.\n\t\t\t\t\t'\\[(?<section>.+?)\\]|'.\n\t\t\t\t\t'(?<lval>[^\\h\\r\\n;].*?)\\h*=\\h*'.\n\t\t\t\t\t'(?<rval>(?:\\\\\\\\\\h*\\r?\\n|.+?)*)'.\n\t\t\t\t')(?=\\r?\\n|$)/',\n\t\t\t\t$this->read($file),\n\t\t\t\t$matches,PREG_SET_ORDER);\n\t\t\tif ($matches) {\n\t\t\t\t$sec='globals';\n\t\t\t\t$cmd=[];\n\t\t\t\tforeach ($matches as $match) {\n\t\t\t\t\tif ($match['section']) {\n\t\t\t\t\t\t$sec=$match['section'];\n\t\t\t\t\t\tif (preg_match(\n\t\t\t\t\t\t\t'/^(?!(?:global|config|route|map|redirect)s\\b)'.\n\t\t\t\t\t\t\t'(.*?)(?:\\s*[:>])/i',$sec,$msec) &&\n\t\t\t\t\t\t\t!$this->exists($msec[1]))\n\t\t\t\t\t\t\t$this->set($msec[1],NULL);\n\t\t\t\t\t\tpreg_match('/^(config|route|map|redirect)s\\b|'.\n\t\t\t\t\t\t\t'^(.+?)\\s*\\>\\s*(.*)/i',$sec,$cmd);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif ($allow)\n\t\t\t\t\t\tforeach (['lval','rval'] as $ndx)\n\t\t\t\t\t\t\t$match[$ndx]=$preview->\n\t\t\t\t\t\t\t\tresolve($match[$ndx],NULL,0,FALSE,FALSE);\n\t\t\t\t\tif (!empty($cmd)) {\n\t\t\t\t\t\tisset($cmd[3])?\n\t\t\t\t\t\t$this->call($cmd[3],\n\t\t\t\t\t\t\t[$match['lval'],$match['rval'],$cmd[2]]):\n\t\t\t\t\t\tcall_user_func_array(\n\t\t\t\t\t\t\t[$this,$cmd[1]],\n\t\t\t\t\t\t\tarray_merge([$match['lval']],\n\t\t\t\t\t\t\t\tstr_getcsv($cmd[1]=='config'?\n\t\t\t\t\t\t\t\t$this->cast($match['rval']):\n\t\t\t\t\t\t\t\t\t$match['rval'],\",\",'\"', \"\\\\\"))\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\t$rval=preg_replace(\n\t\t\t\t\t\t\t'/\\\\\\\\\\h*(\\r?\\n)/','\\1',$match['rval']);\n\t\t\t\t\t\t$ttl=NULL;\n\t\t\t\t\t\tif (preg_match('/^(.+)\\|\\h*(\\d+)$/',$rval,$tmp)) {\n\t\t\t\t\t\t\tarray_shift($tmp);\n\t\t\t\t\t\t\tlist($rval,$ttl)=$tmp;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t$args=array_map(\n\t\t\t\t\t\t\tfunction($val) {\n\t\t\t\t\t\t\t\t$val=$this->cast($val);\n\t\t\t\t\t\t\t\tif (is_string($val))\n\t\t\t\t\t\t\t\t\t$val=strlen($val)?\n\t\t\t\t\t\t\t\t\t\tpreg_replace('/\\\\\\\\\"/','\"',$val):\n\t\t\t\t\t\t\t\t\t\tNULL;\n\t\t\t\t\t\t\t\treturn $val;\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t// Mark quoted strings with 0x00 whitespace\n\t\t\t\t\t\t\tstr_getcsv(preg_replace(\n\t\t\t\t\t\t\t\t'/(?<!\\\\\\\\)(\")(.*?)\\1/',\n\t\t\t\t\t\t\t\t\"\\\\1\\x00\\\\2\\\\1\",trim($rval)),\",\",'\"', \"\\\\\")\n\t\t\t\t\t\t);\n\t\t\t\t\t\tpreg_match('/^(?<section>[^:]+)(?:\\:(?<func>.+))?/',\n\t\t\t\t\t\t\t$sec,$parts);\n\t\t\t\t\t\t$func=isset($parts['func'])?$parts['func']:NULL;\n\t\t\t\t\t\t$custom=(strtolower($parts['section'])!='globals');\n\t\t\t\t\t\tif ($func)\n\t\t\t\t\t\t\t$args=[$this->call($func,$args)];\n\t\t\t\t\t\tif (count($args)>1)\n\t\t\t\t\t\t\t$args=[$args];\n\t\t\t\t\t\tif (isset($ttl))\n\t\t\t\t\t\t\t$args=array_merge($args,[$ttl]);\n\t\t\t\t\t\tcall_user_func_array(\n\t\t\t\t\t\t\t[$this,'set'],\n\t\t\t\t\t\t\tarray_merge(\n\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t($custom?($parts['section'].'.'):'').\n\t\t\t\t\t\t\t\t\t$match['lval']\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t$args\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn $this;\n\t}\n\n\t/**\n\t*\tCreate mutex, invoke callback then drop ownership when done\n\t*\t@return mixed\n\t*\t@param $id string\n\t*\t@param $func callback\n\t*\t@param $args mixed\n\t**/\n\tfunction mutex($id,$func,$args=NULL) {\n\t\tif (!is_dir($tmp=$this->hive['TEMP']))\n\t\t\tmkdir($tmp,self::MODE,TRUE);\n\t\t// Use filesystem lock\n\t\tif (is_file($lock=$tmp.\n\t\t\t$this->hive['SEED'].'.'.$this->hash($id).'.lock') &&\n\t\t\tfilemtime($lock)+ini_get('max_execution_time')<microtime(TRUE))\n\t\t\t// Stale lock\n\t\t\t@unlink($lock);\n\t\twhile (!($handle=@fopen($lock,'x')) && !connection_aborted())\n\t\t\tusleep(mt_rand(0,100));\n\t\t$this->locks[$id]=$lock;\n\t\t$out=$this->call($func,$args);\n\t\tfclose($handle);\n\t\t@unlink($lock);\n\t\tunset($this->locks[$id]);\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tRead file (with option to apply Unix LF as standard line ending)\n\t*\t@return string\n\t*\t@param $file string\n\t*\t@param $lf bool\n\t**/\n\tfunction read($file, $lf = FALSE)\n\t{\n\t\tif (!file_exists($file)) {\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\t$out = file_get_contents($file);\n\n\t\treturn $lf ? preg_replace('/\\r\\n|\\r/', \"\\n\", $out) : $out;\n\t}\n\n\t/**\n\t*\tExclusive file write\n\t*\t@return int|FALSE\n\t*\t@param $file string\n\t*\t@param $data mixed\n\t*\t@param $append bool\n\t**/\n\tfunction write($file,$data,$append=FALSE) {\n\t\treturn file_put_contents($file,$data,$this->hive['LOCK']|($append?FILE_APPEND:0));\n\t}\n\n\t/**\n\t*\tApply syntax highlighting\n\t*\t@return string\n\t*\t@param $text string\n\t**/\n\tfunction highlight($text) {\n\t\t$out='';\n\t\t$pre=FALSE;\n\t\t$text=trim($text);\n\t\tif ($text && !preg_match('/^<\\?php/',$text)) {\n\t\t\t$text='<?php '.$text;\n\t\t\t$pre=TRUE;\n\t\t}\n\t\tforeach (token_get_all($text) as $token)\n\t\t\tif ($pre)\n\t\t\t\t$pre=FALSE;\n\t\t\telse\n\t\t\t\t$out.='<span'.\n\t\t\t\t\t(is_array($token)?\n\t\t\t\t\t\t(' class=\"'.\n\t\t\t\t\t\t\tsubstr(strtolower(token_name($token[0])),2).'\">'.\n\t\t\t\t\t\t\t$this->encode($token[1]).''):\n\t\t\t\t\t\t('>'.$this->encode($token))).\n\t\t\t\t\t'</span>';\n\t\treturn $out?('<code>'.$out.'</code>'):$text;\n\t}\n\n\t/**\n\t*\tDump expression with syntax highlighting\n\t*\t@param $expr mixed\n\t**/\n\tfunction dump($expr) {\n\t\techo $this->highlight($this->stringify($expr));\n\t}\n\n\t/**\n\t*\tReturn path (and query parameters) relative to the base directory\n\t*\t@return string\n\t*\t@param $url string\n\t**/\n\tfunction rel($url) {\n\t\treturn preg_replace('/^(?:https?:\\/\\/)?'.\n\t\t\tpreg_quote($this->hive['BASE'],'/').'(\\/.*|$)/','\\1',$url);\n\t}\n\n\t/**\n\t*\tNamespace-aware class autoloader\n\t*\t@return mixed\n\t*\t@param $class string\n\t**/\n\tprotected function autoload($class) {\n\t\t$class=$this->fixslashes(ltrim($class,'\\\\'));\n\t\t/** @var callable $func */\n\t\t$func=NULL;\n\t\tif (is_array($path=$this->hive['AUTOLOAD']) &&\n\t\t\tisset($path[1]) && is_callable($path[1]))\n\t\t\tlist($path,$func)=$path;\n\t\tforeach ($this->split($this->hive['PLUGINS'].';'.$path) as $auto)\n\t\t\tif (($func && is_file($file=$func($auto.$class).'.php')) ||\n\t\t\t\tis_file($file=$auto.$class.'.php') ||\n\t\t\t\tis_file($file=$auto.strtolower($class).'.php') ||\n\t\t\t\tis_file($file=strtolower($auto.$class).'.php'))\n\t\t\t\treturn require($file);\n\t}\n\n\t/**\n\t*\tExecute framework/application shutdown sequence\n\t*\t@param $cwd string\n\t**/\n\tfunction unload($cwd) {\n\t\tchdir($cwd);\n\t\tif (!($error=error_get_last()) &&\n\t\t\tsession_status()==PHP_SESSION_ACTIVE)\n\t\t\tsession_commit();\n\t\tforeach ($this->locks as $lock)\n\t\t\t@unlink($lock);\n\t\t$handler=$this->hive['UNLOAD'];\n\t\tif ((!$handler || $this->call($handler,$this)===FALSE) &&\n\t\t\t$error && in_array($error['type'],\n\t\t\t[E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR]))\n\t\t\t// Fatal error detected\n\t\t\t$this->error(500,\n\t\t\t\tsprintf(self::E_Fatal,$error['message']),[$error]);\n\t}\n\n\t/**\n\t*\tConvenience method for checking hive key\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\t#[\\ReturnTypeWillChange]\n\tfunction offsetexists($key) {\n\t\treturn $this->exists($key);\n\t}\n\n\t/**\n\t*\tConvenience method for assigning hive value\n\t*\t@return mixed\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\t#[\\ReturnTypeWillChange]\n\tfunction offsetset($key,$val) {\n\t\treturn $this->set($key,$val);\n\t}\n\n\t/**\n\t*\tConvenience method for retrieving hive value\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\t#[\\ReturnTypeWillChange]\n\tfunction &offsetget($key) {\n\t\t$val=&$this->ref($key);\n\t\treturn $val;\n\t}\n\n\t/**\n\t*\tConvenience method for removing hive key\n\t*\t@param $key string\n\t**/\n\t#[\\ReturnTypeWillChange]\n\tfunction offsetunset($key) {\n\t\t$this->clear($key);\n\t}\n\n\t/**\n\t*\tAlias for offsetexists()\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\tfunction __isset($key) {\n\t\treturn $this->offsetexists($key);\n\t}\n\n\t/**\n\t*\tAlias for offsetset()\n\t*\t@return mixed\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\tfunction __set($key,$val) {\n\t\treturn $this->offsetset($key,$val);\n\t}\n\n\t/**\n\t*\tAlias for offsetget()\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\tfunction &__get($key) {\n\t\t$val=&$this->offsetget($key);\n\t\treturn $val;\n\t}\n\n\t/**\n\t*\tAlias for offsetunset()\n\t*\t@param $key string\n\t**/\n\tfunction __unset($key) {\n\t\t$this->offsetunset($key);\n\t}\n\n\t/**\n\t*\tCall function identified by hive key\n\t*\t@return mixed\n\t*\t@param $key string\n\t*\t@param $args array\n\t**/\n\tfunction __call($key,array $args) {\n\t\tif ($this->exists($key,$val))\n\t\t\treturn call_user_func_array($val,$args);\n        throw new \\Exception(sprintf(self::E_Method,$key));\n\t}\n\n\t//! Prohibit cloning\n\tprivate function __clone() {\n\t}\n\n\t//! Bootstrap\n\tfunction __construct() {\n\t\t// Managed directives\n\t\tini_set('default_charset',$charset='UTF-8');\n\t\tif (extension_loaded('mbstring'))\n\t\t\tmb_internal_encoding($charset);\n\t\tini_set('display_errors',0);\n\t\t// Deprecated directives\n\t\t@ini_set('magic_quotes_gpc',0);\n\t\t@ini_set('register_globals',0);\n\t\t// Intercept errors/exceptions; PHP5.3-compatible\n\t\tif (PHP_VERSION_ID >= 80400) {\n\t\t\t$check = error_reporting((E_ALL) & ~(E_NOTICE | E_USER_NOTICE));\n\t\t} else {\n\t\t\t$check = error_reporting((E_ALL | E_STRICT) & ~(E_NOTICE | E_USER_NOTICE));\n\t\t}\n\t\tset_exception_handler(\n\t\t\tfunction($obj) {\n\t\t\t\t/** @var Exception $obj */\n\t\t\t\t$this->hive['EXCEPTION']=$obj;\n\t\t\t\t$this->error(500,\n\t\t\t\t\t$obj->getmessage().' '.\n\t\t\t\t\t'['.$obj->getFile().':'.$obj->getLine().']',\n\t\t\t\t\t$obj->gettrace());\n\t\t\t}\n\t\t);\n\t\tset_error_handler(\n\t\t\tfunction($level,$text,$file,$line) {\n\t\t\t\tif ($level & error_reporting()) {\n\t\t\t\t\t$trace=$this->trace(null, false);\n\t\t\t\t\tarray_unshift($trace,['file'=>$file,'line'=>$line]);\n\t\t\t\t\t$this->error(500,$text,$trace,$level);\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t\tif (!isset($_SERVER['SERVER_NAME']) || $_SERVER['SERVER_NAME']==='')\n\t\t\t$_SERVER['SERVER_NAME']=gethostname();\n\t\t$headers=[];\n\t\tif ($cli=(PHP_SAPI=='cli')) {\n\t\t\t// Emulate HTTP request\n\t\t\t$_SERVER['REQUEST_METHOD']='GET';\n\t\t\tif (!isset($_SERVER['argv'][1])) {\n\t\t\t\t++$_SERVER['argc'];\n\t\t\t\t$_SERVER['argv'][1]='/';\n\t\t\t}\n\t\t\t$req=$query='';\n\t\t\tif (substr($_SERVER['argv'][1],0,1)=='/') {\n\t\t\t\t$req=$_SERVER['argv'][1];\n\t\t\t\t$query=parse_url($req,PHP_URL_QUERY);\n\t\t\t} else {\n\t\t\t\tforeach($_SERVER['argv'] as $i=>$arg) {\n\t\t\t\t\tif (!$i) continue;\n\t\t\t\t\tif (preg_match('/^\\-(\\-)?(\\w+)(?:\\=(.*))?$/',$arg,$m)) {\n\t\t\t\t\t\tforeach($m[1]?[$m[2]]:str_split($m[2]) as $k)\n\t\t\t\t\t\t\t$query.=($query?'&':'').urlencode($k).'=';\n\t\t\t\t\t\tif (isset($m[3]))\n\t\t\t\t\t\t\t$query.=urlencode($m[3]);\n\t\t\t\t\t} else\n\t\t\t\t\t\t$req.='/'.$arg;\n\t\t\t\t}\n\t\t\t\tif (!$req)\n\t\t\t\t\t$req='/';\n\t\t\t\tif ($query)\n\t\t\t\t\t$req.='?'.$query;\n\t\t\t}\n\t\t\t$_SERVER['REQUEST_URI']=$req;\n\t\t\tparse_str($query?:'',$GLOBALS['_GET']);\n\t\t}\n\t\telseif (function_exists('getallheaders')) {\n\t\t\tforeach (getallheaders() as $key=>$val) {\n\t\t\t\t$tmp=strtoupper(strtr($key,'-','_'));\n\t\t\t\t// TODO: use ucwords delimiters for php 5.4.32+ & 5.5.16+\n\t\t\t\t$key=strtr(ucwords(strtolower(strtr($key,'-',' '))),' ','-');\n\t\t\t\t$headers[$key]=$val;\n\t\t\t\tif (isset($_SERVER['HTTP_'.$tmp]))\n\t\t\t\t\t$headers[$key]=&$_SERVER['HTTP_'.$tmp];\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tif (isset($_SERVER['CONTENT_LENGTH']))\n\t\t\t\t$headers['Content-Length']=&$_SERVER['CONTENT_LENGTH'];\n\t\t\tif (isset($_SERVER['CONTENT_TYPE']))\n\t\t\t\t$headers['Content-Type']=&$_SERVER['CONTENT_TYPE'];\n\t\t\tforeach (array_keys($_SERVER) as $key)\n\t\t\t\tif (substr($key,0,5)=='HTTP_')\n\t\t\t\t\t$headers[strtr(ucwords(strtolower(strtr(\n\t\t\t\t\t\tsubstr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key];\n\t\t}\n\t\tif (isset($headers['X-Http-Method-Override']))\n\t\t\t$_SERVER['REQUEST_METHOD']=$headers['X-Http-Method-Override'];\n\t\telseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method']))\n\t\t\t$_SERVER['REQUEST_METHOD']=strtoupper($_POST['_method']);\n\t\t$scheme=isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' ||\n\t\t\tisset($headers['X-Forwarded-Proto']) &&\n\t\t\t$headers['X-Forwarded-Proto']=='https'?'https':'http';\n\t\t// Create hive early on to expose header methods\n\t\t$this->hive=['HEADERS'=>&$headers];\n\t\tif (function_exists('apache_setenv')) {\n\t\t\t// Work around Apache pre-2.4 VirtualDocumentRoot bug\n\t\t\t$_SERVER['DOCUMENT_ROOT']=str_replace($_SERVER['SCRIPT_NAME'],'',\n\t\t\t\t$_SERVER['SCRIPT_FILENAME']);\n\t\t\tapache_setenv(\"DOCUMENT_ROOT\",$_SERVER['DOCUMENT_ROOT']);\n\t\t}\n\t\t$_SERVER['DOCUMENT_ROOT']=realpath($_SERVER['DOCUMENT_ROOT']);\n\t\t$base='';\n\t\tif (!$cli)\n\t\t\t$base=rtrim($this->fixslashes(\n\t\t\t\tdirname($_SERVER['SCRIPT_NAME'])),'/');\n\t\t$uri=parse_url((preg_match('/^\\w+:\\/\\//',$_SERVER['REQUEST_URI'])?'':\n\t\t\t\t$scheme.'://'.$_SERVER['SERVER_NAME']).$_SERVER['REQUEST_URI']);\n\t\t$_SERVER['REQUEST_URI']=$uri['path'].\n\t\t\t(isset($uri['query'])?'?'.$uri['query']:'').\n\t\t\t(isset($uri['fragment'])?'#'.$uri['fragment']:'');\n\t\t$path=preg_replace('/^'.preg_quote($base,'/').'/','',$uri['path']);\n\t\t$jar=[\n\t\t\t'expire'=>0,\n\t\t\t'lifetime'=>0,\n\t\t\t'path'=>$base?:'/',\n\t\t\t'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) &&\n\t\t\t\t!filter_var($_SERVER['SERVER_NAME'],FILTER_VALIDATE_IP)?\n\t\t\t\t$_SERVER['SERVER_NAME']:'',\n\t\t\t'secure'=>($scheme=='https'),\n\t\t\t'httponly'=>TRUE,\n\t\t\t'samesite'=>'Lax',\n\t\t];\n\t\t$port=80;\n\t\tif (!empty($headers['X-Forwarded-Port']))\n\t\t\t$port=$headers['X-Forwarded-Port'];\n\t\telseif (!empty($_SERVER['SERVER_PORT']))\n\t\t\t$port=$_SERVER['SERVER_PORT'];\n\t\t// Default configuration\n\t\t$this->hive+=[\n\t\t\t'AGENT'=>$this->agent(),\n\t\t\t'AJAX'=>$this->ajax(),\n\t\t\t'ALIAS'=>NULL,\n\t\t\t'ALIASES'=>[],\n\t\t\t'AUTOLOAD'=>'./',\n\t\t\t'BASE'=>$base,\n\t\t\t'BITMASK'=>ENT_COMPAT,\n\t\t\t'BODY'=>NULL,\n\t\t\t'CACHE'=>FALSE,\n\t\t\t'CASELESS'=>TRUE,\n\t\t\t'CLI'=>$cli,\n\t\t\t'CORS'=>[],\n\t\t\t'DEBUG'=>0,\n\t\t\t'DIACRITICS'=>[],\n\t\t\t'DNSBL'=>'',\n\t\t\t'EMOJI'=>[],\n\t\t\t'ENCODING'=>$charset,\n\t\t\t'ERROR'=>NULL,\n\t\t\t'ESCAPE'=>TRUE,\n\t\t\t'EXCEPTION'=>NULL,\n\t\t\t'EXEMPT'=>NULL,\n\t\t\t'FALLBACK'=>$this->fallback,\n\t\t\t'FORMATS'=>[],\n\t\t\t'FRAGMENT'=>isset($uri['fragment'])?$uri['fragment']:'',\n\t\t\t'HALT'=>TRUE,\n\t\t\t'HIGHLIGHT'=>FALSE,\n\t\t\t'HOST'=>$_SERVER['SERVER_NAME'],\n\t\t\t'IP'=>$this->ip(),\n\t\t\t'JAR'=>$jar,\n\t\t\t'LANGUAGE'=>isset($headers['Accept-Language'])?\n\t\t\t\t$this->language($headers['Accept-Language']):\n\t\t\t\t$this->fallback,\n\t\t\t'LOCALES'=>'./',\n\t\t\t'LOCK'=>LOCK_EX,\n\t\t\t'LOGGABLE'=>'*',\n\t\t\t'LOGS'=>'./',\n\t\t\t'MB'=>extension_loaded('mbstring'),\n\t\t\t'ONERROR'=>NULL,\n\t\t\t'ONREROUTE'=>NULL,\n\t\t\t'PACKAGE'=>self::PACKAGE,\n\t\t\t'PARAMS'=>[],\n\t\t\t'PATH'=>$path,\n\t\t\t'REROUTE_TRAILING_SLASH'=>TRUE,\n\t\t\t'PATTERN'=>NULL,\n\t\t\t'PLUGINS'=>$this->fixslashes(__DIR__).'/',\n\t\t\t'PORT'=>$port,\n\t\t\t'PREFIX'=>NULL,\n\t\t\t'PREMAP'=>'',\n\t\t\t'QUERY'=>isset($uri['query'])?$uri['query']:'',\n\t\t\t'QUIET'=>FALSE,\n\t\t\t'RAW'=>FALSE,\n\t\t\t'REALM'=>$scheme.'://'.$_SERVER['SERVER_NAME'].\n\t\t\t\t(!in_array($port,[80,443])?(':'.$port):'').\n\t\t\t\t$_SERVER['REQUEST_URI'],\n\t\t\t'RESPONSE'=>'',\n\t\t\t'ROOT'=>$_SERVER['DOCUMENT_ROOT'],\n\t\t\t'ROUTES'=>[],\n\t\t\t'SCHEME'=>$scheme,\n\t\t\t'SEED'=>$this->hash($_SERVER['SERVER_NAME'].$base),\n\t\t\t'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php',\n\t\t\t'TEMP'=>'tmp/',\n\t\t\t'TIME'=>&$_SERVER['REQUEST_TIME_FLOAT'],\n\t\t\t'TZ'=>@date_default_timezone_get(),\n\t\t\t'UI'=>'./',\n\t\t\t'UNLOAD'=>NULL,\n\t\t\t'UPLOADS'=>'./',\n\t\t\t'URI'=>&$_SERVER['REQUEST_URI'],\n\t\t\t'VERB'=>&$_SERVER['REQUEST_METHOD'],\n\t\t\t'VERSION'=>self::VERSION,\n\t\t\t'XFRAME'=>'SAMEORIGIN'\n\t\t];\n\t\t$this->hive['CORS']+=[\n\t\t\t'headers'=>'',\n\t\t\t'origin'=>FALSE,\n\t\t\t'credentials'=>FALSE,\n\t\t\t'expose'=>FALSE,\n\t\t\t'ttl'=>0\n\t\t];\n\t\tif (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) {\n\t\t\tunset($jar['expire']);\n\t\t\tsession_cache_limiter('');\n\t\t\tif (version_compare(PHP_VERSION, '7.3.0') >= 0)\n\t\t\t\tsession_set_cookie_params($jar);\n\t\t\telse {\n\t\t\t\tunset($jar['samesite']);\n\t\t\t\tcall_user_func_array('session_set_cookie_params',$jar);\n\t\t\t}\n\t\t}\n\t\tif (PHP_SAPI=='cli-server' &&\n\t\t\tpreg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI']))\n\t\t\t$this->reroute('/');\n\t\tif (ini_get('auto_globals_jit')) {\n\t\t\t// Override setting\n\t\t\t$GLOBALS['_ENV']=$_ENV;\n\t\t\t$GLOBALS['_REQUEST']=$_REQUEST;\n\t\t}\n\t\t// Sync PHP globals with corresponding hive keys\n\t\t$this->init=$this->hive;\n\t\tforeach (explode('|',self::GLOBALS) as $global) {\n\t\t\t$sync=$this->sync($global);\n\t\t\t$this->init+=[\n\t\t\t\t$global=>preg_match('/SERVER|ENV/',$global)?$sync:[]\n\t\t\t];\n\t\t}\n\t\tif ($check && $error=error_get_last())\n\t\t\t// Error detected\n\t\t\t$this->error(500,\n\t\t\t\tsprintf(self::E_Fatal,$error['message']),[$error]);\n\t\tdate_default_timezone_set($this->hive['TZ']);\n\t\t// Register framework autoloader\n\t\tspl_autoload_register([$this,'autoload']);\n\t\t// Register shutdown handler\n\t\tregister_shutdown_function([$this,'unload'],getcwd());\n\t}\n\n}\n\n//! Cache engine\nclass Cache extends Prefab {\n\n\tprotected\n\t\t//! Cache DSN\n\t\t$dsn,\n\t\t//! Prefix for cache entries\n\t\t$prefix,\n\t\t//! MemCache or Redis object\n\t\t$ref;\n\n\t/**\n\t*\tReturn timestamp and TTL of cache entry or FALSE if not found\n\t*\t@return array|FALSE\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\tfunction exists($key,&$val=NULL) {\n\t\t$fw=Base::instance();\n\t\tif (!$this->dsn)\n\t\t\treturn FALSE;\n\t\t$ndx=$this->prefix.'.'.$key;\n\t\t$parts=explode('=',$this->dsn,2);\n\t\tswitch ($parts[0]) {\n\t\t\tcase 'apc':\n\t\t\tcase 'apcu':\n\t\t\t\t$raw=call_user_func($parts[0].'_fetch',$ndx);\n\t\t\t\tbreak;\n\t\t\tcase 'redis':\n\t\t\t\t$raw=$this->ref->get($ndx);\n\t\t\t\tbreak;\n\t\t\tcase 'memcache':\n\t\t\t\t$raw=memcache_get($this->ref,$ndx);\n\t\t\t\tbreak;\n\t\t\tcase 'memcached':\n\t\t\t\t$raw=$this->ref->get($ndx);\n\t\t\t\tbreak;\n\t\t\tcase 'wincache':\n\t\t\t\t$raw=wincache_ucache_get($ndx);\n\t\t\t\tbreak;\n\t\t\tcase 'xcache':\n\t\t\t\t$raw=xcache_get($ndx);\n\t\t\t\tbreak;\n\t\t\tcase 'folder':\n\t\t\t\t$raw=$fw->read($parts[1]\n\t\t\t\t\t.str_replace(['/','\\\\'],'',$ndx));\n\t\t\t\tbreak;\n\t\t}\n\t\tif (!empty($raw)) {\n\t\t\tlist($val,$time,$ttl)=(array)$fw->unserialize($raw);\n\t\t\tif ($ttl===0 || $time+$ttl>microtime(TRUE))\n\t\t\t\treturn [$time,$ttl];\n\t\t\t$val=null;\n\t\t\t$this->clear($key);\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tStore value in cache\n\t*\t@return mixed|FALSE\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t*\t@param $ttl int\n\t**/\n\tfunction set($key,$val,$ttl=0) {\n\t\t$fw=Base::instance();\n\t\tif (!$this->dsn)\n\t\t\treturn TRUE;\n\t\t$ndx=$this->prefix.'.'.$key;\n\t\tif ($cached=$this->exists($key))\n\t\t\t$ttl=$cached[1];\n\t\t$data=$fw->serialize([$val,microtime(TRUE),$ttl]);\n\t\t$parts=explode('=',$this->dsn,2);\n\t\tswitch ($parts[0]) {\n\t\t\tcase 'apc':\n\t\t\tcase 'apcu':\n\t\t\t\treturn call_user_func($parts[0].'_store',$ndx,$data,$ttl);\n\t\t\tcase 'redis':\n\t\t\t\treturn $this->ref->set($ndx,$data,$ttl?['ex'=>$ttl]:[]);\n\t\t\tcase 'memcache':\n\t\t\t\treturn memcache_set($this->ref,$ndx,$data,0,$ttl);\n\t\t\tcase 'memcached':\n\t\t\t\treturn $this->ref->set($ndx,$data,$ttl);\n\t\t\tcase 'wincache':\n\t\t\t\treturn wincache_ucache_set($ndx,$data,$ttl);\n\t\t\tcase 'xcache':\n\t\t\t\treturn xcache_set($ndx,$data,$ttl);\n\t\t\tcase 'folder':\n\t\t\t\treturn $fw->write($parts[1].\n\t\t\t\t\tstr_replace(['/','\\\\'],'',$ndx),$data);\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tRetrieve value of cache entry\n\t*\t@return mixed|FALSE\n\t*\t@param $key string\n\t**/\n\tfunction get($key) {\n\t\treturn $this->dsn && $this->exists($key,$data)?$data:FALSE;\n\t}\n\n\t/**\n\t*\tDelete cache entry\n\t*\t@return bool\n\t*\t@param $key string\n\t**/\n\tfunction clear($key) {\n\t\tif (!$this->dsn)\n\t\t\treturn;\n\t\t$ndx=$this->prefix.'.'.$key;\n\t\t$parts=explode('=',$this->dsn,2);\n\t\tswitch ($parts[0]) {\n\t\t\tcase 'apc':\n\t\t\tcase 'apcu':\n\t\t\t\treturn call_user_func($parts[0].'_delete',$ndx);\n\t\t\tcase 'redis':\n\t\t\t\treturn $this->ref->del($ndx);\n\t\t\tcase 'memcache':\n\t\t\t\treturn memcache_delete($this->ref,$ndx);\n\t\t\tcase 'memcached':\n\t\t\t\treturn $this->ref->delete($ndx);\n\t\t\tcase 'wincache':\n\t\t\t\treturn wincache_ucache_delete($ndx);\n\t\t\tcase 'xcache':\n\t\t\t\treturn xcache_unset($ndx);\n\t\t\tcase 'folder':\n\t\t\t\treturn @unlink($parts[1].$ndx);\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tClear contents of cache backend\n\t*\t@return bool\n\t*\t@param $suffix string\n\t**/\n\tfunction reset($suffix=NULL) {\n\t\tif (!$this->dsn)\n\t\t\treturn TRUE;\n\t\t$regex='/'.preg_quote($this->prefix.'.','/').'.*'.\n\t\t\tpreg_quote($suffix?:'','/').'/';\n\t\t$parts=explode('=',$this->dsn,2);\n\t\tswitch ($parts[0]) {\n\t\t\tcase 'apc':\n\t\t\tcase 'apcu':\n\t\t\t\t$info=call_user_func($parts[0].'_cache_info',\n\t\t\t\t\t$parts[0]=='apcu'?FALSE:'user');\n\t\t\t\tif (!empty($info['cache_list'])) {\n\t\t\t\t\t$key=array_key_exists('info',\n\t\t\t\t\t\t$info['cache_list'][0])?'info':'key';\n\t\t\t\t\tforeach ($info['cache_list'] as $item)\n\t\t\t\t\t\tif (preg_match($regex,$item[$key]))\n\t\t\t\t\t\t\tcall_user_func($parts[0].'_delete',$item[$key]);\n\t\t\t\t}\n\t\t\t\treturn TRUE;\n\t\t\tcase 'redis':\n\t\t\t\t$keys=$this->ref->keys($this->prefix.'.*'.$suffix);\n\t\t\t\tforeach($keys as $key)\n\t\t\t\t\t$this->ref->del($key);\n\t\t\t\treturn TRUE;\n\t\t\tcase 'memcache':\n\t\t\t\tforeach (memcache_get_extended_stats(\n\t\t\t\t\t$this->ref,'slabs') as $slabs)\n\t\t\t\t\tforeach (array_filter(array_keys($slabs),'is_numeric')\n\t\t\t\t\t\tas $id)\n\t\t\t\t\t\tforeach (memcache_get_extended_stats(\n\t\t\t\t\t\t\t$this->ref,'cachedump',$id) as $data)\n\t\t\t\t\t\t\tif (is_array($data))\n\t\t\t\t\t\t\t\tforeach (array_keys($data) as $key)\n\t\t\t\t\t\t\t\t\tif (preg_match($regex,$key))\n\t\t\t\t\t\t\t\t\t\tmemcache_delete($this->ref,$key);\n\t\t\t\treturn TRUE;\n\t\t\tcase 'memcached':\n\t\t\t\tforeach ($this->ref->getallkeys()?:[] as $key)\n\t\t\t\t\tif (preg_match($regex,$key))\n\t\t\t\t\t\t$this->ref->delete($key);\n\t\t\t\treturn TRUE;\n\t\t\tcase 'wincache':\n\t\t\t\t$info=wincache_ucache_info();\n\t\t\t\tforeach ($info['ucache_entries'] as $item)\n\t\t\t\t\tif (preg_match($regex,$item['key_name']))\n\t\t\t\t\t\twincache_ucache_delete($item['key_name']);\n\t\t\t\treturn TRUE;\n\t\t\tcase 'xcache':\n\t\t\t\tif ($suffix && !ini_get('xcache.admin.enable_auth')) {\n\t\t\t\t\t$cnt=xcache_count(XC_TYPE_VAR);\n\t\t\t\t\tfor ($i=0;$i<$cnt;++$i) {\n\t\t\t\t\t\t$list=xcache_list(XC_TYPE_VAR,$i);\n\t\t\t\t\t\tforeach ($list['cache_list'] as $item)\n\t\t\t\t\t\t\tif (preg_match($regex,$item['name']))\n\t\t\t\t\t\t\t\txcache_unset($item['name']);\n\t\t\t\t\t}\n\t\t\t\t} else\n\t\t\t\t\txcache_unset_by_prefix($this->prefix.'.');\n\t\t\t\treturn TRUE;\n\t\t\tcase 'folder':\n\t\t\t\tif ($glob=@glob($parts[1].'*'))\n\t\t\t\t\tforeach ($glob as $file)\n\t\t\t\t\t\tif (preg_match($regex,basename($file)))\n\t\t\t\t\t\t\t@unlink($file);\n\t\t\t\treturn TRUE;\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tLoad/auto-detect cache backend\n\t*\t@return string\n\t*\t@param $dsn bool|string\n\t*\t@param $seed bool|string\n\t**/\n\tfunction load($dsn,$seed=NULL) {\n\t\t$fw=Base::instance();\n\t\tif ($dsn=trim($dsn)) {\n\t\t\tif (preg_match('/^redis=(.+)/',$dsn,$parts) &&\n\t\t\t\textension_loaded('redis')) {\n\t\t\t\tlist($host,$port,$db,$password)=explode(':',$parts[1])+[1=>6379,2=>NULL,3=>NULL];\n\t\t\t\t$this->ref=new Redis;\n\t\t\t\tif(!$this->ref->connect($host,$port,2))\n\t\t\t\t\t$this->ref=NULL;\n\t\t\t\tif(!empty($password))\n\t\t\t\t\t$this->ref->auth($password);\n\t\t\t\tif(isset($db))\n\t\t\t\t\t$this->ref->select($db);\n\t\t\t}\n\t\t\telseif (preg_match('/^memcache=(.+)/',$dsn,$parts) &&\n\t\t\t\textension_loaded('memcache'))\n\t\t\t\tforeach ($fw->split($parts[1]) as $server) {\n\t\t\t\t\tlist($host,$port)=explode(':',$server)+[1=>11211];\n\t\t\t\t\tif (empty($this->ref))\n\t\t\t\t\t\t$this->ref=@memcache_connect($host,$port)?:NULL;\n\t\t\t\t\telse\n\t\t\t\t\t\tmemcache_add_server($this->ref,$host,$port);\n\t\t\t\t}\n\t\t\telseif (preg_match('/^memcached=(.+)/',$dsn,$parts) &&\n\t\t\t\textension_loaded('memcached'))\n\t\t\t\tforeach ($fw->split($parts[1]) as $server) {\n\t\t\t\t\tlist($host,$port)=explode(':',$server)+[1=>11211];\n\t\t\t\t\tif (empty($this->ref))\n\t\t\t\t\t\t$this->ref=new Memcached();\n\t\t\t\t\t$this->ref->addServer($host,$port);\n\t\t\t\t}\n\t\t\tif (empty($this->ref) && !preg_match('/^folder\\h*=/',$dsn))\n\t\t\t\t$dsn=($grep=preg_grep('/^(apc|wincache|xcache)/',\n\t\t\t\t\tarray_map('strtolower',get_loaded_extensions())))?\n\t\t\t\t\t\t// Auto-detect\n\t\t\t\t\t\tcurrent($grep):\n\t\t\t\t\t\t// Use filesystem as fallback\n\t\t\t\t\t\t('folder='.$fw->TEMP.'cache/');\n\t\t\tif (preg_match('/^folder\\h*=\\h*(.+)/',$dsn,$parts) &&\n\t\t\t\t!is_dir($parts[1]))\n\t\t\t\tmkdir($parts[1],Base::MODE,TRUE);\n\t\t}\n\t\t$this->prefix=$seed?:$fw->SEED;\n\t\treturn $this->dsn=$dsn;\n\t}\n\n\t/**\n\t*\tClass constructor\n\t*\t@param $dsn bool|string\n\t**/\n\tfunction __construct($dsn=FALSE) {\n\t\tif ($dsn)\n\t\t\t$this->load($dsn);\n\t}\n\n}\n\n//! View handler\nclass View extends Prefab {\n\n\tprivate\n\t\t//! Temporary hive\n\t\t$temp;\n\n\tprotected\n\t\t//! Template file\n\t\t$file,\n\t\t//! Post-rendering handler\n\t\t$trigger,\n\t\t//! Nesting level\n\t\t$level=0;\n\n\t/** @var \\Base Framework instance */\n\tprotected $fw;\n\n\tfunction __construct() {\n\t\t$this->fw=\\Base::instance();\n\t}\n\n\t/**\n\t*\tEncode characters to equivalent HTML entities\n\t*\t@return string\n\t*\t@param $arg mixed\n\t**/\n\tfunction esc($arg) {\n\t\treturn $this->fw->recursive($arg,\n\t\t\tfunction($val) {\n\t\t\t\treturn is_string($val)?$this->fw->encode($val):$val;\n\t\t\t}\n\t\t);\n\t}\n\n\t/**\n\t*\tDecode HTML entities to equivalent characters\n\t*\t@return string\n\t*\t@param $arg mixed\n\t**/\n\tfunction raw($arg) {\n\t\treturn $this->fw->recursive($arg,\n\t\t\tfunction($val) {\n\t\t\t\treturn is_string($val)?$this->fw->decode($val):$val;\n\t\t\t}\n\t\t);\n\t}\n\n\t/**\n\t*\tCreate sandbox for template execution\n\t*\t@return string\n\t*\t@param $hive array\n\t*\t@param $mime string\n\t**/\n\tprotected function sandbox(?array $hive=NULL,$mime=NULL) {\n\t\t$fw=$this->fw;\n\t\t$implicit=FALSE;\n\t\tif (is_null($hive)) {\n\t\t\t$implicit=TRUE;\n\t\t\t$hive=$fw->hive();\n\t\t}\n\t\tif ($this->level<1 || $implicit) {\n\t\t\tif (!$fw->CLI && $mime && !headers_sent() &&\n\t\t\t\t!preg_grep ('/^Content-Type:/',headers_list()))\n\t\t\t\theader('Content-Type: '.$mime.'; '.\n\t\t\t\t\t'charset='.$fw->ENCODING);\n\t\t\tif ($fw->ESCAPE && (!$mime ||\n\t\t\t\t\tpreg_match('/^(text\\/html|(application|text)\\/(.+\\+)?xml)$/i',$mime)))\n\t\t\t\t$hive=$this->esc($hive);\n\t\t\tif (isset($hive['ALIASES']))\n\t\t\t\t$hive['ALIASES']=$fw->build($hive['ALIASES']);\n\t\t}\n\t\t$this->temp=$hive;\n\t\tunset($fw,$hive,$implicit,$mime);\n\t\textract($this->temp);\n\t\t$this->temp=NULL;\n\t\t++$this->level;\n\t\tob_start();\n\t\trequire($this->file);\n\t\t--$this->level;\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t*\tRender template\n\t*\t@return string\n\t*\t@param $file string\n\t*\t@param $mime string\n\t*\t@param $hive array\n\t*\t@param $ttl int\n\t**/\n\tfunction render($file,$mime='text/html',?array $hive=NULL,$ttl=0) {\n\t\t$fw=$this->fw;\n\t\t$cache=Cache::instance();\n\t\tforeach ($fw->split($fw->UI) as $dir) {\n\t\t\tif ($cache->exists($hash=$fw->hash($dir.$file),$data))\n\t\t\t\treturn $data;\n\t\t\tif (is_file($this->file=$fw->fixslashes($dir.$file))) {\n\t\t\t\tif (isset($_COOKIE[session_name()]) &&\n\t\t\t\t\t!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)\n\t\t\t\t\tsession_start();\n\t\t\t\t$fw->sync('SESSION');\n\t\t\t\t$data=$this->sandbox($hive,$mime);\n\t\t\t\tif (isset($this->trigger['afterrender']))\n\t\t\t\t\tforeach($this->trigger['afterrender'] as $func)\n\t\t\t\t\t\t$data=$fw->call($func,[$data, $dir.$file]);\n\t\t\t\tif ($ttl)\n\t\t\t\t\t$cache->set($hash,$data,$ttl);\n\t\t\t\treturn $data;\n\t\t\t}\n\t\t}\n        throw new \\Exception(sprintf(Base::E_Open,$file));\n\t}\n\n\t/**\n\t*\tpost rendering handler\n\t*\t@param $func callback\n\t*/\n\tfunction afterrender($func) {\n\t\t$this->trigger['afterrender'][]=$func;\n\t}\n\n}\n\n//! Lightweight template engine\nclass Preview extends View {\n\n\tprotected\n\t\t//! token filter\n\t\t$filter=[\n\t\t\t'c'=>'$this->c',\n\t\t\t'esc'=>'$this->esc',\n\t\t\t'raw'=>'$this->raw',\n\t\t\t'export'=>'Base::instance()->export',\n\t\t\t'alias'=>'Base::instance()->alias',\n\t\t\t'format'=>'Base::instance()->format'\n\t\t];\n\n\tprotected\n\t\t//! newline interpolation\n\t\t$interpolation=true;\n\n\t/**\n\t * Enable/disable markup parsing interpolation\n\t * mainly used for adding appropriate newlines\n\t * @param $bool bool\n\t */\n\tfunction interpolation($bool) {\n\t\t$this->interpolation=$bool;\n\t}\n\n\t/**\n\t*\tReturn C-locale equivalent of number\n\t*\t@return string\n\t*\t@param $val int|float\n\t**/\n\tfunction c($val) {\n\t\t$locale=setlocale(LC_NUMERIC,0);\n\t\tsetlocale(LC_NUMERIC,'C');\n\t\t$out=(string)(float)$val;\n\t\t$locale=setlocale(LC_NUMERIC,$locale);\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tConvert token to variable\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction token($str) {\n\t\t$str=trim(preg_replace('/\\{\\{(.+?)\\}\\}/s','\\1',$this->fw->compile($str)));\n\t\tif (preg_match('/^(.+)(?<!\\|)\\|((?:\\h*\\w+(?:\\h*[,;]?))+)$/s',\n\t\t\t$str,$parts)) {\n\t\t\t$str=trim($parts[1]);\n\t\t\tforeach ($this->fw->split(trim($parts[2],\"\\xC2\\xA0\")) as $func)\n\t\t\t\t$str=((empty($this->filter[$cmd=$func]) &&\n\t\t\t\t\tfunction_exists($cmd)) ||\n\t\t\t\t\tis_string($cmd=$this->filter($func)))?\n\t\t\t\t\t$cmd.'('.$str.')':\n\t\t\t\t\t'Base::instance()->'.\n\t\t\t\t\t\t'call($this->filter(\\''.$func.'\\'),['.$str.'])';\n\t\t}\n\t\treturn $str;\n\t}\n\n\t/**\n\t*\tRegister or get (one specific or all) token filters\n\t*\t@param string $key\n\t*\t@param string|closure $func\n\t*\t@return array|closure|string\n\t*/\n\tfunction filter($key=NULL,$func=NULL) {\n\t\tif (!$key)\n\t\t\treturn array_keys($this->filter);\n\t\t$key=strtolower($key);\n\t\tif (!$func)\n\t\t\treturn $this->filter[$key];\n\t\t$this->filter[$key]=$func;\n\t}\n\n\t/**\n\t*\tAssemble markup\n\t*\t@return string\n\t*\t@param $node string\n\t**/\n\tprotected function build($node) {\n\t\treturn preg_replace_callback(\n\t\t\t'/\\{~(.+?)~\\}|\\{\\*(.+?)\\*\\}|\\{\\-(.+?)\\-\\}|'.\n\t\t\t'\\{\\{(.+?)\\}\\}((\\r?\\n)*)/s',\n\t\t\tfunction($expr) {\n\t\t\t\tif ($expr[1])\n\t\t\t\t\t$str='<?php '.$this->token($expr[1]).' ?>';\n\t\t\t\telseif ($expr[2])\n\t\t\t\t\treturn '';\n\t\t\t\telseif ($expr[3])\n\t\t\t\t\t$str=$expr[3];\n\t\t\t\telse {\n\t\t\t\t\t$str='<?= ('.trim($this->token($expr[4])).')'.\n\t\t\t\t\t\t($this->interpolation?\n\t\t\t\t\t\t\t(!empty($expr[6])?'.\"'.$expr[6].'\"':''):'').' ?>';\n\t\t\t\t\tif (isset($expr[5]))\n\t\t\t\t\t\t$str.=$expr[5];\n\t\t\t\t}\n\t\t\t\treturn $str;\n\t\t\t},\n\t\t\t$node\n\t\t);\n\t}\n\n\t/**\n\t*\tRender template string\n\t*\t@return string\n\t*\t@param $node string|array\n\t*\t@param $hive array\n\t*\t@param $ttl int\n\t*\t@param $persist bool\n\t*\t@param $escape bool\n\t**/\n\tfunction resolve($node,?array $hive=NULL,$ttl=0,$persist=FALSE,$escape=NULL) {\n\t\t$hash=null;\n\t\t$fw=$this->fw;\n\t\t$cache=Cache::instance();\n\t\tif ($escape!==NULL) {\n\t\t\t$esc=$fw->ESCAPE;\n\t\t\t$fw->ESCAPE=$escape;\n\t\t}\n\t\tif ($ttl || $persist)\n\t\t\t$hash=$fw->hash($fw->serialize($node));\n\t\tif ($ttl && $cache->exists($hash,$data))\n\t\t\treturn $data;\n\t\tif ($persist) {\n\t\t\tif (!is_dir($tmp=$fw->TEMP))\n\t\t\t\tmkdir($tmp,Base::MODE,TRUE);\n\t\t\tif (!is_file($this->file=($tmp.\n\t\t\t\t$fw->SEED.'.'.$hash.'.php')))\n\t\t\t\t$fw->write($this->file,$this->build($node));\n\t\t\tif (isset($_COOKIE[session_name()]) &&\n\t\t\t\t!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)\n\t\t\t\tsession_start();\n\t\t\t$fw->sync('SESSION');\n\t\t\t$data=$this->sandbox($hive);\n\t\t}\n\t\telse {\n\t\t\tif (!$hive)\n\t\t\t\t$hive=$fw->hive();\n\t\t\tif ($fw->ESCAPE)\n\t\t\t\t$hive=$this->esc($hive);\n\t\t\textract($hive);\n\t\t\tunset($hive);\n\t\t\tob_start();\n\t\t\teval(' ?>'.$this->build($node).'<?php ');\n\t\t\t$data=ob_get_clean();\n\t\t}\n\t\tif ($ttl)\n\t\t\t$cache->set($hash,$data,$ttl);\n\t\tif ($escape!==NULL)\n\t\t\t$fw->ESCAPE=$esc;\n\t\treturn $data;\n\t}\n\n\t/**\n\t *\tParse template string\n\t *\t@return string\n\t *\t@param $text string\n\t **/\n\tfunction parse($text) {\n\t\t// Remove PHP code and comments\n\t\treturn preg_replace(\n\t\t\t'/\\h*<\\?(?!xml)(?:php|\\s*=)?.+?\\?>\\h*|'.\n\t\t\t'\\{\\*.+?\\*\\}/is','', $text);\n\t}\n\n\t/**\n\t*\tRender template\n\t*\t@return string\n\t*\t@param $file string\n\t*\t@param $mime string\n\t*\t@param $hive array\n\t*\t@param $ttl int\n\t**/\n\tfunction render($file,$mime='text/html',?array $hive=NULL,$ttl=0) {\n\t\t$fw=$this->fw;\n\t\t$cache=Cache::instance();\n\t\tif (!is_dir($tmp=$fw->TEMP))\n\t\t\tmkdir($tmp,Base::MODE,TRUE);\n\t\tforeach ($fw->split($fw->UI) as $dir) {\n\t\t\tif ($cache->exists($hash=$fw->hash($dir.$file),$data))\n\t\t\t\treturn $data;\n\t\t\tif (is_file($view=$fw->fixslashes($dir.$file))) {\n\t\t\t\tif (!is_file($this->file=($tmp.\n\t\t\t\t\t$fw->SEED.'.'.$fw->hash($view).'.php')) ||\n\t\t\t\t\tfilemtime($this->file)<filemtime($view)) {\n\t\t\t\t\t$contents=$fw->read($view);\n\t\t\t\t\tif (isset($this->trigger['beforerender']))\n\t\t\t\t\t\tforeach ($this->trigger['beforerender'] as $func)\n\t\t\t\t\t\t\t$contents=$fw->call($func, [$contents, $view]);\n\t\t\t\t\t$text=$this->parse($contents);\n\t\t\t\t\t$fw->write($this->file,$this->build($text));\n\t\t\t\t}\n\t\t\t\tif (isset($_COOKIE[session_name()]) &&\n\t\t\t\t\t!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)\n\t\t\t\t\tsession_start();\n\t\t\t\t$fw->sync('SESSION');\n\t\t\t\t$data=$this->sandbox($hive,$mime);\n\t\t\t\tif(isset($this->trigger['afterrender']))\n\t\t\t\t\tforeach ($this->trigger['afterrender'] as $func)\n\t\t\t\t\t\t$data=$fw->call($func, [$data, $view]);\n\t\t\t\tif ($ttl)\n\t\t\t\t\t$cache->set($hash,$data,$ttl);\n\t\t\t\treturn $data;\n\t\t\t}\n\t\t}\n        throw new \\Exception(sprintf(Base::E_Open,$file));\n\t}\n\n\t/**\n\t *\tpost rendering handler\n\t *\t@param $func callback\n\t */\n\tfunction beforerender($func) {\n\t\t$this->trigger['beforerender'][]=$func;\n\t}\n\n}\n\n//! ISO language/country codes\nclass ISO extends Prefab {\n\n\t//@{ ISO 3166-1 country codes\n\tconst\n\t\tCC_af='Afghanistan',\n\t\tCC_ax='Åland Islands',\n\t\tCC_al='Albania',\n\t\tCC_dz='Algeria',\n\t\tCC_as='American Samoa',\n\t\tCC_ad='Andorra',\n\t\tCC_ao='Angola',\n\t\tCC_ai='Anguilla',\n\t\tCC_aq='Antarctica',\n\t\tCC_ag='Antigua and Barbuda',\n\t\tCC_ar='Argentina',\n\t\tCC_am='Armenia',\n\t\tCC_aw='Aruba',\n\t\tCC_au='Australia',\n\t\tCC_at='Austria',\n\t\tCC_az='Azerbaijan',\n\t\tCC_bs='Bahamas',\n\t\tCC_bh='Bahrain',\n\t\tCC_bd='Bangladesh',\n\t\tCC_bb='Barbados',\n\t\tCC_by='Belarus',\n\t\tCC_be='Belgium',\n\t\tCC_bz='Belize',\n\t\tCC_bj='Benin',\n\t\tCC_bm='Bermuda',\n\t\tCC_bt='Bhutan',\n\t\tCC_bo='Bolivia',\n\t\tCC_bq='Bonaire, Sint Eustatius and Saba',\n\t\tCC_ba='Bosnia and Herzegovina',\n\t\tCC_bw='Botswana',\n\t\tCC_bv='Bouvet Island',\n\t\tCC_br='Brazil',\n\t\tCC_io='British Indian Ocean Territory',\n\t\tCC_bn='Brunei Darussalam',\n\t\tCC_bg='Bulgaria',\n\t\tCC_bf='Burkina Faso',\n\t\tCC_bi='Burundi',\n\t\tCC_kh='Cambodia',\n\t\tCC_cm='Cameroon',\n\t\tCC_ca='Canada',\n\t\tCC_cv='Cape Verde',\n\t\tCC_ky='Cayman Islands',\n\t\tCC_cf='Central African Republic',\n\t\tCC_td='Chad',\n\t\tCC_cl='Chile',\n\t\tCC_cn='China',\n\t\tCC_cx='Christmas Island',\n\t\tCC_cc='Cocos (Keeling) Islands',\n\t\tCC_co='Colombia',\n\t\tCC_km='Comoros',\n\t\tCC_cg='Congo',\n\t\tCC_cd='Congo, The Democratic Republic of',\n\t\tCC_ck='Cook Islands',\n\t\tCC_cr='Costa Rica',\n\t\tCC_ci='Côte d\\'ivoire',\n\t\tCC_hr='Croatia',\n\t\tCC_cu='Cuba',\n\t\tCC_cw='Curaçao',\n\t\tCC_cy='Cyprus',\n\t\tCC_cz='Czech Republic',\n\t\tCC_dk='Denmark',\n\t\tCC_dj='Djibouti',\n\t\tCC_dm='Dominica',\n\t\tCC_do='Dominican Republic',\n\t\tCC_ec='Ecuador',\n\t\tCC_eg='Egypt',\n\t\tCC_sv='El Salvador',\n\t\tCC_gq='Equatorial Guinea',\n\t\tCC_er='Eritrea',\n\t\tCC_ee='Estonia',\n\t\tCC_et='Ethiopia',\n\t\tCC_fk='Falkland Islands (Malvinas)',\n\t\tCC_fo='Faroe Islands',\n\t\tCC_fj='Fiji',\n\t\tCC_fi='Finland',\n\t\tCC_fr='France',\n\t\tCC_gf='French Guiana',\n\t\tCC_pf='French Polynesia',\n\t\tCC_tf='French Southern Territories',\n\t\tCC_ga='Gabon',\n\t\tCC_gm='Gambia',\n\t\tCC_ge='Georgia',\n\t\tCC_de='Germany',\n\t\tCC_gh='Ghana',\n\t\tCC_gi='Gibraltar',\n\t\tCC_gr='Greece',\n\t\tCC_gl='Greenland',\n\t\tCC_gd='Grenada',\n\t\tCC_gp='Guadeloupe',\n\t\tCC_gu='Guam',\n\t\tCC_gt='Guatemala',\n\t\tCC_gg='Guernsey',\n\t\tCC_gn='Guinea',\n\t\tCC_gw='Guinea-Bissau',\n\t\tCC_gy='Guyana',\n\t\tCC_ht='Haiti',\n\t\tCC_hm='Heard Island and McDonald Islands',\n\t\tCC_va='Holy See (Vatican City State)',\n\t\tCC_hn='Honduras',\n\t\tCC_hk='Hong Kong',\n\t\tCC_hu='Hungary',\n\t\tCC_is='Iceland',\n\t\tCC_in='India',\n\t\tCC_id='Indonesia',\n\t\tCC_ir='Iran, Islamic Republic of',\n\t\tCC_iq='Iraq',\n\t\tCC_ie='Ireland',\n\t\tCC_im='Isle of Man',\n\t\tCC_il='Israel',\n\t\tCC_it='Italy',\n\t\tCC_jm='Jamaica',\n\t\tCC_jp='Japan',\n\t\tCC_je='Jersey',\n\t\tCC_jo='Jordan',\n\t\tCC_kz='Kazakhstan',\n\t\tCC_ke='Kenya',\n\t\tCC_ki='Kiribati',\n\t\tCC_kp='Korea, Democratic People\\'s Republic of',\n\t\tCC_kr='Korea, Republic of',\n\t\tCC_kw='Kuwait',\n\t\tCC_kg='Kyrgyzstan',\n\t\tCC_la='Lao People\\'s Democratic Republic',\n\t\tCC_lv='Latvia',\n\t\tCC_lb='Lebanon',\n\t\tCC_ls='Lesotho',\n\t\tCC_lr='Liberia',\n\t\tCC_ly='Libya',\n\t\tCC_li='Liechtenstein',\n\t\tCC_lt='Lithuania',\n\t\tCC_lu='Luxembourg',\n\t\tCC_mo='Macao',\n\t\tCC_mk='Macedonia, The Former Yugoslav Republic of',\n\t\tCC_mg='Madagascar',\n\t\tCC_mw='Malawi',\n\t\tCC_my='Malaysia',\n\t\tCC_mv='Maldives',\n\t\tCC_ml='Mali',\n\t\tCC_mt='Malta',\n\t\tCC_mh='Marshall Islands',\n\t\tCC_mq='Martinique',\n\t\tCC_mr='Mauritania',\n\t\tCC_mu='Mauritius',\n\t\tCC_yt='Mayotte',\n\t\tCC_mx='Mexico',\n\t\tCC_fm='Micronesia, Federated States of',\n\t\tCC_md='Moldova, Republic of',\n\t\tCC_mc='Monaco',\n\t\tCC_mn='Mongolia',\n\t\tCC_me='Montenegro',\n\t\tCC_ms='Montserrat',\n\t\tCC_ma='Morocco',\n\t\tCC_mz='Mozambique',\n\t\tCC_mm='Myanmar',\n\t\tCC_na='Namibia',\n\t\tCC_nr='Nauru',\n\t\tCC_np='Nepal',\n\t\tCC_nl='Netherlands',\n\t\tCC_nc='New Caledonia',\n\t\tCC_nz='New Zealand',\n\t\tCC_ni='Nicaragua',\n\t\tCC_ne='Niger',\n\t\tCC_ng='Nigeria',\n\t\tCC_nu='Niue',\n\t\tCC_nf='Norfolk Island',\n\t\tCC_mp='Northern Mariana Islands',\n\t\tCC_no='Norway',\n\t\tCC_om='Oman',\n\t\tCC_pk='Pakistan',\n\t\tCC_pw='Palau',\n\t\tCC_ps='Palestinian Territory, Occupied',\n\t\tCC_pa='Panama',\n\t\tCC_pg='Papua New Guinea',\n\t\tCC_py='Paraguay',\n\t\tCC_pe='Peru',\n\t\tCC_ph='Philippines',\n\t\tCC_pn='Pitcairn',\n\t\tCC_pl='Poland',\n\t\tCC_pt='Portugal',\n\t\tCC_pr='Puerto Rico',\n\t\tCC_qa='Qatar',\n\t\tCC_re='Réunion',\n\t\tCC_ro='Romania',\n\t\tCC_ru='Russian Federation',\n\t\tCC_rw='Rwanda',\n\t\tCC_bl='Saint Barthélemy',\n\t\tCC_sh='Saint Helena, Ascension and Tristan da Cunha',\n\t\tCC_kn='Saint Kitts and Nevis',\n\t\tCC_lc='Saint Lucia',\n\t\tCC_mf='Saint Martin (French Part)',\n\t\tCC_pm='Saint Pierre and Miquelon',\n\t\tCC_vc='Saint Vincent and The Grenadines',\n\t\tCC_ws='Samoa',\n\t\tCC_sm='San Marino',\n\t\tCC_st='Sao Tome and Principe',\n\t\tCC_sa='Saudi Arabia',\n\t\tCC_sn='Senegal',\n\t\tCC_rs='Serbia',\n\t\tCC_sc='Seychelles',\n\t\tCC_sl='Sierra Leone',\n\t\tCC_sg='Singapore',\n\t\tCC_sk='Slovakia',\n\t\tCC_sx='Sint Maarten (Dutch Part)',\n\t\tCC_si='Slovenia',\n\t\tCC_sb='Solomon Islands',\n\t\tCC_so='Somalia',\n\t\tCC_za='South Africa',\n\t\tCC_gs='South Georgia and The South Sandwich Islands',\n\t\tCC_ss='South Sudan',\n\t\tCC_es='Spain',\n\t\tCC_lk='Sri Lanka',\n\t\tCC_sd='Sudan',\n\t\tCC_sr='Suriname',\n\t\tCC_sj='Svalbard and Jan Mayen',\n\t\tCC_sz='Swaziland',\n\t\tCC_se='Sweden',\n\t\tCC_ch='Switzerland',\n\t\tCC_sy='Syrian Arab Republic',\n\t\tCC_tw='Taiwan, Province of China',\n\t\tCC_tj='Tajikistan',\n\t\tCC_tz='Tanzania, United Republic of',\n\t\tCC_th='Thailand',\n\t\tCC_tl='Timor-Leste',\n\t\tCC_tg='Togo',\n\t\tCC_tk='Tokelau',\n\t\tCC_to='Tonga',\n\t\tCC_tt='Trinidad and Tobago',\n\t\tCC_tn='Tunisia',\n\t\tCC_tr='Turkey',\n\t\tCC_tm='Turkmenistan',\n\t\tCC_tc='Turks and Caicos Islands',\n\t\tCC_tv='Tuvalu',\n\t\tCC_ug='Uganda',\n\t\tCC_ua='Ukraine',\n\t\tCC_ae='United Arab Emirates',\n\t\tCC_gb='United Kingdom',\n\t\tCC_us='United States',\n\t\tCC_um='United States Minor Outlying Islands',\n\t\tCC_uy='Uruguay',\n\t\tCC_uz='Uzbekistan',\n\t\tCC_vu='Vanuatu',\n\t\tCC_ve='Venezuela',\n\t\tCC_vn='Viet Nam',\n\t\tCC_vg='Virgin Islands, British',\n\t\tCC_vi='Virgin Islands, U.S.',\n\t\tCC_wf='Wallis and Futuna',\n\t\tCC_eh='Western Sahara',\n\t\tCC_ye='Yemen',\n\t\tCC_zm='Zambia',\n\t\tCC_zw='Zimbabwe';\n\t//@}\n\n\t//@{ ISO 639-1 language codes (Windows-compatibility subset)\n\tconst\n\t\tLC_af='Afrikaans',\n\t\tLC_am='Amharic',\n\t\tLC_ar='Arabic',\n\t\tLC_as='Assamese',\n\t\tLC_ba='Bashkir',\n\t\tLC_be='Belarusian',\n\t\tLC_bg='Bulgarian',\n\t\tLC_bn='Bengali',\n\t\tLC_bo='Tibetan',\n\t\tLC_br='Breton',\n\t\tLC_ca='Catalan',\n\t\tLC_co='Corsican',\n\t\tLC_cs='Czech',\n\t\tLC_cy='Welsh',\n\t\tLC_da='Danish',\n\t\tLC_de='German',\n\t\tLC_dv='Divehi',\n\t\tLC_el='Greek',\n\t\tLC_en='English',\n\t\tLC_es='Spanish',\n\t\tLC_et='Estonian',\n\t\tLC_eu='Basque',\n\t\tLC_fa='Persian',\n\t\tLC_fi='Finnish',\n\t\tLC_fo='Faroese',\n\t\tLC_fr='French',\n\t\tLC_gd='Scottish Gaelic',\n\t\tLC_gl='Galician',\n\t\tLC_gu='Gujarati',\n\t\tLC_he='Hebrew',\n\t\tLC_hi='Hindi',\n\t\tLC_hr='Croatian',\n\t\tLC_hu='Hungarian',\n\t\tLC_hy='Armenian',\n\t\tLC_id='Indonesian',\n\t\tLC_ig='Igbo',\n\t\tLC_is='Icelandic',\n\t\tLC_it='Italian',\n\t\tLC_ja='Japanese',\n\t\tLC_ka='Georgian',\n\t\tLC_kk='Kazakh',\n\t\tLC_km='Khmer',\n\t\tLC_kn='Kannada',\n\t\tLC_ko='Korean',\n\t\tLC_lb='Luxembourgish',\n\t\tLC_lo='Lao',\n\t\tLC_lt='Lithuanian',\n\t\tLC_lv='Latvian',\n\t\tLC_mi='Maori',\n\t\tLC_ml='Malayalam',\n\t\tLC_mr='Marathi',\n\t\tLC_ms='Malay',\n\t\tLC_mt='Maltese',\n\t\tLC_ne='Nepali',\n\t\tLC_nl='Dutch',\n\t\tLC_no='Norwegian',\n\t\tLC_oc='Occitan',\n\t\tLC_or='Oriya',\n\t\tLC_pl='Polish',\n\t\tLC_ps='Pashto',\n\t\tLC_pt='Portuguese',\n\t\tLC_qu='Quechua',\n\t\tLC_ro='Romanian',\n\t\tLC_ru='Russian',\n\t\tLC_rw='Kinyarwanda',\n\t\tLC_sa='Sanskrit',\n\t\tLC_si='Sinhala',\n\t\tLC_sk='Slovak',\n\t\tLC_sl='Slovenian',\n\t\tLC_sq='Albanian',\n\t\tLC_sv='Swedish',\n\t\tLC_ta='Tamil',\n\t\tLC_te='Telugu',\n\t\tLC_th='Thai',\n\t\tLC_tk='Turkmen',\n\t\tLC_tr='Turkish',\n\t\tLC_tt='Tatar',\n\t\tLC_uk='Ukrainian',\n\t\tLC_ur='Urdu',\n\t\tLC_vi='Vietnamese',\n\t\tLC_wo='Wolof',\n\t\tLC_yo='Yoruba',\n\t\tLC_zh='Chinese';\n\t//@}\n\n\t/**\n\t*\tReturn list of languages indexed by ISO 639-1 language code\n\t*\t@return array\n\t**/\n\tfunction languages() {\n\t\treturn \\Base::instance()->constants($this,'LC_');\n\t}\n\n\t/**\n\t*\tReturn list of countries indexed by ISO 3166-1 country code\n\t*\t@return array\n\t**/\n\tfunction countries() {\n\t\treturn \\Base::instance()->constants($this,'CC_');\n\t}\n\n}\n\n//! Container for singular object instances\nfinal class Registry {\n\n\tprivate static\n\t\t//! Object catalog\n\t\t$table;\n\n\t/**\n\t*\tReturn TRUE if object exists in catalog\n\t*\t@return bool\n\t*\t@param $key string\n\t**/\n\tstatic function exists($key) {\n\t\treturn isset(self::$table[$key]);\n\t}\n\n\t/**\n\t*\tAdd object to catalog\n\t*\t@return object\n\t*\t@param $key string\n\t*\t@param $obj object\n\t**/\n\tstatic function set($key,$obj) {\n\t\treturn self::$table[$key]=$obj;\n\t}\n\n\t/**\n\t*\tRetrieve object from catalog\n\t*\t@return object\n\t*\t@param $key string\n\t**/\n\tstatic function get($key) {\n\t\treturn self::$table[$key];\n\t}\n\n\t/**\n\t*\tDelete object from catalog\n\t*\t@param $key string\n\t**/\n\tstatic function clear($key) {\n\t\tself::$table[$key]=NULL;\n\t\tunset(self::$table[$key]);\n\t}\n\n\t//! Prohibit cloning\n\tprivate function __clone() {\n\t}\n\n\t//! Prohibit instantiation\n\tprivate function __construct() {\n\t}\n\n}\n\nreturn Base::instance();\n"
  },
  {
    "path": "basket.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Session-based pseudo-mapper\nclass Basket extends Magic {\n\n\t//@{ Error messages\n\tconst\n\t\tE_Field='Undefined field %s';\n\t//@}\n\n\tprotected\n\t\t//! Session key\n\t\t$key,\n\t\t//! Current item identifier\n\t\t$id,\n\t\t//! Current item contents\n\t\t$item=[];\n\n\t/**\n\t*\tReturn TRUE if field is defined\n\t*\t@return bool\n\t*\t@param $key string\n\t**/\n\tfunction exists($key) {\n\t\treturn array_key_exists($key,$this->item);\n\t}\n\n\t/**\n\t*\tAssign value to field\n\t*\t@return scalar|FALSE\n\t*\t@param $key string\n\t*\t@param $val scalar\n\t**/\n\tfunction set($key,$val) {\n\t\treturn ($key=='_id')?FALSE:($this->item[$key]=$val);\n\t}\n\n\t/**\n\t*\tRetrieve value of field\n\t*\t@return scalar|FALSE\n\t*\t@param $key string\n\t**/\n\tfunction &get($key) {\n\t\tif ($key=='_id')\n\t\t\treturn $this->id;\n\t\tif (array_key_exists($key,$this->item))\n\t\t\treturn $this->item[$key];\n        throw new \\Exception(sprintf(self::E_Field,$key));\n\t}\n\n\t/**\n\t*\tDelete field\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\tfunction clear($key) {\n\t\tunset($this->item[$key]);\n\t}\n\n\t/**\n\t*\tReturn items that match key/value pair;\n\t*\tIf no key/value pair specified, return all items\n\t*\t@return array\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\tfunction find($key=NULL,$val=NULL) {\n\t\t$out=[];\n\t\tif (isset($_SESSION[$this->key])) {\n\t\t\tforeach ($_SESSION[$this->key] as $id=>$item)\n\t\t\t\tif (!isset($key) ||\n\t\t\t\t\tarray_key_exists($key,$item) && $item[$key]==$val ||\n\t\t\t\t\t$key=='_id' && $id==$val) {\n\t\t\t\t\t$obj=clone($this);\n\t\t\t\t\t$obj->id=$id;\n\t\t\t\t\t$obj->item=$item;\n\t\t\t\t\t$out[]=$obj;\n\t\t\t\t}\n\t\t}\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tReturn first item that matches key/value pair\n\t*\t@return object|FALSE\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\tfunction findone($key,$val) {\n\t\treturn ($data=$this->find($key,$val))?$data[0]:FALSE;\n\t}\n\n\t/**\n\t*\tMap current item to matching key/value pair\n\t*\t@return array\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\tfunction load($key,$val) {\n\t\tif ($found=$this->find($key,$val)) {\n\t\t\t$this->id=$found[0]->id;\n\t\t\treturn $this->item=$found[0]->item;\n\t\t}\n\t\t$this->reset();\n\t\treturn [];\n\t}\n\n\t/**\n\t*\tReturn TRUE if current item is empty/undefined\n\t*\t@return bool\n\t**/\n\tfunction dry() {\n\t\treturn !$this->item;\n\t}\n\n\t/**\n\t*\tReturn number of items in basket\n\t*\t@return int\n\t**/\n\tfunction count() {\n\t\treturn isset($_SESSION[$this->key])?count($_SESSION[$this->key]):0;\n\t}\n\n\t/**\n\t*\tSave current item\n\t*\t@return array\n\t**/\n\tfunction save() {\n\t\tif (!$this->id)\n\t\t\t$this->id=uniqid('',TRUE);\n\t\t$_SESSION[$this->key][$this->id]=$this->item;\n\t\treturn $this->item;\n\t}\n\n\t/**\n\t*\tErase item matching key/value pair\n\t*\t@return bool\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\tfunction erase($key,$val) {\n\t\t$found=$this->find($key,$val);\n\t\tif ($found && $id=$found[0]->id) {\n\t\t\tunset($_SESSION[$this->key][$id]);\n\t\t\tif ($id==$this->id)\n\t\t\t\t$this->reset();\n\t\t\treturn TRUE;\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tReset cursor\n\t*\t@return NULL\n\t**/\n\tfunction reset() {\n\t\t$this->id=NULL;\n\t\t$this->item=[];\n\t}\n\n\t/**\n\t*\tEmpty basket\n\t*\t@return NULL\n\t**/\n\tfunction drop() {\n\t\tunset($_SESSION[$this->key]);\n\t}\n\n\t/**\n\t*\tHydrate item using hive array variable\n\t*\t@return NULL\n\t*\t@param $var array|string\n\t**/\n\tfunction copyfrom($var) {\n\t\tif (is_string($var))\n\t\t\t$var=\\Base::instance()->$var;\n\t\tforeach ($var as $key=>$val)\n\t\t\t$this->set($key,$val);\n\t}\n\n\t/**\n\t*\tPopulate hive array variable with item contents\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\tfunction copyto($key) {\n\t\t$var=&\\Base::instance()->ref($key);\n\t\tforeach ($this->item as $key=>$field)\n\t\t\t$var[$key]=$field;\n\t}\n\n\t/**\n\t*\tCheck out basket contents\n\t*\t@return array\n\t**/\n\tfunction checkout() {\n\t\tif (isset($_SESSION[$this->key])) {\n\t\t\t$out=$_SESSION[$this->key];\n\t\t\tunset($_SESSION[$this->key]);\n\t\t\treturn $out;\n\t\t}\n\t\treturn [];\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@return void\n\t*\t@param $key string\n\t**/\n\tfunction __construct($key='basket') {\n\t\t$this->key=$key;\n\t\tif (session_status()!=PHP_SESSION_ACTIVE)\n\t\t\tsession_start();\n\t\tBase::instance()->sync('SESSION');\n\t\t$this->reset();\n\t}\n\n}\n"
  },
  {
    "path": "bcrypt.php",
    "content": "<?php\n\n/**\n*\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n*\n*\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n*\n*\tThis is free software: you can redistribute it and/or modify it under the\n*\tterms of the GNU General Public License as published by the Free Software\n*\tFoundation, either version 3 of the License, or later.\n*\n*\tFat-Free Framework is distributed in the hope that it will be useful,\n*\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n*\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n*\tGeneral Public License for more details.\n*\n*\tYou should have received a copy of the GNU General Public License along\n*\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n*\n**/\n\n/**\n*\tLightweight password hashing library (PHP 5.5+ only)\n*\t@deprecated Use http://php.net/manual/en/ref.password.php instead\n**/\nclass Bcrypt extends Prefab {\n\n\t//@{ Error messages\n\tconst\n\t\tE_CostArg='Invalid cost parameter',\n\t\tE_SaltArg='Salt must be at least 22 alphanumeric characters';\n\t//@}\n\n\t//! Default cost\n\tconst\n\t\tCOST=10;\n\n\t/**\n\t*\tGenerate bcrypt hash of string\n\t*\t@return string|FALSE\n\t*\t@param $pw string\n\t*\t@param $salt string\n\t*\t@param $cost int\n\t**/\n\tfunction hash($pw,$salt=NULL,$cost=self::COST) {\n\t\tif ($cost<4 || $cost>31)\n            throw new \\Exception(self::E_CostArg);\n\t\t$len=22;\n\t\tif ($salt) {\n\t\t\tif (!preg_match('/^[[:alnum:]\\.\\/]{'.$len.',}$/',$salt))\n                throw new \\Exception(self::E_SaltArg);\n\t\t}\n\t\telse {\n\t\t\t$raw=16;\n\t\t\t$iv='';\n\t\t\tif (!$iv && extension_loaded('openssl'))\n\t\t\t\t$iv=openssl_random_pseudo_bytes($raw);\n\t\t\tif (!$iv)\n\t\t\t\tfor ($i=0;$i<$raw;++$i)\n\t\t\t\t\t$iv.=chr(mt_rand(0,255));\n\t\t\t$salt=str_replace('+','.',base64_encode($iv));\n\t\t}\n\t\t$salt=substr($salt,0,$len);\n\t\t$hash=crypt($pw,sprintf('$2y$%02d$',$cost).$salt);\n\t\treturn strlen($hash)>13?$hash:FALSE;\n\t}\n\n\t/**\n\t*\tCheck if password is still strong enough\n\t*\t@return bool\n\t*\t@param $hash string\n\t*\t@param $cost int\n\t**/\n\tfunction needs_rehash($hash,$cost=self::COST) {\n\t\tlist($pwcost)=sscanf($hash,\"$2y$%d$\");\n\t\treturn $pwcost<$cost;\n\t}\n\n\t/**\n\t*\tVerify password against hash using timing attack resistant approach\n\t*\t@return bool\n\t*\t@param $pw string\n\t*\t@param $hash string\n\t**/\n\tfunction verify($pw,$hash) {\n\t\t$val=crypt($pw,$hash);\n\t\t$len=strlen($val);\n\t\tif ($len!=strlen($hash) || $len<14)\n\t\t\treturn FALSE;\n\t\t$out=0;\n\t\tfor ($i=0;$i<$len;++$i)\n\t\t\t$out|=(ord($val[$i])^ord($hash[$i]));\n\t\treturn $out===0;\n\t}\n\n}\n"
  },
  {
    "path": "cli/ws.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace CLI;\n\n//! RFC6455 WebSocket server\nclass WS {\n\n\tconst\n\t\t//! UUID magic string\n\t\tMagic='258EAFA5-E914-47DA-95CA-C5AB0DC85B11',\n\t\t//! Max packet size\n\t\tPacket=65536;\n\n\t//@{ Mask bits for first byte of header\n\tconst\n\t\tText=0x01,\n\t\tBinary=0x02,\n\t\tClose=0x08,\n\t\tPing=0x09,\n\t\tPong=0x0a,\n\t\tOpCode=0x0f,\n\t\tFinale=0x80;\n\t//@}\n\n\t//@{ Mask bits for second byte of header\n\tconst\n\t\tLength=0x7f;\n\t//@}\n\n\tprotected\n\t\t$addr,\n\t\t$ctx,\n\t\t$wait,\n\t\t$sockets,\n\t\t$protocol,\n\t\t$agents=[],\n\t\t$events=[];\n\n\t/**\n\t*\tAllocate stream socket\n\t*\t@return NULL\n\t*\t@param $socket resource\n\t**/\n\tfunction alloc($socket) {\n\t\tif (is_bool($buf=$this->read($socket)))\n\t\t\treturn;\n\t\t// Get WebSocket headers\n\t\t$hdrs=[];\n\t\t$EOL=\"\\r\\n\";\n\t\t$verb=NULL;\n\t\t$uri=NULL;\n\t\tforeach (explode($EOL,trim($buf)) as $line)\n\t\t\tif (preg_match('/^(\\w+)\\s(.+)\\sHTTP\\/[\\d.]{1,3}$/',\n\t\t\t\ttrim($line),$match)) {\n\t\t\t\t$verb=$match[1];\n\t\t\t\t$uri=$match[2];\n\t\t\t}\n\t\t\telse\n\t\t\tif (preg_match('/^(.+): (.+)/',trim($line),$match))\n\t\t\t\t// Standardize header\n\t\t\t\t$hdrs[\n\t\t\t\t\tstrtr(\n\t\t\t\t\t\tucwords(\n\t\t\t\t\t\t\tstrtolower(\n\t\t\t\t\t\t\t\tstrtr($match[1],'-',' ')\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t),' ','-'\n\t\t\t\t\t)\n\t\t\t\t]=$match[2];\n\t\t\telse {\n\t\t\t\t$this->close($socket);\n\t\t\t\treturn;\n\t\t\t}\n\t\tif (empty($hdrs['Upgrade']) &&\n\t\t\tempty($hdrs['Sec-Websocket-Key'])) {\n\t\t\t// Not a WebSocket request\n\t\t\tif ($verb && $uri)\n\t\t\t\t$this->write(\n\t\t\t\t\t$socket,\n\t\t\t\t\t'HTTP/1.1 400 Bad Request'.$EOL.\n\t\t\t\t\t'Connection: close'.$EOL.$EOL\n\t\t\t\t);\n\t\t\t$this->close($socket);\n\t\t\treturn;\n\t\t}\n\t\t// Handshake\n\t\t$buf='HTTP/1.1 101 Switching Protocols'.$EOL.\n\t\t\t'Upgrade: websocket'.$EOL.\n\t\t\t'Connection: Upgrade'.$EOL;\n\t\tif (isset($hdrs['Sec-Websocket-Protocol']))\n\t\t\t$buf.='Sec-WebSocket-Protocol: '.\n\t\t\t\t$hdrs['Sec-Websocket-Protocol'].$EOL;\n\t\t$buf.='Sec-WebSocket-Accept: '.\n\t\t\tbase64_encode(\n\t\t\t\tsha1($hdrs['Sec-Websocket-Key'].WS::Magic,TRUE)\n\t\t\t).$EOL.$EOL;\n\t\tif ($this->write($socket,$buf)) {\n\t\t\t// Connect agent to server\n\t\t\t$this->sockets[(int)$socket]=$socket;\n\t\t\t$this->agents[(int)$socket]=\n\t\t\t\tnew Agent($this,$socket,$verb,$uri,$hdrs);\n\t\t}\n\t}\n\n\t/**\n\t*\tClose stream socket\n\t*\t@return NULL\n\t*\t@param $socket resource\n\t**/\n\tfunction close($socket) {\n\t\tif (isset($this->agents[(int)$socket]))\n\t\t\tunset($this->sockets[(int)$socket],$this->agents[(int)$socket]);\n\t\tstream_socket_shutdown($socket,STREAM_SHUT_WR);\n\t\t@fclose($socket);\n\t}\n\n\t/**\n\t*\tRead from stream socket\n\t*\t@return string|FALSE\n\t*\t@param $socket resource\n\t*\t@param $len int\n\t**/\n\tfunction read($socket,$len=0) {\n\t\tif (!$len)\n\t\t\t$len=WS::Packet;\n\t\tif (is_string($buf=@fread($socket,$len)) &&\n\t\t\tstrlen($buf) && strlen($buf)<$len)\n\t\t\treturn $buf;\n\t\tif (isset($this->events['error']) &&\n\t\t\tis_callable($func=$this->events['error']))\n\t\t\t$func($this);\n\t\t$this->close($socket);\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tWrite to stream socket\n\t*\t@return int|FALSE\n\t*\t@param $socket resource\n\t*\t@param $buf string\n\t**/\n\tfunction write($socket,$buf) {\n\t\tfor ($i=0,$bytes=0;$i<strlen($buf);$i+=$bytes) {\n\t\t\tif (($bytes=@fwrite($socket,substr($buf,$i))) &&\n\t\t\t\t@fflush($socket))\n\t\t\t\tcontinue;\n\t\t\tif (isset($this->events['error']) &&\n\t\t\t\tis_callable($func=$this->events['error']))\n\t\t\t\t$func($this);\n\t\t\t$this->close($socket);\n\t\t\treturn FALSE;\n\t\t}\n\t\treturn $bytes;\n\t}\n\n\t/**\n\t*\tReturn socket agents\n\t*\t@return array\n\t*\t@param $uri string\n\t***/\n\tfunction agents($uri=NULL) {\n\t\treturn array_filter(\n\t\t\t$this->agents,\n\t\t\t/**\n\t\t\t * @var $val Agent\n\t\t\t * @return bool\n\t\t\t */\n\t\t\tfunction($val) use($uri) {\n\t\t\t\treturn $uri?($val->uri()==$uri):TRUE;\n\t\t\t}\n\t\t);\n\t}\n\n\t/**\n\t*\tReturn event handlers\n\t*\t@return array\n\t**/\n\tfunction events() {\n\t\treturn $this->events;\n\t}\n\n\t/**\n\t*\tBind function to event handler\n\t*\t@return object\n\t*\t@param $event string\n\t*\t@param $func callable\n\t**/\n\tfunction on($event,$func) {\n\t\t$this->events[$event]=$func;\n\t\treturn $this;\n\t}\n\n\t/**\n\t*\tTerminate server\n\t**/\n\tfunction kill() {\n\t\tdie;\n\t}\n\n\t/**\n\t*\tExecute the server process\n\t**/\n\tfunction run() {\n\t\t// Assign signal handlers\n\t\tdeclare(ticks=1);\n\t\tpcntl_signal(SIGINT,[$this,'kill']);\n\t\tpcntl_signal(SIGTERM,[$this,'kill']);\n\t\tgc_enable();\n\t\t// Activate WebSocket listener\n\t\t$listen=stream_socket_server(\n\t\t\t$this->addr,$errno,$errstr,\n\t\t\tSTREAM_SERVER_BIND|STREAM_SERVER_LISTEN,\n\t\t\t$this->ctx\n\t\t);\n\t\t$socket=socket_import_stream($listen);\n\t\tregister_shutdown_function(function() use($listen) {\n\t\t\tforeach ($this->sockets as $socket)\n\t\t\t\tif ($socket!=$listen)\n\t\t\t\t\t$this->close($socket);\n\t\t\t$this->close($listen);\n\t\t\tif (isset($this->events['stop']) &&\n\t\t\t\tis_callable($func=$this->events['stop']))\n\t\t\t\t$func($this);\n\t\t});\n\t\tif ($errstr)\n            throw new \\Exception($errstr);\n\t\tif (isset($this->events['start']) &&\n\t\t\tis_callable($func=$this->events['start']))\n\t\t\t$func($this);\n\t\t$this->sockets=[(int)$listen=>$listen];\n\t\t$empty=[];\n\t\t$wait=$this->wait;\n\t\twhile (TRUE) {\n\t\t\t$active=$this->sockets;\n\t\t\t$mark=microtime(TRUE);\n\t\t\t$count=@stream_select(\n\t\t\t\t$active,$empty,$empty,(int)$wait,round(1e6*($wait-(int)$wait))\n\t\t\t);\n\t\t\tif (is_bool($count) && $wait) {\n\t\t\t\tif (isset($this->events['error']) &&\n\t\t\t\t\tis_callable($func=$this->events['error']))\n\t\t\t\t\t$func($this);\n\t\t\t\tdie;\n\t\t\t}\n\t\t\tif ($count) {\n\t\t\t\t// Process active connections\n\t\t\t\tforeach ($active as $socket) {\n\t\t\t\t\tif (!is_resource($socket))\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tif ($socket==$listen) {\n\t\t\t\t\t\tif ($socket=@stream_socket_accept($listen,0))\n\t\t\t\t\t\t\t$this->alloc($socket);\n\t\t\t\t\t\telse\n\t\t\t\t\t\tif (isset($this->events['error']) &&\n\t\t\t\t\t\t\tis_callable($func=$this->events['error']))\n\t\t\t\t\t\t\t$func($this);\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\t$id=(int)$socket;\n\t\t\t\t\t\tif (isset($this->agents[$id]))\n\t\t\t\t\t\t\t$this->agents[$id]->fetch();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t$wait-=microtime(TRUE)-$mark;\n\t\t\t\twhile ($wait<1e-6) {\n\t\t\t\t\t$wait+=$this->wait;\n\t\t\t\t\t$count=0;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!$count) {\n\t\t\t\t$mark=microtime(TRUE);\n\t\t\t\tforeach ($this->sockets as $id=>$socket) {\n\t\t\t\t\tif (!is_resource($socket))\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\tif ($socket!=$listen &&\n\t\t\t\t\t\tisset($this->agents[$id]) &&\n\t\t\t\t\t\tisset($this->events['idle']) &&\n\t\t\t\t\t\tis_callable($func=$this->events['idle']))\n\t\t\t\t\t\t$func($this->agents[$id]);\n\t\t\t\t}\n\t\t\t\t$wait=$this->wait-microtime(TRUE)+$mark;\n\t\t\t}\n\t\t\tgc_collect_cycles();\n\t\t}\n\t}\n\n\t/**\n\t*\t@param $addr string\n\t*\t@param $ctx resource\n\t*\t@param $wait int\n\t**/\n\tfunction __construct($addr,$ctx=NULL,$wait=60) {\n\t\t$this->addr=$addr;\n\t\t$this->ctx=$ctx?:stream_context_create();\n\t\t$this->wait=$wait;\n\t\t$this->events=[];\n\t}\n\n}\n\n//! RFC6455 remote socket\nclass Agent {\n\n\tprotected\n\t\t$server,\n\t\t$id,\n\t\t$socket,\n\t\t$flag,\n\t\t$verb,\n\t\t$uri,\n\t\t$headers;\n\n\t/**\n\t*\tReturn server instance\n\t*\t@return WS\n\t**/\n\tfunction server() {\n\t\treturn $this->server;\n\t}\n\n\t/**\n\t*\tReturn socket ID\n\t*\t@return string\n\t**/\n\tfunction id() {\n\t\treturn $this->id;\n\t}\n\n\t/**\n\t*\tReturn socket\n\t*\t@return resource\n\t**/\n\tfunction socket() {\n\t\treturn $this->socket;\n\t}\n\n\t/**\n\t*\tReturn request method\n\t*\t@return string\n\t**/\n\tfunction verb() {\n\t\treturn $this->verb;\n\t}\n\n\t/**\n\t*\tReturn request URI\n\t*\t@return string\n\t**/\n\tfunction uri() {\n\t\treturn $this->uri;\n\t}\n\n\t/**\n\t*\tReturn socket headers\n\t*\t@return array\n\t**/\n\tfunction headers() {\n\t\treturn $this->headers;\n\t}\n\n\t/**\n\t*\tFrame and transmit payload\n\t*\t@return string|FALSE\n\t*\t@param $op int\n\t*\t@param $data string\n\t**/\n\tfunction send($op,$data='') {\n\t\t$server=$this->server;\n\t\t$mask=WS::Finale | $op & WS::OpCode;\n\t\t$len=strlen($data);\n\t\t$buf='';\n\t\tif ($len>0xffff)\n\t\t\t$buf=pack('CCNN',$mask,0x7f,$len);\n\t\telseif ($len>0x7d)\n\t\t\t$buf=pack('CCn',$mask,0x7e,$len);\n\t\telse\n\t\t\t$buf=pack('CC',$mask,$len);\n\t\t$buf.=$data;\n\t\tif (is_bool($server->write($this->socket,$buf)))\n\t\t\treturn FALSE;\n\t\tif (!in_array($op,[WS::Pong,WS::Close]) &&\n\t\t\tisset($this->server->events()['send']) &&\n\t\t\tis_callable($func=$this->server->events()['send']))\n\t\t\t$func($this,$op,$data);\n\t\treturn $data;\n\t}\n\n\t/**\n\t*\tRetrieve and unmask payload\n\t*\t@return bool|NULL\n\t**/\n\tfunction fetch() {\n\t\t// Unmask payload\n\t\t$server=$this->server;\n\t\tif (is_bool($buf=$server->read($this->socket)))\n\t\t\treturn FALSE;\n\t\twhile($buf) {\n\t\t\t$op=ord($buf[0]) & WS::OpCode;\n\t\t\t$len=ord($buf[1]) & WS::Length;\n\t\t\t$pos=2;\n\t\t\tif ($len==0x7e) {\n\t\t\t\t$len=ord($buf[2])*256+ord($buf[3]);\n\t\t\t\t$pos+=2;\n\t\t\t}\n\t\t\telse\n\t\t\tif ($len==0x7f) {\n\t\t\t\tfor ($i=0,$len=0;$i<8;++$i)\n\t\t\t\t\t$len=$len*256+ord($buf[$i+2]);\n\t\t\t\t$pos+=8;\n\t\t\t}\n\t\t\tfor ($i=0,$mask=[];$i<4;++$i)\n\t\t\t\t$mask[$i]=ord($buf[$pos+$i]);\n\t\t\t$pos+=4;\n\t\t\tif (strlen($buf)<$len+$pos)\n\t\t\t\treturn FALSE;\n\t\t\tfor ($i=0,$data='';$i<$len;++$i)\n\t\t\t\t$data.=chr(ord($buf[$pos+$i])^$mask[$i%4]);\n\t\t\t// Dispatch\n\t\t\tswitch ($op & WS::OpCode) {\n\t\t\tcase WS::Ping:\n\t\t\t\t$this->send(WS::Pong);\n\t\t\t\tbreak;\n\t\t\tcase WS::Close:\n\t\t\t\t$server->close($this->socket);\n\t\t\t\tbreak;\n\t\t\tcase WS::Text:\n\t\t\t\t$data=trim($data);\n\t\t\tcase WS::Binary:\n\t\t\t\tif (isset($this->server->events()['receive']) &&\n\t\t\t\t\tis_callable($func=$this->server->events()['receive']))\n\t\t\t\t\t$func($this,$op,$data);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t$buf = substr($buf, $len+$pos);\n\t\t}\n\t}\n\n\t/**\n\t*\tDestroy object\n\t**/\n\tfunction __destruct() {\n\t\tif (isset($this->server->events()['disconnect']) &&\n\t\t\tis_callable($func=$this->server->events()['disconnect']))\n\t\t\t$func($this);\n\t}\n\n\t/**\n\t*\t@param $server WS\n\t*\t@param $socket resource\n\t*\t@param $verb string\n\t*\t@param $uri string\n\t*\t@param $hdrs array\n\t**/\n\tfunction __construct($server,$socket,$verb,$uri,array $hdrs) {\n\t\t$this->server=$server;\n\t\t$this->id=stream_socket_get_name($socket,TRUE);\n\t\t$this->socket=$socket;\n\t\t$this->verb=$verb;\n\t\t$this->uri=$uri;\n\t\t$this->headers=$hdrs;\n\n\t\tif (isset($server->events()['connect']) &&\n\t\t\tis_callable($func=$server->events()['connect']))\n\t\t\t$func($this);\n\t}\n\n}\n"
  },
  {
    "path": "code.css",
    "content": "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}\n"
  },
  {
    "path": "composer.json",
    "content": "{\n\t\"name\": \"bcosca/fatfree-core\",\n\t\"description\": \"A powerful yet easy-to-use PHP micro-framework designed to help you build dynamic and robust Web applications - fast!\",\n\t\"homepage\": \"http://fatfreeframework.com/\",\n\t\"license\": \"GPL-3.0\",\n\t\"require\": {\n\t\t\"php\": \">=7.2\"\n\t},\n\t\"autoload\": {\n\t\t\"classmap\": [\".\"]\n\t}\n}\n"
  },
  {
    "path": "db/cursor.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace DB;\n\n//! Simple cursor implementation\nabstract class Cursor extends \\Magic implements \\IteratorAggregate {\n\n\t//@{ Error messages\n\tconst\n\t\tE_Field='Undefined field %s';\n\t//@}\n\n\tprotected\n\t\t//! Query results\n\t\t$query=[],\n\t\t//! Current position\n\t\t$ptr=0,\n\t\t//! Event listeners\n\t\t$trigger=[];\n\n\t/**\n\t*\tReturn database type\n\t*\t@return string\n\t**/\n\tabstract function dbtype();\n\n\t/**\n\t*\tReturn field names\n\t*\t@return array\n\t**/\n\tabstract function fields();\n\n\t/**\n\t*\tReturn fields of mapper object as an associative array\n\t*\t@return array\n\t*\t@param $obj object\n\t**/\n\tabstract function cast($obj=NULL);\n\n\t/**\n\t*\tReturn records (array of mapper objects) that match criteria\n\t*\t@return array\n\t*\t@param $filter string|array\n\t*\t@param $options array\n\t*\t@param $ttl int\n\t**/\n\tabstract function find($filter=NULL,?array $options=NULL,$ttl=0);\n\n\t/**\n\t*\tCount records that match criteria\n\t*\t@return int\n\t*\t@param $filter array\n\t*\t@param $options array\n\t*\t@param $ttl int\n\t**/\n\tabstract function count($filter=NULL,?array $options=NULL,$ttl=0);\n\n\t/**\n\t*\tInsert new record\n\t*\t@return array\n\t**/\n\tabstract function insert();\n\n\t/**\n\t*\tUpdate current record\n\t*\t@return array\n\t**/\n\tabstract function update();\n\n\t/**\n\t*\tHydrate mapper object using hive array variable\n\t*\t@return NULL\n\t*\t@param $var array|string\n\t*\t@param $func callback\n\t**/\n\tabstract function copyfrom($var,$func=NULL);\n\n\t/**\n\t*\tPopulate hive array variable with mapper fields\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\tabstract function copyto($key);\n\n\t/**\n\t*\tGet cursor's equivalent external iterator\n\t*\tCauses a fatal error in PHP 5.3.5 if uncommented\n\t*\treturn ArrayIterator\n\t**/\n\t#[\\ReturnTypeWillChange]\n\tabstract function getiterator();\n\n\n\t/**\n\t*\tReturn TRUE if current cursor position is not mapped to any record\n\t*\t@return bool\n\t**/\n\tfunction dry() {\n\t\treturn empty($this->query[$this->ptr]);\n\t}\n\n\t/**\n\t*\tReturn first record (mapper object) that matches criteria\n\t*\t@return static|FALSE\n\t*\t@param $filter string|array\n\t*\t@param $options array\n\t*\t@param $ttl int\n\t**/\n\tfunction findone($filter=NULL,?array $options=NULL,$ttl=0) {\n\t\tif (!$options)\n\t\t\t$options=[];\n\t\t// Override limit\n\t\t$options['limit']=1;\n\t\treturn ($data=$this->find($filter,$options,$ttl))?$data[0]:FALSE;\n\t}\n\n\t/**\n\t*\tReturn array containing subset of records matching criteria,\n\t*\ttotal number of records in superset, specified limit, number of\n\t*\tsubsets available, and actual subset position\n\t*\t@return array\n\t*\t@param $pos int\n\t*\t@param $size int\n\t*\t@param $filter string|array\n\t*\t@param $options array\n\t*\t@param $ttl int\n\t*\t@param $bounce bool\n\t**/\n\tfunction paginate(\n\t\t$pos=0,$size=10,$filter=NULL,?array $options=NULL,$ttl=0,$bounce=TRUE) {\n\t\t$total=$this->count($filter,$options,$ttl);\n\t\t$count=(int)ceil($total/$size);\n\t\tif ($bounce)\n\t\t\t$pos=max(0,min($pos,$count-1));\n\t\treturn [\n\t\t\t'subset'=>($bounce || $pos<$count)?$this->find($filter,\n\t\t\t\tarray_merge(\n\t\t\t\t\t$options?:[],\n\t\t\t\t\t['limit'=>$size,'offset'=>$pos*$size]\n\t\t\t\t),\n\t\t\t\t$ttl\n\t\t\t):[],\n\t\t\t'total'=>$total,\n\t\t\t'limit'=>$size,\n\t\t\t'count'=>$count,\n\t\t\t'pos'=>$bounce?($pos<$count?$pos:0):$pos\n\t\t];\n\t}\n\n\t/**\n\t*\tMap to first record that matches criteria\n\t*\t@return \\DB\\SQL\\Mapper|FALSE\n\t*\t@param $filter string|array\n\t*\t@param $options array\n\t*\t@param $ttl int\n\t**/\n\tfunction load($filter=NULL,?array $options=NULL,$ttl=0) {\n\t\t$this->reset();\n\t\treturn ($this->query=$this->find($filter,$options,$ttl)) &&\n\t\t\t$this->skip(0)?$this->query[$this->ptr]:FALSE;\n\t}\n\n\t/**\n\t*\tReturn the count of records loaded\n\t*\t@return int\n\t**/\n\tfunction loaded() {\n\t\treturn count($this->query);\n\t}\n\n\t/**\n\t*\tMap to first record in cursor\n\t*\t@return mixed\n\t**/\n\tfunction first() {\n\t\treturn $this->skip(-$this->ptr);\n\t}\n\n\t/**\n\t*\tMap to last record in cursor\n\t*\t@return mixed\n\t**/\n\tfunction last() {\n\t\treturn $this->skip(($ofs=count($this->query)-$this->ptr)?$ofs-1:0);\n\t}\n\n\t/**\n\t*\tMap to nth record relative to current cursor position\n\t*\t@return mixed\n\t*\t@param $ofs int\n\t**/\n\tfunction skip($ofs=1) {\n\t\t$this->ptr+=$ofs;\n\t\treturn $this->ptr>-1 && $this->ptr<count($this->query)?\n\t\t\t$this->query[$this->ptr]:FALSE;\n\t}\n\n\t/**\n\t*\tMap next record\n\t*\t@return mixed\n\t**/\n\tfunction next() {\n\t\treturn $this->skip();\n\t}\n\n\t/**\n\t*\tMap previous record\n\t*\t@return mixed\n\t**/\n\tfunction prev() {\n\t\treturn $this->skip(-1);\n\t}\n\n\t/**\n\t * Return whether current iterator position is valid.\n\t */\n\tfunction valid() {\n\t\treturn !$this->dry();\n\t}\n\n\t/**\n\t*\tSave mapped record\n\t*\t@return mixed\n\t**/\n\tfunction save() {\n\t\treturn $this->query?$this->update():$this->insert();\n\t}\n\n\t/**\n\t*\tDelete current record\n\t*\t@return int|bool\n\t**/\n\tfunction erase() {\n\t\t$this->query=array_slice($this->query,0,$this->ptr,TRUE)+\n\t\t\tarray_slice($this->query,$this->ptr,NULL,TRUE);\n\t\t$this->skip(0);\n\t}\n\n\t/**\n\t*\tDefine onload trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction onload($func) {\n\t\treturn $this->trigger['load']=$func;\n\t}\n\n\t/**\n\t*\tDefine beforeinsert trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction beforeinsert($func) {\n\t\treturn $this->trigger['beforeinsert']=$func;\n\t}\n\n\t/**\n\t*\tDefine afterinsert trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction afterinsert($func) {\n\t\treturn $this->trigger['afterinsert']=$func;\n\t}\n\n\t/**\n\t*\tDefine oninsert trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction oninsert($func) {\n\t\treturn $this->afterinsert($func);\n\t}\n\n\t/**\n\t*\tDefine beforeupdate trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction beforeupdate($func) {\n\t\treturn $this->trigger['beforeupdate']=$func;\n\t}\n\n\t/**\n\t*\tDefine afterupdate trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction afterupdate($func) {\n\t\treturn $this->trigger['afterupdate']=$func;\n\t}\n\n\t/**\n\t*\tDefine onupdate trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction onupdate($func) {\n\t\treturn $this->afterupdate($func);\n\t}\n\n\t/**\n\t*\tDefine beforesave trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction beforesave($func) {\n\t\t$this->trigger['beforeinsert']=$func;\n\t\t$this->trigger['beforeupdate']=$func;\n\t\treturn $func;\n\t}\n\n\t/**\n\t*\tDefine aftersave trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction aftersave($func) {\n\t\t$this->trigger['afterinsert']=$func;\n\t\t$this->trigger['afterupdate']=$func;\n\t\treturn $func;\n\t}\n\n\t/**\n\t*\tDefine onsave trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction onsave($func) {\n\t\treturn $this->aftersave($func);\n\t}\n\n\t/**\n\t*\tDefine beforeerase trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction beforeerase($func) {\n\t\treturn $this->trigger['beforeerase']=$func;\n\t}\n\n\t/**\n\t*\tDefine aftererase trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction aftererase($func) {\n\t\treturn $this->trigger['aftererase']=$func;\n\t}\n\n\t/**\n\t*\tDefine onerase trigger\n\t*\t@return callback\n\t*\t@param $func callback\n\t**/\n\tfunction onerase($func) {\n\t\treturn $this->aftererase($func);\n\t}\n\n\t/**\n\t*\tReset cursor\n\t*\t@return NULL\n\t**/\n\tfunction reset() {\n\t\t$this->query=[];\n\t\t$this->ptr=0;\n\t}\n\n}\n"
  },
  {
    "path": "db/jig/mapper.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace DB\\Jig;\n\n//! Flat-file DB mapper\nclass Mapper extends \\DB\\Cursor {\n\n\tprotected\n\t\t//! Flat-file DB wrapper\n\t\t$db,\n\t\t//! Data file\n\t\t$file,\n\t\t//! Document identifier\n\t\t$id,\n\t\t//! Document contents\n\t\t$document=[],\n\t\t//! field map-reduce handlers\n\t\t$_reduce;\n\n\t/**\n\t*\tReturn database type\n\t*\t@return string\n\t**/\n\tfunction dbtype() {\n\t\treturn 'Jig';\n\t}\n\n\t/**\n\t*\tReturn TRUE if field is defined\n\t*\t@return bool\n\t*\t@param $key string\n\t**/\n\tfunction exists($key) {\n\t\treturn array_key_exists($key,$this->document);\n\t}\n\n\t/**\n\t*\tAssign value to field\n\t*\t@return scalar|FALSE\n\t*\t@param $key string\n\t*\t@param $val scalar\n\t**/\n\tfunction set($key,$val) {\n\t\treturn ($key=='_id')?FALSE:($this->document[$key]=$val);\n\t}\n\n\t/**\n\t*\tRetrieve value of field\n\t*\t@return scalar|FALSE\n\t*\t@param $key string\n\t**/\n\tfunction &get($key) {\n\t\tif ($key=='_id')\n\t\t\treturn $this->id;\n\t\tif (array_key_exists($key,$this->document))\n\t\t\treturn $this->document[$key];\n        throw new \\Exception(sprintf(self::E_Field,$key));\n\t}\n\n\t/**\n\t*\tDelete field\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\tfunction clear($key) {\n\t\tif ($key!='_id')\n\t\t\tunset($this->document[$key]);\n\t}\n\n\t/**\n\t*\tConvert array to mapper object\n\t*\t@return object\n\t*\t@param $id string\n\t*\t@param $row array\n\t**/\n\tfunction factory($id,$row) {\n\t\t$mapper=clone($this);\n\t\t$mapper->reset();\n\t\t$mapper->id=$id;\n\t\tforeach ($row as $field=>$val)\n\t\t\t$mapper->document[$field]=$val;\n\t\t$mapper->query=[clone($mapper)];\n\t\tif (isset($mapper->trigger['load']))\n\t\t\t\\Base::instance()->call($mapper->trigger['load'],$mapper);\n\t\treturn $mapper;\n\t}\n\n\t/**\n\t*\tReturn fields of mapper object as an associative array\n\t*\t@return array\n\t*\t@param $obj object\n\t**/\n\tfunction cast($obj=NULL) {\n\t\tif (!$obj)\n\t\t\t$obj=$this;\n\t\treturn $obj->document+['_id'=>$this->id];\n\t}\n\n\t/**\n\t*\tConvert tokens in string expression to variable names\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction token($str) {\n\t\t$str=preg_replace_callback(\n\t\t\t'/(?<!\\w)@(\\w(?:[\\w\\.\\[\\]])*)/',\n\t\t\tfunction($token) {\n\t\t\t\t// Convert from JS dot notation to PHP array notation\n\t\t\t\treturn '$'.preg_replace_callback(\n\t\t\t\t\t'/(\\.\\w+)|\\[((?:[^\\[\\]]*|(?R))*)\\]/',\n\t\t\t\t\tfunction($expr) {\n\t\t\t\t\t\t$fw=\\Base::instance();\n\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t'['.\n\t\t\t\t\t\t\t($expr[1]?\n\t\t\t\t\t\t\t\t$fw->stringify(substr($expr[1],1)):\n\t\t\t\t\t\t\t\t(preg_match('/^\\w+/',\n\t\t\t\t\t\t\t\t\t$mix=$this->token($expr[2]))?\n\t\t\t\t\t\t\t\t\t$fw->stringify($mix):\n\t\t\t\t\t\t\t\t\t$mix)).\n\t\t\t\t\t\t\t']';\n\t\t\t\t\t},\n\t\t\t\t\t$token[1]\n\t\t\t\t);\n\t\t\t},\n\t\t\t$str\n\t\t);\n\t\treturn trim($str);\n\t}\n\n\t/**\n\t*\tReturn records that match criteria\n\t*\t@return static[]|FALSE\n\t*\t@param $filter array\n\t*\t@param $options array\n\t*\t@param $ttl int|array\n\t*\t@param $log bool\n\t**/\n\tfunction find($filter=NULL,?array $options=NULL,$ttl=0,$log=TRUE) {\n\t\tif (!$options)\n\t\t\t$options=[];\n\t\t$options+=[\n\t\t\t'order'=>NULL,\n\t\t\t'limit'=>0,\n\t\t\t'offset'=>0,\n\t\t\t'group'=>NULL,\n\t\t];\n\t\t$fw=\\Base::instance();\n\t\t$cache=\\Cache::instance();\n\t\t$db=$this->db;\n\t\t$now=microtime(TRUE);\n\t\t$data=[];\n\t\t$tag='';\n\t\tif (is_array($ttl))\n\t\t\tlist($ttl,$tag)=$ttl;\n\t\tif (!$fw->CACHE || !$ttl || !($cached=$cache->exists(\n\t\t\t$hash=$fw->hash($this->db->dir().\n\t\t\t\t$fw->stringify([$filter,$options])).($tag?'.'.$tag:'').'.jig',$data)) ||\n\t\t\t$cached[0]+$ttl<microtime(TRUE)) {\n\t\t\t$data=$db->read($this->file);\n\t\t\tif (is_null($data))\n\t\t\t\treturn FALSE;\n\t\t\tforeach ($data as $id=>&$doc) {\n\t\t\t\t$doc['_id']=$id;\n\t\t\t\tunset($doc);\n\t\t\t}\n\t\t\tif ($filter) {\n\t\t\t\tif (!is_array($filter))\n\t\t\t\t\treturn FALSE;\n\t\t\t\t// Normalize equality operator\n\t\t\t\t$expr=preg_replace('/(?<=[^<>!=])=(?!=)/','==',$filter[0]);\n\t\t\t\t// Prepare query arguments\n\t\t\t\t$args=isset($filter[1]) && is_array($filter[1])?\n\t\t\t\t\t$filter[1]:\n\t\t\t\t\tarray_slice($filter,1,NULL,TRUE);\n\t\t\t\t$args=is_array($args)?$args:[1=>$args];\n\t\t\t\t$keys=$vals=[];\n\t\t\t\t$tokens=array_slice(\n\t\t\t\t\ttoken_get_all('<?php '.$this->token($expr)),1);\n\t\t\t\t$data=array_filter($data,\n\t\t\t\t\tfunction($_row) use($fw,$args,$tokens) {\n\t\t\t\t\t\t$_expr='';\n\t\t\t\t\t\t$ctr=0;\n\t\t\t\t\t\t$named=FALSE;\n\t\t\t\t\t\tforeach ($tokens as $token) {\n\t\t\t\t\t\t\tif (is_string($token))\n\t\t\t\t\t\t\t\tif ($token=='?') {\n\t\t\t\t\t\t\t\t\t// Positional\n\t\t\t\t\t\t\t\t\t++$ctr;\n\t\t\t\t\t\t\t\t\t$key=$ctr;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\t\tif ($token==':')\n\t\t\t\t\t\t\t\t\t\t$named=TRUE;\n\t\t\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t\t\t$_expr.=$token;\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telseif ($named &&\n\t\t\t\t\t\t\t\ttoken_name($token[0])=='T_STRING') {\n\t\t\t\t\t\t\t\t$key=':'.$token[1];\n\t\t\t\t\t\t\t\t$named=FALSE;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\t$_expr.=$token[1];\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t$_expr.=$fw->stringify(\n\t\t\t\t\t\t\t\tis_string($args[$key])?\n\t\t\t\t\t\t\t\t\taddcslashes($args[$key],'\\''):\n\t\t\t\t\t\t\t\t\t$args[$key]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Avoid conflict with user code\n\t\t\t\t\t\tunset($fw,$tokens,$args,$ctr,$token,$key,$named);\n\t\t\t\t\t\textract($_row);\n\t\t\t\t\t\t// Evaluate pseudo-SQL expression\n\t\t\t\t\t\treturn eval('return '.$_expr.';');\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (isset($options['group'])) {\n\t\t\t\t$cols=array_reverse($fw->split($options['group']));\n\t\t\t\t// sort into groups\n\t\t\t\t$data=$this->sort($data,$options['group']);\n\t\t\t\tforeach($data as $i=>&$row) {\n\t\t\t\t\tif (!isset($prev)) {\n\t\t\t\t\t\t$prev=$row;\n\t\t\t\t\t\t$prev_i=$i;\n\t\t\t\t\t}\n\t\t\t\t\t$drop=false;\n\t\t\t\t\tforeach ($cols as $col)\n\t\t\t\t\t\tif ($prev_i!=$i && array_key_exists($col,$row) &&\n\t\t\t\t\t\t\tarray_key_exists($col,$prev) && $row[$col]==$prev[$col])\n\t\t\t\t\t\t\t// reduce/modify\n\t\t\t\t\t\t\t$drop=!isset($this->_reduce[$col]) || call_user_func_array(\n\t\t\t\t\t\t\t\t$this->_reduce[$col][0],[&$prev,&$row])!==FALSE;\n\t\t\t\t\t\telseif (isset($this->_reduce[$col])) {\n\t\t\t\t\t\t\t$null=null;\n\t\t\t\t\t\t\t// initial\n\t\t\t\t\t\t\tcall_user_func_array($this->_reduce[$col][0],[&$row,&$null]);\n\t\t\t\t\t\t}\n\t\t\t\t\tif ($drop)\n\t\t\t\t\t\tunset($data[$i]);\n\t\t\t\t\telse {\n\t\t\t\t\t\t$prev=&$row;\n\t\t\t\t\t\t$prev_i=$i;\n\t\t\t\t\t}\n\t\t\t\t\tunset($row);\n\t\t\t\t}\n\t\t\t\t// finalize\n\t\t\t\tif ($this->_reduce[$col][1])\n\t\t\t\t\tforeach($data as $i=>&$row) {\n\t\t\t\t\t\t$row=call_user_func($this->_reduce[$col][1],$row);\n\t\t\t\t\t\tif (!$row)\n\t\t\t\t\t\t\tunset($data[$i]);\n\t\t\t\t\t\tunset($row);\n\t\t\t\t\t}\n\t\t\t}\n\t\t\tif (isset($options['order']))\n\t\t\t\t$data=$this->sort($data,$options['order']);\n\t\t\t$data=array_slice($data,\n\t\t\t\t$options['offset'],$options['limit']?:NULL,TRUE);\n\t\t\tif ($fw->CACHE && $ttl)\n\t\t\t\t// Save to cache backend\n\t\t\t\t$cache->set($hash,$data,$ttl);\n\t\t}\n\t\t$out=[];\n\t\tforeach ($data as $id=>&$doc) {\n\t\t\tunset($doc['_id']);\n\t\t\t$out[]=$this->factory($id,$doc);\n\t\t\tunset($doc);\n\t\t}\n\t\tif ($log && isset($args)) {\n\t\t\tif ($filter)\n\t\t\t\tforeach ($args as $key=>$val) {\n\t\t\t\t\t$vals[]=$fw->stringify(is_array($val)?$val[0]:$val);\n\t\t\t\t\t$keys[]='/'.(is_numeric($key)?'\\?':preg_quote($key)).'/';\n\t\t\t\t}\n\t\t\t$db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.\n\t\t\t\t$this->file.' [find] '.\n\t\t\t\t($filter?preg_replace($keys,$vals,$filter[0],1):''));\n\t\t}\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tSort a collection\n\t*\t@param $data\n\t*\t@param $cond\n\t*\t@return mixed\n\t*/\n\tprotected function sort($data,$cond) {\n\t\t$cols=\\Base::instance()->split($cond);\n\t\tuasort(\n\t\t\t$data,\n\t\t\tfunction($val1,$val2) use($cols) {\n\t\t\t\tforeach ($cols as $col) {\n\t\t\t\t\t$parts=explode(' ',$col,2);\n\t\t\t\t\t$order=empty($parts[1])?\n\t\t\t\t\t\tSORT_ASC:\n\t\t\t\t\t\tconstant($parts[1]);\n\t\t\t\t\t$col=$parts[0];\n\t\t\t\t\tif (!array_key_exists($col,$val1))\n\t\t\t\t\t\t$val1[$col]=NULL;\n\t\t\t\t\tif (!array_key_exists($col,$val2))\n\t\t\t\t\t\t$val2[$col]=NULL;\n\t\t\t\t\tlist($v1,$v2)=[$val1[$col],$val2[$col]];\n\t\t\t\t\tif ($out=strnatcmp($v1?:'',$v2?:'')*\n\t\t\t\t\t\t(($order==SORT_ASC)*2-1))\n\t\t\t\t\t\treturn $out;\n\t\t\t\t}\n\t\t\t\treturn 0;\n\t\t\t}\n\t\t);\n\t\treturn $data;\n\t}\n\n\t/**\n\t*\tAdd reduce handler for grouped fields\n\t*\t@param $key string\n\t*\t@param $handler callback\n\t*\t@param $finalize callback\n\t*/\n\tfunction reduce($key,$handler,$finalize=null){\n\t\t$this->_reduce[$key]=[$handler,$finalize];\n\t}\n\n\t/**\n\t*\tCount records that match criteria\n\t*\t@return int\n\t*\t@param $filter array\n\t*\t@param $options array\n\t*\t@param $ttl int|array\n\t**/\n\tfunction count($filter=NULL,?array $options=NULL,$ttl=0) {\n\t\t$now=microtime(TRUE);\n\t\t$out=count($this->find($filter,$options,$ttl,FALSE));\n\t\t$this->db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.\n\t\t\t$this->file.' [count] '.($filter?json_encode($filter):''));\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tReturn record at specified offset using criteria of previous\n\t*\tload() call and make it active\n\t*\t@return array\n\t*\t@param $ofs int\n\t**/\n\tfunction skip($ofs=1) {\n\t\t$this->document=($out=parent::skip($ofs))?$out->document:[];\n\t\t$this->id=$out?$out->id:NULL;\n\t\tif ($this->document && isset($this->trigger['load']))\n\t\t\t\\Base::instance()->call($this->trigger['load'],$this);\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tInsert new record\n\t*\t@return array\n\t**/\n\tfunction insert() {\n\t\tif ($this->id)\n\t\t\treturn $this->update();\n\t\t$db=$this->db;\n\t\t$now=microtime(TRUE);\n\t\twhile (($id=uniqid('',TRUE)) &&\n\t\t\t($data=&$db->read($this->file)) && isset($data[$id]) &&\n\t\t\t!connection_aborted())\n\t\t\tusleep(mt_rand(0,100));\n\t\t$this->id=$id;\n\t\t$pkey=['_id'=>$this->id];\n\t\tif (isset($this->trigger['beforeinsert']) &&\n\t\t\t\\Base::instance()->call($this->trigger['beforeinsert'],\n\t\t\t\t[$this,$pkey])===FALSE)\n\t\t\treturn $this->document;\n\t\t$data[$id]=$this->document;\n\t\t$db->write($this->file,$data);\n\t\t$db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.\n\t\t\t$this->file.' [insert] '.json_encode($this->document));\n\t\tif (isset($this->trigger['afterinsert']))\n\t\t\t\\Base::instance()->call($this->trigger['afterinsert'],\n\t\t\t\t[$this,$pkey]);\n\t\t$this->load(['@_id=?',$this->id]);\n\t\treturn $this->document;\n\t}\n\n\t/**\n\t*\tUpdate current record\n\t*\t@return array\n\t**/\n\tfunction update() {\n\t\t$db=$this->db;\n\t\t$now=microtime(TRUE);\n\t\t$data=&$db->read($this->file);\n\t\tif (isset($this->trigger['beforeupdate']) &&\n\t\t\t\\Base::instance()->call($this->trigger['beforeupdate'],\n\t\t\t\t[$this,['_id'=>$this->id]])===FALSE)\n\t\t\treturn $this->document;\n\t\t$data[$this->id]=$this->document;\n\t\t$db->write($this->file,$data);\n\t\t$db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.\n\t\t\t$this->file.' [update] '.json_encode($this->document));\n\t\tif (isset($this->trigger['afterupdate']))\n\t\t\t\\Base::instance()->call($this->trigger['afterupdate'],\n\t\t\t\t[$this,['_id'=>$this->id]]);\n\t\treturn $this->document;\n\t}\n\n\t/**\n\t*\tDelete current record\n\t*\t@return bool\n\t*\t@param $filter array\n\t*\t@param $quick bool\n\t**/\n\tfunction erase($filter=NULL,$quick=FALSE) {\n\t\t$db=$this->db;\n\t\t$now=microtime(TRUE);\n\t\t$data=&$db->read($this->file);\n\t\t$pkey=['_id'=>$this->id];\n\t\tif ($filter) {\n\t\t\tforeach ($this->find($filter,NULL,FALSE) as $mapper)\n\t\t\t\tif (!$mapper->erase(null,$quick))\n\t\t\t\t\treturn FALSE;\n\t\t\treturn TRUE;\n\t\t}\n\t\telseif (isset($this->id)) {\n\t\t\tunset($data[$this->id]);\n\t\t\tparent::erase();\n\t\t}\n\t\telse\n\t\t\treturn FALSE;\n\t\tif (!$quick && isset($this->trigger['beforeerase']) &&\n\t\t\t\\Base::instance()->call($this->trigger['beforeerase'],\n\t\t\t\t[$this,$pkey])===FALSE)\n\t\t\treturn FALSE;\n\t\t$db->write($this->file,$data);\n\t\tif ($filter) {\n\t\t\t$args=isset($filter[1]) && is_array($filter[1])?\n\t\t\t\t$filter[1]:\n\t\t\t\tarray_slice($filter,1,NULL,TRUE);\n\t\t\t$args=is_array($args)?$args:[1=>$args];\n\t\t\tforeach ($args as $key=>$val) {\n\t\t\t\t$vals[]=\\Base::instance()->\n\t\t\t\t\tstringify(is_array($val)?$val[0]:$val);\n\t\t\t\t$keys[]='/'.(is_numeric($key)?'\\?':preg_quote($key)).'/';\n\t\t\t}\n\t\t}\n\t\t$db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.\n\t\t\t$this->file.' [erase] '.\n\t\t\t($filter?preg_replace($keys,$vals,$filter[0],1):''));\n\t\tif (!$quick && isset($this->trigger['aftererase']))\n\t\t\t\\Base::instance()->call($this->trigger['aftererase'],\n\t\t\t\t[$this,$pkey]);\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tReset cursor\n\t*\t@return NULL\n\t**/\n\tfunction reset() {\n\t\t$this->id=NULL;\n\t\t$this->document=[];\n\t\tparent::reset();\n\t}\n\n\t/**\n\t*\tHydrate mapper object using hive array variable\n\t*\t@return NULL\n\t*\t@param $var array|string\n\t*\t@param $func callback\n\t**/\n\tfunction copyfrom($var,$func=NULL) {\n\t\tif (is_string($var))\n\t\t\t$var=\\Base::instance()->$var;\n\t\tif ($func)\n\t\t\t$var=call_user_func($func,$var);\n\t\tforeach ($var as $key=>$val)\n\t\t\t$this->set($key,$val);\n\t}\n\n\t/**\n\t*\tPopulate hive array variable with mapper fields\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\tfunction copyto($key) {\n\t\t$var=&\\Base::instance()->ref($key);\n\t\tforeach ($this->document as $key=>$field)\n\t\t\t$var[$key]=$field;\n\t}\n\n\t/**\n\t*\tReturn field names\n\t*\t@return array\n\t**/\n\tfunction fields() {\n\t\treturn array_keys($this->document);\n\t}\n\n\t/**\n\t*\tRetrieve external iterator for fields\n\t*\t@return object\n\t**/\n\tfunction getiterator() {\n\t\treturn new \\ArrayIterator($this->cast());\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@return void\n\t*\t@param $db object\n\t*\t@param $file string\n\t**/\n\tfunction __construct(\\DB\\Jig $db,$file) {\n\t\t$this->db=$db;\n\t\t$this->file=$file;\n\t\t$this->reset();\n\t}\n\n}\n"
  },
  {
    "path": "db/jig/session.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace DB\\Jig;\n\nuse ReturnTypeWillChange;\nuse SessionAdapter;\n\n//! Jig-managed session handler\nclass Session extends Mapper {\n\n\tprotected\n\t\t//! Session ID\n\t\t$sid,\n\t\t//! Anti-CSRF token\n\t\t$_csrf,\n\t\t//! User agent\n\t\t$_agent,\n\t\t//! IP,\n\t\t$_ip,\n\t\t//! Suspect callback\n\t\t$onsuspect;\n\n\t/**\n\t*\tOpen session\n\t*\t@return TRUE\n\t*\t@param $path string\n\t*\t@param $name string\n\t**/\n    function open(string $path, string $name): bool\n    {\n        return TRUE;\n    }\n\n\t/**\n\t*\tClose session\n\t*\t@return TRUE\n\t**/\n\tfunction close(): bool\n    {\n\t\t$this->reset();\n\t\t$this->sid=NULL;\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tReturn session data in serialized format\n\t*\t@return string|false\n\t*\t@param $id string\n\t**/\n    #[ReturnTypeWillChange]\n\tfunction read($id)\n    {\n\t\t$this->load(['@session_id=?',$this->sid=$id]);\n\t\tif ($this->dry())\n\t\t\treturn '';\n\t\tif ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) {\n\t\t\t$fw=\\Base::instance();\n\t\t\tif (!isset($this->onsuspect) ||\n\t\t\t\t$fw->call($this->onsuspect,[$this,$id])===FALSE) {\n\t\t\t\t// NB: `session_destroy` can't be called at that stage;\n\t\t\t\t// `session_start` not completed\n\t\t\t\t$this->destroy($id);\n\t\t\t\t$this->close();\n\t\t\t\tunset($fw->{'COOKIE.'.session_name()});\n\t\t\t\t$fw->error(403);\n\t\t\t}\n\t\t}\n\t\treturn $this->get('data');\n\t}\n\n\t/**\n\t*\tWrite session data\n\t*\t@return TRUE\n\t*\t@param $id string\n\t*\t@param $data string\n\t**/\n    function write(string $id, string $data): bool\n    {\n\t\t$this->set('session_id',$id);\n\t\t$this->set('data',$data);\n\t\t$this->set('ip',$this->_ip);\n\t\t$this->set('agent',$this->_agent);\n\t\t$this->set('stamp',time());\n\t\t$this->save();\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tDestroy session\n\t*\t@return TRUE\n\t*\t@param $id string\n\t**/\n\tfunction destroy($id): bool\n    {\n\t\t$this->erase(['@session_id=?',$id]);\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tGarbage collector\n\t**/\n    #[ReturnTypeWillChange]\n    function gc(int $max_lifetime): int\n    {\n\t\treturn (int) $this->erase(['@stamp+?<?',$max_lifetime,time()]);\n\t}\n\n\t/**\n\t *\tReturn session id (if session has started)\n\t *\t@return string|NULL\n\t **/\n\tfunction sid() {\n\t\treturn $this->sid;\n\t}\n\n\t/**\n\t *\tReturn anti-CSRF token\n\t *\t@return string\n\t **/\n\tfunction csrf() {\n\t\treturn $this->_csrf;\n\t}\n\n\t/**\n\t *\tReturn IP address\n\t *\t@return string\n\t **/\n\tfunction ip() {\n\t\treturn $this->_ip;\n\t}\n\n\t/**\n\t*\tReturn Unix timestamp\n\t*\t@return string|FALSE\n\t**/\n\tfunction stamp() {\n\t\tif (!$this->sid)\n\t\t\tsession_start();\n\t\treturn $this->dry()?FALSE:$this->get('stamp');\n\t}\n\n\t/**\n\t*\tReturn HTTP user agent\n\t*\t@return string|FALSE\n\t**/\n\tfunction agent() {\n\t\treturn $this->_agent;\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@param $db \\DB\\Jig\n\t*\t@param $file string\n\t*\t@param $onsuspect callback\n\t*\t@param $key string\n\t**/\n\tfunction __construct(\\DB\\Jig $db,$file='sessions',$onsuspect=NULL,$key=NULL) {\n\t\tparent::__construct($db,$file);\n\t\t$this->onsuspect=$onsuspect;\n        if (version_compare(PHP_VERSION, '8.4.0')>=0) {\n            // TODO: remove this when php7 support is dropped\n            session_set_save_handler(new SessionAdapter($this));\n        } else {\n            session_set_save_handler(\n                [$this,'open'],\n                [$this,'close'],\n                [$this,'read'],\n                [$this,'write'],\n                [$this,'destroy'],\n                [$this,'gc']\n            );\n        }\n\t\tregister_shutdown_function('session_commit');\n\t\t$fw=\\Base::instance();\n\t\t$headers=$fw->HEADERS;\n\t\t$this->_csrf=$fw->hash($fw->SEED.\n\t\t\textension_loaded('openssl')?\n\t\t\t\timplode(unpack('L',openssl_random_pseudo_bytes(4))):\n\t\t\t\tmt_rand()\n\t\t\t);\n\t\tif ($key)\n\t\t\t$fw->$key=$this->_csrf;\n\t\t$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:'';\n\t\t$this->_ip=$fw->IP;\n\t}\n\n}\n"
  },
  {
    "path": "db/jig.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace DB;\n\n//! In-memory/flat-file DB wrapper\nclass Jig {\n\n\t//@{ Storage formats\n\tconst\n\t\tFORMAT_JSON=0,\n\t\tFORMAT_Serialized=1;\n\t//@}\n\n\tprotected\n\t\t//! UUID\n\t\t$uuid,\n\t\t//! Storage location\n\t\t$dir,\n\t\t//! Current storage format\n\t\t$format,\n\t\t//! Jig log\n\t\t$log,\n\t\t//! Memory-held data\n\t\t$data,\n\t\t//! lazy load/save files\n\t\t$lazy;\n\n\t/**\n\t*\tRead data from memory/file\n\t*\t@return array\n\t*\t@param $file string\n\t**/\n\tfunction &read($file) {\n\t\tif (!$this->dir || !is_file($dst=$this->dir.$file)) {\n\t\t\tif (!isset($this->data[$file]))\n\t\t\t\t$this->data[$file]=[];\n\t\t\treturn $this->data[$file];\n\t\t}\n\t\tif ($this->lazy && isset($this->data[$file]))\n\t\t\treturn $this->data[$file];\n\t\t$fw=\\Base::instance();\n\t\t$raw=$fw->read($dst);\n\t\tswitch ($this->format) {\n\t\t\tcase self::FORMAT_JSON:\n\t\t\t\t$data=json_decode($raw,TRUE);\n\t\t\t\tbreak;\n\t\t\tcase self::FORMAT_Serialized:\n\t\t\t\t$data=$fw->unserialize($raw);\n\t\t\t\tbreak;\n\t\t}\n\t\t$this->data[$file] = $data;\n\t\treturn $this->data[$file];\n\t}\n\n\t/**\n\t*\tWrite data to memory/file\n\t*\t@return int\n\t*\t@param $file string\n\t*\t@param $data array\n\t**/\n\tfunction write($file,?array $data=NULL) {\n\t\tif (!$this->dir || $this->lazy)\n\t\t\treturn count($this->data[$file]=$data);\n\t\t$fw=\\Base::instance();\n\t\tswitch ($this->format) {\n\t\t\tcase self::FORMAT_JSON:\n\t\t\t\tif(version_compare(PHP_VERSION, '7.2.0') >= 0)\n\t\t\t\t\t$out=json_encode($data,JSON_PRETTY_PRINT | JSON_INVALID_UTF8_IGNORE);\n\t\t\t\telse\n\t\t\t\t\t$out=json_encode($data,JSON_PRETTY_PRINT | JSON_PARTIAL_OUTPUT_ON_ERROR);\n\t\t\t\tbreak;\n\t\t\tcase self::FORMAT_Serialized:\n\t\t\t\t$out=$fw->serialize($data);\n\t\t\t\tbreak;\n\t\t}\n\t\treturn $fw->write($this->dir.$file,$out);\n\t}\n\n\t/**\n\t*\tReturn directory\n\t*\t@return string\n\t**/\n\tfunction dir() {\n\t\treturn $this->dir;\n\t}\n\n\t/**\n\t*\tReturn UUID\n\t*\t@return string\n\t**/\n\tfunction uuid() {\n\t\treturn $this->uuid;\n\t}\n\n\t/**\n\t*\tReturn profiler results (or disable logging)\n\t*\t@param $flag bool\n\t*\t@return string\n\t**/\n\tfunction log($flag=TRUE) {\n\t\tif ($flag)\n\t\t\treturn $this->log;\n\t\t$this->log=FALSE;\n\t}\n\n\t/**\n\t*\tJot down log entry\n\t*\t@return NULL\n\t*\t@param $frame string\n\t**/\n\tfunction jot($frame) {\n\t\tif ($frame)\n\t\t\t$this->log.=date('r').' '.$frame.PHP_EOL;\n\t}\n\n\t/**\n\t*\tClean storage\n\t*\t@return NULL\n\t**/\n\tfunction drop() {\n\t\tif ($this->lazy) // intentional\n\t\t\t$this->data=[];\n\t\tif (!$this->dir)\n\t\t\t$this->data=[];\n\t\telseif ($glob=@glob($this->dir.'/*',GLOB_NOSORT))\n\t\t\tforeach ($glob as $file)\n\t\t\t\t@unlink($file);\n\t}\n\n\t//! Prohibit cloning\n\tprivate function __clone() {\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@param $dir string\n\t*\t@param $format int\n\t**/\n\tfunction __construct($dir=NULL,$format=self::FORMAT_JSON,$lazy=FALSE) {\n\t\tif ($dir && !is_dir($dir))\n\t\t\tmkdir($dir,\\Base::MODE,TRUE);\n\t\t$this->uuid=\\Base::instance()->hash($this->dir=$dir);\n\t\t$this->format=$format;\n\t\t$this->lazy=$lazy;\n\t}\n\n\t/**\n\t*\tsave file on destruction\n\t**/\n\tfunction __destruct() {\n\t\tif ($this->lazy) {\n\t\t\t$this->lazy = FALSE;\n\t\t\tforeach ($this->data?:[] as $file => $data)\n\t\t\t\t$this->write($file,$data);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "db/mongo/mapper.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace DB\\Mongo;\n\n//! MongoDB mapper\nclass Mapper extends \\DB\\Cursor {\n\n\tprotected\n\t\t//! MongoDB wrapper\n\t\t$db,\n\t\t//! Legacy flag\n\t\t$legacy,\n\t\t//! Mongo collection\n\t\t$collection,\n\t\t//! Mongo document\n\t\t$document=[],\n\t\t//! Mongo cursor\n\t\t$cursor,\n\t\t//! Defined fields\n\t\t$fields;\n\n\t/**\n\t*\tReturn database type\n\t*\t@return string\n\t**/\n\tfunction dbtype() {\n\t\treturn 'Mongo';\n\t}\n\n\t/**\n\t*\tReturn TRUE if field is defined\n\t*\t@return bool\n\t*\t@param $key string\n\t**/\n\tfunction exists($key) {\n\t\treturn array_key_exists($key,$this->document);\n\t}\n\n\t/**\n\t*\tAssign value to field\n\t*\t@return scalar|FALSE\n\t*\t@param $key string\n\t*\t@param $val scalar\n\t**/\n\tfunction set($key,$val) {\n\t\treturn $this->document[$key]=$val;\n\t}\n\n\t/**\n\t*\tRetrieve value of field\n\t*\t@return scalar|FALSE\n\t*\t@param $key string\n\t**/\n\tfunction &get($key) {\n\t\tif ($this->exists($key))\n\t\t\treturn $this->document[$key];\n        throw new \\Exception(sprintf(self::E_Field,$key));\n\t}\n\n\t/**\n\t*\tDelete field\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\tfunction clear($key) {\n\t\tunset($this->document[$key]);\n\t}\n\n\t/**\n\t*\tConvert array to mapper object\n\t*\t@return static\n\t*\t@param $row array\n\t**/\n\tfunction factory($row) {\n\t\t$mapper=clone($this);\n\t\t$mapper->reset();\n\t\tforeach ($row as $key=>$val)\n\t\t\t$mapper->document[$key]=$val;\n\t\t$mapper->query=[clone($mapper)];\n\t\tif (isset($mapper->trigger['load']))\n\t\t\t\\Base::instance()->call($mapper->trigger['load'],$mapper);\n\t\treturn $mapper;\n\t}\n\n\t/**\n\t*\tReturn fields of mapper object as an associative array\n\t*\t@return array\n\t*\t@param $obj object\n\t**/\n\tfunction cast($obj=NULL) {\n\t\tif (!$obj)\n\t\t\t$obj=$this;\n\t\treturn $obj->document;\n\t}\n\n\t/**\n\t*\tBuild query and execute\n\t*\t@return static[]\n\t*\t@param $fields string\n\t*\t@param $filter array\n\t*\t@param $options array\n\t*\t@param $ttl int|array\n\t**/\n\tfunction select($fields=NULL,$filter=NULL,?array $options=NULL,$ttl=0) {\n\t\tif (!$options)\n\t\t\t$options=[];\n\t\t$options+=[\n\t\t\t'group'=>NULL,\n\t\t\t'order'=>NULL,\n\t\t\t'limit'=>0,\n\t\t\t'offset'=>0\n\t\t];\n\t\t$tag='';\n\t\tif (is_array($ttl))\n\t\t\tlist($ttl,$tag)=$ttl;\n\t\t$fw=\\Base::instance();\n\t\t$cache=\\Cache::instance();\n\t\tif (!($cached=$cache->exists($hash=$fw->hash($this->db->dsn().\n\t\t\t$fw->stringify([$fields,$filter,$options])).($tag?'.'.$tag:'').'.mongo',\n\t\t\t$result)) || !$ttl || $cached[0]+$ttl<microtime(TRUE)) {\n\t\t\tif ($options['group']) {\n\t\t\t\t$grp=$this->collection->group(\n\t\t\t\t\t$options['group']['keys'],\n\t\t\t\t\t$options['group']['initial'],\n\t\t\t\t\t$options['group']['reduce'],\n\t\t\t\t\t[\n\t\t\t\t\t\t'condition'=>$filter,\n\t\t\t\t\t\t'finalize'=>$options['group']['finalize']\n\t\t\t\t\t]\n\t\t\t\t);\n\t\t\t\t$tmp=$this->db->selectcollection(\n\t\t\t\t\t$fw->HOST.'.'.$fw->BASE.'.'.\n\t\t\t\t\tuniqid('',TRUE).'.tmp'\n\t\t\t\t);\n\t\t\t\t$tmp->batchinsert($grp['retval'],['w'=>1]);\n\t\t\t\t$filter=[];\n\t\t\t\t$collection=$tmp;\n\t\t\t}\n\t\t\telse {\n\t\t\t\t$filter=$filter?:[];\n\t\t\t\t$collection=$this->collection;\n\t\t\t}\n\t\t\tif ($this->legacy) {\n\t\t\t\t$this->cursor=$collection->find($filter,$fields?:[]);\n\t\t\t\tif ($options['order'])\n\t\t\t\t\t$this->cursor=$this->cursor->sort($options['order']);\n\t\t\t\tif ($options['limit'])\n\t\t\t\t\t$this->cursor=$this->cursor->limit($options['limit']);\n\t\t\t\tif ($options['offset'])\n\t\t\t\t\t$this->cursor=$this->cursor->skip($options['offset']);\n\t\t\t\t$result=[];\n\t\t\t\twhile ($this->cursor->hasnext())\n\t\t\t\t\t$result[]=$this->cursor->getnext();\n\t\t\t}\n\t\t\telse {\n\t\t\t\t$this->cursor=$collection->find($filter,[\n\t\t\t\t\t'sort'=>$options['order'],\n\t\t\t\t\t'limit'=>$options['limit'],\n\t\t\t\t\t'skip'=>$options['offset']\n\t\t\t\t]);\n\t\t\t\t$result=$this->cursor->toarray();\n\t\t\t}\n\t\t\tif ($options['group'])\n\t\t\t\t$tmp->drop();\n\t\t\tif ($fw->CACHE && $ttl)\n\t\t\t\t// Save to cache backend\n\t\t\t\t$cache->set($hash,$result,$ttl);\n\t\t}\n\t\t$out=[];\n\t\tforeach ($result as $doc)\n\t\t\t$out[]=$this->factory($doc);\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tReturn records that match criteria\n\t*\t@return static[]\n\t*\t@param $filter array\n\t*\t@param $options array\n\t*\t@param $ttl int|array\n\t**/\n\tfunction find($filter=NULL,?array $options=NULL,$ttl=0) {\n\t\tif (!$options)\n\t\t\t$options=[];\n\t\t$options+=[\n\t\t\t'group'=>NULL,\n\t\t\t'order'=>NULL,\n\t\t\t'limit'=>0,\n\t\t\t'offset'=>0\n\t\t];\n\t\treturn $this->select($this->fields,$filter,$options,$ttl);\n\t}\n\n\t/**\n\t*\tCount records that match criteria\n\t*\t@return int\n\t*\t@param $filter array\n\t*\t@param $options array\n\t*\t@param $ttl int|array\n\t**/\n\tfunction count($filter=NULL,?array $options=NULL,$ttl=0) {\n\t\t$fw=\\Base::instance();\n\t\t$cache=\\Cache::instance();\n\t\t$tag='';\n\t\tif (is_array($ttl))\n\t\t\tlist($ttl,$tag)=$ttl;\n\t\tif (!($cached=$cache->exists($hash=$fw->hash($fw->stringify(\n\t\t\t[$filter])).($tag?'.'.$tag:'').'.mongo',$result)) || !$ttl ||\n\t\t\t$cached[0]+$ttl<microtime(TRUE)) {\n\t\t\t$result=$this->collection->count($filter?:[]);\n\t\t\tif ($fw->CACHE && $ttl)\n\t\t\t\t// Save to cache backend\n\t\t\t\t$cache->set($hash,$result,$ttl);\n\t\t}\n\t\treturn $result;\n\t}\n\n\t/**\n\t*\tReturn record at specified offset using criteria of previous\n\t*\tload() call and make it active\n\t*\t@return array\n\t*\t@param $ofs int\n\t**/\n\tfunction skip($ofs=1) {\n\t\t$this->document=($out=parent::skip($ofs))?$out->document:[];\n\t\tif ($this->document && isset($this->trigger['load']))\n\t\t\t\\Base::instance()->call($this->trigger['load'],$this);\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tInsert new record\n\t*\t@return array\n\t**/\n\tfunction insert() {\n\t\tif (isset($this->document['_id']))\n\t\t\treturn $this->update();\n\t\tif (isset($this->trigger['beforeinsert']) &&\n\t\t\t\\Base::instance()->call($this->trigger['beforeinsert'],\n\t\t\t\t[$this,['_id'=>$this->document['_id']]])===FALSE)\n\t\t\treturn $this->document;\n\t\tif ($this->legacy) {\n\t\t\t$this->collection->insert($this->document);\n\t\t\t$pkey=['_id'=>$this->document['_id']];\n\t\t}\n\t\telse {\n\t\t\t$result=$this->collection->insertone($this->document);\n\t\t\t$pkey=['_id'=>$result->getinsertedid()];\n\t\t}\n\t\tif (isset($this->trigger['afterinsert']))\n\t\t\t\\Base::instance()->call($this->trigger['afterinsert'],\n\t\t\t\t[$this,$pkey]);\n\t\t$this->load($pkey);\n\t\treturn $this->document;\n\t}\n\n\t/**\n\t*\tUpdate current record\n\t*\t@return array\n\t**/\n\tfunction update() {\n\t\t$pkey=['_id'=>$this->document['_id']];\n\t\tif (isset($this->trigger['beforeupdate']) &&\n\t\t\t\\Base::instance()->call($this->trigger['beforeupdate'],\n\t\t\t\t[$this,$pkey])===FALSE)\n\t\t\treturn $this->document;\n\t\t$upsert=['upsert'=>TRUE];\n\t\tif ($this->legacy)\n\t\t\t$this->collection->update($pkey,$this->document,$upsert);\n\t\telse\n\t\t\t$this->collection->replaceone($pkey,$this->document,$upsert);\n\t\tif (isset($this->trigger['afterupdate']))\n\t\t\t\\Base::instance()->call($this->trigger['afterupdate'],\n\t\t\t\t[$this,$pkey]);\n\t\treturn $this->document;\n\t}\n\n\t/**\n\t*\tDelete current record\n\t*\t@return bool\n\t*\t@param $quick bool\n\t*\t@param $filter array\n\t**/\n\tfunction erase($filter=NULL,$quick=TRUE) {\n\t\tif ($filter) {\n\t\t\tif (!$quick) {\n\t\t\t\tforeach ($this->find($filter) as $mapper)\n\t\t\t\t\tif (!$mapper->erase())\n\t\t\t\t\t\treturn FALSE;\n\t\t\t\treturn TRUE;\n\t\t\t}\n\t\t\treturn $this->legacy?\n\t\t\t\t$this->collection->remove($filter):\n\t\t\t\t$this->collection->deletemany($filter);\n\t\t}\n\t\t$pkey=['_id'=>$this->document['_id']];\n\t\tif (isset($this->trigger['beforeerase']) &&\n\t\t\t\\Base::instance()->call($this->trigger['beforeerase'],\n\t\t\t\t[$this,$pkey])===FALSE)\n\t\t\treturn FALSE;\n\t\t$result=$this->legacy?\n\t\t\t$this->collection->remove(['_id'=>$this->document['_id']]):\n\t\t\t$this->collection->deleteone(['_id'=>$this->document['_id']]);\n\t\tparent::erase();\n\t\tif (isset($this->trigger['aftererase']))\n\t\t\t\\Base::instance()->call($this->trigger['aftererase'],\n\t\t\t\t[$this,$pkey]);\n\t\treturn $result;\n\t}\n\n\t/**\n\t*\tReset cursor\n\t*\t@return NULL\n\t**/\n\tfunction reset() {\n\t\t$this->document=[];\n\t\tparent::reset();\n\t}\n\n\t/**\n\t*\tHydrate mapper object using hive array variable\n\t*\t@return NULL\n\t*\t@param $var array|string\n\t*\t@param $func callback\n\t**/\n\tfunction copyfrom($var,$func=NULL) {\n\t\tif (is_string($var))\n\t\t\t$var=\\Base::instance()->$var;\n\t\tif ($func)\n\t\t\t$var=call_user_func($func,$var);\n\t\tforeach ($var as $key=>$val)\n\t\t\t$this->set($key,$val);\n\t}\n\n\t/**\n\t*\tPopulate hive array variable with mapper fields\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\tfunction copyto($key) {\n\t\t$var=&\\Base::instance()->ref($key);\n\t\tforeach ($this->document as $key=>$field)\n\t\t\t$var[$key]=$field;\n\t}\n\n\t/**\n\t*\tReturn field names\n\t*\t@return array\n\t**/\n\tfunction fields() {\n\t\treturn array_keys($this->document);\n\t}\n\n\t/**\n\t*\tReturn the cursor from last query\n\t*\t@return object|NULL\n\t**/\n\tfunction cursor() {\n\t\treturn $this->cursor;\n\t}\n\n\t/**\n\t*\tRetrieve external iterator for fields\n\t*\t@return object\n\t**/\n\tfunction getiterator() {\n\t\treturn new \\ArrayIterator($this->cast());\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@return void\n\t*\t@param $db object\n\t*\t@param $collection string\n\t*\t@param $fields array\n\t**/\n\tfunction __construct(\\DB\\Mongo $db,$collection,$fields=NULL) {\n\t\t$this->db=$db;\n\t\t$this->legacy=$db->legacy();\n\t\t$this->collection=$db->selectcollection($collection);\n\t\t$this->fields=$fields;\n\t\t$this->reset();\n\t}\n\n}\n"
  },
  {
    "path": "db/mongo/session.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace DB\\Mongo;\n\nuse ReturnTypeWillChange;\nuse SessionAdapter;\n\n//! MongoDB-managed session handler\nclass Session extends Mapper {\n\n\tprotected\n\t\t//! Session ID\n\t\t$sid,\n\t\t//! Anti-CSRF token\n\t\t$_csrf,\n\t\t//! User agent\n\t\t$_agent,\n\t\t//! IP,\n\t\t$_ip,\n\t\t//! Suspect callback\n\t\t$onsuspect;\n\n\t/**\n\t*\tOpen session\n\t*\t@return TRUE\n\t*\t@param $path string\n\t*\t@param $name string\n\t**/\n    function open(string $path, string $name): bool\n    {\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tClose session\n\t*\t@return TRUE\n\t**/\n\tfunction close(): bool\n    {\n\t\t$this->reset();\n\t\t$this->sid=NULL;\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tReturn session data in serialized format\n\t*\t@return string\n\t*\t@param $id string\n\t**/\n    #[ReturnTypeWillChange]\n    function read(string $id)\n    {\n\t\t$this->load(['session_id'=>$this->sid=$id]);\n\t\tif ($this->dry())\n\t\t\treturn '';\n\t\tif ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) {\n\t\t\t$fw=\\Base::instance();\n\t\t\tif (!isset($this->onsuspect) ||\n\t\t\t\t$fw->call($this->onsuspect,[$this,$id])===FALSE) {\n\t\t\t\t// NB: `session_destroy` can't be called at that stage;\n\t\t\t\t// `session_start` not completed\n\t\t\t\t$this->destroy($id);\n\t\t\t\t$this->close();\n\t\t\t\tunset($fw->{'COOKIE.'.session_name()});\n\t\t\t\t$fw->error(403);\n\t\t\t}\n\t\t}\n\t\treturn $this->get('data');\n\t}\n\n\t/**\n\t*\tWrite session data\n\t*\t@return TRUE\n\t*\t@param $id string\n\t*\t@param $data string\n\t**/\n    function write(string $id, string $data): bool\n    {\n\t\t$this->set('session_id',$id);\n\t\t$this->set('data',$data);\n\t\t$this->set('ip',$this->_ip);\n\t\t$this->set('agent',$this->_agent);\n\t\t$this->set('stamp',time());\n\t\t$this->save();\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tDestroy session\n\t*\t@return TRUE\n\t*\t@param $id string\n\t**/\n\tfunction destroy($id): bool\n    {\n\t\t$this->erase(['session_id'=>$id]);\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tGarbage collector\n\t**/\n    #[ReturnTypeWillChange]\n    function gc(int $max_lifetime): int\n    {\n\t\treturn (int) $this->erase(['$where'=>'this.stamp+'.$max_lifetime.'<'.time()]);\n\t}\n\n\t/**\n\t *\tReturn session id (if session has started)\n\t *\t@return string|NULL\n\t **/\n\tfunction sid() {\n\t\treturn $this->sid;\n\t}\n\n\t/**\n\t *\tReturn anti-CSRF token\n\t *\t@return string\n\t **/\n\tfunction csrf() {\n\t\treturn $this->_csrf;\n\t}\n\n\t/**\n\t *\tReturn IP address\n\t *\t@return string\n\t **/\n\tfunction ip() {\n\t\treturn $this->_ip;\n\t}\n\n\t/**\n\t *\tReturn Unix timestamp\n\t *\t@return string|FALSE\n\t **/\n\tfunction stamp() {\n\t\tif (!$this->sid)\n\t\t\tsession_start();\n\t\treturn $this->dry()?FALSE:$this->get('stamp');\n\t}\n\n\t/**\n\t *\tReturn HTTP user agent\n\t *\t@return string\n\t **/\n\tfunction agent() {\n\t\treturn $this->_agent;\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@param $db \\DB\\Mongo\n\t*\t@param $table string\n\t*\t@param $onsuspect callback\n\t*\t@param $key string\n\t**/\n\tfunction __construct(\\DB\\Mongo $db,$table='sessions',$onsuspect=NULL,$key=NULL) {\n\t\tparent::__construct($db,$table);\n\t\t$this->onsuspect=$onsuspect;\n        if (version_compare(PHP_VERSION, '8.4.0')>=0) {\n            // TODO: remove this when php7 support is dropped\n            session_set_save_handler(new SessionAdapter($this));\n        } else {\n            session_set_save_handler(\n                [$this,'open'],\n                [$this,'close'],\n                [$this,'read'],\n                [$this,'write'],\n                [$this,'destroy'],\n                [$this,'gc']\n            );\n        }\n\t\tregister_shutdown_function('session_commit');\n\t\t$fw=\\Base::instance();\n\t\t$headers=$fw->HEADERS;\n\t\t$this->_csrf=$fw->hash($fw->SEED.\n\t\t\textension_loaded('openssl')?\n\t\t\t\timplode(unpack('L',openssl_random_pseudo_bytes(4))):\n\t\t\t\tmt_rand()\n\t\t\t);\n\t\tif ($key)\n\t\t\t$fw->$key=$this->_csrf;\n\t\t$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:'';\n\t\t$this->_ip=$fw->IP;\n\t}\n\n}\n"
  },
  {
    "path": "db/mongo.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace DB;\n\n//! MongoDB wrapper\nclass Mongo {\n\n\t//@{\n\tconst\n\t\tE_Profiler='MongoDB profiler is disabled';\n\t//@}\n\n\tprotected\n\t\t//! UUID\n\t\t$uuid,\n\t\t//! Data source name\n\t\t$dsn,\n\t\t//! MongoDB object\n\t\t$db,\n\t\t//! Legacy flag\n\t\t$legacy,\n\t\t//! MongoDB log\n\t\t$log;\n\n\t/**\n\t*\tReturn data source name\n\t*\t@return string\n\t**/\n\tfunction dsn() {\n\t\treturn $this->dsn;\n\t}\n\n\t/**\n\t*\tReturn UUID\n\t*\t@return string\n\t**/\n\tfunction uuid() {\n\t\treturn $this->uuid;\n\t}\n\n\t/**\n\t*\tReturn MongoDB profiler results (or disable logging)\n\t*\t@param $flag bool\n\t*\t@return string\n\t**/\n\tfunction log($flag=TRUE) {\n\t\tif ($flag) {\n\t\t\t$cursor=$this->db->selectcollection('system.profile')->find();\n\t\t\tforeach (iterator_to_array($cursor) as $frame)\n\t\t\t\tif (!preg_match('/\\.system\\..+$/',$frame['ns']))\n\t\t\t\t\t$this->log.=date('r',$this->legacy() ?\n\t\t\t\t\t\t$frame['ts']->sec : (round((string)$frame['ts'])/1000)).\n\t\t\t\t\t\t' ('.sprintf('%.1f',$frame['millis']).'ms) '.\n\t\t\t\t\t\t$frame['ns'].' ['.$frame['op'].'] '.\n\t\t\t\t\t\t(empty($frame['query'])?\n\t\t\t\t\t\t\t'':json_encode($frame['query'])).\n\t\t\t\t\t\t(empty($frame['command'])?\n\t\t\t\t\t\t\t'':json_encode($frame['command'])).\n\t\t\t\t\t\tPHP_EOL;\n\t\t} else {\n\t\t\t$this->log=FALSE;\n\t\t\tif ($this->legacy)\n\t\t\t\t$this->db->setprofilinglevel(-1);\n\t\t\telse\n\t\t\t\t$this->db->command(['profile'=>-1]);\n\t\t}\n\t\treturn $this->log;\n\t}\n\n\t/**\n\t*\tIntercept native call to re-enable profiler\n\t*\t@return int\n\t**/\n\tfunction drop() {\n\t\t$out=$this->db->drop();\n\t\tif ($this->log!==FALSE) {\n\t\t\tif ($this->legacy)\n\t\t\t\t$this->db->setprofilinglevel(2);\n\t\t\telse\n\t\t\t\t$this->db->command(['profile'=>2]);\n\t\t}\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tRedirect call to MongoDB object\n\t*\t@return mixed\n\t*\t@param $func string\n\t*\t@param $args array\n\t**/\n\tfunction __call($func,array $args) {\n\t\treturn call_user_func_array([$this->db,$func],$args);\n\t}\n\n\t/**\n\t*\tReturn TRUE if legacy driver is loaded\n\t*\t@return bool\n\t**/\n\tfunction legacy() {\n\t\treturn $this->legacy;\n\t}\n\n\t//! Prohibit cloning\n\tprivate function __clone() {\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@param $dsn string\n\t*\t@param $dbname string\n\t*\t@param $options array\n\t**/\n\tfunction __construct($dsn,$dbname,?array $options=NULL) {\n\t\t$this->uuid=\\Base::instance()->hash($this->dsn=$dsn);\n\t\tif ($this->legacy=class_exists('\\MongoClient')) {\n\t\t\t$this->db=new \\MongoDB(new \\MongoClient($dsn,$options?:[]),$dbname);\n\t\t\t$this->db->setprofilinglevel(2);\n\t\t}\n\t\telse {\n\t\t\t$this->db=(new \\MongoDB\\Client($dsn,$options?:[]))->$dbname;\n\t\t\t$this->db->command(['profile'=>2]);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "db/sql/mapper.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace DB\\SQL;\n\n//! SQL data mapper\nclass Mapper extends \\DB\\Cursor {\n\n\t//@{ Error messages\n\tconst\n\t\tE_PKey='Table %s does not have a primary key';\n\t//@}\n\n\tprotected\n\t\t//! PDO wrapper\n\t\t$db,\n\t\t//! Database engine\n\t\t$engine,\n\t\t//! SQL table\n\t\t$source,\n\t\t//! SQL table (quoted)\n\t\t$table,\n\t\t//! Alias for SQL table\n\t\t$as,\n\t\t//! Last insert ID\n\t\t$_id,\n\t\t//! Defined fields\n\t\t$fields,\n\t\t//! Adhoc fields\n\t\t$adhoc=[],\n\t\t//! Dynamic properties\n\t\t$props=[];\n\n\t/**\n\t*\tReturn database type\n\t*\t@return string\n\t**/\n\tfunction dbtype() {\n\t\treturn 'SQL';\n\t}\n\n\t/**\n\t*\tReturn mapped table\n\t*\t@return string\n\t**/\n\tfunction table() {\n\t\treturn $this->source;\n\t}\n\n\t/**\n\t*\tReturn TRUE if any/specified field value has changed\n\t*\t@return bool\n\t*\t@param $key string\n\t**/\n\tfunction changed($key=NULL) {\n\t\tif (isset($key))\n\t\t\treturn $this->fields[$key]['changed'];\n\t\tforeach($this->fields as $key=>$field)\n\t\t\tif ($field['changed'])\n\t\t\t\treturn TRUE;\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tReturn TRUE if field is defined\n\t*\t@return bool\n\t*\t@param $key string\n\t**/\n\tfunction exists($key) {\n\t\treturn array_key_exists($key,$this->fields+$this->adhoc);\n\t}\n\n\t/**\n\t*\tAssign value to field\n\t*\t@return scalar\n\t*\t@param $key string\n\t*\t@param $val scalar\n\t**/\n\tfunction set($key,$val) {\n\t\tif (array_key_exists($key,$this->fields)) {\n\t\t\t$val=is_null($val) && $this->fields[$key]['nullable']?\n\t\t\t\tNULL:$this->db->value($this->fields[$key]['pdo_type'],$val);\n\t\t\tif ($this->fields[$key]['initial']!==$val ||\n\t\t\t\t$this->fields[$key]['default']!==$val && is_null($val))\n\t\t\t\t$this->fields[$key]['changed']=TRUE;\n\t\t\treturn $this->fields[$key]['value']=$val;\n\t\t}\n\t\t// Adjust result on existing expressions\n\t\tif (isset($this->adhoc[$key]))\n\t\t\t$this->adhoc[$key]['value']=$val;\n\t\telseif (is_string($val))\n\t\t\t// Parenthesize expression in case it's a subquery\n\t\t\t$this->adhoc[$key]=['expr'=>'('.$val.')','value'=>NULL];\n\t\telse\n\t\t\t$this->props[$key]=$val;\n\t\treturn $val;\n\t}\n\n\t/**\n\t*\tRetrieve value of field\n\t*\t@return scalar\n\t*\t@param $key string\n\t**/\n\tfunction &get($key) {\n\t\tif ($key=='_id')\n\t\t\treturn $this->_id;\n\t\telseif (array_key_exists($key,$this->fields))\n\t\t\treturn $this->fields[$key]['value'];\n\t\telseif (array_key_exists($key,$this->adhoc))\n\t\t\treturn $this->adhoc[$key]['value'];\n\t\telseif (array_key_exists($key,$this->props))\n\t\t\treturn $this->props[$key];\n        throw new \\Exception(sprintf(self::E_Field,$key));\n\t}\n\n\t/**\n\t*\tClear value of field\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\tfunction clear($key) {\n\t\tif (array_key_exists($key,$this->adhoc))\n\t\t\tunset($this->adhoc[$key]);\n\t\telse\n\t\t\tunset($this->props[$key]);\n\t}\n\n\t/**\n\t*\tInvoke dynamic method\n\t*\t@return mixed\n\t*\t@param $func string\n\t*\t@param $args array\n\t*\t@deprecated (this is only used for custom dynamic properties that are callables\n\t**/\n\tfunction __call($func,$args) {\n\t\t$callable = (array_key_exists($func,$this->props) ? $this->props[$func] : $this->$func);\n\t\treturn $callable ? call_user_func_array($callable,$args) : null;\n\t}\n\n\t/**\n\t*\tConvert array to mapper object\n\t*\t@return static\n\t*\t@param $row array\n\t**/\n\tfunction factory($row) {\n\t\t$mapper=clone($this);\n\t\t$mapper->reset();\n\t\tforeach ($row as $key=>$val) {\n\t\t\tif (array_key_exists($key,$this->fields))\n\t\t\t\t$var='fields';\n\t\t\telseif (array_key_exists($key,$this->adhoc))\n\t\t\t\t$var='adhoc';\n\t\t\telse\n\t\t\t\tcontinue;\n\t\t\t$mapper->{$var}[$key]['value']=$val;\n\t\t\t$mapper->{$var}[$key]['initial']=$val;\n\t\t\tif ($var=='fields' && $mapper->{$var}[$key]['pkey'])\n\t\t\t\t$mapper->{$var}[$key]['previous']=$val;\n\t\t}\n\t\t$mapper->query=[clone($mapper)];\n\t\tif (isset($mapper->trigger['load']))\n\t\t\t\\Base::instance()->call($mapper->trigger['load'],$mapper);\n\t\treturn $mapper;\n\t}\n\n\t/**\n\t*\tReturn fields of mapper object as an associative array\n\t*\t@return array\n\t*\t@param $obj object\n\t**/\n\tfunction cast($obj=NULL) {\n\t\tif (!$obj)\n\t\t\t$obj=$this;\n\t\treturn array_map(\n\t\t\tfunction($row) {\n\t\t\t\treturn $row['value'];\n\t\t\t},\n\t\t\t$obj->fields+$obj->adhoc\n\t\t);\n\t}\n\n\t/**\n\t*\tBuild query string and arguments\n\t*\t@return array\n\t*\t@param $fields string\n\t*\t@param $filter string|array\n\t*\t@param $options array\n\t**/\n\tfunction stringify($fields,$filter=NULL,?array $options=NULL) {\n\t\tif (!$options)\n\t\t\t$options=[];\n\t\t$options+=[\n\t\t\t'group'=>NULL,\n\t\t\t'order'=>NULL,\n\t\t\t'limit'=>0,\n\t\t\t'offset'=>0,\n\t\t\t'comment'=>NULL\n\t\t];\n\t\t$db=$this->db;\n\t\t$sql='SELECT '.$fields.' FROM '.$this->table;\n\t\tif (isset($this->as))\n\t\t\t$sql.=' AS '.$this->db->quotekey($this->as);\n\t\t$args=[];\n\t\tif (is_array($filter) && !empty($filter)) {\n\t\t\t$args=isset($filter[1]) && is_array($filter[1])?\n\t\t\t\t$filter[1]:\n\t\t\t\tarray_slice($filter,1,NULL,TRUE);\n\t\t\t$args=is_array($args)?$args:[1=>$args];\n\t\t\tlist($filter)=$filter;\n\t\t}\n\t\tif ($filter)\n\t\t\t$sql.=' WHERE '.$filter;\n\t\tif ($options['group']) {\n\t\t\t$sql.=' GROUP BY '.implode(',',array_map(\n\t\t\t\tfunction($str) use($db) {\n\t\t\t\t\treturn preg_replace_callback(\n\t\t\t\t\t\t'/\\b(\\w+[._\\-\\w]*)\\h*(HAVING.+|$)/i',\n\t\t\t\t\t\tfunction($parts) use($db) {\n\t\t\t\t\t\t\treturn $db->quotekey($parts[1]).\n\t\t\t\t\t\t\t\t(isset($parts[2])?(' '.$parts[2]):'');\n\t\t\t\t\t\t},\n\t\t\t\t\t\t$str\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\texplode(',',$options['group'])));\n\t\t}\n\t\tif ($options['order']) {\n\t\t\t$char=substr($db->quotekey(''),0,1);// quoting char\n\t\t\t$order=' ORDER BY '.(is_bool(strpos($options['order'],$char))?\n\t\t\t\timplode(',',array_map(function($str) use($db) {\n\t\t\t\t\treturn preg_match('/^\\h*(\\w+[._\\-\\w]*)'.\n\t\t\t\t\t\t'(?:\\h+((?:ASC|DESC)[\\w\\h]*))?\\h*$/i',\n\t\t\t\t\t\t$str,$parts)?\n\t\t\t\t\t\t($db->quotekey($parts[1]).\n\t\t\t\t\t\t(isset($parts[2])?(' '.$parts[2]):'')):$str;\n\t\t\t\t},explode(',',$options['order']))):\n\t\t\t\t$options['order']);\n\t\t}\n\t\t// SQL Server fixes\n\t\tif (preg_match('/mssql|sqlsrv|odbc/', $this->engine) &&\n\t\t\t($options['limit'] || $options['offset'])) {\n\t\t\t// order by pkey when no ordering option was given\n\t\t\tif (!$options['order'])\n\t\t\t\tforeach ($this->fields as $key=>$field)\n\t\t\t\t\tif ($field['pkey']) {\n\t\t\t\t\t\t$order=' ORDER BY '.$db->quotekey($key);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t$ofs=$options['offset']?(int)$options['offset']:0;\n\t\t\t$lmt=$options['limit']?(int)$options['limit']:0;\n\t\t\tif (strncmp($db->version(),'11',2)>=0) {\n\t\t\t\t// SQL Server >= 2012\n\t\t\t\t$sql.=$order.' OFFSET '.$ofs.' ROWS';\n\t\t\t\tif ($lmt)\n\t\t\t\t\t$sql.=' FETCH NEXT '.$lmt.' ROWS ONLY';\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// SQL Server 2008\n\t\t\t\t$sql=preg_replace('/SELECT/',\n\t\t\t\t\t'SELECT '.\n\t\t\t\t\t($lmt>0?'TOP '.($ofs+$lmt):'').' ROW_NUMBER() '.\n\t\t\t\t\t'OVER ('.$order.') AS rnum,',$sql.$order,1);\n\t\t\t\t$sql='SELECT * FROM ('.$sql.') x WHERE rnum > '.($ofs);\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tif (isset($order))\n\t\t\t\t$sql.=$order;\n\t\t\tif ($options['limit'])\n\t\t\t\t$sql.=' LIMIT '.(int)$options['limit'];\n\t\t\tif ($options['offset'])\n\t\t\t\t$sql.=' OFFSET '.(int)$options['offset'];\n\t\t}\n\t\tif ($options['comment'])\n\t\t\t$sql.=\"\\n\".' /* '.$options['comment'].' */';\n\t\treturn [$sql,$args];\n\t}\n\n\t/**\n\t*\tBuild query string and execute\n\t*\t@return static[]\n\t*\t@param $fields string\n\t*\t@param $filter string|array\n\t*\t@param $options array\n\t*\t@param $ttl int|array\n\t**/\n\tfunction select($fields,$filter=NULL,?array $options=NULL,$ttl=0) {\n\t\tlist($sql,$args)=$this->stringify($fields,$filter,$options);\n\t\t$result=$this->db->exec($sql,$args,$ttl);\n\t\t$out=[];\n\t\tforeach ($result as &$row) {\n\t\t\tforeach ($row as $field=>&$val) {\n\t\t\t\tif (array_key_exists($field,$this->fields)) {\n\t\t\t\t\tif (!is_null($val) || !$this->fields[$field]['nullable'])\n\t\t\t\t\t\t$val=$this->db->value(\n\t\t\t\t\t\t\t$this->fields[$field]['pdo_type'],$val);\n\t\t\t\t}\n\t\t\t\tunset($val);\n\t\t\t}\n\t\t\t$out[]=$this->factory($row);\n\t\t\tunset($row);\n\t\t}\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tReturn records that match criteria\n\t*\t@return static[]\n\t*\t@param $filter string|array\n\t*\t@param $options array\n\t*\t@param $ttl int|array\n\t**/\n\tfunction find($filter=NULL,?array $options=NULL,$ttl=0) {\n\t\tif (!$options)\n\t\t\t$options=[];\n\t\t$options+=[\n\t\t\t'group'=>NULL,\n\t\t\t'order'=>NULL,\n\t\t\t'limit'=>0,\n\t\t\t'offset'=>0\n\t\t];\n\t\t$adhoc='';\n\t\tforeach ($this->adhoc as $key=>$field)\n\t\t\t$adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key);\n\t\treturn $this->select(\n\t\t\t($options['group'] && !preg_match('/mysql|sqlite/',$this->engine)?\n\t\t\t\t$options['group']:\n\t\t\t\timplode(',',array_map([$this->db,'quotekey'],\n\t\t\t\t\tarray_keys($this->fields)))).$adhoc,$filter,$options,$ttl);\n\t}\n\n\t/**\n\t*\tCount records that match criteria\n\t*\t@return int\n\t*\t@param $filter string|array\n\t*\t@param $options array\n\t*\t@param $ttl int|array\n\t**/\n\tfunction count($filter=NULL,?array $options=NULL,$ttl=0) {\n\t\t$adhoc=[];\n\t\t// with grouping involved, we need to wrap the actualy query and count the results\n\t\tif ($subquery_mode=($options && !empty($options['group']))) {\n\t\t\t$group_string=preg_replace('/HAVING.+$/i','',$options['group']);\n\t\t\t$group_fields=array_flip(array_map('trim',explode(',',$group_string)));\n\t\t\tforeach ($this->adhoc as $key=>$field)\n\t\t\t\t// add adhoc fields that are used for grouping\n\t\t\t\tif (isset($group_fields[$key]))\n\t\t\t\t\t$adhoc[]=$field['expr'].' AS '.$this->db->quotekey($key);\n\t\t\t$fields=implode(',',$adhoc);\n\t\t\tif (empty($fields))\n\t\t\t\t// Select at least one field, ideally the grouping fields\n\t\t\t\t// or sqlsrv fails\n\t\t\t\t$fields=$group_string;\n\t\t\tif (preg_match('/mssql|dblib|sqlsrv/',$this->engine))\n\t\t\t\t$fields='TOP 100 PERCENT '.$fields;\n\t\t} else {\n\t\t\t// for simple count just add a new adhoc counter\n\t\t\t$fields='COUNT(*) AS '.$this->db->quotekey('_rows');\n\t\t}\n\t\t// no need to order for a count query as that could include virtual\n\t\t// field references that are not present here\n\t\tunset($options['order']);\n\t\tlist($sql,$args)=$this->stringify($fields,$filter,$options);\n\t\tif ($subquery_mode)\n\t\t\t$sql='SELECT COUNT(*) AS '.$this->db->quotekey('_rows').' '.\n\t\t\t\t'FROM ('.$sql.') AS '.$this->db->quotekey('_temp');\n\t\t$result=$this->db->exec($sql,$args,$ttl);\n\t\tunset($this->adhoc['_rows']);\n\t\treturn (int)$result[0]['_rows'];\n\t}\n\t/**\n\t*\tReturn record at specified offset using same criteria as\n\t*\tprevious load() call and make it active\n\t*\t@return static\n\t*\t@param $ofs int\n\t**/\n\tfunction skip($ofs=1) {\n\t\t$out=parent::skip($ofs);\n\t\t$dry=$this->dry();\n\t\tforeach ($this->fields as $key=>&$field) {\n\t\t\t$field['value']=$dry?NULL:$out->fields[$key]['value'];\n\t\t\t$field['initial']=$field['value'];\n\t\t\t$field['changed']=FALSE;\n\t\t\tif ($field['pkey'])\n\t\t\t\t$field['previous']=$dry?NULL:$out->fields[$key]['value'];\n\t\t\tunset($field);\n\t\t}\n\t\tforeach ($this->adhoc as $key=>&$field) {\n\t\t\t$field['value']=$dry?NULL:$out->adhoc[$key]['value'];\n\t\t\tunset($field);\n\t\t}\n\t\tif (!$dry && isset($this->trigger['load']))\n\t\t\t\\Base::instance()->call($this->trigger['load'],$this);\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tInsert new record\n\t*\t@return static\n\t**/\n\tfunction insert() {\n\t\t$args=[];\n\t\t$actr=0;\n\t\t$nctr=0;\n\t\t$fields='';\n\t\t$values='';\n\t\t$filter='';\n\t\t$pkeys=[];\n\t\t$aikeys=[];\n\t\t$nkeys=[];\n\t\t$ckeys=[];\n\t\t$inc=NULL;\n\t\tforeach ($this->fields as $key=>$field)\n\t\t\tif ($field['pkey'])\n\t\t\t\t$pkeys[$key]=$field['previous'];\n\t\tif (isset($this->trigger['beforeinsert']) &&\n\t\t\t\\Base::instance()->call($this->trigger['beforeinsert'],\n\t\t\t\t[$this,$pkeys])===FALSE)\n\t\t\treturn $this;\n\t\tif ($this->valid())\n\t\t\t// duplicate record\n\t\t\tforeach ($this->fields as $key=>&$field) {\n\t\t\t\t$field['changed']=true;\n\t\t\t\tif ($field['pkey'] && !$inc && ($field['auto_inc'] === TRUE ||\n\t\t\t\t\t\t($field['auto_inc'] === NULL && !$field['nullable']\n\t\t\t\t\t\t\t&& $field['pdo_type']==\\PDO::PARAM_INT)\n\t\t\t\t))\n\t\t\t\t\t$inc=$key;\n\t\t\t\tunset($field);\n\t\t\t}\n\t\tforeach ($this->fields as $key=>&$field) {\n\t\t\tif ($field['auto_inc']) {\n                $aikeys[] = $key;\n            }\n\t\t\tif ($field['pkey']) {\n\t\t\t\t$field['previous']=$field['value'];\n\t\t\t\tif (!$inc && empty($field['value']) &&\n\t\t\t\t\t($field['auto_inc'] === TRUE || ($field['auto_inc'] === NULL\n\t\t\t\t\t\t&& $field['pdo_type']==\\PDO::PARAM_INT && !$field['nullable']))\n\t\t\t\t)\n\t\t\t\t\t$inc=$key;\n\t\t\t\t$filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?';\n\t\t\t\t$nkeys[$nctr+1]=[$field['value'],$field['pdo_type']];\n\t\t\t\t++$nctr;\n\t\t\t}\n\t\t\tif ($field['changed'] && $key!=$inc) {\n\t\t\t\t$fields.=($actr?',':'').$this->db->quotekey($key);\n\t\t\t\t$values.=($actr?',':'').'?';\n\t\t\t\t$args[$actr+1]=[$field['value'],$field['pdo_type']];\n\t\t\t\t++$actr;\n\t\t\t\t$ckeys[]=$key;\n\t\t\t}\n\t\t\tunset($field);\n\t\t}\n        if ($fields) {\n\t\t\t$add=$aik='';\n\t\t\tif ($this->engine=='pgsql' && !empty($pkeys)) {\n\t\t\t\t$names=array_keys($pkeys);\n\t\t\t\t$aik=end($names);\n\t\t\t\t$add=' RETURNING '.$this->db->quotekey($aik);\n\t\t\t}\n\t\t\t$lID=$this->db->exec(\n\t\t\t\t(preg_match('/mssql|dblib|sqlsrv/',$this->engine) &&\n\t\t\t\tarray_intersect(array_keys($aikeys),$ckeys)?\n\t\t\t\t\t'SET IDENTITY_INSERT '.$this->table.' ON;':'').\n\t\t\t\t'INSERT INTO '.$this->table.' ('.$fields.') '.\n\t\t\t\t'VALUES ('.$values.')'.$add,$args\n\t\t\t);\n\t\t\tif ($this->engine=='pgsql' && $lID && $aik)\n\t\t\t\t$this->_id=$lID[0][$aik];\n\t\t\telseif ($this->engine!='oci')\n\t\t\t\t$this->_id=$this->db->lastinsertid();\n\t\t\t// Reload to obtain default and auto-increment field values\n\t\t\tif ($reload=(($inc && $this->_id) || $filter))\n\t\t\t\t$this->load($inc?\n\t\t\t\t\t[$inc.'=?',$this->db->value(\n\t\t\t\t\t\t$this->fields[$inc]['pdo_type'],$this->_id)]:\n\t\t\t\t\t[$filter,$nkeys]);\n\t\t\tif (isset($this->trigger['afterinsert']))\n\t\t\t\t\\Base::instance()->call($this->trigger['afterinsert'],\n\t\t\t\t\t[$this,$pkeys]);\n\t\t\t// reset changed flag after calling afterinsert\n\t\t\tif (!$reload)\n\t\t\t\tforeach ($this->fields as $key=>&$field) {\n\t\t\t\t\t$field['changed']=FALSE;\n\t\t\t\t\t$field['initial']=$field['value'];\n\t\t\t\t\tunset($field);\n\t\t\t\t}\n\t\t}\n\t\treturn $this;\n\t}\n\n\t/**\n\t*\tUpdate current record\n\t*\t@return static\n\t**/\n\tfunction update() {\n\t\t$args=[];\n\t\t$ctr=0;\n\t\t$pairs='';\n\t\t$pkeys=[];\n\t\tforeach ($this->fields as $key=>$field)\n\t\t\tif ($field['pkey'])\n\t\t\t\t$pkeys[$key]=$field['previous'];\n\t\tif (isset($this->trigger['beforeupdate']) &&\n\t\t\t\\Base::instance()->call($this->trigger['beforeupdate'],\n\t\t\t\t[$this,$pkeys])===FALSE)\n\t\t\treturn $this;\n\t\tforeach ($this->fields as $key=>$field)\n\t\t\tif ($field['changed']) {\n\t\t\t\t$pairs.=($pairs?',':'').$this->db->quotekey($key).'=?';\n\t\t\t\t$args[++$ctr]=[$field['value'],$field['pdo_type']];\n\t\t\t}\n\t\tif ($pairs) {\n\t\t\t$filter='';\n\t\t\tforeach ($this->fields as $key=>$field)\n\t\t\t\tif ($field['pkey']) {\n\t\t\t\t\t$filter.=($filter?' AND ':' WHERE ').\n\t\t\t\t\t\t$this->db->quotekey($key).'=?';\n\t\t\t\t\t$args[++$ctr]=[$field['previous'],$field['pdo_type']];\n\t\t\t\t}\n\t\t\tif (!$filter)\n                throw new \\Exception(sprintf(self::E_PKey,$this->source));\n\t\t\t$sql='UPDATE '.$this->table.' SET '.$pairs.$filter;\n\t\t\t$this->db->exec($sql,$args);\n\t\t}\n\t\tif (isset($this->trigger['afterupdate']))\n\t\t\t\\Base::instance()->call($this->trigger['afterupdate'],\n\t\t\t\t[$this,$pkeys]);\n\t\t// reset changed flag after calling afterupdate\n\t\tforeach ($this->fields as $key=>&$field) {\n\t\t\t\t$field['changed']=FALSE;\n\t\t\t\t$field['initial']=$field['value'];\n\t\t\t\tunset($field);\n\t\t\t}\n\t\treturn $this;\n\t}\n\n\t/**\n\t * batch-update multiple records at once\n\t * @param string|array $filter\n\t * @return int\n\t */\n\tfunction updateAll($filter=NULL) {\n\t\t$args=[];\n\t\t$ctr=$out=0;\n\t\t$pairs='';\n\t\tforeach ($this->fields as $key=>$field)\n\t\t\tif ($field['changed']) {\n\t\t\t\t$pairs.=($pairs?',':'').$this->db->quotekey($key).'=?';\n\t\t\t\t$args[++$ctr]=[$field['value'],$field['pdo_type']];\n\t\t\t}\n\t\tif ($filter)\n\t\t\tif (is_array($filter)) {\n\t\t\t\t$cond=array_shift($filter);\n\t\t\t\t$args=array_merge($args,$filter);\n\t\t\t\t$filter=' WHERE '.$cond;\n\t\t\t} else\n\t\t\t\t$filter=' WHERE '.$filter;\n\t\tif ($pairs) {\n\t\t\t$sql='UPDATE '.$this->table.' SET '.$pairs.$filter;\n\t\t\t$out = $this->db->exec($sql,$args);\n\t\t}\n\t\t// reset changed flag after calling afterupdate\n\t\tforeach ($this->fields as $key=>&$field) {\n\t\t\t$field['changed']=FALSE;\n\t\t\t$field['initial']=$field['value'];\n\t\t\tunset($field);\n\t\t}\n\t\treturn $out;\n\t}\n\n\n\t/**\n\t*\tDelete current record\n\t*\t@return int\n\t*\t@param $quick bool\n\t*\t@param $filter string|array\n\t**/\n\tfunction erase($filter=NULL,$quick=TRUE) {\n\t\tif (isset($filter)) {\n\t\t\tif (!$quick) {\n\t\t\t\t$out=0;\n\t\t\t\tforeach ($this->find($filter) as $mapper)\n\t\t\t\t\t$out+=$mapper->erase();\n\t\t\t\treturn $out;\n\t\t\t}\n\t\t\t$args=[];\n\t\t\tif (is_array($filter)) {\n\t\t\t\t$args=isset($filter[1]) && is_array($filter[1])?\n\t\t\t\t\t$filter[1]:\n\t\t\t\t\tarray_slice($filter,1,NULL,TRUE);\n\t\t\t\t$args=is_array($args)?$args:[1=>$args];\n\t\t\t\tlist($filter)=$filter;\n\t\t\t}\n\t\t\treturn $this->db->\n\t\t\t\texec('DELETE FROM '.$this->table.\n\t\t\t\t($filter?' WHERE '.$filter:'').';',$args);\n\t\t}\n\t\t$args=[];\n\t\t$ctr=0;\n\t\t$filter='';\n\t\t$pkeys=[];\n\t\tforeach ($this->fields as $key=>&$field) {\n\t\t\tif ($field['pkey']) {\n\t\t\t\t$filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?';\n\t\t\t\t$args[$ctr+1]=[$field['previous'],$field['pdo_type']];\n\t\t\t\t$pkeys[$key]=$field['previous'];\n\t\t\t\t++$ctr;\n\t\t\t}\n\t\t\t$field['value']=NULL;\n\t\t\t$field['changed']=(bool)$field['default'];\n\t\t\tif ($field['pkey'])\n\t\t\t\t$field['previous']=NULL;\n\t\t\tunset($field);\n\t\t}\n\t\tif (!$filter)\n            throw new \\Exception(sprintf(self::E_PKey,$this->source));\n\t\tforeach ($this->adhoc as &$field) {\n\t\t\t$field['value']=NULL;\n\t\t\tunset($field);\n\t\t}\n\t\tparent::erase();\n\t\tif (isset($this->trigger['beforeerase']) &&\n\t\t\t\\Base::instance()->call($this->trigger['beforeerase'],\n\t\t\t\t[$this,$pkeys])===FALSE)\n\t\t\treturn 0;\n\t\t$out=$this->db->\n\t\t\texec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args);\n\t\tif (isset($this->trigger['aftererase']))\n\t\t\t\\Base::instance()->call($this->trigger['aftererase'],\n\t\t\t\t[$this,$pkeys]);\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tReset cursor\n\t*\t@return NULL\n\t**/\n\tfunction reset() {\n\t\tforeach ($this->fields as &$field) {\n\t\t\t$field['value']=NULL;\n\t\t\t$field['initial']=NULL;\n\t\t\t$field['changed']=FALSE;\n\t\t\tif ($field['pkey'])\n\t\t\t\t$field['previous']=NULL;\n\t\t\tunset($field);\n\t\t}\n\t\tforeach ($this->adhoc as &$field) {\n\t\t\t$field['value']=NULL;\n\t\t\tunset($field);\n\t\t}\n\t\tparent::reset();\n\t}\n\n\t/**\n\t*\tHydrate mapper object using hive array variable\n\t*\t@return NULL\n\t*\t@param $var array|string\n\t*\t@param $func callback\n\t**/\n\tfunction copyfrom($var,$func=NULL) {\n\t\tif (is_string($var))\n\t\t\t$var=\\Base::instance()->$var;\n\t\tif ($func)\n\t\t\t$var=call_user_func($func,$var);\n\t\tforeach ($var as $key=>$val)\n\t\t\tif (in_array($key,array_keys($this->fields)))\n\t\t\t\t$this->set($key,$val);\n\t}\n\n\t/**\n\t*\tPopulate hive array variable with mapper fields\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\tfunction copyto($key) {\n\t\t$var=&\\Base::instance()->ref($key);\n\t\tforeach ($this->fields+$this->adhoc as $key=>$field)\n\t\t\t$var[$key]=$field['value'];\n\t}\n\n\t/**\n\t*\tReturn schema and, if the first argument is provided, update it\n\t*\t@return array\n\t*\t@param $fields NULL|array\n\t**/\n\tfunction schema($fields=null) {\n\t\tif ($fields)\n\t\t\t$this->fields = $fields;\n\t\treturn $this->fields;\n\t}\n\n\t/**\n\t*\tReturn field names\n\t*\t@return array\n\t*\t@param $adhoc bool\n\t**/\n\tfunction fields($adhoc=TRUE) {\n\t\treturn array_keys($this->fields+($adhoc?$this->adhoc:[]));\n\t}\n\n\t/**\n\t*\tReturn TRUE if field is not nullable\n\t*\t@return bool\n\t*\t@param $field string\n\t**/\n\tfunction required($field) {\n\t\treturn isset($this->fields[$field]) &&\n\t\t\t!$this->fields[$field]['nullable'];\n\t}\n\n\t/**\n\t*\tRetrieve external iterator for fields\n\t*\t@return object\n\t**/\n\tfunction getiterator() {\n\t\treturn new \\ArrayIterator($this->cast());\n\t}\n\n\t/**\n\t*\tAssign alias for table\n\t*\t@param $alias string\n\t**/\n\tfunction alias($alias) {\n\t\t$this->as=$alias;\n\t\treturn $this;\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@param $db \\DB\\SQL\n\t*\t@param $table string\n\t*\t@param $fields array|string\n\t*\t@param $ttl int|array\n\t**/\n\tfunction __construct(\\DB\\SQL $db,$table,$fields=NULL,$ttl=60) {\n\t\t$this->db=$db;\n\t\t$this->engine=$db->driver();\n\t\tif ($this->engine=='oci')\n\t\t\t$table=strtoupper($table);\n\t\t$this->source=$table;\n\t\t$this->table=$this->db->quotekey($table);\n\t\t$this->fields=$db->schema($table,$fields,$ttl);\n\t\t$this->reset();\n\t}\n\n}\n"
  },
  {
    "path": "db/sql/session.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace DB\\SQL;\n\nuse ReturnTypeWillChange;\nuse SessionAdapter;\n\n//! SQL-managed session handler\nclass Session extends Mapper {\n\n\tprotected\n\t\t//! Session ID\n\t\t$sid,\n\t\t//! Anti-CSRF token\n\t\t$_csrf,\n\t\t//! User agent\n\t\t$_agent,\n\t\t//! IP,\n\t\t$_ip,\n\t\t//! Suspect callback\n\t\t$onsuspect;\n\n\t/**\n\t*\tOpen session\n\t*\t@return TRUE\n\t*\t@param $path string\n\t*\t@param $name string\n\t**/\n    function open(string $path, string $name): bool\n    {\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tClose session\n\t*\t@return TRUE\n\t**/\n    function close(): bool\n    {\n\t\t$this->reset();\n\t\t$this->sid=NULL;\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tReturn session data in serialized format\n\t*\t@return string|false\n\t*\t@param $id string\n\t**/\n    #[ReturnTypeWillChange]\n    function read(string $id)\n    {\n\t\t$this->load(['session_id=?',$this->sid=$id]);\n\t\tif ($this->dry())\n\t\t\treturn '';\n\t\tif ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) {\n\t\t\t$fw=\\Base::instance();\n\t\t\tif (!isset($this->onsuspect) ||\n\t\t\t\t$fw->call($this->onsuspect,[$this,$id])===FALSE) {\n\t\t\t\t//NB: `session_destroy` can't be called at that stage (`session_start` not completed)\n\t\t\t\t$this->destroy($id);\n\t\t\t\t$this->close();\n\t\t\t\tunset($fw->{'COOKIE.'.session_name()});\n\t\t\t\t$fw->error(403);\n\t\t\t}\n\t\t}\n\t\treturn $this->get('data');\n\t}\n\n\t/**\n\t*\tWrite session data\n\t*\t@return TRUE\n\t*\t@param $id string\n\t*\t@param $data string\n\t**/\n    function write(string $id, string $data): bool\n    {\n\t\t$this->set('session_id',$id);\n\t\t$this->set('data',$data);\n\t\t$this->set('ip',$this->_ip);\n\t\t$this->set('agent',$this->_agent);\n\t\t$this->set('stamp',time());\n\t\t$this->save();\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tDestroy session\n\t*\t@return TRUE\n\t*\t@param $id string\n\t**/\n\tfunction destroy($id): bool\n    {\n\t\t$this->erase(['session_id=?',$id]);\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tGarbage collector\n\t**/\n    #[ReturnTypeWillChange]\n    function gc(int $max_lifetime): int\n    {\n\t\treturn (int) $this->erase(['stamp+?<?',$max_lifetime,time()]);\n\t}\n\n\t/**\n\t*\tReturn session id (if session has started)\n\t*\t@return string|NULL\n\t**/\n\tfunction sid() {\n\t\treturn $this->sid;\n\t}\n\n\t/**\n\t*\tReturn anti-CSRF token\n\t*\t@return string\n\t**/\n\tfunction csrf() {\n\t\treturn $this->_csrf;\n\t}\n\n\t/**\n\t*\tReturn IP address\n\t*\t@return string\n\t**/\n\tfunction ip() {\n\t\treturn $this->_ip;\n\t}\n\n\t/**\n\t*\tReturn Unix timestamp\n\t*\t@return string|FALSE\n\t**/\n\tfunction stamp() {\n\t\tif (!$this->sid)\n\t\t\tsession_start();\n\t\treturn $this->dry()?FALSE:$this->get('stamp');\n\t}\n\n\t/**\n\t*\tReturn HTTP user agent\n\t*\t@return string\n\t**/\n\tfunction agent() {\n\t\treturn $this->_agent;\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@param $db \\DB\\SQL\n\t*\t@param $table string\n\t*\t@param $force bool\n\t*\t@param $onsuspect callback\n\t*\t@param $key string\n\t*\t@param $type string, column type for data field\n\t**/\n\tfunction __construct(\\DB\\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL,$key=NULL,$type='TEXT') {\n\t\tif ($force) {\n\t\t\t$eol=\"\\n\";\n\t\t\t$tab=\"\\t\";\n\t\t\t$sqlsrv=preg_match('/mssql|sqlsrv|sybase/',$db->driver());\n\t\t\t$db->exec(\n\t\t\t\t($sqlsrv?\n\t\t\t\t\t('IF NOT EXISTS (SELECT * FROM sysobjects WHERE '.\n\t\t\t\t\t\t'name='.$db->quote($table).' AND xtype=\\'U\\') '.\n\t\t\t\t\t\t'CREATE TABLE dbo.'):\n\t\t\t\t\t('CREATE TABLE IF NOT EXISTS '.\n\t\t\t\t\t\t((($name=$db->name())&&$db->driver()!='pgsql')?\n\t\t\t\t\t\t\t($db->quotekey($name,FALSE).'.'):''))).\n\t\t\t\t$db->quotekey($table,FALSE).' ('.$eol.\n\t\t\t\t\t($sqlsrv?$tab.$db->quotekey('id').' INT IDENTITY,'.$eol:'').\n\t\t\t\t\t$tab.$db->quotekey('session_id').' VARCHAR(255),'.$eol.\n\t\t\t\t\t$tab.$db->quotekey('data').' '.$type.','.$eol.\n\t\t\t\t\t$tab.$db->quotekey('ip').' VARCHAR(45),'.$eol.\n\t\t\t\t\t$tab.$db->quotekey('agent').' VARCHAR(300),'.$eol.\n\t\t\t\t\t$tab.$db->quotekey('stamp').' INTEGER,'.$eol.\n\t\t\t\t\t$tab.'PRIMARY KEY ('.$db->quotekey($sqlsrv?'id':'session_id').')'.$eol.\n\t\t\t\t($sqlsrv?',CONSTRAINT [UK_session_id] UNIQUE(session_id)':'').\n\t\t\t\t');'\n\t\t\t);\n\t\t}\n\t\tparent::__construct($db,$table);\n\t\t$this->onsuspect=$onsuspect;\n        if (version_compare(PHP_VERSION, '8.4.0')>=0) {\n            // TODO: remove this when php7 support is dropped\n            session_set_save_handler(new SessionAdapter($this));\n        } else {\n            session_set_save_handler(\n                [$this,'open'],\n                [$this,'close'],\n                [$this,'read'],\n                [$this,'write'],\n                [$this,'destroy'],\n                [$this,'gc']\n            );\n        }\n\t\tregister_shutdown_function('session_commit');\n\t\t$fw=\\Base::instance();\n\t\t$headers=$fw->HEADERS;\n\t\t$this->_csrf=$fw->hash($fw->SEED.\n\t\t\textension_loaded('openssl')?\n\t\t\t\timplode(unpack('L',openssl_random_pseudo_bytes(4))):\n\t\t\t\tmt_rand()\n\t\t\t);\n\t\tif ($key)\n\t\t\t$fw->$key=$this->_csrf;\n\t\t$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:'';\n\t\tif (strlen($this->_agent) > 300) {\n\t\t\t$this->_agent = substr($this->_agent, 0, 300);\n\t\t}\n\t\t$this->_ip=$fw->IP;\n\t}\n\n}\n"
  },
  {
    "path": "db/sql.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace DB;\n\n//! PDO wrapper\nclass SQL {\n\n\t//@{ Error messages\n\tconst\n\t\tE_PKey='Table %s does not have a primary key';\n\t//@}\n\n\tconst\n\t\tPARAM_FLOAT='float';\n\n\tprotected\n\t\t//! UUID\n\t\t$uuid,\n\t\t//! Raw PDO\n\t\t$pdo,\n\t\t//! Data source name\n\t\t$dsn,\n\t\t//! Database engine\n\t\t$engine,\n\t\t//! Database name\n\t\t$dbname,\n\t\t//! Transaction flag\n\t\t$trans=FALSE,\n\t\t//! Number of rows affected by query\n\t\t$rows=0,\n\t\t//! SQL log\n\t\t$log;\n\n\t/**\n\t*\tBegin SQL transaction\n\t*\t@return bool\n\t**/\n\tfunction begin() {\n\t\t$out=$this->pdo->begintransaction();\n\t\t$this->trans=TRUE;\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tRollback SQL transaction\n\t*\t@return bool\n\t**/\n\tfunction rollback() {\n\t\t$out=FALSE;\n\t\tif ($this->pdo->inTransaction())\n\t\t\t$out=$this->pdo->rollback();\n\t\t$this->trans=FALSE;\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tCommit SQL transaction\n\t*\t@return bool\n\t**/\n\tfunction commit() {\n\t\t$out=FALSE;\n\t\tif ($this->pdo->inTransaction())\n\t\t\t$out=$this->pdo->commit();\n\t\t$this->trans=FALSE;\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tReturn transaction flag\n\t*\t@return bool\n\t**/\n\tfunction trans() {\n\t\treturn $this->trans;\n\t}\n\n\t/**\n\t*\tMap data type of argument to a PDO constant\n\t*\t@return int\n\t*\t@param $val scalar\n\t**/\n\tfunction type($val) {\n\t\tswitch (gettype($val)) {\n\t\t\tcase 'NULL':\n\t\t\t\treturn \\PDO::PARAM_NULL;\n\t\t\tcase 'boolean':\n\t\t\t\treturn \\PDO::PARAM_BOOL;\n\t\t\tcase 'integer':\n\t\t\t\treturn \\PDO::PARAM_INT;\n\t\t\tcase 'resource':\n\t\t\t\treturn \\PDO::PARAM_LOB;\n\t\t\tcase 'float':\n\t\t\t\treturn self::PARAM_FLOAT;\n\t\t\tdefault:\n\t\t\t\treturn \\PDO::PARAM_STR;\n\t\t}\n\t}\n\n\t/**\n\t*\tCast value to PHP type\n\t*\t@return mixed\n\t*\t@param $type string\n\t*\t@param $val mixed\n\t**/\n\tfunction value($type,$val) {\n\t\tswitch ($type) {\n\t\t\tcase self::PARAM_FLOAT:\n\t\t\t\tif (!is_string($val) && $val !== NULL)\n\t\t\t\t\t$val=str_replace(',','.',(string) $val);\n\t\t\t\treturn $val;\n\t\t\tcase \\PDO::PARAM_NULL:\n\t\t\t\treturn NULL;\n\t\t\tcase \\PDO::PARAM_INT:\n\t\t\t\treturn (int)$val;\n\t\t\tcase \\PDO::PARAM_BOOL:\n\t\t\t\treturn (bool)$val;\n\t\t\tcase \\PDO::PARAM_STR:\n\t\t\tcase \\PDO::PARAM_LOB:\n\t\t\t\treturn (string)$val;\n\t\t}\n\t}\n\n\t/**\n\t*\tExecute SQL statement(s)\n\t*\t@return array|int|FALSE\n\t*\t@param $cmds string|array\n\t*\t@param $args string|array\n\t*\t@param $ttl int|array\n\t*\t@param $log bool\n\t*\t@param $stamp bool\n\t**/\n\tfunction exec($cmds,$args=NULL,$ttl=0,$log=TRUE,$stamp=FALSE) {\n\t\t$tag='';\n\t\tif (is_array($ttl))\n\t\t\tlist($ttl,$tag)=$ttl;\n\t\t$auto=FALSE;\n\t\tif (is_null($args))\n\t\t\t$args=[];\n\t\telseif (is_scalar($args))\n\t\t\t$args=[1=>$args];\n\t\tif (is_array($cmds)) {\n\t\t\tif (count($args)<($count=count($cmds)))\n\t\t\t\t// Apply arguments to SQL commands\n\t\t\t\t$args=array_fill(0,$count,$args);\n\t\t\tif (!$this->trans) {\n\t\t\t\t$this->begin();\n\t\t\t\t$auto=TRUE;\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t$count=1;\n\t\t\t$cmds=[$cmds];\n\t\t\t$args=[$args];\n\t\t}\n\t\tif ($this->log===FALSE)\n\t\t\t$log=FALSE;\n\t\t$fw=\\Base::instance();\n\t\t$cache=\\Cache::instance();\n\t\t$result=FALSE;\n\t\tfor ($i=0;$i<$count;++$i) {\n\t\t\t$cmd=$cmds[$i];\n\t\t\t$arg=$args[$i];\n\t\t\t// ensure 1-based arguments\n\t\t\tif (array_key_exists(0,$arg)) {\n\t\t\t\tarray_unshift($arg,'');\n\t\t\t\tunset($arg[0]);\n\t\t\t}\n\t\t\tif (!preg_replace('/(^\\s+|[\\s;]+$)/','',$cmd))\n\t\t\t\tcontinue;\n\t\t\t$now=microtime(TRUE);\n\t\t\t$keys=$vals=[];\n\t\t\tif ($fw->CACHE && $ttl && ($cached=$cache->exists(\n\t\t\t\t$hash=$fw->hash($this->dsn.$cmd.\n\t\t\t\t$fw->stringify($arg)).($tag?'.'.$tag:'').'.sql',$result)) &&\n\t\t\t\t$cached[0]+$ttl>microtime(TRUE)) {\n\t\t\t\tforeach ($arg as $key=>$val) {\n\t\t\t\t\t$vals[]=$fw->stringify(is_array($val)?$val[0]:$val);\n\t\t\t\t\t$keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key).\n\t\t\t\t\t\t'/';\n\t\t\t\t}\n\t\t\t\tif ($log)\n\t\t\t\t\t$this->log.=($stamp?(date('r').' '):'').'('.\n\t\t\t\t\t\tsprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '.\n\t\t\t\t\t\t'[CACHED] '.\n\t\t\t\t\t\tpreg_replace($keys,$vals,\n\t\t\t\t\t\t\tstr_replace('?',chr(0).'?',$cmd),1).PHP_EOL;\n\t\t\t}\n\t\t\telseif (is_object($query=$this->pdo->prepare($cmd))) {\n\t\t\t\tforeach ($arg as $key=>$val) {\n\t\t\t\t\tif (is_array($val)) {\n\t\t\t\t\t\t// User-specified data type\n\t\t\t\t\t\t$query->bindvalue($key,$val[0],\n\t\t\t\t\t\t\t$val[1]==self::PARAM_FLOAT?\\PDO::PARAM_STR:$val[1]);\n\t\t\t\t\t\t$vals[]=$fw->stringify($this->value($val[1],$val[0]));\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\t// Convert to PDO data type\n\t\t\t\t\t\t$query->bindvalue($key,$val,\n\t\t\t\t\t\t\t($type=$this->type($val))==self::PARAM_FLOAT?\n\t\t\t\t\t\t\t\t\\PDO::PARAM_STR:$type);\n\t\t\t\t\t\t$vals[]=$fw->stringify($this->value($type,$val));\n\t\t\t\t\t}\n\t\t\t\t\t$keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key).\n\t\t\t\t\t\t'/';\n\t\t\t\t}\n\t\t\t\tif ($log)\n\t\t\t\t\t$this->log.=($stamp?(date('r').' '):'').'(-0ms) '.\n\t\t\t\t\t\tpreg_replace($keys,$vals,\n\t\t\t\t\t\t\tstr_replace('?',chr(0).'?',$cmd),1).PHP_EOL;\n\t\t\t\t$query->execute();\n\t\t\t\tif ($log)\n\t\t\t\t\t$this->log=str_replace('(-0ms)',\n\t\t\t\t\t\t'('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms)',\n\t\t\t\t\t\t$this->log);\n\t\t\t\tif (($error=$query->errorinfo()) && $error[0]!=\\PDO::ERR_NONE) {\n\t\t\t\t\t// Statement-level error occurred\n\t\t\t\t\tif ($this->trans)\n\t\t\t\t\t\t$this->rollback();\n                    throw new \\Exception('PDOStatement: '.$error[2]);\n\t\t\t\t}\n\t\t\t\tif (preg_match('/(?:^[\\s\\(]*'.\n\t\t\t\t\t'(?:WITH|EXPLAIN|SELECT|PRAGMA|SHOW)|RETURNING)\\b/is',$cmd) ||\n\t\t\t\t\t(preg_match('/^\\s*(?:CALL|EXEC)\\b/is',$cmd) &&\n\t\t\t\t\t\t$query->columnCount())) {\n\t\t\t\t\t$result=$query->fetchall(\\PDO::FETCH_ASSOC);\n\t\t\t\t\t// Work around SQLite quote bug\n\t\t\t\t\tif (preg_match('/sqlite2?/',$this->engine))\n\t\t\t\t\t\tforeach ($result as $pos=>$rec) {\n\t\t\t\t\t\t\tunset($result[$pos]);\n\t\t\t\t\t\t\t$result[$pos]=[];\n\t\t\t\t\t\t\tforeach ($rec as $key=>$val)\n\t\t\t\t\t\t\t\t$result[$pos][trim($key,'\\'\"[]`')]=$val;\n\t\t\t\t\t\t}\n\t\t\t\t\t$this->rows=count($result);\n\t\t\t\t\tif ($fw->CACHE && $ttl)\n\t\t\t\t\t\t// Save to cache backend\n\t\t\t\t\t\t$cache->set($hash,$result,$ttl);\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t\t$this->rows=$result=$query->rowcount();\n\t\t\t\t$query->closecursor();\n\t\t\t\tunset($query);\n\t\t\t}\n\t\t\telseif (($error=$this->pdo->errorInfo()) && $error[0]!=\\PDO::ERR_NONE) {\n\t\t\t\t// PDO-level error occurred\n\t\t\t\tif ($this->trans)\n\t\t\t\t\t$this->rollback();\n                throw new \\Exception('PDO: '.$error[2]);\n\t\t\t}\n\n\t\t}\n\t\tif ($this->trans && $auto)\n\t\t\t$this->commit();\n\t\treturn $result;\n\t}\n\n\t/**\n\t*\tReturn number of rows affected by last query\n\t*\t@return int\n\t**/\n\tfunction count() {\n\t\treturn $this->rows;\n\t}\n\n\t/**\n\t*\tReturn SQL profiler results (or disable logging)\n\t*\t@return string\n\t*\t@param $flag bool\n\t**/\n\tfunction log($flag=TRUE) {\n\t\tif ($flag)\n\t\t\treturn $this->log;\n\t\t$this->log=FALSE;\n\t}\n\n\t/**\n\t*\tReturn TRUE if table exists\n\t*\t@return bool\n\t*\t@param $table string\n\t**/\n\tfunction exists($table) {\n\t\t$mode=$this->pdo->getAttribute(\\PDO::ATTR_ERRMODE);\n\t\t$this->pdo->setAttribute(\\PDO::ATTR_ERRMODE,\\PDO::ERRMODE_SILENT);\n\t\t$out=$this->pdo->\n\t\t\tquery('SELECT 1 FROM '.$this->quotekey($table).' LIMIT 1');\n\t\t$this->pdo->setAttribute(\\PDO::ATTR_ERRMODE,$mode);\n\t\treturn is_object($out);\n\t}\n\n\t/**\n\t*\tRetrieve schema of SQL table\n\t*\t@return array|FALSE\n\t*\t@param $table string\n\t*\t@param $fields array|string\n\t*\t@param $ttl int|array\n\t**/\n\tfunction schema($table,$fields=NULL,$ttl=0) {\n\t\t$fw=\\Base::instance();\n\t\t$cache=\\Cache::instance();\n\t\tif ($fw->CACHE && $ttl &&\n\t\t\t($cached=$cache->exists(\n\t\t\t\t$hash=$fw->hash($this->dsn.$table.(is_array($fields) ? implode(',',$fields) : $fields)).'.schema',$result)) &&\n\t\t\t$cached[0]+$ttl>microtime(TRUE))\n\t\t\treturn $result;\n\t\tif (strpos($table,'.'))\n\t\t\tlist($schema,$table)=explode('.',$table);\n\t\t// Supported engines\n\t\t// format: engine_name => array of:\n\t\t//\t0: query\n\t\t//\t1: field name of column name\n\t\t//\t2: field name of column type\n\t\t//\t3: field name of default value\n\t\t//\t4: field name of nullable value\n\t\t//\t5: expected field value to be nullable\n\t\t//\t6: field name of primary key flag\n\t\t//\t7: expected field value to be a primary key\n\t\t//\t8: field name of auto increment check (optional)\n\t\t//\t9: expected field value to be an auto-incremented identifier\n\t\t$cmd=[\n\t\t\t'sqlite2?'=>[\n\t\t\t\t'SELECT * FROM pragma_table_info('.$this->quote($table).') JOIN ('.\n\t\t\t\t\t'SELECT sql FROM sqlite_master WHERE (type=\\'table\\' OR type=\\'view\\')  AND '.\n\t\t\t\t\t'name='.$this->quote($table).')',\n\t\t\t\t'name','type','dflt_value','notnull',0,'pk',TRUE,'sql',\n\t\t\t\t\t'/\\W(%s)\\W+[^,]+?AUTOINCREMENT\\W/i'],\n\t\t\t'mysql'=>[\n\t\t\t\t'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`',\n\t\t\t\t'Field','Type','Default','Null','YES','Key','PRI','Extra','auto_increment'],\n\t\t\t'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>[\n\t\t\t\t'SELECT '.\n\t\t\t\t\t'C.COLUMN_NAME AS field,'.\n\t\t\t\t\t'C.DATA_TYPE AS type,'.\n\t\t\t\t\t'C.COLUMN_DEFAULT AS defval,'.\n\t\t\t\t\t'C.IS_NULLABLE AS nullable,'.\n\t\t\t\t($this->engine=='pgsql'\n\t\t\t\t\t?'COALESCE(POSITION(\\'nextval\\' IN C.COLUMN_DEFAULT),0) AS autoinc,'\n\t\t\t\t\t:'columnproperty(object_id(C.TABLE_NAME),C.COLUMN_NAME,\\'IsIdentity\\')'\n\t\t\t\t\t\t.' AS autoinc,').\n\t\t\t\t\t'T.CONSTRAINT_TYPE AS pkey '.\n\t\t\t\t'FROM INFORMATION_SCHEMA.COLUMNS AS C '.\n\t\t\t\t'LEFT OUTER JOIN '.\n\t\t\t\t\t'INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS K '.\n\t\t\t\t\t'ON '.\n\t\t\t\t\t\t'C.TABLE_NAME=K.TABLE_NAME AND '.\n\t\t\t\t\t\t'C.COLUMN_NAME=K.COLUMN_NAME AND '.\n\t\t\t\t\t\t'C.TABLE_SCHEMA=K.TABLE_SCHEMA '.\n\t\t\t\t\t\t($this->dbname?\n\t\t\t\t\t\t\t('AND C.TABLE_CATALOG=K.TABLE_CATALOG '):'').\n\t\t\t\t'LEFT OUTER JOIN '.\n\t\t\t\t\t'INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS T ON '.\n\t\t\t\t\t\t'K.TABLE_NAME=T.TABLE_NAME AND '.\n\t\t\t\t\t\t'K.CONSTRAINT_NAME=T.CONSTRAINT_NAME AND '.\n\t\t\t\t\t\t'K.TABLE_SCHEMA=T.TABLE_SCHEMA '.\n\t\t\t\t\t\t($this->dbname?\n\t\t\t\t\t\t\t('AND K.TABLE_CATALOG=T.TABLE_CATALOG '):'').\n\t\t\t\t'WHERE '.\n\t\t\t\t\t'C.TABLE_NAME='.$this->quote($table).\n\t\t\t\t\t(empty($schema) ? '' : ' AND C.TABLE_SCHEMA='.$this->quote($schema)).\n\t\t\t\t\t($this->dbname?\n\t\t\t\t\t\t(' AND C.TABLE_CATALOG='.\n\t\t\t\t\t\t\t$this->quote($this->dbname)):''),\n\t\t\t\t'field','type','defval','nullable','YES','pkey','PRIMARY KEY','autoinc',1],\n\t\t\t'oci'=>[\n\t\t\t\t'SELECT c.column_name AS field, '.\n\t\t\t\t\t'c.data_type AS type, '.\n\t\t\t\t\t'c.data_default AS defval, '.\n\t\t\t\t\t'c.nullable AS nullable, '.\n\t\t\t\t\t'(SELECT t.constraint_type '.\n\t\t\t\t\t\t'FROM all_cons_columns acc '.\n\t\t\t\t\t\t'LEFT OUTER JOIN all_constraints t '.\n\t\t\t\t\t\t'ON acc.constraint_name=t.constraint_name '.\n\t\t\t\t\t\t'WHERE acc.table_name='.$this->quote($table).' '.\n\t\t\t\t\t\t'AND acc.column_name=c.column_name '.\n\t\t\t\t\t\t'AND constraint_type='.$this->quote('P').') AS pkey '.\n\t\t\t\t'FROM all_tab_cols c '.\n\t\t\t\t'WHERE c.table_name='.$this->quote($table),\n\t\t\t\t'FIELD','TYPE','DEFVAL','NULLABLE','Y','PKEY','P']\n\t\t];\n\t\tif (is_string($fields))\n\t\t\t$fields=\\Base::instance()->split($fields);\n\t\t$conv=[\n\t\t\t'int\\b|integer'=>\\PDO::PARAM_INT,\n\t\t\t'bool'=>\\PDO::PARAM_BOOL,\n\t\t\t'blob|bytea|image|binary'=>\\PDO::PARAM_LOB,\n\t\t\t'float|real|double|decimal|numeric'=>self::PARAM_FLOAT,\n\t\t\t'.+'=>\\PDO::PARAM_STR\n\t\t];\n\t\tforeach ($cmd as $key=>$val)\n\t\t\tif (preg_match('/'.$key.'/',$this->engine)) {\n\t\t\t\t$rows=[];\n\t\t\t\tforeach ($this->exec($val[0],NULL) as $row)\n\t\t\t\t\tif (!$fields || in_array($row[$val[1]],$fields)) {\n\t\t\t\t\t\tforeach ($conv as $regex=>$type)\n\t\t\t\t\t\t\tif (preg_match('/'.$regex.'/i',$row[$val[2]]))\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\tif (!isset($rows[$row[$val[1]]])) // handle duplicate rows in PgSQL\n\t\t\t\t\t\t\t$rows[$row[$val[1]]]=[\n\t\t\t\t\t\t\t\t'type'=>$row[$val[2]],\n\t\t\t\t\t\t\t\t'pdo_type'=>$type,\n\t\t\t\t\t\t\t\t'default'=>is_string($row[$val[3]])?\n\t\t\t\t\t\t\t\t\tpreg_replace('/^\\s*([\\'\"])(.*)\\1\\s*/','\\2',\n\t\t\t\t\t\t\t\t\t$row[$val[3]]):$row[$val[3]],\n\t\t\t\t\t\t\t\t'nullable'=>$row[$val[4]]==$val[5],\n\t\t\t\t\t\t\t\t'pkey'=>$row[$val[6]]==$val[7],\n\t\t\t\t\t\t\t\t'auto_inc'=>isset($val[8]) && isset($row[$val[8]])\n\t\t\t\t\t\t\t\t\t? ($this->engine=='sqlite'?\n\t\t\t\t\t\t\t\t\t\t(bool) preg_match(sprintf($val[9],$row[$val[1]]),\n\t\t\t\t\t\t\t\t\t\t\t$row[$val[8]]):\n\t\t\t\t\t\t\t\t\t\t($row[$val[8]]==$val[9])\n\t\t\t\t\t\t\t\t\t) : NULL,\n\t\t\t\t\t\t\t];\n\t\t\t\t\t}\n\t\t\t\tif ($fw->CACHE && $ttl)\n\t\t\t\t\t// Save to cache backend\n\t\t\t\t\t$cache->set($hash,$rows,$ttl);\n\t\t\t\treturn $rows;\n\t\t\t}\n        throw new \\Exception(sprintf(self::E_PKey,$table));\n\t}\n\n\t/**\n\t*\tQuote string\n\t*\t@return string\n\t*\t@param $val mixed\n\t*\t@param $type int\n\t**/\n\tfunction quote($val,$type=\\PDO::PARAM_STR) {\n\t\treturn $this->engine=='odbc'?\n\t\t\t(is_string($val)?\n\t\t\t\t\\Base::instance()->stringify(str_replace('\\'','\\'\\'',$val)):\n\t\t\t\t$val):\n\t\t\t$this->pdo->quote($val,$type);\n\t}\n\n\t/**\n\t*\tReturn UUID\n\t*\t@return string\n\t**/\n\tfunction uuid() {\n\t\treturn $this->uuid;\n\t}\n\n\t/**\n\t*\tReturn parent object\n\t*\t@return \\PDO\n\t**/\n\tfunction pdo() {\n\t\treturn $this->pdo;\n\t}\n\n\t/**\n\t*\tReturn database engine\n\t*\t@return string\n\t**/\n\tfunction driver() {\n\t\treturn $this->engine;\n\t}\n\n\t/**\n\t*\tReturn server version\n\t*\t@return string\n\t**/\n\tfunction version() {\n\t\treturn $this->pdo->getattribute(\\PDO::ATTR_SERVER_VERSION);\n\t}\n\n\t/**\n\t*\tReturn database name\n\t*\t@return string\n\t**/\n\tfunction name() {\n\t\treturn $this->dbname;\n\t}\n\n\t/**\n\t*\tReturn quoted identifier name\n\t*\t@return string\n\t*\t@param $key\n\t*\t@param bool $split\n\t **/\n\tfunction quotekey($key, $split=TRUE) {\n\t\t$delims=[\n\t\t\t'sqlite2?|mysql'=>'``',\n\t\t\t'pgsql|oci'=>'\"\"',\n\t\t\t'mssql|sqlsrv|odbc|sybase|dblib'=>'[]'\n\t\t];\n\t\t$use='';\n\t\tforeach ($delims as $engine=>$delim)\n\t\t\tif (preg_match('/'.$engine.'/',$this->engine)) {\n\t\t\t\t$use=$delim;\n\t\t\t\tbreak;\n\t\t\t}\n\t\treturn $use[0].($split ? implode($use[1].'.'.$use[0],explode('.',$key))\n\t\t\t: $key).$use[1];\n\t}\n\n\t/**\n\t*\tRedirect call to PDO object\n\t*\t@return mixed\n\t*\t@param $func string\n\t*\t@param $args array\n\t**/\n\tfunction __call($func,array $args) {\n\t\treturn call_user_func_array([$this->pdo,$func],$args);\n\t}\n\n\t//! Prohibit cloning\n\tprivate function __clone() {\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@param $dsn string\n\t*\t@param $user string\n\t*\t@param $pw string\n\t*\t@param $options array\n\t**/\n\tfunction __construct($dsn,$user=NULL,$pw=NULL,?array $options=NULL) {\n\t\t$fw=\\Base::instance();\n\t\t$this->uuid=$fw->hash($this->dsn=$dsn);\n\t\tif (preg_match('/^.+?(?:dbname|database)=(.+?)(?=;|$)/is',$dsn,$parts))\n\t\t\t$this->dbname=str_replace('\\\\ ',' ',$parts[1]);\n\t\tif (!$options)\n\t\t\t$options=[];\n\t\tif (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql')\n\t\t\tif (version_compare(PHP_VERSION, '8.5.0')<0)\n\t\t\t\t$options+=[\\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '.\n\t\t\t\t\tstrtolower(str_replace('-','',$fw->ENCODING)).';'];\n\t\t\telse\n\t\t\t\t$options+=[\\PDO\\Mysql::ATTR_INIT_COMMAND=>'SET NAMES '.\n\t\t\t\t\tstrtolower(str_replace('-','',$fw->ENCODING)).';'];\n\t\t$this->pdo=new \\PDO($dsn,$user,$pw,$options);\n\t\t$this->engine=$this->pdo->getattribute(\\PDO::ATTR_DRIVER_NAME);\n\t}\n\n}\n"
  },
  {
    "path": "f3.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Legacy mode enabler\nclass F3 {\n\n\tstatic\n\t\t//! Framework instance\n\t\t$fw;\n\n\t/**\n\t*\tForward function calls to framework\n\t*\t@return mixed\n\t*\t@param $func callback\n\t*\t@param $args array\n\t**/\n\tstatic function __callstatic($func,array $args) {\n\t\tif (!self::$fw)\n\t\t\tself::$fw=Base::instance();\n\t\treturn call_user_func_array([self::$fw,$func],$args);\n\t}\n\n}\n"
  },
  {
    "path": "image.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Image manipulation tools\nclass Image {\n\n\t//@{ Messages\n\tconst\n\t\tE_Color='Invalid color specified: %s',\n\t\tE_File='File not found',\n\t\tE_Font='CAPTCHA font not found',\n\t\tE_TTF='No TrueType support in GD module',\n\t\tE_Length='Invalid CAPTCHA length: %s';\n\t//@}\n\n\t//@{ Positional cues\n\tconst\n\t\tPOS_Left=1,\n\t\tPOS_Center=2,\n\t\tPOS_Right=4,\n\t\tPOS_Top=8,\n\t\tPOS_Middle=16,\n\t\tPOS_Bottom=32;\n\t//@}\n\n\tprotected\n\t\t//! Source filename\n\t\t$file,\n\t\t//! Image resource\n\t\t$data,\n\t\t//! Enable/disable history\n\t\t$flag=FALSE,\n\t\t//! Filter count\n\t\t$count=0;\n\n\t/**\n\t*\tConvert RGB hex triad to array\n\t*\t@return array|FALSE\n\t*\t@param $color int|string\n\t**/\n\tfunction rgb($color) {\n\t\tif (is_string($color))\n\t\t\t$color=hexdec($color);\n\t\t$hex=str_pad($hex=dechex($color),$color<4096?3:6,'0',STR_PAD_LEFT);\n\t\tif (($len=strlen($hex))>6)\n            throw new \\Exception(sprintf(self::E_Color,'0x'.$hex));\n\t\t$color=str_split($hex,$len/3);\n\t\tforeach ($color as &$hue) {\n\t\t\t$hue=hexdec(str_repeat($hue,6/$len));\n\t\t\tunset($hue);\n\t\t}\n\t\treturn $color;\n\t}\n\n\t/**\n\t*\tInvert image\n\t*\t@return object\n\t**/\n\tfunction invert() {\n\t\timagefilter($this->data,IMG_FILTER_NEGATE);\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tAdjust brightness (range:-255 to 255)\n\t*\t@return object\n\t*\t@param $level int\n\t**/\n\tfunction brightness($level) {\n\t\timagefilter($this->data,IMG_FILTER_BRIGHTNESS,$level);\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tAdjust contrast (range:-100 to 100)\n\t*\t@return object\n\t*\t@param $level int\n\t**/\n\tfunction contrast($level) {\n\t\timagefilter($this->data,IMG_FILTER_CONTRAST,$level);\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tConvert to grayscale\n\t*\t@return object\n\t**/\n\tfunction grayscale() {\n\t\timagefilter($this->data,IMG_FILTER_GRAYSCALE);\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tAdjust smoothness\n\t*\t@return object\n\t*\t@param $level int\n\t**/\n\tfunction smooth($level) {\n\t\timagefilter($this->data,IMG_FILTER_SMOOTH,$level);\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tEmboss the image\n\t*\t@return object\n\t**/\n\tfunction emboss() {\n\t\timagefilter($this->data,IMG_FILTER_EMBOSS);\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tApply sepia effect\n\t*\t@return object\n\t**/\n\tfunction sepia() {\n\t\timagefilter($this->data,IMG_FILTER_GRAYSCALE);\n\t\timagefilter($this->data,IMG_FILTER_COLORIZE,90,60,45);\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tPixelate the image\n\t*\t@return object\n\t*\t@param $size int\n\t**/\n\tfunction pixelate($size) {\n\t\timagefilter($this->data,IMG_FILTER_PIXELATE,$size,TRUE);\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tBlur the image using Gaussian filter\n\t*\t@return object\n\t*\t@param $selective bool\n\t**/\n\tfunction blur($selective=FALSE) {\n\t\timagefilter($this->data,\n\t\t\t$selective?IMG_FILTER_SELECTIVE_BLUR:IMG_FILTER_GAUSSIAN_BLUR);\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tApply sketch effect\n\t*\t@return object\n\t**/\n\tfunction sketch() {\n\t\timagefilter($this->data,IMG_FILTER_MEAN_REMOVAL);\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tFlip on horizontal axis\n\t*\t@return object\n\t**/\n\tfunction hflip() {\n\t\t$tmp=imagecreatetruecolor(\n\t\t\t$width=$this->width(),$height=$this->height());\n\t\timagesavealpha($tmp,TRUE);\n\t\timagefill($tmp,0,0,IMG_COLOR_TRANSPARENT);\n\t\timagecopyresampled($tmp,$this->data,\n\t\t\t0,0,$width-1,0,$width,$height,-$width,$height);\n\t\tif (version_compare(PHP_VERSION, '8.5.0')<0)\n\t\t\t// TODO: remove this when php7 support is dropped\n\t\t\timagedestroy($this->data);\n\t\t$this->data=$tmp;\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tFlip on vertical axis\n\t*\t@return object\n\t**/\n\tfunction vflip() {\n\t\t$tmp=imagecreatetruecolor(\n\t\t\t$width=$this->width(),$height=$this->height());\n\t\timagesavealpha($tmp,TRUE);\n\t\timagefill($tmp,0,0,IMG_COLOR_TRANSPARENT);\n\t\timagecopyresampled($tmp,$this->data,\n\t\t\t0,0,0,$height-1,$width,$height,$width,-$height);\n\t\tif (version_compare(PHP_VERSION, '8.5.0')<0)\n\t\t\t// TODO: remove this when php7 support is dropped\n\t\t\timagedestroy($this->data);\n\t\t$this->data=$tmp;\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tCrop the image\n\t*\t@return object\n\t*\t@param $x1 int\n\t*\t@param $y1 int\n\t*\t@param $x2 int\n\t*\t@param $y2 int\n\t**/\n\tfunction crop($x1,$y1,$x2,$y2) {\n\t\t$tmp=imagecreatetruecolor($width=$x2-$x1+1,$height=$y2-$y1+1);\n\t\timagesavealpha($tmp,TRUE);\n\t\timagefill($tmp,0,0,IMG_COLOR_TRANSPARENT);\n\t\timagecopyresampled($tmp,$this->data,\n\t\t\t0,0,$x1,$y1,$width,$height,$width,$height);\n\t\tif (version_compare(PHP_VERSION, '8.5.0')<0)\n\t\t\t// TODO: remove this when php7 support is dropped\n\t\t\timagedestroy($this->data);\n\t\t$this->data=$tmp;\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tResize image (Maintain aspect ratio); Crop relative to center\n\t*\tif flag is enabled; Enlargement allowed if flag is enabled\n\t*\t@return object\n\t*\t@param $width int\n\t*\t@param $height int\n\t*\t@param $crop bool\n\t*\t@param $enlarge bool\n\t**/\n\tfunction resize($width=NULL,$height=NULL,$crop=TRUE,$enlarge=TRUE) {\n\t\tif (is_null($width) && is_null($height))\n\t\t\treturn $this;\n\t\t$origw=$this->width();\n\t\t$origh=$this->height();\n\t\tif (is_null($width))\n\t\t\t$width=round(($height/$origh)*$origw);\n\t\tif (is_null($height))\n\t\t\t$height=round(($width/$origw)*$origh);\n\t\t// Adjust dimensions; retain aspect ratio\n\t\t$ratio=$origw/$origh;\n\t\tif (!$crop) {\n\t\t\tif ($width/$ratio<=$height)\n\t\t\t\t$height=round($width/$ratio);\n\t\t\telse\n\t\t\t\t$width=round($height*$ratio);\n\t\t}\n\t\tif (!$enlarge) {\n\t\t\t$width=min($origw,$width);\n\t\t\t$height=min($origh,$height);\n\t\t}\n\t\t// Create blank image\n\t\t$tmp=imagecreatetruecolor($width,$height);\n\t\timagesavealpha($tmp,TRUE);\n\t\timagefill($tmp,0,0,IMG_COLOR_TRANSPARENT);\n\t\t// Resize\n\t\tif ($crop) {\n\t\t\tif ($width/$ratio<=$height) {\n\t\t\t\t$cropw=round($origh*$width/$height);\n\t\t\t\timagecopyresampled($tmp,$this->data,\n\t\t\t\t\t0,0,round(($origw-$cropw)/2),0,$width,$height,$cropw,$origh);\n\t\t\t}\n\t\t\telse {\n\t\t\t\t$croph=round($origw*$height/$width);\n\t\t\t\timagecopyresampled($tmp,$this->data,\n\t\t\t\t\t0,0,0,round(($origh-$croph)/2),$width,$height,$origw,$croph);\n\t\t\t}\n\t\t}\n\t\telse\n\t\t\timagecopyresampled($tmp,$this->data,\n\t\t\t\t0,0,0,0,$width,$height,$origw,$origh);\n\t\tif (version_compare(PHP_VERSION, '8.5.0')<0)\n\t\t\t// TODO: remove this when php7 support is dropped\n\t\t\timagedestroy($this->data);\n\t\t$this->data=$tmp;\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tRotate image\n\t*\t@return object\n\t*\t@param $angle int\n\t**/\n\tfunction rotate($angle) {\n\t\t$this->data=imagerotate($this->data,$angle,\n\t\t\timagecolorallocatealpha($this->data,0,0,0,127));\n\t\timagesavealpha($this->data,TRUE);\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tApply an image overlay\n\t*\t@return object\n\t*\t@param $img object\n\t*\t@param $align int|array\n\t*\t@param $alpha int\n\t**/\n\tfunction overlay(Image $img,$align=NULL,$alpha=100) {\n\t\tif (is_null($align))\n\t\t\t$align=self::POS_Right|self::POS_Bottom;\n\t\tif (is_array($align)) {\n\t\t\tlist($posx,$posy)=$align;\n\t\t\t$align = 0;\n\t\t}\n\t\t$ovr=imagecreatefromstring($img->dump());\n\t\timagesavealpha($ovr,TRUE);\n\t\t$imgw=$this->width();\n\t\t$imgh=$this->height();\n\t\t$ovrw=imagesx($ovr);\n\t\t$ovrh=imagesy($ovr);\n\t\tif ($align & self::POS_Left)\n\t\t\t$posx=0;\n\t\tif ($align & self::POS_Center)\n\t\t\t$posx=round(($imgw-$ovrw)/2);\n\t\tif ($align & self::POS_Right)\n\t\t\t$posx=$imgw-$ovrw;\n\t\tif ($align & self::POS_Top)\n\t\t\t$posy=0;\n\t\tif ($align & self::POS_Middle)\n\t\t\t$posy=round(($imgh-$ovrh)/2);\n\t\tif ($align & self::POS_Bottom)\n\t\t\t$posy=$imgh-$ovrh;\n\t\tif (empty($posx))\n\t\t\t$posx=0;\n\t\tif (empty($posy))\n\t\t\t$posy=0;\n\t\tif ($alpha==100)\n\t\t\timagecopy($this->data,$ovr,$posx,$posy,0,0,$ovrw,$ovrh);\n\t\telse {\n\t\t\t$cut=imagecreatetruecolor($ovrw,$ovrh);\n\t\t\timagecopy($cut,$this->data,0,0,$posx,$posy,$ovrw,$ovrh);\n\t\t\timagecopy($cut,$ovr,0,0,0,0,$ovrw,$ovrh);\n\t\t\timagecopymerge($this->data,\n\t\t\t\t$cut,$posx,$posy,0,0,$ovrw,$ovrh,$alpha);\n\t\t}\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tGenerate identicon\n\t*\t@return object\n\t*\t@param $str string\n\t*\t@param $size int\n\t*\t@param $blocks int\n\t**/\n\tfunction identicon($str,$size=64,$blocks=4) {\n\t\t$sprites=[\n\t\t\t[.5,1,1,0,1,1],\n\t\t\t[.5,0,1,0,.5,1,0,1],\n\t\t\t[.5,0,1,0,1,1,.5,1,1,.5],\n\t\t\t[0,.5,.5,0,1,.5,.5,1,.5,.5],\n\t\t\t[0,.5,1,0,1,1,0,1,1,.5],\n\t\t\t[1,0,1,1,.5,1,1,.5,.5,.5],\n\t\t\t[0,0,1,0,1,.5,0,0,.5,1,0,1],\n\t\t\t[0,0,.5,0,1,.5,.5,1,0,1,.5,.5],\n\t\t\t[.5,0,.5,.5,1,.5,1,1,.5,1,.5,.5,0,.5],\n\t\t\t[0,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1],\n\t\t\t[0,.5,.5,1,1,.5,.5,0,1,0,1,1,0,1],\n\t\t\t[.5,0,1,0,1,1,.5,1,1,.75,.5,.5,1,.25],\n\t\t\t[0,.5,.5,0,.5,.5,1,0,1,.5,.5,1,.5,.5,0,1],\n\t\t\t[0,0,1,0,1,1,0,1,1,.5,.5,.25,.5,.75,0,.5,.5,.25],\n\t\t\t[0,.5,.5,.5,.5,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1],\n\t\t\t[0,0,1,0,.5,.5,.5,0,0,.5,1,.5,.5,1,.5,.5,0,1]\n\t\t];\n\t\t$hash=sha1($str);\n\t\t$this->data=imagecreatetruecolor($size,$size);\n\t\tlist($r,$g,$b)=$this->rgb(hexdec(substr($hash,-3)));\n\t\t$fg=imagecolorallocate($this->data,$r,$g,$b);\n\t\timagefill($this->data,0,0,IMG_COLOR_TRANSPARENT);\n\t\t$ctr=count($sprites);\n\t\t$dim=$blocks*floor($size/$blocks)*2/$blocks;\n\t\tfor ($j=0,$y=ceil($blocks/2);$j<$y;++$j)\n\t\t\tfor ($i=$j,$x=$blocks-1-$j;$i<$x;++$i) {\n\t\t\t\t$sprite=imagecreatetruecolor($dim,$dim);\n\t\t\t\timagefill($sprite,0,0,IMG_COLOR_TRANSPARENT);\n\t\t\t\t$block=$sprites[hexdec($hash[($j*$blocks+$i)*2])%$ctr];\n\t\t\t\tfor ($k=0,$pts=count($block);$k<$pts;++$k)\n\t\t\t\t\t$block[$k]*=$dim;\n\t\t\t\tif (version_compare(PHP_VERSION, '8.1.0') >= 0) {\n\t\t\t\t\timagefilledpolygon($sprite,$block,$fg);\n\t\t\t\t} else {\n\t\t\t\t\timagefilledpolygon($sprite,$block,$pts/2,$fg);\n\t\t\t\t}\n\t\t\t\tfor ($k=0;$k<4;++$k) {\n\t\t\t\t\timagecopyresampled($this->data,$sprite,\n\t\t\t\t\t\tround($i*$dim/2),round($j*$dim/2),0,0,round($dim/2),round($dim/2),$dim,$dim);\n\t\t\t\t\t$this->data=imagerotate($this->data,90,\n\t\t\t\t\t\timagecolorallocatealpha($this->data,0,0,0,127));\n\t\t\t\t}\n\t\t\t\tif (version_compare(PHP_VERSION, '8.5.0')<0)\n\t\t\t\t\t// TODO: remove this when php7 support is dropped\n\t\t\t\t\timagedestroy($sprite);\n\t\t\t}\n\t\timagesavealpha($this->data,TRUE);\n\t\treturn $this->save();\n\t}\n\n\t/**\n\t*\tGenerate CAPTCHA image\n\t*\t@return object|FALSE\n\t*\t@param $font string\n\t*\t@param $size int\n\t*\t@param $len int\n\t*\t@param $key string\n\t*\t@param $path string\n\t*\t@param $fg int\n\t*\t@param $bg int\n\t**/\n\tfunction captcha($font,$size=24,$len=5,\n\t\t$key=NULL,$path='',$fg=0xFFFFFF,$bg=0x000000) {\n\t\tif ((!$ssl=extension_loaded('openssl')) && ($len<4 || $len>13)) {\n            throw new \\Exception(sprintf(self::E_Length,$len));\n\t\t}\n\t\tif (!function_exists('imagettftext')) {\n            throw new \\Exception(self::E_TTF);\n\t\t}\n\t\t$fw=Base::instance();\n\t\tforeach ($fw->split($path?:$fw->UI.';./') as $dir)\n\t\t\tif (is_file($path=realpath($dir.$font))) {\n\t\t\t\t$seed=strtoupper(substr(\n\t\t\t\t\t$ssl?bin2hex(openssl_random_pseudo_bytes($len)):uniqid(),\n\t\t\t\t\t-$len));\n\t\t\t\t$block=$size*3;\n\t\t\t\t$tmp=[];\n\t\t\t\tfor ($i=0,$width=0,$height=0;$i<$len;++$i) {\n\t\t\t\t\t// Process at 2x magnification\n\t\t\t\t\t$box=imagettfbbox($size*2,0,$path,$seed[$i]);\n\t\t\t\t\t$w=$box[2]-$box[0];\n\t\t\t\t\t$h=$box[1]-$box[5];\n\t\t\t\t\t$char=imagecreatetruecolor($block,$block);\n\t\t\t\t\timagefill($char,0,0,$bg);\n\t\t\t\t\timagettftext($char,$size*2,0,\n\t\t\t\t\t\tround(($block-$w)/2),round($block-($block-$h)/2),\n\t\t\t\t\t\t$fg,$path,$seed[$i]);\n\t\t\t\t\t$char=imagerotate($char,mt_rand(-30,30),\n\t\t\t\t\t\timagecolorallocatealpha($char,0,0,0,127));\n\t\t\t\t\t// Reduce to normal size\n\t\t\t\t\t$tmp[$i]=imagecreatetruecolor(\n\t\t\t\t\t\tround(($w=imagesx($char))/2),round(($h=imagesy($char))/2));\n\t\t\t\t\timagefill($tmp[$i],0,0,IMG_COLOR_TRANSPARENT);\n\t\t\t\t\timagecopyresampled($tmp[$i],\n\t\t\t\t\t\t$char,0,0,0,0,round($w/2),round($h/2),$w,$h);\n\t\t\t\t\tif (version_compare(PHP_VERSION, '8.5.0')<0)\n\t\t\t\t\t\timagedestroy($char);\n\t\t\t\t\t$width+=$i+1<$len?$block/2:$w/2;\n\t\t\t\t\t$height=max($height,$h/2);\n\t\t\t\t}\n\t\t\t\t$this->data=imagecreatetruecolor(round($width),round($height));\n\t\t\t\timagefill($this->data,0,0,IMG_COLOR_TRANSPARENT);\n\t\t\t\tfor ($i=0;$i<$len;++$i) {\n\t\t\t\t\timagecopy($this->data,$tmp[$i],\n\t\t\t\t\t\tround($i*$block/2),round(($height-imagesy($tmp[$i]))/2),0,0,\n\t\t\t\t\t\timagesx($tmp[$i]),imagesy($tmp[$i]));\n\t\t\t\t\tif (version_compare(PHP_VERSION, '8.5.0')<0)\n\t\t\t\t\t\timagedestroy($tmp[$i]);\n\t\t\t\t}\n\t\t\t\timagesavealpha($this->data,TRUE);\n\t\t\t\tif ($key)\n\t\t\t\t\t$fw->$key=$seed;\n\t\t\t\treturn $this->save();\n\t\t\t}\n        throw new \\Exception(self::E_Font);\n\t}\n\n\t/**\n\t*\tReturn image width\n\t*\t@return int\n\t**/\n\tfunction width() {\n\t\treturn imagesx($this->data);\n\t}\n\n\t/**\n\t*\tReturn image height\n\t*\t@return int\n\t**/\n\tfunction height() {\n\t\treturn imagesy($this->data);\n\t}\n\n\t/**\n\t*\tSend image to HTTP client\n\t*\t@return NULL\n\t**/\n\tfunction render() {\n\t\t$args=func_get_args();\n\t\t$format=$args?array_shift($args):'png';\n\t\tif (PHP_SAPI!='cli') {\n\t\t\theader('Content-Type: image/'.$format);\n\t\t\theader('X-Powered-By: '.Base::instance()->PACKAGE);\n\t\t}\n\t\tcall_user_func_array(\n\t\t\t'image'.$format,\n\t\t\tarray_merge([$this->data,NULL],$args)\n\t\t);\n\t}\n\n\t/**\n\t*\tReturn image as a string\n\t*\t@return string\n\t**/\n\tfunction dump() {\n\t\t$args=func_get_args();\n\t\t$format=$args?array_shift($args):'png';\n\t\tob_start();\n\t\tcall_user_func_array(\n\t\t\t'image'.$format,\n\t\t\tarray_merge([$this->data,NULL],$args)\n\t\t);\n\t\treturn ob_get_clean();\n\t}\n\n\t/**\n\t*\tReturn image resource\n\t*\t@return resource\n\t**/\n\tfunction data() {\n\t\treturn $this->data;\n\t}\n\n\t/**\n\t*\tSave current state\n\t*\t@return object\n\t**/\n\tfunction save() {\n\t\t$fw=Base::instance();\n\t\tif ($this->flag) {\n\t\t\tif (!is_dir($dir=$fw->TEMP))\n\t\t\t\tmkdir($dir,Base::MODE,TRUE);\n\t\t\t++$this->count;\n\t\t\t$fw->write($dir.'/'.$fw->SEED.'.'.\n\t\t\t\t$fw->hash($this->file).'-'.$this->count.'.png',\n\t\t\t\t$this->dump());\n\t\t}\n\t\treturn $this;\n\t}\n\n\t/**\n\t*\tRevert to specified state\n\t*\t@return object\n\t*\t@param $state int\n\t**/\n\tfunction restore($state=1) {\n\t\t$fw=Base::instance();\n\t\tif ($this->flag && is_file($file=($path=$fw->TEMP.\n\t\t\t$fw->SEED.'.'.$fw->hash($this->file).'-').$state.'.png')) {\n\t\t\tif (version_compare(PHP_VERSION, '8.5.0')<0 && isset($this->data))\n\t\t\t\t// TODO: remove this when php7 support is dropped\n\t\t\t\timagedestroy($this->data);\n\t\t\t$this->data=imagecreatefromstring($fw->read($file));\n\t\t\timagesavealpha($this->data,TRUE);\n\t\t\tforeach (glob($path.'*.png',GLOB_NOSORT) as $match)\n\t\t\t\tif (preg_match('/-(\\d+)\\.png/',$match,$parts) &&\n\t\t\t\t\t$parts[1]>$state)\n\t\t\t\t\t@unlink($match);\n\t\t\t$this->count=$state;\n\t\t}\n\t\treturn $this;\n\t}\n\n\t/**\n\t*\tUndo most recently applied filter\n\t*\t@return object\n\t**/\n\tfunction undo() {\n\t\tif ($this->flag) {\n\t\t\tif ($this->count)\n\t\t\t\t$this->count--;\n\t\t\treturn $this->restore($this->count);\n\t\t}\n\t\treturn $this;\n\t}\n\n\t/**\n\t*\tLoad string\n\t*\t@return object|FALSE\n\t*\t@param $str string\n\t**/\n\tfunction load($str) {\n\t\tif (!$this->data=@imagecreatefromstring($str))\n\t\t\treturn FALSE;\n\t\timagesavealpha($this->data,TRUE);\n\t\t$this->save();\n\t\treturn $this;\n\t}\n\n\t/**\n\t*\tInstantiate image\n\t*\t@param $file string\n\t*\t@param $flag bool\n\t*\t@param $path string\n\t**/\n\tfunction __construct($file=NULL,$flag=FALSE,$path=NULL) {\n\t\t$this->flag=$flag;\n\t\tif ($file) {\n\t\t\t$fw=Base::instance();\n\t\t\t// Create image from file\n\t\t\t$this->file=$file;\n\t\t\tif (!isset($path))\n\t\t\t\t$path=$fw->UI.';./';\n\t\t\tforeach ($fw->split($path,FALSE) as $dir)\n\t\t\t\tif (is_file($dir.$file))\n\t\t\t\t\treturn $this->load($fw->read($dir.$file));\n            throw new \\Exception(self::E_File);\n\t\t}\n\t}\n\n\t/**\n\t*\tWrap-up\n\t*\t@return NULL\n\t**/\n\tfunction __destruct() {\n\t\tif (isset($this->data)) {\n\t\t\tif (version_compare(PHP_VERSION, '8.5.0')<0)\n\t\t\t\t// TODO: remove this when php7 support is dropped\n\t\t\t\timagedestroy($this->data);\n\t\t\t$fw=Base::instance();\n\t\t\t$path=$fw->TEMP.$fw->SEED.'.'.$fw->hash($this->file);\n\t\t\tif ($glob=@glob($path.'*.png',GLOB_NOSORT))\n\t\t\t\tforeach ($glob as $match)\n\t\t\t\t\tif (preg_match('/-(\\d+)\\.png/',$match))\n\t\t\t\t\t\t@unlink($match);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "log.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Custom logger\nclass Log {\n\n\tprotected\n\t\t//! File name\n\t\t$file;\n\n\t/**\n\t*\tWrite specified text to log file\n\t*\t@return string\n\t*\t@param $text string\n\t*\t@param $format string\n\t**/\n\tfunction write($text,$format='r') {\n\t\t$fw=Base::instance();\n\t\tforeach (preg_split('/\\r?\\n|\\r/',trim($text)) as $line)\n\t\t\t$fw->write(\n\t\t\t\t$this->file,\n\t\t\t\tdate($format).\n\t\t\t\t(isset($_SERVER['REMOTE_ADDR'])?\n\t\t\t\t\t(' ['.$_SERVER['REMOTE_ADDR'].\n\t\t\t\t\t(($fwd=filter_var($fw->get('HEADERS.X-Forwarded-For'),\n\t\t\t\t\t\tFILTER_VALIDATE_IP))?(' ('.$fwd.')'):'')\n\t\t\t\t\t.']'):'').' '.\n\t\t\t\ttrim($line).PHP_EOL,\n\t\t\t\tTRUE\n\t\t\t);\n\t}\n\n\t/**\n\t*\tErase log\n\t*\t@return NULL\n\t**/\n\tfunction erase() {\n\t\t@unlink($this->file);\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@param $file string\n\t**/\n\tfunction __construct($file) {\n\t\t$fw=Base::instance();\n\t\tif (!is_dir($dir=$fw->LOGS))\n\t\t\tmkdir($dir,Base::MODE,TRUE);\n\t\t$this->file=$dir.$file;\n\t}\n\n}\n"
  },
  {
    "path": "magic.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! PHP magic wrapper\nabstract class Magic implements ArrayAccess {\n\n\t/**\n\t*\tReturn TRUE if key is not empty\n\t*\t@return bool\n\t*\t@param $key string\n\t**/\n\tabstract function exists($key);\n\n\t/**\n\t*\tBind value to key\n\t*\t@return mixed\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\tabstract function set($key,$val);\n\n\t/**\n\t*\tRetrieve contents of key\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\tabstract function &get($key);\n\n\t/**\n\t*\tUnset key\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\tabstract function clear($key);\n\n\t/**\n\t*\tConvenience method for checking property value\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\t#[\\ReturnTypeWillChange]\n\tfunction offsetexists($key) {\n\t\treturn Base::instance()->visible($this,$key)?\n\t\t\tisset($this->$key):\n\t\t\t($this->exists($key) && $this->get($key)!==NULL);\n\t}\n\n\t/**\n\t*\tConvenience method for assigning property value\n\t*\t@return mixed\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\t#[\\ReturnTypeWillChange]\n\tfunction offsetset($key,$val) {\n\t\treturn Base::instance()->visible($this,$key)?\n\t\t\t($this->$key=$val):$this->set($key,$val);\n\t}\n\n\t/**\n\t*\tConvenience method for retrieving property value\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\t#[\\ReturnTypeWillChange]\n\tfunction &offsetget($key) {\n\t\tif (Base::instance()->visible($this,$key))\n\t\t\t$val=&$this->$key;\n\t\telse\n\t\t\t$val=&$this->get($key);\n\t\treturn $val;\n\t}\n\n\t/**\n\t*\tConvenience method for removing property value\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\t#[\\ReturnTypeWillChange]\n\tfunction offsetunset($key) {\n\t\tif (Base::instance()->visible($this,$key))\n\t\t\tunset($this->$key);\n\t\telse\n\t\t\t$this->clear($key);\n\t}\n\n\t/**\n\t*\tAlias for offsetexists()\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\tfunction __isset($key) {\n\t\treturn $this->offsetexists($key);\n\t}\n\n\t/**\n\t*\tAlias for offsetset()\n\t*\t@return mixed\n\t*\t@param $key string\n\t*\t@param $val mixed\n\t**/\n\tfunction __set($key,$val) {\n\t\treturn $this->offsetset($key,$val);\n\t}\n\n\t/**\n\t*\tAlias for offsetget()\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\tfunction &__get($key) {\n\t\t$val=&$this->offsetget($key);\n\t\treturn $val;\n\t}\n\n\t/**\n\t*\tAlias for offsetunset()\n\t*\t@param $key string\n\t**/\n\tfunction __unset($key) {\n\t\t$this->offsetunset($key);\n\t}\n\n}\n"
  },
  {
    "path": "markdown.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Markdown-to-HTML converter\nclass Markdown extends Prefab {\n\n\tprotected\n\t\t//! Parsing rules\n\t\t$blocks,\n\t\t//! Special characters\n\t\t$special;\n\n\t/**\n\t*\tProcess blockquote\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tprotected function _blockquote($str) {\n\t\t$str=preg_replace('/(?<=^|\\n)\\h?>\\h?(.*?(?:\\n+|$))/','\\1',$str);\n\t\treturn strlen($str)?\n\t\t\t('<blockquote>'.$this->build($str).'</blockquote>'.\"\\n\\n\"):'';\n\t}\n\n\t/**\n\t*\tProcess whitespace-prefixed code block\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tprotected function _pre($str) {\n\t\t$str=preg_replace('/(?<=^|\\n)(?: {4}|\\t)(.+?(?:\\n+|$))/','\\1',\n\t\t\t$this->esc($str));\n\t\treturn strlen($str)?\n\t\t\t('<pre><code>'.\n\t\t\t\t$this->esc($this->snip($str)).\n\t\t\t'</code></pre>'.\"\\n\\n\"):\n\t\t\t'';\n\t}\n\n\t/**\n\t*\tProcess fenced code block\n\t*\t@return string\n\t*\t@param $hint string\n\t*\t@param $str string\n\t**/\n\tprotected function _fence($hint,$str) {\n\t\t$str=$this->snip($str);\n\t\t$fw=Base::instance();\n\t\tif ($fw->HIGHLIGHT) {\n\t\t\tswitch (strtolower($hint)) {\n\t\t\t\tcase 'php':\n\t\t\t\t\t$str=$fw->highlight($str);\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'apache':\n\t\t\t\t\tpreg_match_all('/(?<=^|\\n)(\\h*)'.\n\t\t\t\t\t\t'(?:(<\\/?)(\\w+)((?:\\h+[^>]+)*)(>)|'.\n\t\t\t\t\t\t'(?:(\\w+)(\\h.+?)))(\\h*(?:\\n+|$))/',\n\t\t\t\t\t\t$str,$matches,PREG_SET_ORDER);\n\t\t\t\t\t$out='';\n\t\t\t\t\tforeach ($matches as $match)\n\t\t\t\t\t\t$out.=$match[1].\n\t\t\t\t\t\t\t($match[3]?\n\t\t\t\t\t\t\t\t('<span class=\"section\">'.\n\t\t\t\t\t\t\t\t\t$this->esc($match[2]).$match[3].\n\t\t\t\t\t\t\t\t'</span>'.\n\t\t\t\t\t\t\t\t($match[4]?\n\t\t\t\t\t\t\t\t\t('<span class=\"data\">'.\n\t\t\t\t\t\t\t\t\t\t$this->esc($match[4]).\n\t\t\t\t\t\t\t\t\t'</span>'):\n\t\t\t\t\t\t\t\t\t'').\n\t\t\t\t\t\t\t\t'<span class=\"section\">'.\n\t\t\t\t\t\t\t\t\t$this->esc($match[5]).\n\t\t\t\t\t\t\t\t'</span>'):\n\t\t\t\t\t\t\t\t('<span class=\"directive\">'.\n\t\t\t\t\t\t\t\t\t$match[6].\n\t\t\t\t\t\t\t\t'</span>'.\n\t\t\t\t\t\t\t\t'<span class=\"data\">'.\n\t\t\t\t\t\t\t\t\t$this->esc($match[7]).\n\t\t\t\t\t\t\t\t'</span>')).\n\t\t\t\t\t\t\t$match[8];\n\t\t\t\t\t$str='<code>'.$out.'</code>';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'html':\n\t\t\t\t\tpreg_match_all(\n\t\t\t\t\t\t'/(?:(?:<(\\/?)(\\w+)'.\n\t\t\t\t\t\t'((?:\\h+(?:\\w+\\h*=\\h*)?\".+?\"|[^>]+)*|'.\n\t\t\t\t\t\t'\\h+.+?)(\\h*\\/?)>)|(.+?))/s',\n\t\t\t\t\t\t$str,$matches,PREG_SET_ORDER\n\t\t\t\t\t);\n\t\t\t\t\t$out='';\n\t\t\t\t\tforeach ($matches as $match) {\n\t\t\t\t\t\tif ($match[2]) {\n\t\t\t\t\t\t\t$out.='<span class=\"xml_tag\">&lt;'.\n\t\t\t\t\t\t\t\t$match[1].$match[2].'</span>';\n\t\t\t\t\t\t\tif ($match[3]) {\n\t\t\t\t\t\t\t\tpreg_match_all(\n\t\t\t\t\t\t\t\t\t'/(?:\\h+(?:(?:(\\w+)\\h*=\\h*)?'.\n\t\t\t\t\t\t\t\t\t'(\".+?\")|(.+)))/',\n\t\t\t\t\t\t\t\t\t$match[3],$parts,PREG_SET_ORDER\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tforeach ($parts as $part)\n\t\t\t\t\t\t\t\t\t$out.=' '.\n\t\t\t\t\t\t\t\t\t\t(empty($part[3])?\n\t\t\t\t\t\t\t\t\t\t\t((empty($part[1])?\n\t\t\t\t\t\t\t\t\t\t\t\t'':\n\t\t\t\t\t\t\t\t\t\t\t\t('<span class=\"xml_attr\">'.\n\t\t\t\t\t\t\t\t\t\t\t\t\t$part[1].'</span>=')).\n\t\t\t\t\t\t\t\t\t\t\t'<span class=\"xml_data\">'.\n\t\t\t\t\t\t\t\t\t\t\t\t$part[2].'</span>'):\n\t\t\t\t\t\t\t\t\t\t\t('<span class=\"xml_tag\">'.\n\t\t\t\t\t\t\t\t\t\t\t\t$part[3].'</span>'));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t$out.='<span class=\"xml_tag\">'.\n\t\t\t\t\t\t\t\t$match[4].'&gt;</span>';\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\t$out.=$this->esc($match[5]);\n\t\t\t\t\t}\n\t\t\t\t\t$str='<code>'.$out.'</code>';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'ini':\n\t\t\t\t\tpreg_match_all(\n\t\t\t\t\t\t'/(?<=^|\\n)(?:'.\n\t\t\t\t\t\t'(;[^\\n]*)|(?:<\\?php.+?\\?>?)|'.\n\t\t\t\t\t\t'(?:\\[(.+?)\\])|'.\n\t\t\t\t\t\t'(.+?)(\\h*=\\h*)'.\n\t\t\t\t\t\t'((?:\\\\\\\\\\h*\\r?\\n|.+?)*)'.\n\t\t\t\t\t\t')((?:\\r?\\n)+|$)/',\n\t\t\t\t\t\t$str,$matches,PREG_SET_ORDER\n\t\t\t\t\t);\n\t\t\t\t\t$out='';\n\t\t\t\t\tforeach ($matches as $match) {\n\t\t\t\t\t\tif ($match[1])\n\t\t\t\t\t\t\t$out.='<span class=\"comment\">'.$match[1].\n\t\t\t\t\t\t\t\t'</span>';\n\t\t\t\t\t\telseif ($match[2])\n\t\t\t\t\t\t\t$out.='<span class=\"ini_section\">['.$match[2].']'.\n\t\t\t\t\t\t\t\t'</span>';\n\t\t\t\t\t\telseif ($match[3])\n\t\t\t\t\t\t\t$out.='<span class=\"ini_key\">'.$match[3].\n\t\t\t\t\t\t\t\t'</span>'.$match[4].\n\t\t\t\t\t\t\t\t($match[5]?\n\t\t\t\t\t\t\t\t\t('<span class=\"ini_value\">'.\n\t\t\t\t\t\t\t\t\t\t$match[5].'</span>'):'');\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\t$out.=$match[0];\n\t\t\t\t\t\tif (isset($match[6]))\n\t\t\t\t\t\t\t$out.=$match[6];\n\t\t\t\t\t}\n\t\t\t\t\t$str='<code>'.$out.'</code>';\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\t$str='<code>'.$this->esc($str).'</code>';\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\telse\n\t\t\t$str='<code>'.$this->esc($str).'</code>';\n\t\treturn '<pre>'.$str.'</pre>'.\"\\n\\n\";\n\t}\n\n\t/**\n\t*\tProcess horizontal rule\n\t*\t@return string\n\t**/\n\tprotected function _hr() {\n\t\treturn '<hr />'.\"\\n\\n\";\n\t}\n\n\t/**\n\t*\tProcess atx-style heading\n\t*\t@return string\n\t*\t@param $type string\n\t*\t@param $str string\n\t**/\n\tprotected function _atx($type,$str) {\n\t\t$level=strlen($type);\n\t\treturn '<h'.$level.' id=\"'.Web::instance()->slug($str).'\">'.\n\t\t\t$this->scan($str).'</h'.$level.'>'.\"\\n\\n\";\n\t}\n\n\t/**\n\t*\tProcess setext-style heading\n\t*\t@return string\n\t*\t@param $str string\n\t*\t@param $type string\n\t**/\n\tprotected function _setext($str,$type) {\n\t\t$level=strpos('=-',$type)+1;\n\t\treturn '<h'.$level.' id=\"'.Web::instance()->slug($str).'\">'.\n\t\t\t$this->scan($str).'</h'.$level.'>'.\"\\n\\n\";\n\t}\n\n\t/**\n\t*\tProcess ordered/unordered list\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tprotected function _li($str) {\n\t\t// Initialize list parser\n\t\t$len=strlen($str);\n\t\t$ptr=0;\n\t\t$dst='';\n\t\t$first=TRUE;\n\t\t$tight=TRUE;\n\t\t$type='ul';\n\t\t// Main loop\n\t\twhile ($ptr<$len) {\n\t\t\tif (preg_match('/^\\h*[*\\-](?:\\h?[*\\-]){2,}(?:\\n+|$)/',\n\t\t\t\tsubstr($str,$ptr),$match)) {\n\t\t\t\t$ptr+=strlen($match[0]);\n\t\t\t\t// Embedded horizontal rule\n\t\t\t\treturn (strlen($dst)?\n\t\t\t\t\t('<'.$type.'>'.\"\\n\".$dst.'</'.$type.'>'.\"\\n\\n\"):'').\n\t\t\t\t\t'<hr />'.\"\\n\\n\".$this->build(substr($str,$ptr));\n\t\t\t}\n\t\t\telseif (preg_match('/(?<=^|\\n)([*+\\-]|\\d+\\.)\\h'.\n\t\t\t\t'(.+?(?:\\n+|$))((?:(?: {4}|\\t)+.+?(?:\\n+|$))*)/s',\n\t\t\t\tsubstr($str,$ptr),$match)) {\n\t\t\t\t$match[3]=preg_replace('/(?<=^|\\n)(?: {4}|\\t)/','',$match[3]);\n\t\t\t\t$found=FALSE;\n\t\t\t\tforeach (array_slice($this->blocks,0,-1) as $regex)\n\t\t\t\t\tif (preg_match($regex,$match[3])) {\n\t\t\t\t\t\t$found=TRUE;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t// List\n\t\t\t\tif ($first) {\n\t\t\t\t\t// First pass\n\t\t\t\t\tif (is_numeric($match[1]))\n\t\t\t\t\t\t$type='ol';\n\t\t\t\t\tif (preg_match('/\\n{2,}$/',$match[2].\n\t\t\t\t\t\t($found?'':$match[3])))\n\t\t\t\t\t\t// Loose structure; Use paragraphs\n\t\t\t\t\t\t$tight=FALSE;\n\t\t\t\t\t$first=FALSE;\n\t\t\t\t}\n\t\t\t\t// Strip leading whitespaces\n\t\t\t\t$ptr+=strlen($match[0]);\n\t\t\t\t$tmp=$this->snip($match[2].$match[3]);\n\t\t\t\tif ($tight) {\n\t\t\t\t\tif ($found)\n\t\t\t\t\t\t$tmp=$match[2].$this->build($this->snip($match[3]));\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t\t$tmp=$this->build($tmp);\n\t\t\t\t$dst.='<li>'.$this->scan(trim($tmp)).'</li>'.\"\\n\";\n\t\t\t}\n\t\t}\n\t\treturn strlen($dst)?\n\t\t\t('<'.$type.'>'.\"\\n\".$dst.'</'.$type.'>'.\"\\n\\n\"):'';\n\t}\n\n\t/**\n\t*\tIgnore raw HTML\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tprotected function _raw($str) {\n\t\treturn $str;\n\t}\n\n\t/**\n\t*\tProcess paragraph\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tprotected function _p($str) {\n\t\t$str=trim($str);\n\t\tif (strlen($str)) {\n\t\t\tif (preg_match('/^(.+?\\n)([>#].+)$/s',$str,$parts))\n\t\t\t\treturn $this->_p($parts[1]).$this->build($parts[2]);\n\t\t\t$str=preg_replace_callback(\n\t\t\t\t'/([^<>\\[]+)?(<[\\?%].+?[\\?%]>|<.+?>|\\[.+?\\]\\s*\\(.+?\\))|'.\n\t\t\t\t'(.+)/s',\n\t\t\t\tfunction($expr) {\n\t\t\t\t\t$tmp='';\n\t\t\t\t\tif (isset($expr[4]))\n\t\t\t\t\t\t$tmp.=$this->esc($expr[4]);\n\t\t\t\t\telse {\n\t\t\t\t\t\tif (isset($expr[1]))\n\t\t\t\t\t\t\t$tmp.=$this->esc($expr[1]);\n\t\t\t\t\t\t$tmp.=$expr[2];\n\t\t\t\t\t\tif (isset($expr[3]))\n\t\t\t\t\t\t\t$tmp.=$this->esc($expr[3]);\n\t\t\t\t\t}\n\t\t\t\t\treturn $tmp;\n\t\t\t\t},\n\t\t\t\t$str\n\t\t\t);\n\t\t\t$str=preg_replace('/\\s{2}\\r?\\n/','<br />',$str);\n\t\t\treturn '<p>'.$this->scan($str).'</p>'.\"\\n\\n\";\n\t\t}\n\t\treturn '';\n\t}\n\n\t/**\n\t*\tProcess strong/em/strikethrough spans\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tprotected function _text($str) {\n\t\t$tmp='';\n\t\twhile ($str!=$tmp)\n\t\t\t$str=preg_replace_callback(\n\t\t\t\t'/(?<=\\s|^)(?<!\\\\\\\\)([*_])([*_]?)([*_]?)(.*?)(?!\\\\\\\\)\\3\\2\\1(?=[\\s[:punct:]]|$)/',\n\t\t\t\tfunction($expr) {\n\t\t\t\t\tif ($expr[3])\n\t\t\t\t\t\treturn '<strong><em>'.$expr[4].'</em></strong>';\n\t\t\t\t\tif ($expr[2])\n\t\t\t\t\t\treturn '<strong>'.$expr[4].'</strong>';\n\t\t\t\t\treturn '<em>'.$expr[4].'</em>';\n\t\t\t\t},\n\t\t\t\tpreg_replace(\n\t\t\t\t\t'/(?<!\\\\\\\\)~~(.*?)(?!\\\\\\\\)~~(?=[\\s[:punct:]]|$)/',\n\t\t\t\t\t'<del>\\1</del>',\n\t\t\t\t\t$tmp=$str\n\t\t\t\t)\n\t\t\t);\n\t\treturn $str;\n\t}\n\n\t/**\n\t*\tProcess image span\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tprotected function _img($str) {\n\t\treturn preg_replace_callback(\n\t\t\t'/!(?:\\[(.+?)\\])?\\h*\\(<?(.*?)>?(?:\\h*\"(.*?)\"\\h*)?\\)/',\n\t\t\tfunction($expr) {\n\t\t\t\treturn '<img src=\"'.$expr[2].'\"'.\n\t\t\t\t\t(empty($expr[1])?\n\t\t\t\t\t\t'':\n\t\t\t\t\t\t(' alt=\"'.$this->esc($expr[1]).'\"')).\n\t\t\t\t\t(empty($expr[3])?\n\t\t\t\t\t\t'':\n\t\t\t\t\t\t(' title=\"'.$this->esc($expr[3]).'\"')).' />';\n\t\t\t},\n\t\t\t$str\n\t\t);\n\t}\n\n\t/**\n\t*\tProcess anchor span\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tprotected function _a($str) {\n\t\treturn preg_replace_callback(\n\t\t\t'/(?<!\\\\\\\\)\\[(.+?)(?!\\\\\\\\)\\]\\h*\\(<?(.*?)>?(?:\\h*\"(.*?)\"\\h*)?\\)/',\n\t\t\tfunction($expr) {\n\t\t\t\treturn '<a href=\"'.$this->esc($expr[2]).'\"'.\n\t\t\t\t\t(empty($expr[3])?\n\t\t\t\t\t\t'':\n\t\t\t\t\t\t(' title=\"'.$this->esc($expr[3]).'\"')).\n\t\t\t\t\t'>'.$this->scan($expr[1]).'</a>';\n\t\t\t},\n\t\t\t$str\n\t\t);\n\t}\n\n\t/**\n\t*\tAuto-convert links\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tprotected function _auto($str) {\n\t\treturn preg_replace_callback(\n\t\t\t'/`.*?<(.+?)>.*?`|<(.+?)>/',\n\t\t\tfunction($expr) {\n\t\t\t\tif (empty($expr[1]) && parse_url($expr[2],PHP_URL_SCHEME)) {\n\t\t\t\t\t$expr[2]=$this->esc($expr[2]);\n\t\t\t\t\treturn '<a href=\"'.$expr[2].'\">'.$expr[2].'</a>';\n\t\t\t\t}\n\t\t\t\treturn $expr[0];\n\t\t\t},\n\t\t\t$str\n\t\t);\n\t}\n\n\t/**\n\t*\tProcess code span\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tprotected function _code($str) {\n\t\treturn preg_replace_callback(\n\t\t\t'/`` (.+?) ``|(?<!\\\\\\\\)`(.+?)(?!\\\\\\\\)`/',\n\t\t\tfunction($expr) {\n\t\t\t\treturn '<code>'.\n\t\t\t\t\t$this->esc(empty($expr[1])?$expr[2]:$expr[1]).'</code>';\n\t\t\t},\n\t\t\t$str\n\t\t);\n\t}\n\n\t/**\n\t*\tConvert characters to HTML entities\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction esc($str) {\n\t\tif (!$this->special)\n\t\t\t$this->special=[\n\t\t\t\t'...'=>'&hellip;',\n\t\t\t\t'(tm)'=>'&trade;',\n\t\t\t\t'(r)'=>'&reg;',\n\t\t\t\t'(c)'=>'&copy;'\n\t\t\t];\n\t\tforeach ($this->special as $key=>$val)\n\t\t\t$str=preg_replace('/'.preg_quote($key,'/').'/i',$val,$str);\n\t\treturn htmlspecialchars($str,ENT_COMPAT,\n\t\t\tBase::instance()->ENCODING,FALSE);\n\t}\n\n\t/**\n\t*\tReduce multiple line feeds\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tprotected function snip($str) {\n\t\treturn preg_replace('/(?:(?<=\\n)\\n+)|\\n+$/',\"\\n\",$str);\n\t}\n\n\t/**\n\t*\tScan line for convertible spans\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction scan($str) {\n\t\t$inline=['img','a','text','auto','code'];\n\t\tforeach ($inline as $func)\n\t\t\t$str=$this->{'_'.$func}($str);\n\t\treturn $str;\n\t}\n\n\t/**\n\t*\tAssemble blocks\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tprotected function build($str) {\n\t\tif (!$this->blocks) {\n\t\t\t// Regexes for capturing entire blocks\n\t\t\t$this->blocks=[\n\t\t\t\t'blockquote'=>'/^(?:\\h?>\\h?.*?(?:\\n+|$))+/',\n\t\t\t\t'pre'=>'/^(?:(?: {4}|\\t).+?(?:\\n+|$))+/',\n\t\t\t\t'fence'=>'/^`{3}\\h*(\\w+)?.*?[^\\n]*\\n+(.+?)`{3}[^\\n]*'.\n\t\t\t\t\t'(?:\\n+|$)/s',\n\t\t\t\t'hr'=>'/^\\h*[*_\\-](?:\\h?[\\*_\\-]){2,}\\h*(?:\\n+|$)/',\n\t\t\t\t'atx'=>'/^\\h*(#{1,6})\\h?(.+?)\\h*(?:#.*)?(?:\\n+|$)/u',\n\t\t\t\t'setext'=>'/^\\h*(.+?)\\h*\\n([=\\-])+\\h*(?:\\n+|$)/u',\n\t\t\t\t'li'=>'/^(?:(?:[*+\\-]|\\d+\\.)\\h.+?(?:\\n+|$)'.\n\t\t\t\t\t'(?:(?: {4}|\\t)+.+?(?:\\n+|$))*)+/s',\n\t\t\t\t'raw'=>'/^((?:<!--.+?-->|'.\n\t\t\t\t\t'<(address|article|aside|audio|blockquote|canvas|dd|'.\n\t\t\t\t\t'div|dl|fieldset|figcaption|figure|footer|form|h\\d|'.\n\t\t\t\t\t'header|hgroup|hr|noscript|object|ol|output|p|pre|'.\n\t\t\t\t\t'section|table|tfoot|ul|video).*?'.\n\t\t\t\t\t'(?:\\/>|>(?:(?>[^><]+)|(?R))*<\\/\\2>))'.\n\t\t\t\t\t'\\h*(?:\\n{2,}|\\n*$)|<[\\?%].+?[\\?%]>\\h*(?:\\n?$|\\n*))/s',\n\t\t\t\t'p'=>'/^(.+?(?:\\n{2,}|\\n*$))/s'\n\t\t\t];\n\t\t}\n\t\t// Treat lines with nothing but whitespaces as empty lines\n\t\t$str=preg_replace('/\\n\\h+(?=\\n)/',\"\\n\",$str);\n\t\t// Initialize block parser\n\t\t$len=strlen($str);\n\t\t$ptr=0;\n\t\t$dst='';\n\t\t// Main loop\n\t\twhile ($ptr<$len) {\n\t\t\tif (preg_match('/^ {0,3}\\[([^\\[\\]]+)\\]:\\s*<?(.*?)>?\\s*'.\n\t\t\t\t'(?:\"([^\\n]*)\")?(?:\\n+|$)/s',substr($str,$ptr),$match)) {\n\t\t\t\t// Reference-style link; Backtrack\n\t\t\t\t$ptr+=strlen($match[0]);\n\t\t\t\t$tmp='';\n\t\t\t\t// Catch line breaks in title attribute\n\t\t\t\t$ref=preg_replace('/\\h/','\\s',preg_quote($match[1],'/'));\n\t\t\t\twhile ($dst!=$tmp) {\n\t\t\t\t\t$dst=preg_replace_callback(\n\t\t\t\t\t\t'/(?<!\\\\\\\\)\\[('.$ref.')(?!\\\\\\\\)\\]\\s*\\[\\]|'.\n\t\t\t\t\t\t'(!?)(?:\\[([^\\[\\]]+)\\]\\s*)?'.\n\t\t\t\t\t\t'(?<!\\\\\\\\)\\[('.$ref.')(?!\\\\\\\\)\\]/',\n\t\t\t\t\t\tfunction($expr) use($match) {\n\t\t\t\t\t\t\treturn (empty($expr[2]))?\n\t\t\t\t\t\t\t\t// Anchor\n\t\t\t\t\t\t\t\t('<a href=\"'.$this->esc($match[2]).'\"'.\n\t\t\t\t\t\t\t\t(empty($match[3])?\n\t\t\t\t\t\t\t\t\t'':\n\t\t\t\t\t\t\t\t\t(' title=\"'.\n\t\t\t\t\t\t\t\t\t\t$this->esc($match[3]).'\"')).'>'.\n\t\t\t\t\t\t\t\t// Link\n\t\t\t\t\t\t\t\t$this->scan(\n\t\t\t\t\t\t\t\t\tempty($expr[3])?\n\t\t\t\t\t\t\t\t\t\t(empty($expr[1])?\n\t\t\t\t\t\t\t\t\t\t\t$expr[4]:\n\t\t\t\t\t\t\t\t\t\t\t$expr[1]):\n\t\t\t\t\t\t\t\t\t\t$expr[3]\n\t\t\t\t\t\t\t\t).'</a>'):\n\t\t\t\t\t\t\t\t// Image\n\t\t\t\t\t\t\t\t('<img src=\"'.$match[2].'\"'.\n\t\t\t\t\t\t\t\t(empty($expr[2])?\n\t\t\t\t\t\t\t\t\t'':\n\t\t\t\t\t\t\t\t\t(' alt=\"'.\n\t\t\t\t\t\t\t\t\t\t$this->esc($expr[3]).'\"')).\n\t\t\t\t\t\t\t\t(empty($match[3])?\n\t\t\t\t\t\t\t\t\t'':\n\t\t\t\t\t\t\t\t\t(' title=\"'.\n\t\t\t\t\t\t\t\t\t\t$this->esc($match[3]).'\"')).\n\t\t\t\t\t\t\t\t' />');\n\t\t\t\t\t\t},\n\t\t\t\t\t\t$tmp=$dst\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t\tforeach ($this->blocks as $func=>$regex)\n\t\t\t\t\tif (preg_match($regex,substr($str,$ptr),$match)) {\n\t\t\t\t\t\t$ptr+=strlen($match[0]);\n\t\t\t\t\t\t$dst.=call_user_func_array(\n\t\t\t\t\t\t\t[$this,'_'.$func],\n\t\t\t\t\t\t\tcount($match)>1?array_slice($match,1):$match\n\t\t\t\t\t\t);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t}\n\t\treturn $dst;\n\t}\n\n\t/**\n\t*\tRender HTML equivalent of markdown\n\t*\t@return string\n\t*\t@param $txt string\n\t**/\n\tfunction convert($txt) {\n\t\t$txt=preg_replace_callback(\n\t\t\t'/(<code.*?>.+?<\\/code>|'.\n\t\t\t'<[^>\\n]+>|\\([^\\n\\)]+\\)|\"[^\"\\n]+\")|'.\n\t\t\t'\\\\\\\\(.)/s',\n\t\t\tfunction($expr) {\n\t\t\t\t// Process escaped characters\n\t\t\t\treturn empty($expr[1])?$expr[2]:$expr[1];\n\t\t\t},\n\t\t\t$this->build(preg_replace('/\\r\\n|\\r/',\"\\n\",$txt))\n\t\t);\n\t\treturn $this->snip($txt);\n\t}\n\n}\n"
  },
  {
    "path": "matrix.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Generic array utilities\nclass Matrix extends Prefab {\n\n\t/**\n\t*\tRetrieve values from a specified column of a multi-dimensional\n\t*\tarray variable\n\t*\t@return array\n\t*\t@param $var array\n\t*\t@param $col mixed\n\t**/\n\tfunction pick(array $var,$col) {\n\t\treturn array_map(\n\t\t\tfunction($row) use($col) {\n\t\t\t\treturn $row[$col];\n\t\t\t},\n\t\t\t$var\n\t\t);\n\t}\n\n\t/**\n\t * select a subset of fields from an input array\n\t * @param string|array $fields splittable string or array\n\t * @param string|array $data hive key or array\n\t * @return array\n\t */\n\tfunction select($fields, $data) {\n\t\treturn array_intersect_key(is_array($data) ? $data : \\Base::instance()->get($data),\n\t\t\tarray_flip(is_array($fields) ? $fields : \\Base::instance()->split($fields)));\n\t}\n\n\t/**\n\t * walk with a callback function through a subset of fields from an input array\n\t * the callback receives the value, index-key and the full input array as parameters\n\t * set value parameter as reference and you're able to modify the data as well\n\t * @param string|array $fields splittable string or array of fields\n\t * @param string|array $data hive key or input array\n\t * @param callable $callback (mixed &$value, string $key, array $data)\n\t * @return array modified subset data\n\t */\n\tfunction walk($fields, $data, $callback) {\n\t\t$subset=$this->select($fields, $data);\n\t\tarray_walk($subset, $callback, $data);\n\t\treturn $subset;\n\t}\n\n\t/**\n\t*\tRotate a two-dimensional array variable\n\t*\t@return NULL\n\t*\t@param $var array\n\t**/\n\tfunction transpose(array &$var) {\n\t\t$out=[];\n\t\tforeach ($var as $keyx=>$cols)\n\t\t\tforeach ($cols as $keyy=>$valy)\n\t\t\t\t$out[$keyy][$keyx]=$valy;\n\t\t$var=$out;\n\t}\n\n\t/**\n\t*\tSort a multi-dimensional array variable on a specified column\n\t*\t@return bool\n\t*\t@param $var array\n\t*\t@param $col mixed\n\t*\t@param $order int\n\t**/\n\tfunction sort(array &$var,$col,$order=SORT_ASC) {\n\t\tuasort(\n\t\t\t$var,\n\t\t\tfunction($val1,$val2) use($col,$order) {\n\t\t\t\tlist($v1,$v2)=[$val1[$col],$val2[$col]];\n\t\t\t\t$out=is_numeric($v1) && is_numeric($v2)?\n\t\t\t\t\tBase::instance()->sign($v1-$v2):strcmp($v1,$v2);\n\t\t\t\tif ($order==SORT_DESC)\n\t\t\t\t\t$out=-$out;\n\t\t\t\treturn $out;\n\t\t\t}\n\t\t);\n\t\t$var=array_values($var);\n\t}\n\n\t/**\n\t*\tChange the key of a two-dimensional array element\n\t*\t@return NULL\n\t*\t@param $var array\n\t*\t@param $old string\n\t*\t@param $new string\n\t**/\n\tfunction changekey(array &$var,$old,$new) {\n\t\t$keys=array_keys($var);\n\t\t$vals=array_values($var);\n\t\t$keys[array_search($old,$keys)]=$new;\n\t\t$var=array_combine($keys,$vals);\n\t}\n\n\t/**\n\t*\tReturn month calendar of specified date, with optional setting for\n\t*\tfirst day of week (0 for Sunday)\n\t*\t@return array\n\t*\t@param $date string|int\n\t*\t@param $first int\n\t**/\n\tfunction calendar($date='now',$first=0) {\n\t\t$out=FALSE;\n\t\tif (extension_loaded('calendar')) {\n\t\t\tif (is_string($date))\n\t\t\t\t$date=strtotime($date);\n\t\t\t$parts=getdate($date);\n\t\t\t$days=cal_days_in_month(CAL_GREGORIAN,$parts['mon'],$parts['year']);\n\t\t\t$ref=date('w',strtotime(date('Y-m',$parts[0]).'-01'))+(7-$first)%7;\n\t\t\t$out=[];\n\t\t\tfor ($i=0;$i<$days;++$i)\n\t\t\t\t$out[floor(($ref+$i)/7)][($ref+$i)%7]=$i+1;\n\t\t}\n\t\treturn $out;\n\t}\n\n}\n"
  },
  {
    "path": "session.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Cache-based session handler\nclass Session extends Magic {\n\n\tprotected\n\t\t//! Session ID\n\t\t$sid,\n\t\t//! Anti-CSRF token\n\t\t$_csrf,\n\t\t//! User agent\n\t\t$_agent,\n\t\t//! IP,\n\t\t$_ip,\n\t\t//! Suspect callback\n\t\t$onsuspect,\n\t\t//! Cache instance\n\t\t$_cache,\n\t\t//! Session meta data\n\t\t$_data=[];\n\n\t/**\n\t*\tOpen session\n\t*\t@return TRUE\n\t*\t@param $path string\n\t*\t@param $name string\n\t**/\n\tfunction open(string $path, string $name): bool\n    {\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tClose session\n\t*\t@return TRUE\n\t**/\n\tfunction close(): bool\n    {\n\t\t$this->sid=NULL;\n\t\t$this->_data=[];\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tReturn session data in serialized format\n\t*\t@return false|string\n\t*\t@param $id string\n\t**/\n\t#[ReturnTypeWillChange]\n    function read(string $id)\n    {\n\t\t$this->sid=$id;\n\t\tif (!$data=$this->_cache->get($id.'.@'))\n\t\t\treturn '';\n\t\t$this->_data = $data;\n\t\tif ($data['ip']!=$this->_ip || $data['agent']!=$this->_agent) {\n\t\t\t$fw=Base::instance();\n\t\t\tif (!isset($this->onsuspect) ||\n\t\t\t\t$fw->call($this->onsuspect,[$this,$id])===FALSE) {\n\t\t\t\t//NB: `session_destroy` can't be called at that stage (`session_start` not completed)\n\t\t\t\t$this->destroy($id);\n\t\t\t\t$this->close();\n\t\t\t\tunset($fw->{'COOKIE.'.session_name()});\n\t\t\t\t$fw->error(403);\n\t\t\t}\n\t\t}\n\t\treturn $data['data'];\n\t}\n\n\t/**\n\t*\tWrite session data\n\t*/\n\tfunction write(string $id, string $data): bool\n    {\n\t\t$fw=Base::instance();\n\t\t$jar=$fw->JAR;\n\t\t$this->_cache->set($id.'.@',\n\t\t\t[\n\t\t\t\t'data'=>$data,\n\t\t\t\t'ip'=>$this->_ip,\n\t\t\t\t'agent'=>$this->_agent,\n\t\t\t\t'stamp'=>time()\n\t\t\t],\n\t\t\t$jar['expire']\n\t\t);\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tDestroy session\n\t*/\n\tfunction destroy(string $id): bool\n    {\n\t\t$this->_cache->clear($id.'.@');\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tGarbage collector\n\t**/\n    #[ReturnTypeWillChange]\n    function gc(int $max_lifetime)\n    {\n\t\t$this->_cache->reset('.@',$max_lifetime);\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t *\tReturn session id (if session has started)\n\t *\t@return string|NULL\n\t **/\n\tfunction sid() {\n\t\treturn $this->sid;\n\t}\n\n\t/**\n\t *\tReturn anti-CSRF token\n\t *\t@return string\n\t **/\n\tfunction csrf() {\n\t\treturn $this->_csrf;\n\t}\n\n\t/**\n\t *\tReturn IP address\n\t *\t@return string\n\t **/\n\tfunction ip() {\n\t\treturn $this->_ip;\n\t}\n\n\t/**\n\t *\tReturn Unix timestamp\n\t *\t@return string|FALSE\n\t **/\n\tfunction stamp() {\n\t\tif (!$this->sid)\n\t\t\tsession_start();\n\t\treturn $this->_cache->exists($this->sid.'.@',$data)?\n\t\t\t$data['stamp']:FALSE;\n\t}\n\n\t/**\n\t *\tReturn HTTP user agent\n\t *\t@return string\n\t **/\n\tfunction agent() {\n\t\treturn $this->_agent;\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@param $onsuspect callback\n\t*\t@param $key string\n\t**/\n\tfunction __construct($onsuspect=NULL,$key=NULL,$cache=null) {\n\t\t$this->onsuspect=$onsuspect;\n\t\t$this->_cache=$cache?:Cache::instance();\n        if (version_compare(PHP_VERSION, '8.4.0')>=0) {\n            // TODO: remove this when php7 support is dropped\n            session_set_save_handler(new SessionAdapter($this));\n        } else {\n            session_set_save_handler(\n                [$this,'open'],\n                [$this,'close'],\n                [$this,'read'],\n                [$this,'write'],\n                [$this,'destroy'],\n                [$this,'gc']\n            );\n        }\n\t\tregister_shutdown_function('session_commit');\n\t\t$fw=\\Base::instance();\n\t\t$headers=$fw->HEADERS;\n\t\t$this->_csrf=$fw->hash($fw->SEED.\n\t\t\textension_loaded('openssl')?\n\t\t\t\timplode(unpack('L',openssl_random_pseudo_bytes(4))):\n\t\t\t\tmt_rand()\n\t\t\t);\n\t\tif ($key)\n\t\t\t$fw->$key=$this->_csrf;\n\t\t$this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:'';\n\t\t$this->_ip=$fw->IP;\n\t}\n\n\t/**\n\t * check latest meta data existence\n\t * @param string $key\n\t * @return bool\n\t */\n\tfunction exists($key) {\n\t\treturn isset($this->_data[$key]);\n\t}\n\n\t/**\n\t * get meta data from latest session\n\t * @param string $key\n\t * @return mixed\n\t */\n\tfunction &get($key) {\n\t\treturn $this->_data[$key];\n\t}\n\n\tfunction set($key,$val) {\n\t\ttrigger_error('Unable to set data on previous session');\n\t}\n\n\tfunction clear($key) {\n\t\ttrigger_error('Unable to clear data on previous session');\n\t}\n}\n"
  },
  {
    "path": "sessionadapter.php",
    "content": "<?php\n\n/**\n * To be removed once the legacy session handlers are reworked when php 7 support is dropped\n */\nclass SessionAdapter implements \\SessionHandlerInterface\n{\n    protected $_handler;\n\n    public function __construct($handler)\n    {\n        $this->_handler = $handler;\n    }\n\n    public function close(): bool\n    {\n        return $this->_handler->close();\n    }\n\n    public function destroy(string $id): bool\n    {\n        return $this->_handler->destroy($id);\n    }\n\n    #[ReturnTypeWillChange]\n    public function gc(int $max_lifetime): int\n    {\n        return $this->_handler->gc($max_lifetime);\n    }\n\n    public function open(string $path, string $name): bool\n    {\n        return $this->_handler->open($path, $name);\n    }\n\n    #[ReturnTypeWillChange]\n    public function read(string $id): string\n    {\n        return $this->_handler->read($id);\n    }\n\n    public function write(string $id, string $data): bool\n    {\n        return $this->_handler->write($id, $data);\n    }\n}"
  },
  {
    "path": "smtp.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! SMTP plug-in\nclass SMTP extends Magic {\n\n\t//@{ Locale-specific error/exception messages\n\tconst\n\t\tE_Header='%s: header is required',\n\t\tE_Blank='Message must not be blank',\n\t\tE_Attach='Attachment %s not found',\n\t\tE_DIALOG='SMTP dialog error: %s';\n\t//@}\n\n\tprotected\n\t\t//! Message properties\n\t\t$headers,\n\t\t//! E-mail attachments\n\t\t$attachments,\n\t\t//! SMTP host\n\t\t$host,\n\t\t//! SMTP port\n\t\t$port,\n\t\t//! TLS/SSL\n\t\t$scheme,\n\t\t//! User ID\n\t\t$user,\n\t\t//! Password\n\t\t$pw,\n\t\t//! TLS/SSL stream context\n\t\t$context,\n\t\t//! TCP/IP socket\n\t\t$socket,\n\t\t//! Server-client conversation\n\t\t$log;\n\n\t/**\n\t*\tFix header\n\t*\t@return string\n\t*\t@param $key string\n\t**/\n\tprotected function fixheader($key) {\n\t\treturn str_replace(' ','-',\n\t\t\tucwords(preg_replace('/[_\\-]/',' ',strtolower($key))));\n\t}\n\n\t/**\n\t*\tReturn TRUE if header exists\n\t*\t@return bool\n\t*\t@param $key\n\t**/\n\tfunction exists($key) {\n\t\t$key=$this->fixheader($key);\n\t\treturn isset($this->headers[$key]);\n\t}\n\n\t/**\n\t*\tBind value to e-mail header\n\t*\t@return string\n\t*\t@param $key string\n\t*\t@param $val string\n\t**/\n\tfunction set($key,$val) {\n\t\t$key=$this->fixheader($key);\n\t\treturn $this->headers[$key]=$val;\n\t}\n\n\t/**\n\t*\tReturn value of e-mail header\n\t*\t@return string|NULL\n\t*\t@param $key string\n\t**/\n\tfunction &get($key) {\n\t\t$key=$this->fixheader($key);\n\t\tif (isset($this->headers[$key]))\n\t\t\t$val=&$this->headers[$key];\n\t\telse\n\t\t\t$val=NULL;\n\t\treturn $val;\n\t}\n\n\t/**\n\t*\tRemove header\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\tfunction clear($key) {\n\t\t$key=$this->fixheader($key);\n\t\tunset($this->headers[$key]);\n\t}\n\n\t/**\n\t*\tReturn client-server conversation history\n\t*\t@return string\n\t**/\n\tfunction log() {\n\t\treturn str_replace(\"\\n\",PHP_EOL,$this->log);\n\t}\n\n\t/**\n\t*\tSend SMTP command and record server response\n\t*\t@return string\n\t*\t@param $cmd string\n\t*\t@param $log bool|string\n\t*\t@param $mock bool\n\t**/\n\tprotected function dialog($cmd=NULL,$log=TRUE,$mock=FALSE) {\n\t\t$reply='';\n\t\tif ($mock) {\n\t\t\t$host=str_replace('ssl://','',$this->host);\n\t\t\tswitch ($cmd) {\n\t\t\tcase NULL:\n\t\t\t\t$reply='220 '.$host.' ESMTP ready'.\"\\n\";\n\t\t\t\tbreak;\n\t\t\tcase 'DATA':\n\t\t\t\t$reply='354 Go ahead'.\"\\n\";\n\t\t\t\tbreak;\n\t\t\tcase 'QUIT':\n\t\t\t\t$reply='221 '.$host.' closing connection'.\"\\n\";\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t$reply='250 OK'.\"\\n\";\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t$socket=&$this->socket;\n\t\t\tif ($cmd)\n\t\t\t\tfputs($socket,$cmd.\"\\r\\n\");\n\t\t\twhile (!feof($socket) && ($info=stream_get_meta_data($socket)) &&\n\t\t\t\t!$info['timed_out'] && $str=fgets($socket,4096)) {\n\t\t\t\t$reply.=$str;\n\t\t\t\tif (preg_match('/(?:^|\\n)\\d{3} .+?\\r\\n/s',$reply))\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif ($log) {\n\t\t\tif ($cmd)\n\t\t\t\t$this->log.=$cmd.\"\\n\";\n\t\t\t$this->log.=str_replace(\"\\r\",'',$reply);\n\t\t}\n\t\tif (preg_match('/^(4|5)\\d{2}\\s.*$/', $reply))\n            throw new \\Exception(sprintf(self::E_DIALOG,$reply));\n\t\treturn $reply;\n\t}\n\n\t/**\n\t*\tAdd e-mail attachment\n\t*\t@return NULL\n\t*\t@param $file string\n\t*\t@param $alias string\n\t*\t@param $cid string\n\t**/\n\tfunction attach($file,$alias=NULL,$cid=NULL) {\n\t\tif (!is_file($file))\n            throw new \\Exception(sprintf(self::E_Attach,$file));\n\t\tif ($alias)\n\t\t\t$file=[$alias,$file];\n\t\t$this->attachments[]=['filename'=>$file,'cid'=>$cid];\n\t}\n\n\t/**\n\t*\tTransmit message\n\t*\t@return bool\n\t*\t@param $message string\n\t*\t@param $log bool|string\n\t*\t@param $mock bool\n\t**/\n\tfunction send($message,$log=TRUE,$mock=FALSE) {\n\t\tif ($this->scheme=='ssl' && !extension_loaded('openssl'))\n\t\t\treturn FALSE;\n\t\t// Message should not be blank\n\t\tif (!$message)\n            throw new \\Exception(self::E_Blank);\n\t\t$fw=Base::instance();\n\t\t// Retrieve headers\n\t\t$headers=$this->headers;\n\t\t// Connect to the server\n\t\tif (!$mock) {\n\t\t\t$socket=&$this->socket;\n\t\t\t$socket=@stream_socket_client($this->host.':'.$this->port,\n\t\t\t\t$errno,$errstr,ini_get('default_socket_timeout'),\n\t\t\t\tSTREAM_CLIENT_CONNECT,$this->context);\n\t\t\tif (!$socket) {\n\t\t\t\t$fw->error(500,$errstr);\n\t\t\t\treturn FALSE;\n\t\t\t}\n\t\t\tstream_set_blocking($socket,TRUE);\n\t\t}\n\t\t// Get server's initial response\n\t\t$this->dialog(NULL,$log,$mock);\n\t\t// Announce presence\n\t\t$reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock);\n\t\tif (strtolower($this->scheme)=='tls') {\n\t\t\t$this->dialog('STARTTLS',$log,$mock);\n\t\t\tif (!$mock) {\n\t\t\t\t$method=STREAM_CRYPTO_METHOD_TLS_CLIENT;\n\t\t\t\tif (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {\n\t\t\t\t\t$method|=STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;\n\t\t\t\t\t$method|=STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;\n\t\t\t\t}\n\t\t\t\tstream_socket_enable_crypto($socket,TRUE,$method);\n\t\t\t}\n\t\t\t$reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock);\n\t\t}\n\t\t$message=wordwrap($message,998);\n\t\tif (preg_match('/8BITMIME/',$reply))\n\t\t\t$headers['Content-Transfer-Encoding']='8bit';\n\t\telse {\n\t\t\t$headers['Content-Transfer-Encoding']='quoted-printable';\n\t\t\t$message=preg_replace('/^\\.(.+)/m',\n\t\t\t\t'..$1',quoted_printable_encode($message));\n\t\t}\n\t\tif ($this->user && $this->pw && preg_match('/AUTH/',$reply)) {\n\t\t\t// Authenticate\n\t\t\t$this->dialog('AUTH LOGIN',$log,$mock);\n\t\t\t$this->dialog(base64_encode($this->user),$log,$mock);\n\t\t\t$reply=$this->dialog(base64_encode($this->pw),$log,$mock);\n\t\t\tif (!preg_match('/^235\\s.*/',$reply)) {\n\t\t\t\t$this->dialog('QUIT',$log,$mock);\n\t\t\t\tif (!$mock && $socket)\n\t\t\t\t\tfclose($socket);\n\t\t\t\treturn FALSE;\n\t\t\t}\n\t\t}\n\t\tif (empty($headers['Message-Id']))\n\t\t\t$headers['Message-Id']='<'.uniqid('',TRUE).'@'.$this->host.'>';\n\t\tif (empty($headers['Date']))\n\t\t\t$headers['Date']=date('r');\n\t\t// Required headers\n\t\t$reqd=['From','To','Subject'];\n\t\tforeach ($reqd as $id)\n\t\t\tif (empty($headers[$id]))\n                throw new \\Exception(sprintf(self::E_Header,$id));\n\t\t$eol=\"\\r\\n\";\n\t\t// Stringify headers\n\t\tforeach ($headers as $key=>&$val) {\n\t\t\tif (in_array($key,['From','To','Cc','Bcc'])) {\n\t\t\t\t$email='';\n\t\t\t\tpreg_match_all('/(?:\".+?\" |=\\?.+?\\?= )?(?:<.+?>|[^ ,]+)/',\n\t\t\t\t\t$val,$matches,PREG_SET_ORDER);\n\t\t\t\tforeach ($matches as $raw)\n\t\t\t\t\t$email.=($email?', ':'').\n\t\t\t\t\t\t(preg_match('/<.+?>/',$raw[0])?\n\t\t\t\t\t\t\t$raw[0]:\n\t\t\t\t\t\t\t('<'.$raw[0].'>'));\n\t\t\t\t$val=$email;\n\t\t\t}\n\t\t\tunset($val);\n\t\t}\n\t\t$from=isset($headers['Sender'])?$headers['Sender']:strstr($headers['From'],'<');\n\t\tunset($headers['Sender']);\n\t\t// Start message dialog\n\t\t$this->dialog('MAIL FROM: '.$from,$log,$mock);\n\t\tforeach ($fw->split($headers['To'].\n\t\t\t(isset($headers['Cc'])?(';'.$headers['Cc']):'').\n\t\t\t(isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst) {\n\t\t\t$this->dialog('RCPT TO: '.strstr($dst,'<'),$log,$mock);\n\t\t}\n\t\t$this->dialog('DATA',$log,$mock);\n\t\tif ($this->attachments) {\n\t\t\t// Replace Content-Type\n\t\t\t$type=$headers['Content-Type'];\n\t\t\tunset($headers['Content-Type']);\n\t\t\t$enc=$headers['Content-Transfer-Encoding'];\n\t\t\tunset($headers['Content-Transfer-Encoding']);\n\t\t\t$hash=uniqid('',TRUE);\n\t\t\t// Send mail headers\n\t\t\t$out='Content-Type: multipart/mixed; boundary=\"'.$hash.'\"'.$eol;\n\t\t\tforeach ($headers as $key=>$val)\n\t\t\t\tif ($key!='Bcc')\n\t\t\t\t\t$out.=$key.': '.$val.$eol;\n\t\t\t$out.=$eol;\n\t\t\t$out.='This is a multi-part message in MIME format'.$eol;\n\t\t\t$out.=$eol;\n\t\t\t$out.='--'.$hash.$eol;\n\t\t\t$out.='Content-Type: '.$type.$eol;\n\t\t\t$out.='Content-Transfer-Encoding: '.$enc.$eol;\n\t\t\t$out.=$eol;\n\t\t\t$out.=$message.$eol;\n\t\t\tforeach ($this->attachments as $attachment) {\n\t\t\t\tif (is_array($attachment['filename']))\n\t\t\t\t\tlist($alias,$file)=$attachment['filename'];\n\t\t\t\telse\n\t\t\t\t\t$alias=basename($file=$attachment['filename']);\n\t\t\t\t$out.='--'.$hash.$eol;\n\t\t\t\t$out.='Content-Type: application/octet-stream'.$eol;\n\t\t\t\t$out.='Content-Transfer-Encoding: base64'.$eol;\n\t\t\t\tif ($attachment['cid'])\n\t\t\t\t\t$out.='Content-Id: '.$attachment['cid'].$eol;\n\t\t\t\t$out.='Content-Disposition: attachment; '.\n\t\t\t\t\t'filename=\"'.$alias.'\"'.$eol;\n\t\t\t\t$out.=$eol;\n\t\t\t\t$out.=chunk_split(base64_encode(\n\t\t\t\t\tfile_get_contents($file))).$eol;\n\t\t\t}\n\t\t\t$out.=$eol;\n\t\t\t$out.='--'.$hash.'--'.$eol;\n\t\t\t$out.='.';\n\t\t\t$this->dialog($out,preg_match('/verbose/i',$log),$mock);\n\t\t}\n\t\telse {\n\t\t\t// Send mail headers\n\t\t\t$out='';\n\t\t\tforeach ($headers as $key=>$val)\n\t\t\t\tif ($key!='Bcc')\n\t\t\t\t\t$out.=$key.': '.$val.$eol;\n\t\t\t$out.=$eol;\n\t\t\t$out.=$message.$eol;\n\t\t\t$out.='.';\n\t\t\t// Send message\n\t\t\t$this->dialog($out,preg_match('/verbose/i',$log),$mock);\n\t\t}\n\t\t$this->dialog('QUIT',$log,$mock);\n\t\tif (!$mock && $socket)\n\t\t\tfclose($socket);\n\t\treturn TRUE;\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@param $host string\n\t*\t@param $port int\n\t*\t@param $scheme string\n\t*\t@param $user string\n\t*\t@param $pw string\n\t*\t@param $ctx resource\n\t**/\n\tfunction __construct(\n\t\t$host='localhost',$port=25,$scheme=NULL,$user=NULL,$pw=NULL,$ctx=NULL) {\n\t\t$this->headers=[\n\t\t\t'MIME-Version'=>'1.0',\n\t\t\t'Content-Type'=>'text/plain; '.\n\t\t\t\t'charset='.Base::instance()->ENCODING\n\t\t];\n\t\t$this->host=strtolower((($this->scheme=strtolower($scheme?:''))=='ssl'?\n\t\t\t'ssl':'tcp').'://'.$host);\n\t\t$this->port=$port;\n\t\t$this->user=$user;\n\t\t$this->pw=$pw;\n\t\t$this->context=stream_context_create($ctx);\n\t}\n\n}\n"
  },
  {
    "path": "template.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! XML-style template engine\nclass Template extends Preview {\n\n\t//@{ Error messages\n\tconst\n\t\tE_Method='Call to undefined method %s()';\n\t//@}\n\n\tprotected\n\t\t//! Template tags\n\t\t$tags,\n\t\t//! Custom tag handlers\n\t\t$custom=[];\n\n\t/**\n\t*\tTemplate -set- tag handler\n\t*\t@return string\n\t*\t@param $node array\n\t**/\n\tprotected function _set(array $node) {\n\t\t$out='';\n\t\tforeach ($node['@attrib'] as $key=>$val)\n\t\t\t$out.='$'.$key.'='.\n\t\t\t\t(preg_match('/\\{\\{(.+?)\\}\\}/',$val?:'')?\n\t\t\t\t\t$this->token($val):\n\t\t\t\t\tBase::instance()->stringify($val)).'; ';\n\t\treturn '<?php '.$out.'?>';\n\t}\n\n\t/**\n\t*\tTemplate -include- tag handler\n\t*\t@return string\n\t*\t@param $node array\n\t**/\n\tprotected function _include(array $node) {\n\t\t$attrib=$node['@attrib'];\n\t\t$hive=isset($attrib['with']) &&\n\t\t\t($attrib['with']=$this->token($attrib['with'])) &&\n\t\t\tpreg_match_all('/(\\w+)\\h*=\\h*(.+?)(?=,|$)/',\n\t\t\t\t$attrib['with'],$pairs,PREG_SET_ORDER)?\n\t\t\t\t\t('['.implode(',',\n\t\t\t\t\t\tarray_map(function($pair) {\n\t\t\t\t\t\t\treturn '\\''.$pair[1].'\\'=>'.\n\t\t\t\t\t\t\t\t(preg_match('/^\\'.*\\'$/',$pair[2]) ||\n\t\t\t\t\t\t\t\t\tpreg_match('/\\$/',$pair[2])?\n\t\t\t\t\t\t\t\t\t$pair[2]:Base::instance()->stringify(\n\t\t\t\t\t\t\t\t\t\tBase::instance()->cast($pair[2])));\n\t\t\t\t\t\t},$pairs)).']+get_defined_vars()'):\n\t\t\t\t\t'get_defined_vars()';\n\t\t$ttl=isset($attrib['ttl'])?(int)$attrib['ttl']:0;\n\t\treturn\n\t\t\t'<?php '.(isset($attrib['if'])?\n\t\t\t\t('if ('.$this->token($attrib['if']).') '):'').\n\t\t\t\t('echo $this->render('.\n\t\t\t\t\t(preg_match('/^\\{\\{(.+?)\\}\\}$/',$attrib['href'])?\n\t\t\t\t\t\t$this->token($attrib['href']):\n\t\t\t\t\t\tBase::instance()->stringify($attrib['href'])).','.\n\t\t\t\t\t'NULL,'.$hive.','.$ttl.'); ?>');\n\t}\n\n\t/**\n\t*\tTemplate -exclude- tag handler\n\t*\t@return string\n\t**/\n\tprotected function _exclude() {\n\t\treturn '';\n\t}\n\n\t/**\n\t*\tTemplate -ignore- tag handler\n\t*\t@return string\n\t*\t@param $node array\n\t**/\n\tprotected function _ignore(array $node) {\n\t\treturn $node[0];\n\t}\n\n\t/**\n\t*\tTemplate -loop- tag handler\n\t*\t@return string\n\t*\t@param $node array\n\t**/\n\tprotected function _loop(array $node) {\n\t\t$attrib=$node['@attrib'];\n\t\tunset($node['@attrib']);\n\t\treturn\n\t\t\t'<?php for ('.\n\t\t\t\t$this->token($attrib['from']).';'.\n\t\t\t\t$this->token($attrib['to']).';'.\n\t\t\t\t$this->token($attrib['step']).'): ?>'.\n\t\t\t\t$this->build($node).\n\t\t\t'<?php endfor; ?>';\n\t}\n\n\t/**\n\t*\tTemplate -repeat- tag handler\n\t*\t@return string\n\t*\t@param $node array\n\t**/\n\tprotected function _repeat(array $node) {\n\t\t$attrib=$node['@attrib'];\n\t\tunset($node['@attrib']);\n\t\treturn\n\t\t\t'<?php '.\n\t\t\t\t(isset($attrib['counter'])?\n\t\t\t\t\t(($ctr=$this->token($attrib['counter'])).'=0; '):'').\n\t\t\t\t'foreach (('.\n\t\t\t\t$this->token($attrib['group']).'?:[]) as '.\n\t\t\t\t(isset($attrib['key'])?\n\t\t\t\t\t($this->token($attrib['key']).'=>'):'').\n\t\t\t\t$this->token($attrib['value']).'):'.\n\t\t\t\t(isset($ctr)?(' '.$ctr.'++;'):'').' ?>'.\n\t\t\t\t$this->build($node).\n\t\t\t'<?php endforeach; ?>';\n\t}\n\n\t/**\n\t*\tTemplate -check- tag handler\n\t*\t@return string\n\t*\t@param $node array\n\t**/\n\tprotected function _check(array $node) {\n\t\t$attrib=$node['@attrib'];\n\t\tunset($node['@attrib']);\n\t\t// Grab <true> and <false> blocks\n\t\tforeach ($node as $pos=>$block)\n\t\t\tif (isset($block['true']))\n\t\t\t\t$true=[$pos,$block];\n\t\t\telseif (isset($block['false']))\n\t\t\t\t$false=[$pos,$block];\n\t\tif (isset($true,$false) && $true[0]>$false[0])\n\t\t\t// Reverse <true> and <false> blocks\n\t\t\tlist($node[$true[0]],$node[$false[0]])=[$false[1],$true[1]];\n\t\treturn\n\t\t\t'<?php if ('.$this->token($attrib['if']).'): ?>'.\n\t\t\t\t$this->build($node).\n\t\t\t'<?php endif; ?>';\n\t}\n\n\t/**\n\t*\tTemplate -true- tag handler\n\t*\t@return string\n\t*\t@param $node array\n\t**/\n\tprotected function _true(array $node) {\n\t\treturn $this->build($node);\n\t}\n\n\t/**\n\t*\tTemplate -false- tag handler\n\t*\t@return string\n\t*\t@param $node array\n\t**/\n\tprotected function _false(array $node) {\n\t\treturn '<?php else: ?>'.$this->build($node);\n\t}\n\n\t/**\n\t*\tTemplate -switch- tag handler\n\t*\t@return string\n\t*\t@param $node array\n\t**/\n\tprotected function _switch(array $node) {\n\t\t$attrib=$node['@attrib'];\n\t\tunset($node['@attrib']);\n\t\tforeach ($node as $pos=>$block)\n\t\t\tif (is_string($block) && !preg_replace('/\\s+/','',$block))\n\t\t\t\tunset($node[$pos]);\n\t\treturn\n\t\t\t'<?php switch ('.$this->token($attrib['expr']).'): ?>'.\n\t\t\t\t$this->build($node).\n\t\t\t'<?php endswitch; ?>';\n\t}\n\n\t/**\n\t*\tTemplate -case- tag handler\n\t*\t@return string\n\t*\t@param $node array\n\t**/\n\tprotected function _case(array $node) {\n\t\t$attrib=$node['@attrib'];\n\t\tunset($node['@attrib']);\n\t\treturn\n\t\t\t'<?php case '.(preg_match('/\\{\\{(.+?)\\}\\}/',$attrib['value'])?\n\t\t\t\t$this->token($attrib['value']):\n\t\t\t\tBase::instance()->stringify($attrib['value'])).': ?>'.\n\t\t\t\t$this->build($node).\n\t\t\t'<?php '.(isset($attrib['break'])?\n\t\t\t\t'if ('.$this->token($attrib['break']).') ':'').\n\t\t\t\t'break; ?>';\n\t}\n\n\t/**\n\t*\tTemplate -default- tag handler\n\t*\t@return string\n\t*\t@param $node array\n\t**/\n\tprotected function _default(array $node) {\n\t\treturn\n\t\t\t'<?php default: ?>'.\n\t\t\t\t$this->build($node).\n\t\t\t'<?php break; ?>';\n\t}\n\n\t/**\n\t*\tAssemble markup\n\t*\t@return string\n\t*\t@param $node array|string\n\t**/\n\tfunction build($node) {\n\t\tif (is_string($node))\n\t\t\treturn parent::build($node);\n\t\t$out='';\n\t\tforeach ($node as $key=>$val)\n\t\t\t$out.=is_int($key)?$this->build($val):$this->{'_'.$key}($val);\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tExtend template with custom tag\n\t*\t@return NULL\n\t*\t@param $tag string\n\t*\t@param $func callback\n\t**/\n\tfunction extend($tag,$func) {\n\t\t$this->tags.='|'.$tag;\n\t\t$this->custom['_'.$tag]=$func;\n\t}\n\n\t/**\n\t*\tCall custom tag handler\n\t*\t@return string|FALSE\n\t*\t@param $func string\n\t*\t@param $args array\n\t**/\n\tfunction __call($func,array $args) {\n\t\tif ($func[0]=='_')\n\t\t\treturn call_user_func_array($this->custom[$func],$args);\n\t\tif (method_exists($this,$func))\n\t\t\treturn call_user_func_array([$this,$func],$args);\n        throw new \\Exception(sprintf(self::E_Method,$func));\n\t}\n\n\t/**\n\t*\tParse string for template directives and tokens\n\t*\t@return array\n\t*\t@param $text string\n\t**/\n\tfunction parse($text) {\n\t\t$text=parent::parse($text);\n\t\t// Build tree structure\n\t\tfor ($ptr=0,$w=5,$len=strlen($text),$tree=[],$tmp='';$ptr<$len;)\n\t\t\tif (preg_match('/^(.{0,'.$w.'}?)<(\\/?)(?:F3:)?'.\n\t\t\t\t'('.$this->tags.')\\b((?:\\s+[\\w.:@!\\-]+'.\n\t\t\t\t'(?:\\h*=\\h*(?:\"(?:.*?)\"|\\'(?:.*?)\\'))?|'.\n\t\t\t\t'\\h*\\{\\{.+?\\}\\})*)\\s*(\\/?)>/is',\n\t\t\t\tsubstr($text,$ptr),$match)) {\n\t\t\t\tif (strlen($tmp) || isset($match[1]))\n\t\t\t\t\t$tree[]=$tmp.$match[1];\n\t\t\t\t// Element node\n\t\t\t\tif ($match[2]) {\n\t\t\t\t\t// Find matching start tag\n\t\t\t\t\t$stack=[];\n\t\t\t\t\tfor($i=count($tree)-1;$i>=0;--$i) {\n\t\t\t\t\t\t$item=$tree[$i];\n\t\t\t\t\t\tif (is_array($item) &&\n\t\t\t\t\t\t\tarray_key_exists($k=strtolower($match[3]),$item) &&\n\t\t\t\t\t\t\t!isset($item[$k][0])) {\n\t\t\t\t\t\t\t// Start tag found\n\t\t\t\t\t\t\t$tree[$i][$k]+=array_reverse($stack);\n\t\t\t\t\t\t\t$tree=array_slice($tree,0,$i+1);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse $stack[]=$item;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// Start tag\n\t\t\t\t\t$node=&$tree[][strtolower($match[3])];\n\t\t\t\t\t$node=[];\n\t\t\t\t\tif ($match[4]) {\n\t\t\t\t\t\t// Process attributes\n\t\t\t\t\t\tpreg_match_all(\n\t\t\t\t\t\t\t'/(?:(\\{\\{.+?\\}\\})|([^\\s\\/\"\\'=]+))'.\n\t\t\t\t\t\t\t'\\h*(?:=\\h*(?:\"(.*?)\"|\\'(.*?)\\'))?/s',\n\t\t\t\t\t\t\t$match[4],$attr,PREG_SET_ORDER);\n\t\t\t\t\t\tforeach ($attr as $kv)\n\t\t\t\t\t\t\tif (!empty($kv[1]) && !isset($kv[3]) && !isset($kv[4]))\n\t\t\t\t\t\t\t\t$node['@attrib'][]=$kv[1];\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\t$node['@attrib'][$kv[1]?:$kv[2]]=\n\t\t\t\t\t\t\t\t\t(isset($kv[3]) && $kv[3]!==''?\n\t\t\t\t\t\t\t\t\t\t$kv[3]:\n\t\t\t\t\t\t\t\t\t\t(isset($kv[4]) && $kv[4]!==''?\n\t\t\t\t\t\t\t\t\t\t\t$kv[4]:NULL));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t$tmp='';\n\t\t\t\t$ptr+=strlen($match[0]);\n\t\t\t\t$w=5;\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// Text node\n\t\t\t\t$tmp.=substr($text,$ptr,$w);\n\t\t\t\t$ptr+=$w;\n\t\t\t\tif ($w<50)\n\t\t\t\t\t++$w;\n\t\t\t}\n\t\tif (strlen($tmp))\n\t\t\t// Append trailing text\n\t\t\t$tree[]=$tmp;\n\t\t// Break references\n\t\tunset($node);\n\t\treturn $tree;\n\t}\n\n\t/**\n\t*\tClass constructor\n\t*\treturn object\n\t**/\n\tfunction __construct() {\n\t\t$ref=new ReflectionClass(get_called_class());\n\t\t$this->tags='';\n\t\tforeach ($ref->getmethods() as $method)\n\t\t\tif (preg_match('/^_(?=[[:alpha:]])/',$method->name))\n\t\t\t\t$this->tags.=(strlen($this->tags)?'|':'').\n\t\t\t\t\tsubstr($method->name,1);\n\t\tparent::__construct();\n\t}\n\n}\n"
  },
  {
    "path": "test.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Unit test kit\nclass Test {\n\n\t//@{ Reporting level\n\tconst\n\t\tFLAG_False=0,\n\t\tFLAG_True=1,\n\t\tFLAG_Both=2;\n\t//@}\n\n\tprotected\n\t\t//! Test results\n\t\t$data=[],\n\t\t//! Success indicator\n\t\t$passed=TRUE,\n\t\t//! Reporting level\n\t\t$level;\n\n\t/**\n\t*\tReturn test results\n\t*\t@return array\n\t**/\n\tfunction results() {\n\t\treturn $this->data;\n\t}\n\n\t/**\n\t*\tReturn FALSE if at least one test case fails\n\t*\t@return bool\n\t**/\n\tfunction passed() {\n\t\treturn $this->passed;\n\t}\n\n\t/**\n\t*\tEvaluate condition and save test result\n\t*\t@return object\n\t*\t@param $cond bool\n\t*\t@param $text string\n\t**/\n\tfunction expect($cond,$text=NULL) {\n\t\t$out=(bool)$cond;\n\t\tif ($this->level==$out || $this->level==self::FLAG_Both) {\n\t\t\t$data=['status'=>$out,'text'=>$text,'source'=>NULL];\n\t\t\tforeach (debug_backtrace() as $frame)\n\t\t\t\tif (isset($frame['file'])) {\n\t\t\t\t\t$data['source']=Base::instance()->\n\t\t\t\t\t\tfixslashes($frame['file']).':'.$frame['line'];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t$this->data[]=$data;\n\t\t}\n\t\tif (!$out && $this->passed)\n\t\t\t$this->passed=FALSE;\n\t\treturn $this;\n\t}\n\n\t/**\n\t*\tAppend message to test results\n\t*\t@return NULL\n\t*\t@param $text string\n\t**/\n\tfunction message($text) {\n\t\t$this->expect(TRUE,$text);\n\t}\n\n\t/**\n\t*\tClass constructor\n\t*\t@return NULL\n\t*\t@param $level int\n\t**/\n\tfunction __construct($level=self::FLAG_Both) {\n\t\t$this->level=$level;\n\t}\n\n}\n"
  },
  {
    "path": "utf.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Unicode string manager\nclass UTF extends Prefab {\n\n\t/**\n\t*\tGet string length\n\t*\t@return int\n\t*\t@param $str string\n\t**/\n\tfunction strlen($str) {\n\t\tpreg_match_all('/./us',$str,$parts);\n\t\treturn count($parts[0]);\n\t}\n\n\t/**\n\t*\tReverse a string\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction strrev($str) {\n\t\tpreg_match_all('/./us',$str,$parts);\n\t\treturn implode('',array_reverse($parts[0]));\n\t}\n\n\t/**\n\t*\tFind position of first occurrence of a string (case-insensitive)\n\t*\t@return int|FALSE\n\t*\t@param $stack string\n\t*\t@param $needle string\n\t*\t@param $ofs int\n\t**/\n\tfunction stripos($stack,$needle,$ofs=0) {\n\t\treturn $this->strpos($stack,$needle,$ofs,TRUE);\n\t}\n\n\t/**\n\t*\tFind position of first occurrence of a string\n\t*\t@return int|FALSE\n\t*\t@param $stack string\n\t*\t@param $needle string\n\t*\t@param $ofs int\n\t*\t@param $case bool\n\t**/\n\tfunction strpos($stack,$needle,$ofs=0,$case=FALSE) {\n\t\treturn preg_match('/^(.{'.$ofs.'}.*?)'.\n\t\t\tpreg_quote($needle,'/').'/us'.($case?'i':''),$stack,$match)?\n\t\t\t$this->strlen($match[1]):FALSE;\n\t}\n\n\t/**\n\t*\tReturns part of haystack string from the first occurrence of\n\t*\tneedle to the end of haystack (case-insensitive)\n\t*\t@return string|FALSE\n\t*\t@param $stack string\n\t*\t@param $needle string\n\t*\t@param $before bool\n\t**/\n\tfunction stristr($stack,$needle,$before=FALSE) {\n\t\treturn $this->strstr($stack,$needle,$before,TRUE);\n\t}\n\n\t/**\n\t*\tReturns part of haystack string from the first occurrence of\n\t*\tneedle to the end of haystack\n\t*\t@return string|FALSE\n\t*\t@param $stack string\n\t*\t@param $needle string\n\t*\t@param $before bool\n\t*\t@param $case bool\n\t**/\n\tfunction strstr($stack,$needle,$before=FALSE,$case=FALSE) {\n\t\tif (!$needle)\n\t\t\treturn FALSE;\n\t\tpreg_match('/^(.*?)'.preg_quote($needle,'/').'/us'.($case?'i':''),\n\t\t\t$stack,$match);\n\t\treturn isset($match[1])?\n\t\t\t($before?\n\t\t\t\t$match[1]:\n\t\t\t\t$this->substr($stack,$this->strlen($match[1]))):\n\t\t\tFALSE;\n\t}\n\n\t/**\n\t*\tReturn part of a string\n\t*\t@return string|FALSE\n\t*\t@param $str string\n\t*\t@param $start int\n\t*\t@param $len int\n\t**/\n\tfunction substr($str,$start,$len=0) {\n\t\tif ($start<0)\n\t\t\t$start=$this->strlen($str)+$start;\n\t\tif (!$len)\n\t\t\t$len=$this->strlen($str)-$start;\n\t\treturn preg_match('/^.{'.$start.'}(.{0,'.$len.'})/us',$str,$match)?\n\t\t\t$match[1]:FALSE;\n\t}\n\n\t/**\n\t*\tCount the number of substring occurrences\n\t*\t@return int\n\t*\t@param $stack string\n\t*\t@param $needle string\n\t**/\n\tfunction substr_count($stack,$needle) {\n\t\tpreg_match_all('/'.preg_quote($needle,'/').'/us',$stack,\n\t\t\t$matches,PREG_SET_ORDER);\n\t\treturn count($matches);\n\t}\n\n\t/**\n\t*\tStrip whitespaces from the beginning of a string\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction ltrim($str) {\n\t\treturn preg_replace('/^[\\pZ\\pC]+/u','',$str);\n\t}\n\n\t/**\n\t*\tStrip whitespaces from the end of a string\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction rtrim($str) {\n\t\treturn preg_replace('/[\\pZ\\pC]+$/u','',$str);\n\t}\n\n\t/**\n\t*\tStrip whitespaces from the beginning and end of a string\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction trim($str) {\n\t\treturn preg_replace('/^[\\pZ\\pC]+|[\\pZ\\pC]+$/u','',$str);\n\t}\n\n\t/**\n\t*\tReturn UTF-8 byte order mark\n\t*\t@return string\n\t**/\n\tfunction bom() {\n\t\treturn chr(0xef).chr(0xbb).chr(0xbf);\n\t}\n\n\t/**\n\t*\tConvert code points to Unicode symbols\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction translate($str) {\n\t\treturn html_entity_decode(\n\t\t\tpreg_replace('/\\\\\\\\u([[:xdigit:]]+)/i','&#x\\1;',$str));\n\t}\n\n\t/**\n\t*\tTranslate emoji tokens to Unicode font-supported symbols\n\t*\t@return string\n\t*\t@param $str string\n\t**/\n\tfunction emojify($str) {\n\t\t$map=[\n\t\t\t':('=>'\\u2639', // frown\n\t\t\t':)'=>'\\u263a', // smile\n\t\t\t'<3'=>'\\u2665', // heart\n\t\t\t':D'=>'\\u1f603', // grin\n\t\t\t'XD'=>'\\u1f606', // laugh\n\t\t\t';)'=>'\\u1f609', // wink\n\t\t\t':P'=>'\\u1f60b', // tongue\n\t\t\t':,'=>'\\u1f60f', // think\n\t\t\t':/'=>'\\u1f623', // skeptic\n\t\t\t'8O'=>'\\u1f632', // oops\n\t\t]+Base::instance()->EMOJI;\n\t\treturn $this->translate(str_replace(array_keys($map),\n\t\t\tarray_values($map),$str));\n\t}\n\n}\n"
  },
  {
    "path": "web/geo.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace Web;\n\n//! Geo plug-in\nclass Geo extends \\Prefab {\n\n\t/**\n\t*\tReturn information about specified Unix time zone\n\t*\t@return array\n\t*\t@param $zone string\n\t**/\n\tfunction tzinfo($zone) {\n\t\t$ref=new \\DateTimeZone($zone);\n\t\t$loc=$ref->getLocation();\n\t\t$trn=$ref->getTransitions($now=time(),$now);\n\t\t$out=[\n\t\t\t'offset'=>$ref->\n\t\t\t\tgetOffset(new \\DateTime('now',new \\DateTimeZone('UTC')))/3600,\n\t\t\t'country'=>$loc['country_code'],\n\t\t\t'latitude'=>$loc['latitude'],\n\t\t\t'longitude'=>$loc['longitude'],\n\t\t\t'dst'=>$trn[0]['isdst']\n\t\t];\n\t\tunset($ref);\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tReturn geolocation data based on specified/auto-detected IP address\n\t*\t@return array|FALSE\n\t*\t@param $ip string\n\t**/\n\tfunction location($ip=NULL) {\n\t\t$fw=\\Base::instance();\n\t\t$web=\\Web::instance();\n\t\tif (!$ip)\n\t\t\t$ip=$fw->IP;\n\t\t$public=filter_var($ip,FILTER_VALIDATE_IP,\n\t\t\tFILTER_FLAG_IPV4|FILTER_FLAG_IPV6|\n\t\t\tFILTER_FLAG_NO_RES_RANGE|FILTER_FLAG_NO_PRIV_RANGE);\n\t\tif (function_exists('geoip_db_avail') &&\n\t\t\tgeoip_db_avail(GEOIP_CITY_EDITION_REV1) &&\n\t\t\t$out=@geoip_record_by_name($ip)) {\n\t\t\t$out['request']=$ip;\n\t\t\t$out['region_code']=$out['region'];\n\t\t\t$out['region_name']='';\n\t\t\tif (!empty($out['country_code']) && !empty($out['region']))\n\t\t\t\t$out['region_name']=geoip_region_name_by_code(\n\t\t\t\t\t$out['country_code'],$out['region']\n\t\t\t\t);\n\t\t\tunset($out['country_code3'],$out['region'],$out['postal_code']);\n\t\t\treturn $out;\n\t\t}\n\t\tif (($req=$web->request('http://www.geoplugin.net/json.gp'.\n\t\t\t($public?('?ip='.$ip):''))) &&\n\t\t\t$data=json_decode($req['body'],TRUE)) {\n\t\t\t$out=[];\n\t\t\tforeach ($data as $key=>$val)\n\t\t\t\tif (!strpos($key,'currency') && $key!=='geoplugin_status'\n\t\t\t\t\t&& $key!=='geoplugin_region')\n\t\t\t\t\t$out[$fw->snakecase(substr($key, 10))]=$val;\n\t\t\treturn $out;\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tReturn weather data based on specified latitude/longitude\n\t*\t@return array|FALSE\n\t*\t@param $latitude float\n\t*\t@param $longitude float\n\t*\t@param $key string\n\t**/\n\tfunction weather($latitude,$longitude,$key) {\n\t\t$fw=\\Base::instance();\n\t\t$web=\\Web::instance();\n\t\t$query=[\n\t\t\t'lat'=>$latitude,\n\t\t\t'lon'=>$longitude,\n\t\t\t'APPID'=>$key,\n\t\t\t'units'=>'metric'\n\t\t];\n\t\treturn ($req=$web->request(\n\t\t\t'http://api.openweathermap.org/data/2.5/weather?'.\n\t\t\t\thttp_build_query($query)))?\n\t\t\tjson_decode($req['body'],TRUE):\n\t\t\tFALSE;\n\t}\n\n}\n"
  },
  {
    "path": "web/google/recaptcha.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace Web\\Google;\n\n//! Google ReCAPTCHA v2 plug-in\nclass Recaptcha {\n\n\tconst\n\t\t//! API URL\n\t\tURL_Recaptcha='https://www.google.com/recaptcha/api/siteverify';\n\n\t/**\n\t *\tVerify reCAPTCHA response\n\t *\t@param string $secret\n\t *\t@param string $response\n\t *\t@return bool\n\t **/\n\tstatic function verify($secret,$response=NULL) {\n\t\t$fw=\\Base::instance();\n\t\tif (!isset($response))\n\t\t\t$response=$fw->{'POST.g-recaptcha-response'};\n\t\tif (!$response)\n\t\t\treturn FALSE;\n\t\t$web=\\Web::instance();\n\t\t$out=$web->request(self::URL_Recaptcha,[\n\t\t\t'method'=>'POST',\n\t\t\t'content'=>http_build_query([\n\t\t\t\t'secret'=>$secret,\n\t\t\t\t'response'=>$response,\n\t\t\t\t'remoteip'=>$fw->IP\n\t\t\t]),\n\t\t]);\n\t\treturn isset($out['body']) &&\n\t\t\t($json=json_decode($out['body'],TRUE)) &&\n\t\t\tisset($json['success']) && $json['success'];\n\t}\n\n}\n"
  },
  {
    "path": "web/google/staticmap.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace Web\\Google;\n\n//! Google Static Maps API v2 plug-in\nclass StaticMap {\n\n\tconst\n\t\t//! API URL\n\t\tURL_Static='http://maps.googleapis.com/maps/api/staticmap';\n\n\tprotected\n\t\t//! Query arguments\n\t\t$query=array();\n\n\t/**\n\t*\tSpecify API key-value pair via magic call\n\t*\t@return object\n\t*\t@param $func string\n\t*\t@param $args array\n\t**/\n\tfunction __call($func,array $args) {\n\t\t$this->query[]=array($func,$args[0]);\n\t\treturn $this;\n\t}\n\n\t/**\n\t*\tGenerate map\n\t*\t@return string\n\t**/\n\tfunction dump() {\n\t\t$fw=\\Base::instance();\n\t\t$web=\\Web::instance();\n\t\t$out='';\n\t\treturn ($req=$web->request(\n\t\t\tself::URL_Static.'?'.array_reduce(\n\t\t\t\t$this->query,\n\t\t\t\tfunction($out,$item) {\n\t\t\t\t\treturn ($out.=($out?'&':'').\n\t\t\t\t\t\turlencode($item[0]).'='.urlencode($item[1]));\n\t\t\t\t}\n\t\t\t))) && $req['body']?$req['body']:FALSE;\n\t}\n\n}\n"
  },
  {
    "path": "web/oauth2.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace Web;\n\n//! Lightweight OAuth2 client\nclass OAuth2 extends \\Magic {\n\n\tprotected\n\t\t//! Scopes and claims\n\t\t$args=[],\n\t\t//! Encoding\n\t\t$enc_type = PHP_QUERY_RFC1738;\n\n\t/**\n\t*\tReturn OAuth2 authentication URI\n\t*\t@return string\n\t*\t@param $endpoint string\n\t*\t@param $query bool\n\t**/\n\tfunction uri($endpoint,$query=TRUE) {\n\t\treturn $endpoint.($query?('?'.\n\t\t\t\thttp_build_query($this->args,'','&',$this->enc_type)):'');\n\t}\n\n\t/**\n\t*\tSend request to API/token endpoint\n\t*\t@return string|array|FALSE\n\t*\t@param $uri string\n\t*\t@param $method string\n\t*\t@param $token string\n\t**/\n\tfunction request($uri,$method,$token=NULL) {\n\t\t$web=\\Web::instance();\n\t\t$options=[\n\t\t\t'method'=>$method,\n\t\t\t'content'=>http_build_query($this->args,'','&',$this->enc_type),\n\t\t\t'header'=>['Accept: application/json']\n\t\t];\n\t\tif ($token)\n\t\t\tarray_push($options['header'],'Authorization: Bearer '.$token);\n\t\telseif ($method=='POST' && isset($this->args['client_id']))\n\t\t\tarray_push($options['header'],'Authorization: Basic '.\n\t\t\t\tbase64_encode(\n\t\t\t\t\t$this->args['client_id'].':'.\n\t\t\t\t\t$this->args['client_secret']\n\t\t\t\t)\n\t\t\t);\n\t\t$response=$web->request($uri,$options);\n\t\tif ($response['error'])\n            throw new \\Exception($response['error']);\n\t\tif (isset($response['body'])) {\n\t\t\tif (preg_grep('/^Content-Type:.*application\\/json/i',\n\t\t\t\t$response['headers'])) {\n\t\t\t\t$token=json_decode($response['body'],TRUE);\n\t\t\t\tif (isset($token['error_description']))\n                    throw new \\Exception($token['error_description']);\n\t\t\t\tif (isset($token['error']))\n                    throw new \\Exception($token['error']);\n\t\t\t\treturn $token;\n\t\t\t}\n\t\t\telse\n\t\t\t\treturn $response['body'];\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tParse JSON Web token\n\t*\t@return array\n\t*\t@param $token string\n\t**/\n\tfunction jwt($token) {\n\t\treturn json_decode(\n\t\t\tbase64_decode(\n\t\t\t\tstr_replace(['-','_'],['+','/'],explode('.',$token)[1])\n\t\t\t),\n\t\t\tTRUE\n\t\t);\n\t}\n\n\t/**\n\t * change default url encoding type, i.E. PHP_QUERY_RFC3986\n\t * @param $type\n\t */\n\tfunction setEncoding($type) {\n\t\t$this->enc_type = $type;\n\t}\n\n\t/**\n\t*\tURL-safe base64 encoding\n\t*\t@return array\n\t*\t@param $data string\n\t**/\n\tfunction b64url($data) {\n\t\treturn trim(strtr(base64_encode($data),'+/','-_'),'=');\n\t}\n\n\t/**\n\t*\tReturn TRUE if scope/claim exists\n\t*\t@return bool\n\t*\t@param $key string\n\t**/\n\tfunction exists($key) {\n\t\treturn isset($this->args[$key]);\n\t}\n\n\t/**\n\t*\tBind value to scope/claim\n\t*\t@return string\n\t*\t@param $key string\n\t*\t@param $val string\n\t**/\n\tfunction set($key,$val) {\n\t\treturn $this->args[$key]=$val;\n\t}\n\n\t/**\n\t*\tReturn value of scope/claim\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\tfunction &get($key) {\n\t\tif (isset($this->args[$key]))\n\t\t\t$val=&$this->args[$key];\n\t\telse\n\t\t\t$val=NULL;\n\t\treturn $val;\n\t}\n\n\t/**\n\t*\tRemove scope/claim\n\t*\t@return NULL\n\t*\t@param $key string\n\t**/\n\tfunction clear($key=NULL) {\n\t\tif ($key)\n\t\t\tunset($this->args[$key]);\n\t\telse\n\t\t\t$this->args=[];\n\t}\n\n}\n\n"
  },
  {
    "path": "web/openid.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace Web;\n\n//! OpenID consumer\nclass OpenID extends \\Magic {\n\n\tprotected\n\t\t//! OpenID provider endpoint URL\n\t\t$url,\n\t\t//! HTTP request parameters\n\t\t$args=[];\n\n\t/**\n\t*\tDetermine OpenID provider\n\t*\t@return string|FALSE\n\t*\t@param $proxy string\n\t**/\n\tprotected function discover($proxy) {\n\t\t// Normalize\n\t\tif (!preg_match('/https?:\\/\\//i',$this->args['endpoint']))\n\t\t\t$this->args['endpoint']='http://'.$this->args['endpoint'];\n\t\t$url=parse_url($this->args['endpoint']);\n\t\t// Remove fragment; reconnect parts\n\t\t$this->args['endpoint']=$url['scheme'].'://'.\n\t\t\t(isset($url['user'])?\n\t\t\t\t($url['user'].\n\t\t\t\t(isset($url['pass'])?(':'.$url['pass']):'').'@'):'').\n\t\t\tstrtolower($url['host']).(isset($url['path'])?$url['path']:'/').\n\t\t\t(isset($url['query'])?('?'.$url['query']):'');\n\t\t// HTML-based discovery of OpenID provider\n\t\t$req=\\Web::instance()->\n\t\t\trequest($this->args['endpoint'],['proxy'=>$proxy]);\n\t\tif (!$req)\n\t\t\treturn FALSE;\n\t\t$type=array_values(preg_grep('/Content-Type:/',$req['headers']));\n\t\tif ($type &&\n\t\t\tpreg_match('/application\\/xrds\\+xml|text\\/xml/',$type[0]) &&\n\t\t\t($sxml=simplexml_load_string($req['body'])) &&\n\t\t\t($xrds=json_decode(json_encode($sxml),TRUE)) &&\n\t\t\tisset($xrds['XRD'])) {\n\t\t\t// XRDS document\n\t\t\t$svc=$xrds['XRD']['Service'];\n\t\t\tif (isset($svc[0]))\n\t\t\t\t$svc=$svc[0];\n\t\t\t$svc_type=is_array($svc['Type'])?$svc['Type']:array($svc['Type']);\n\t\t\tif (preg_grep('/http:\\/\\/specs\\.openid\\.net\\/auth\\/2.0\\/'.\n\t\t\t\t\t'(?:server|signon)/',$svc_type)) {\n\t\t\t\t$this->args['provider']=$svc['URI'];\n\t\t\t\tif (isset($svc['LocalID']))\n\t\t\t\t\t$this->args['localidentity']=$svc['LocalID'];\n\t\t\t\telseif (isset($svc['CanonicalID']))\n\t\t\t\t\t$this->args['localidentity']=$svc['CanonicalID'];\n\t\t\t}\n\t\t\t$this->args['server']=$svc['URI'];\n\t\t\tif (isset($svc['Delegate']))\n\t\t\t\t$this->args['delegate']=$svc['Delegate'];\n\t\t}\n\t\telse {\n\t\t\t$len=strlen($req['body']);\n\t\t\t$ptr=0;\n\t\t\t// Parse document\n\t\t\twhile ($ptr<$len)\n\t\t\t\tif (preg_match(\n\t\t\t\t\t'/^<link\\b((?:\\h+\\w+\\h*=\\h*'.\n\t\t\t\t\t'(?:\"(?:.+?)\"|\\'(?:.+?)\\'))*)\\h*\\/?>/is',\n\t\t\t\t\tsubstr($req['body'],$ptr),$parts)) {\n\t\t\t\t\tif ($parts[1] &&\n\t\t\t\t\t\t// Process attributes\n\t\t\t\t\t\tpreg_match_all('/\\b(rel|href)\\h*=\\h*'.\n\t\t\t\t\t\t\t'(?:\"(.+?)\"|\\'(.+?)\\')/s',$parts[1],$attr,\n\t\t\t\t\t\t\tPREG_SET_ORDER)) {\n\t\t\t\t\t\t$node=[];\n\t\t\t\t\t\tforeach ($attr as $kv)\n\t\t\t\t\t\t\t$node[$kv[1]]=isset($kv[2])?$kv[2]:$kv[3];\n\t\t\t\t\t\tif (isset($node['rel']) &&\n\t\t\t\t\t\t\tpreg_match('/openid2?\\.(\\w+)/',\n\t\t\t\t\t\t\t\t$node['rel'],$var) &&\n\t\t\t\t\t\t\tisset($node['href']))\n\t\t\t\t\t\t\t$this->args[$var[1]]=$node['href'];\n\n\t\t\t\t\t}\n\t\t\t\t\t$ptr+=strlen($parts[0]);\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t\t++$ptr;\n\t\t}\n\t\t// Get OpenID provider's endpoint URL\n\t\tif (isset($this->args['provider'])) {\n\t\t\t// OpenID 2.0\n\t\t\t$this->args['ns']='http://specs.openid.net/auth/2.0';\n\t\t\tif (isset($this->args['localidentity']))\n\t\t\t\t$this->args['identity']=$this->args['localidentity'];\n\t\t\tif (isset($this->args['trust_root']))\n\t\t\t\t$this->args['realm']=$this->args['trust_root'];\n\t\t}\n\t\telseif (isset($this->args['server'])) {\n\t\t\t// OpenID 1.1\n\t\t\t$this->args['ns']='http://openid.net/signon/1.1';\n\t\t\tif (isset($this->args['delegate']))\n\t\t\t\t$this->args['identity']=$this->args['delegate'];\n\t\t}\n\t\tif (isset($this->args['provider'])) {\n\t\t\t// OpenID 2.0\n\t\t\tif (empty($this->args['claimed_id']))\n\t\t\t\t$this->args['claimed_id']=$this->args['identity'];\n\t\t\treturn $this->args['provider'];\n\t\t}\n\t\telseif (isset($this->args['server']))\n\t\t\t// OpenID 1.1\n\t\t\treturn $this->args['server'];\n\t\telse\n\t\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tInitiate OpenID authentication sequence; Return FALSE on failure\n\t*\tor redirect to OpenID provider URL\n\t*\t@return bool\n\t*\t@param $proxy string\n\t*\t@param $attr array\n\t*\t@param $reqd string|array\n\t**/\n\tfunction auth($proxy=NULL,$attr=[],?array $reqd=NULL) {\n\t\t$fw=\\Base::instance();\n\t\t$root=$fw->SCHEME.'://'.$fw->HOST;\n\t\tif (empty($this->args['trust_root']))\n\t\t\t$this->args['trust_root']=$root.$fw->BASE.'/';\n\t\tif (empty($this->args['return_to']))\n\t\t\t$this->args['return_to']=$root.$_SERVER['REQUEST_URI'];\n\t\t$this->args['mode']='checkid_setup';\n\t\tif ($this->url=$this->discover($proxy)) {\n\t\t\tif ($attr) {\n\t\t\t\t$this->args['ns.ax']='http://openid.net/srv/ax/1.0';\n\t\t\t\t$this->args['ax.mode']='fetch_request';\n\t\t\t\tforeach ($attr as $key=>$val)\n\t\t\t\t\t$this->args['ax.type.'.$key]=$val;\n\t\t\t\t$this->args['ax.required']=is_string($reqd)?\n\t\t\t\t\t$reqd:implode(',',$reqd);\n\t\t\t}\n\t\t\t$var=[];\n\t\t\tforeach ($this->args as $key=>$val)\n\t\t\t\t$var['openid.'.$key]=$val;\n\t\t\t$fw->reroute($this->url.'?'.http_build_query($var));\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tReturn TRUE if OpenID verification was successful\n\t*\t@return bool\n\t*\t@param $proxy string\n\t**/\n\tfunction verified($proxy=NULL) {\n\t\tpreg_match_all('/(?<=^|&)openid\\.([^=]+)=([^&]+)/',\n\t\t\t$_SERVER['QUERY_STRING'],$matches,PREG_SET_ORDER);\n\t\tforeach ($matches as $match)\n\t\t\t$this->args[$match[1]]=urldecode($match[2]);\n\t\tif (isset($this->args['mode']) &&\n\t\t\t$this->args['mode']!='error' &&\n\t\t\t$this->url=$this->discover($proxy)) {\n\t\t\t$this->args['mode']='check_authentication';\n\t\t\t$var=[];\n\t\t\tforeach ($this->args as $key=>$val)\n\t\t\t\t$var['openid.'.$key]=$val;\n\t\t\t$req=\\Web::instance()->request(\n\t\t\t\t$this->url,\n\t\t\t\t[\n\t\t\t\t\t'method'=>'POST',\n\t\t\t\t\t'content'=>http_build_query($var),\n\t\t\t\t\t'proxy'=>$proxy\n\t\t\t\t]\n\t\t\t);\n\t\t\treturn (bool)preg_match('/is_valid:true/i',$req['body']);\n\t\t}\n\t\treturn FALSE;\n\t}\n\n\t/**\n\t*\tReturn OpenID response fields\n\t*\t@return array\n\t**/\n\tfunction response() {\n\t\treturn $this->args;\n\t}\n\n\t/**\n\t*\tReturn TRUE if OpenID request parameter exists\n\t*\t@return bool\n\t*\t@param $key string\n\t**/\n\tfunction exists($key) {\n\t\treturn isset($this->args[$key]);\n\t}\n\n\t/**\n\t*\tBind value to OpenID request parameter\n\t*\t@return string\n\t*\t@param $key string\n\t*\t@param $val string\n\t**/\n\tfunction set($key,$val) {\n\t\treturn $this->args[$key]=$val;\n\t}\n\n\t/**\n\t*\tReturn value of OpenID request parameter\n\t*\t@return mixed\n\t*\t@param $key string\n\t**/\n\tfunction &get($key) {\n\t\tif (isset($this->args[$key]))\n\t\t\t$val=&$this->args[$key];\n\t\telse\n\t\t\t$val=NULL;\n\t\treturn $val;\n\t}\n\n\t/**\n\t*\tRemove OpenID request parameter\n\t*\t@return NULL\n\t*\t@param $key\n\t**/\n\tfunction clear($key) {\n\t\tunset($this->args[$key]);\n\t}\n\n}\n"
  },
  {
    "path": "web/pingback.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\nnamespace Web;\n\n//! Pingback 1.0 protocol (client and server) implementation\nclass Pingback extends \\Prefab {\n\n\tprotected\n\t\t//! Transaction history\n\t\t$log;\n\n\t/**\n\t*\tReturn TRUE if URL points to a pingback-enabled resource\n\t*\t@return bool\n\t*\t@param $url\n\t**/\n\tprotected function enabled($url) {\n\t\t$web=\\Web::instance();\n\t\t$req=$web->request($url);\n\t\t$found=FALSE;\n\t\tif ($req['body']) {\n\t\t\t// Look for pingback header\n\t\t\tforeach ($req['headers'] as $header)\n\t\t\t\tif (preg_match('/^X-Pingback:\\h*(.+)/',$header,$href)) {\n\t\t\t\t\t$found=$href[1];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\tif (!$found &&\n\t\t\t\t// Scan page for pingback link tag\n\t\t\t\tpreg_match('/<link\\h+(.+?)\\h*\\/?>/i',$req['body'],$parts) &&\n\t\t\t\tpreg_match('/rel\\h*=\\h*\"pingback\"/i',$parts[1]) &&\n\t\t\t\tpreg_match('/href\\h*=\\h*\"\\h*(.+?)\\h*\"/i',$parts[1],$href))\n\t\t\t\t$found=$href[1];\n\t\t}\n\t\treturn $found;\n\t}\n\n\t/**\n\t*\tLoad local page contents, parse HTML anchor tags, find permalinks,\n\t*\tand send XML-RPC calls to corresponding pingback servers\n\t*\t@return NULL\n\t*\t@param $source string\n\t**/\n\tfunction inspect($source) {\n\t\t$fw=\\Base::instance();\n\t\t$web=\\Web::instance();\n\t\t$parts=parse_url($source);\n\t\tif (empty($parts['scheme']) || empty($parts['host']) ||\n\t\t\t$parts['host']==$fw->HOST) {\n\t\t\t$req=$web->request($source);\n\t\t\t$doc=new \\DOMDocument('1.0',$fw->ENCODING);\n\t\t\t$doc->strictErrorChecking=FALSE;\n\t\t\t$doc->recover=TRUE;\n\t\t\tif (@$doc->loadhtml($req['body'])) {\n\t\t\t\t// Parse anchor tags\n\t\t\t\t$links=$doc->getelementsbytagname('a');\n\t\t\t\tforeach ($links as $link) {\n\t\t\t\t\t$permalink=$link->getattribute('href');\n\t\t\t\t\t// Find pingback-enabled resources\n\t\t\t\t\tif ($permalink && $found=$this->enabled($permalink)) {\n\t\t\t\t\t\t$req=$web->request($found,\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t'method'=>'POST',\n\t\t\t\t\t\t\t\t'header'=>'Content-Type: application/xml',\n\t\t\t\t\t\t\t\t'content'=>xmlrpc_encode_request(\n\t\t\t\t\t\t\t\t\t'pingback.ping',\n\t\t\t\t\t\t\t\t\t[$source,$permalink],\n\t\t\t\t\t\t\t\t\t['encoding'=>$fw->ENCODING]\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif ($req['body'])\n\t\t\t\t\t\t\t$this->log.=date('r').' '.\n\t\t\t\t\t\t\t\t$permalink.' [permalink:'.$found.']'.PHP_EOL.\n\t\t\t\t\t\t\t\t$req['body'].PHP_EOL;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tunset($doc);\n\t\t}\n\t}\n\n\t/**\n\t*\tReceive ping, check if local page is pingback-enabled, verify\n\t*\tsource contents, and return XML-RPC response\n\t*\t@return string\n\t*\t@param $func callback\n\t*\t@param $path string\n\t**/\n\tfunction listen($func,$path=NULL) {\n\t\t$fw=\\Base::instance();\n\t\tif (PHP_SAPI!='cli') {\n\t\t\theader('X-Powered-By: '.$fw->PACKAGE);\n\t\t\theader('Content-Type: application/xml; '.\n\t\t\t\t'charset='.$charset=$fw->ENCODING);\n\t\t}\n\t\tif (!$path)\n\t\t\t$path=$fw->BASE;\n\t\t$web=\\Web::instance();\n\t\t$args=xmlrpc_decode_request($fw->BODY,$method,$charset);\n\t\t$options=['encoding'=>$charset];\n\t\tif ($method=='pingback.ping' && isset($args[0],$args[1])) {\n\t\t\tlist($source,$permalink)=$args;\n\t\t\t$doc=new \\DOMDocument('1.0',$fw->ENCODING);\n\t\t\t// Check local page if pingback-enabled\n\t\t\t$parts=parse_url($permalink);\n\t\t\tif ((empty($parts['scheme']) ||\n\t\t\t\t$parts['host']==$fw->HOST) &&\n\t\t\t\tpreg_match('/^'.preg_quote($path,'/').'/'.\n\t\t\t\t\t($fw->CASELESS?'i':''),$parts['path']) &&\n\t\t\t\t$this->enabled($permalink)) {\n\t\t\t\t// Check source\n\t\t\t\t$parts=parse_url($source);\n\t\t\t\tif ((empty($parts['scheme']) ||\n\t\t\t\t\t$parts['host']==$fw->HOST) &&\n\t\t\t\t\t($req=$web->request($source)) &&\n\t\t\t\t\t$doc->loadhtml($req['body'])) {\n\t\t\t\t\t$links=$doc->getelementsbytagname('a');\n\t\t\t\t\tforeach ($links as $link) {\n\t\t\t\t\t\tif ($link->getattribute('href')==$permalink) {\n\t\t\t\t\t\t\tcall_user_func_array($func,[$source,$req['body']]);\n\t\t\t\t\t\t\t// Success\n\t\t\t\t\t\t\tdie(xmlrpc_encode_request(NULL,$source,$options));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// No link to local page\n\t\t\t\t\tdie(xmlrpc_encode_request(NULL,0x11,$options));\n\t\t\t\t}\n\t\t\t\t// Source failure\n\t\t\t\tdie(xmlrpc_encode_request(NULL,0x10,$options));\n\t\t\t}\n\t\t\t// Doesn't exist (or not pingback-enabled)\n\t\t\tdie(xmlrpc_encode_request(NULL,0x21,$options));\n\t\t}\n\t\t// Access denied\n\t\tdie(xmlrpc_encode_request(NULL,0x31,$options));\n\t}\n\n\t/**\n\t*\tReturn transaction history\n\t*\t@return string\n\t**/\n\tfunction log() {\n\t\treturn $this->log;\n\t}\n\n\t/**\n\t*\tInstantiate class\n\t*\t@return object\n\t**/\n\tfunction __construct() {\n\t\t// Suppress errors caused by invalid HTML structures\n\t\tlibxml_use_internal_errors(TRUE);\n\t}\n\n}\n"
  },
  {
    "path": "web.php",
    "content": "<?php\n\n/*\n\n\tCopyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.\n\n\tThis file is part of the Fat-Free Framework (http://fatfreeframework.com).\n\n\tThis is free software: you can redistribute it and/or modify it under the\n\tterms of the GNU General Public License as published by the Free Software\n\tFoundation, either version 3 of the License, or later.\n\n\tFat-Free Framework is distributed in the hope that it will be useful,\n\tbut WITHOUT ANY WARRANTY; without even the implied warranty of\n\tMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n\tGeneral Public License for more details.\n\n\tYou should have received a copy of the GNU General Public License along\n\twith Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n//! Wrapper for various HTTP utilities\nclass Web extends Prefab {\n\n\t//@{ Error messages\n\tconst\n\t\tE_Request='No suitable HTTP request engine found';\n\t//@}\n\n\tprotected\n\t\t//! HTTP request engine\n\t\t$wrapper;\n\n\t/**\n\t*\tDetect MIME type using file extension or file inspection\n\t*\t@return string\n\t*\t@param $file string\n\t*\t@param $inspect bool\n\t**/\n\tfunction mime($file, $inspect=FALSE) {\n\t\tif ($inspect) {\n\t\t\tif (is_file($file) && is_readable($file)) {\n\t\t\t\t// physical files\n\t\t\t\tif (extension_loaded('fileinfo'))\n\t\t\t\t\t$mime=mime_content_type($file);\n\t\t\t\telseif (preg_match('/Darwin/i',PHP_OS))\n\t\t\t\t\t$mime=trim(exec('file -bI '.escapeshellarg($file)));\n\t\t\t\telseif (!preg_match('/^win/i',PHP_OS))\n\t\t\t\t\t$mime=trim(exec('file -bi '.escapeshellarg($file)));\n\t\t\t\tif (isset($mime) && !empty($mime)){\n\t\t\t\t\t// cut charset information if any\n\t\t\t\t\t$exp=explode(';',$mime,2);\n\t\t\t\t\t$mime=$exp[0];\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// remote and stream files\n\t\t\t\tif (ini_get('allow_url_fopen') && ($fhandle=fopen($file,'rb'))) {\n\t\t\t\t\t// only get head bytes instead of whole file\n\t\t\t\t\t$bytes=fread($fhandle,20);\n\t\t\t\t\tfclose($fhandle);\n\t\t\t\t}\n\t\t\t\telseif (($response=$this->request($file,['method' => 'HEAD']))\n\t\t\t\t\t&& preg_grep('/HTTP\\/[\\d.]{1,3} 200/',$response['headers'])\n\t\t\t\t\t&& ($type = preg_grep('/^Content-Type:/i',$response['headers']))) {\n\t\t\t\t\t// get mime type directly from response header\n\t\t\t\t\treturn preg_replace('/^Content-Type:\\s*/i','',array_pop($type));\n\t\t\t\t}\n\t\t\t\telse // load whole file\n\t\t\t\t\t$bytes=file_get_contents($file);\n\t\t\t\tif (extension_loaded('fileinfo')) {\n\t\t\t\t\t// get mime from fileinfo\n\t\t\t\t\t$finfo=finfo_open(FILEINFO_MIME_TYPE);\n\t\t\t\t\t$mime=finfo_buffer($finfo,$bytes);\n\t\t\t\t}\n\t\t\t\telseif ($bytes) {\n\t\t\t\t\t// magic number header fallback\n\t\t\t\t\t$map=[\n\t\t\t\t\t\t'\\x64\\x6E\\x73\\x2E'=>'audio/basic',\n\t\t\t\t\t\t'\\x52\\x49\\x46\\x46.{4}\\x41\\x56\\x49\\x20\\x4C\\x49\\x53\\x54'=>'video/avi',\n\t\t\t\t\t\t'\\x42\\x4d'=>'image/bmp',\n\t\t\t\t\t\t'\\x42\\x5A\\x68'=>'application/x-bzip2',\n\t\t\t\t\t\t'\\x07\\x64\\x74\\x32\\x64\\x64\\x74\\x64'=>'application/xml-dtd',\n\t\t\t\t\t\t'\\xD0\\xCF\\x11\\xE0\\xA1\\xB1\\x1A\\xE1'=>'application/msword',\n\t\t\t\t\t\t'\\x50\\x4B\\x03\\x04\\x14\\x00\\x06\\x00'=>'application/msword',\n\t\t\t\t\t\t'\\x0D\\x44\\x4F\\x43'=>'application/msword',\n\t\t\t\t\t\t'GIF\\d+a'=>'image/gif',\n\t\t\t\t\t\t'\\x1F\\x8B'=>'application/x-gzip',\n\t\t\t\t\t\t'\\xff\\xd8\\xff'=>'image/jpeg',\n\t\t\t\t\t\t'\\x49\\x46\\x00'=>'image/jpeg',\n\t\t\t\t\t\t'\\xFF\\xFB'=>'audio/mpeg',\n\t\t\t\t\t\t'\\x49\\x44\\x33'=>'audio/mpeg',\n\t\t\t\t\t\t'\\x00\\x00\\x01\\xBA'=>'video/mpeg',\n\t\t\t\t\t\t'\\x4F\\x67\\x67\\x53\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'=>'audio/vorbis',\n\t\t\t\t\t\t'\\x25\\x50\\x44\\x46'=>'application/pdf',\n\t\t\t\t\t\t'\\x89PNG\\x0d\\x0a'=>'image/png',\n\t\t\t\t\t\t'.{4}\\x6D\\x6F\\x6F\\x76\\x'=>'video/quicktime',\n\t\t\t\t\t\t'\\x53\\x49\\x54\\x21\\x00'=>'application/x-stuffit',\n\t\t\t\t\t\t'\\x43\\x57\\x53'=>'application/x-shockwave-flash',\n\t\t\t\t\t\t'\\x1F\\x8B\\x08'=>'application/x-tar',\n\t\t\t\t\t\t'\\x49\\x20\\x49'=>'image/tiff',\n\t\t\t\t\t\t'\\x52\\x49\\x46\\x46.{4}\\x57\\x41\\x56\\x45\\x66\\x6D\\x74\\x20'=>'audio/wav',\n\t\t\t\t\t\t'\\xFD\\xFF\\xFF\\xFF\\x20\\x00\\x00\\x00'=>'application/vnd.ms-excel',\n\t\t\t\t\t\t'\\x50\\x4B\\x03\\x04'=>'application/x-zip-compressed',\n\t\t\t\t\t\t'[ -~]+$'=>'text/plain',\n\t\t\t\t\t];\n\t\t\t\t\tforeach ($map as $key=>$val)\n\t\t\t\t\t\tif (preg_match('/^'.$key.'/',substr($bytes,0,128)))\n\t\t\t\t\t\t\treturn $val;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (isset($mime) && !empty($mime))\n\t\t\t\treturn $mime;\n\t\t\t// Fallback to file extension-based check if no mime was found yet\n\t\t}\n\t\tif (preg_match('/\\w+$/',$file,$ext)) {\n\t\t\t$map=[\n\t\t\t\t'au'=>'audio/basic',\n\t\t\t\t'avi'=>'video/avi',\n\t\t\t\t'bmp'=>'image/bmp',\n\t\t\t\t'bz2'=>'application/x-bzip2',\n\t\t\t\t'css'=>'text/css',\n\t\t\t\t'dtd'=>'application/xml-dtd',\n\t\t\t\t'doc'=>'application/msword',\n\t\t\t\t'gif'=>'image/gif',\n\t\t\t\t'gz'=>'application/x-gzip',\n\t\t\t\t'hqx'=>'application/mac-binhex40',\n\t\t\t\t'html?'=>'text/html',\n\t\t\t\t'jar'=>'application/java-archive',\n\t\t\t\t'jpe?g|jfif?'=>'image/jpeg',\n\t\t\t\t'js'=>'application/x-javascript',\n\t\t\t\t'midi'=>'audio/x-midi',\n\t\t\t\t'mp3'=>'audio/mpeg',\n\t\t\t\t'mpe?g'=>'video/mpeg',\n\t\t\t\t'ogg'=>'audio/vorbis',\n\t\t\t\t'pdf'=>'application/pdf',\n\t\t\t\t'png'=>'image/png',\n\t\t\t\t'ppt'=>'application/vnd.ms-powerpoint',\n\t\t\t\t'ps'=>'application/postscript',\n\t\t\t\t'qt'=>'video/quicktime',\n\t\t\t\t'ram?'=>'audio/x-pn-realaudio',\n\t\t\t\t'rdf'=>'application/rdf',\n\t\t\t\t'rtf'=>'application/rtf',\n\t\t\t\t'sgml?'=>'text/sgml',\n\t\t\t\t'sit'=>'application/x-stuffit',\n\t\t\t\t'svg'=>'image/svg+xml',\n\t\t\t\t'swf'=>'application/x-shockwave-flash',\n\t\t\t\t'tgz'=>'application/x-tar',\n\t\t\t\t'tiff'=>'image/tiff',\n\t\t\t\t'txt'=>'text/plain',\n\t\t\t\t'wav'=>'audio/wav',\n\t\t\t\t'xls'=>'application/vnd.ms-excel',\n\t\t\t\t'xml'=>'application/xml',\n\t\t\t\t'zip'=>'application/x-zip-compressed'\n\t\t\t];\n\t\t\tforeach ($map as $key=>$val)\n\t\t\t\tif (preg_match('/'.$key.'/',strtolower($ext[0])))\n\t\t\t\t\treturn $val;\n\t\t}\n\t\treturn 'application/octet-stream';\n\t}\n\n\t/**\n\t*\tReturn the MIME types stated in the HTTP Accept header as an array;\n\t*\tIf a list of MIME types is specified, return the best match; or\n\t*\tFALSE if none found\n\t*\t@return array|string|FALSE\n\t*\t@param $list string|array\n\t**/\n\tfunction acceptable($list=NULL) {\n\t\t$accept=[];\n\t\tforeach (explode(',',str_replace(' ','',@$_SERVER['HTTP_ACCEPT']))\n\t\t\tas $mime)\n\t\t\tif (preg_match('/(.+?)(?:;q=([\\d\\.]+)|$)/',$mime,$parts))\n\t\t\t\t$accept[$parts[1]]=isset($parts[2])?$parts[2]:1;\n\t\tif (!$accept)\n\t\t\t$accept['*/*']=1;\n\t\telse {\n\t\t\tkrsort($accept);\n\t\t\tarsort($accept);\n\t\t}\n\t\tif ($list) {\n\t\t\tif (is_string($list))\n\t\t\t\t$list=explode(',',$list);\n\t\t\tforeach ($accept as $mime=>$q)\n\t\t\t\tif ($q && $out=preg_grep('/'.\n\t\t\t\t\tstr_replace('\\*','.*',preg_quote($mime,'/')).'/',$list))\n\t\t\t\t\treturn current($out);\n\t\t\treturn FALSE;\n\t\t}\n\t\treturn $accept;\n\t}\n\n\t/**\n\t*\tTransmit file to HTTP client; Return file size if successful,\n\t*\tFALSE otherwise\n\t*\t@return int|FALSE\n\t*\t@param $file string\n\t*\t@param $mime string\n\t*\t@param $kbps int\n\t*\t@param $force bool\n\t*\t@param $name string\n\t*\t@param $flush bool\n\t**/\n\tfunction send($file,$mime=NULL,$kbps=0,$force=TRUE,$name=NULL,$flush=TRUE) {\n\t\tif (!is_file($file))\n\t\t\treturn FALSE;\n\t\t$size=filesize($file);\n\t\tif (PHP_SAPI!='cli') {\n\t\t\theader('Content-Type: '.($mime?:$this->mime($file)));\n\t\t\tif ($force)\n\t\t\t\theader('Content-Disposition: attachment; '.\n\t\t\t\t\t'filename=\"'.($name!==NULL?$name:basename($file)).'\"');\n\t\t\theader('Accept-Ranges: bytes');\n\t\t\theader('Content-Length: '.$size);\n\t\t\theader('X-Powered-By: '.Base::instance()->PACKAGE);\n\t\t}\n\t\tif (!$kbps && $flush) {\n\t\t\twhile (ob_get_level())\n\t\t\t\tob_end_clean();\n\t\t\treadfile($file);\n\t\t}\n\t\telse {\n\t\t\t$ctr=0;\n\t\t\t$handle=fopen($file,'rb');\n\t\t\t$start=microtime(TRUE);\n\t\t\twhile (!feof($handle) &&\n\t\t\t\t($info=stream_get_meta_data($handle)) &&\n\t\t\t\t!$info['timed_out'] && !connection_aborted()) {\n\t\t\t\tif ($kbps) {\n\t\t\t\t\t// Throttle output\n\t\t\t\t\t++$ctr;\n\t\t\t\t\tif ($ctr/$kbps>$elapsed=microtime(TRUE)-$start)\n\t\t\t\t\t\tusleep(round(1e6*($ctr/$kbps-$elapsed)));\n\t\t\t\t}\n\t\t\t\t// Send 1KiB and reset timer\n\t\t\t\techo fread($handle,1024);\n\t\t\t\tif ($flush) {\n\t\t\t\t\tob_flush();\n\t\t\t\t\tflush();\n\t\t\t\t}\n\t\t\t}\n\t\t\tfclose($handle);\n\t\t}\n\t\treturn $size;\n\t}\n\n\t/**\n\t*\tReceive file(s) from HTTP client\n\t*\t@return array|bool\n\t*\t@param $func callback\n\t*\t@param $overwrite bool\n\t*\t@param $slug callback|bool\n\t**/\n\tfunction receive($func=NULL,$overwrite=FALSE,$slug=TRUE) {\n\t\t$fw=Base::instance();\n\t\t$dir=$fw->UPLOADS;\n\t\tif (!is_dir($dir))\n\t\t\tmkdir($dir,Base::MODE,TRUE);\n\t\tif ($fw->VERB=='PUT') {\n\t\t\t$tmp=$fw->TEMP.$fw->SEED.'.'.$fw->hash(uniqid());\n\t\t\tif (!$fw->RAW)\n\t\t\t\t$fw->write($tmp,$fw->BODY);\n\t\t\telse {\n\t\t\t\t$src=@fopen('php://input','r');\n\t\t\t\t$dst=@fopen($tmp,'w');\n\t\t\t\tif (!$src || !$dst)\n\t\t\t\t\treturn FALSE;\n\t\t\t\twhile (!feof($src) &&\n\t\t\t\t\t($info=stream_get_meta_data($src)) &&\n\t\t\t\t\t!$info['timed_out'] && $str=fgets($src,4096))\n\t\t\t\t\tfputs($dst,$str,strlen($str));\n\t\t\t\tfclose($dst);\n\t\t\t\tfclose($src);\n\t\t\t}\n\t\t\t$base=basename($fw->URI);\n\t\t\t$file=[\n\t\t\t\t'name'=>$dir.\n\t\t\t\t\t($slug && preg_match('/(.+?)(\\.\\w+)?$/',$base,$parts)?\n\t\t\t\t\t\t(is_callable($slug)?\n\t\t\t\t\t\t\t$slug($base):\n\t\t\t\t\t\t\t($this->slug($parts[1]).\n\t\t\t\t\t\t\t\t(isset($parts[2])?$parts[2]:''))):\n\t\t\t\t\t\t$base),\n\t\t\t\t'tmp_name'=>$tmp,\n\t\t\t\t'type'=>$this->mime($base),\n\t\t\t\t'size'=>filesize($tmp)\n\t\t\t];\n\t\t\treturn (!file_exists($file['name']) || $overwrite) &&\n\t\t\t\t(!$func || $fw->call($func,[$file])!==FALSE) &&\n\t\t\t\trename($tmp,$file['name']);\n\t\t}\n\t\t$fetch=function($arr) use(&$fetch) {\n\t\t\tif (!is_array($arr))\n\t\t\t\treturn [$arr];\n\t\t\t$data=[];\n\t\t\tforeach($arr as $k=>$sub)\n\t\t\t\t$data=array_merge($data,$fetch($sub));\n\t\t\treturn $data;\n\t\t};\n\t\t$out=[];\n\t\tforeach ($_FILES as $name=>$item) {\n\t\t\t$files=[];\n\t\t\tforeach ($item as $k=>$mix)\n\t\t\t\tforeach ($fetch($mix) as $i=>$val)\n\t\t\t\t\t$files[$i][$k]=$val;\n\t\t\tforeach ($files as $file) {\n\t\t\t\tif (empty($file['name']))\n\t\t\t\t\tcontinue;\n\t\t\t\t$base=basename($file['name']);\n\t\t\t\t$file['name']=$dir.\n\t\t\t\t\t($slug && preg_match('/(.+?)(\\.\\w+)?$/',$base,$parts)?\n\t\t\t\t\t\t(is_callable($slug)?\n\t\t\t\t\t\t\t$slug($base,$name):\n\t\t\t\t\t\t\t($this->slug($parts[1]).\n\t\t\t\t\t\t\t\t(isset($parts[2])?$parts[2]:''))):\n\t\t\t\t\t\t$base);\n\t\t\t\t$out[$file['name']]=!$file['error'] &&\n\t\t\t\t\t(!file_exists($file['name']) || $overwrite) &&\n\t\t\t\t\t(!$func || $fw->call($func,[$file,$name])!==FALSE) &&\n\t\t\t\t\tmove_uploaded_file($file['tmp_name'],$file['name']);\n\t\t\t}\n\t\t}\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tReturn upload progress in bytes, FALSE on failure\n\t*\t@return int|FALSE\n\t*\t@param $id string\n\t**/\n\tfunction progress($id) {\n\t\t// ID returned by session.upload_progress.name\n\t\treturn ini_get('session.upload_progress.enabled') &&\n\t\t\tisset($_SESSION[$id]['bytes_processed'])?\n\t\t\t\t$_SESSION[$id]['bytes_processed']:FALSE;\n\t}\n\n\t/**\n\t*\tHTTP request via cURL\n\t*\t@return array\n\t*\t@param $url string\n\t*\t@param $options array\n\t**/\n\tprotected function _curl($url,$options) {\n\t\t$curl=curl_init($url);\n\t\tif (!$open_basedir=ini_get('open_basedir'))\n\t\t\tcurl_setopt($curl,CURLOPT_FOLLOWLOCATION,\n\t\t\t\t$options['follow_location']);\n\t\tcurl_setopt($curl,CURLOPT_MAXREDIRS,\n\t\t\t$options['max_redirects']);\n\t\tcurl_setopt($curl,CURLOPT_PROTOCOLS,CURLPROTO_HTTP|CURLPROTO_HTTPS);\n\t\tcurl_setopt($curl,CURLOPT_REDIR_PROTOCOLS,CURLPROTO_HTTP|CURLPROTO_HTTPS);\n\t\tcurl_setopt($curl,CURLOPT_CUSTOMREQUEST,$options['method']);\n\t\tif (isset($options['header']))\n\t\t\tcurl_setopt($curl,CURLOPT_HTTPHEADER,$options['header']);\n\t\tif (isset($options['content']))\n\t\t\tcurl_setopt($curl,CURLOPT_POSTFIELDS,$options['content']);\n\t\tif (isset($options['proxy']))\n\t\t\tcurl_setopt($curl,CURLOPT_PROXY,$options['proxy']);\n\t\tcurl_setopt($curl,CURLOPT_ENCODING,isset($options['encoding'])\n\t\t\t? $options['encoding'] : 'gzip,deflate');\n\t\t$timeout=isset($options['timeout'])?\n\t\t\t$options['timeout']:\n\t\t\tini_get('default_socket_timeout');\n\t\tcurl_setopt($curl,CURLOPT_CONNECTTIMEOUT,$timeout);\n\t\tcurl_setopt($curl,CURLOPT_TIMEOUT,$timeout);\n\t\t$headers=[];\n\t\tcurl_setopt($curl,CURLOPT_HEADERFUNCTION,\n\t\t\t// Callback for response headers\n\t\t\tfunction($curl,$line) use(&$headers) {\n\t\t\t\tif ($trim=trim($line))\n\t\t\t\t\t$headers[]=$trim;\n\t\t\t\treturn strlen($line);\n\t\t\t}\n\t\t);\n\t\tcurl_setopt($curl,CURLOPT_SSL_VERIFYHOST,2);\n\t\tcurl_setopt($curl,CURLOPT_SSL_VERIFYPEER,FALSE);\n\t\tob_start();\n\t\tcurl_exec($curl);\n\t\t$err=curl_error($curl);\n\t\tif (version_compare(PHP_VERSION, '8.5.0')<0)\n\t\t\t// TODO: remove this when php7 support is dropped\n\t\t\tcurl_close($curl);\n\t\t$body=ob_get_clean();\n\t\tif (!$err &&\n\t\t\t$options['follow_location'] && $open_basedir &&\n\t\t\tpreg_grep('/HTTP\\/[\\d.]{1,3} 3\\d{2}/',$headers) &&\n\t\t\tpreg_match('/^Location: (.+)$/m',implode(PHP_EOL,$headers),$loc)) {\n\t\t\t--$options['max_redirects'];\n\t\t\tif($loc[1][0] == '/') {\n\t\t\t\t$parts=parse_url($url);\n\t\t\t\t$loc[1]=$parts['scheme'].'://'.$parts['host'].\n\t\t\t\t\t((isset($parts['port']) && !in_array($parts['port'],[80,443]))\n\t\t\t\t\t\t?':'.$parts['port']:'').$loc[1];\n\t\t\t}\n\t\t\treturn $this->request($loc[1],$options);\n\t\t}\n\t\treturn [\n\t\t\t'body'=>$body,\n\t\t\t'headers'=>$headers,\n\t\t\t'engine'=>'cURL',\n\t\t\t'cached'=>FALSE,\n\t\t\t'error'=>$err\n\t\t];\n\t}\n\n\t/**\n\t*\tHTTP request via PHP stream wrapper\n\t*\t@return array\n\t*\t@param $url string\n\t*\t@param $options array\n\t**/\n\tprotected function _stream($url,$options) {\n\t\t$eol=\"\\r\\n\";\n\t\tif (isset($options['proxy'])) {\n\t\t\t$options['proxy']=preg_replace('/https?/i','tcp',$options['proxy']);\n\t\t\t$options['request_fulluri']=true;\n\t\t\tif (preg_match('/socks4?/i',$options['proxy']))\n\t\t\t\treturn $this->_socket($url,$options);\n\t\t}\n\t\t$options['header']=implode($eol,$options['header']);\n\t\t$body=@file_get_contents($url,FALSE,\n\t\t\tstream_context_create(['http'=>$options]));\n\t\tif (PHP_VERSION_ID >= 80500)\n\t\t\t$headers=http_get_last_response_headers() ?: [];\n\t\telse {\n\t\t\t$locals=get_defined_vars();\n\t\t\t$headers=isset($locals['http_response_header']) &&\n\t\t\t\tis_array($locals['http_response_header'])?\n\t\t\t\t$locals['http_response_header']:[];\n\t\t}\n\t\t$err='';\n\t\tif (is_string($body)) {\n\t\t\t$match=NULL;\n\t\t\tforeach ($headers as $header)\n\t\t\t\tif (preg_match('/Content-Encoding: (.+)/i',$header,$match))\n\t\t\t\t\tbreak;\n\t\t\tif ($match)\n\t\t\t\tswitch ($match[1]) {\n\t\t\t\t\tcase 'gzip':\n\t\t\t\t\t\t$body=gzdecode($body);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'deflate':\n\t\t\t\t\t\t$body=gzuncompress($body);\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t$tmp=error_get_last();\n\t\t\t$err=$tmp['message'];\n\t\t}\n\t\treturn [\n\t\t\t'body'=>$body,\n\t\t\t'headers'=>$headers,\n\t\t\t'engine'=>'stream',\n\t\t\t'cached'=>FALSE,\n\t\t\t'error'=>$err\n\t\t];\n\t}\n\n\t/**\n\t*\tHTTP request via low-level TCP/IP socket\n\t*\t@return array\n\t*\t@param $url string\n\t*\t@param $options array\n\t**/\n\tprotected function _socket($url,$options) {\n\t\t$eol=\"\\r\\n\";\n\t\t$headers=[];\n\t\t$body='';\n\t\t$parts=parse_url($url);\n\t\t$hostname=$parts['host'];\n\t\t$proxy=false;\n\t\tif ($parts['scheme']=='https')\n\t\t\t$parts['host']='ssl://'.$parts['host'];\n\t\tif (empty($parts['port']))\n\t\t\t$parts['port']=$parts['scheme']=='https'?443:80;\n\t\tif (empty($parts['path']))\n\t\t\t$parts['path']='/';\n\t\tif (empty($parts['query']))\n\t\t\t$parts['query']='';\n\t\tif (isset($options['proxy'])) {\n\t\t\t$req=$url;\n\t\t\t$pp=parse_url($options['proxy']);\n\t\t\t$proxy=$pp['scheme'];\n\t\t\tif ($pp['scheme']=='https')\n\t\t\t\t$pp['host']='ssl://'.$pp['host'];\n\t\t\tif (empty($pp['port']))\n\t\t\t\t$pp['port']=$pp['scheme']=='https'?443:80;\n\t\t\t$socket=@fsockopen($pp['host'],$pp['port'],$code,$err);\n\t\t} else {\n\t\t\t$req=$parts['path'].($parts['query']?('?'.$parts['query']):'');\n\t\t\t$socket=@fsockopen($parts['host'],$parts['port'],$code,$err);\n\t\t}\n\t\tif ($socket) {\n\t\t\tstream_set_blocking($socket,TRUE);\n\t\t\tstream_set_timeout($socket,isset($options['timeout'])?\n\t\t\t\t$options['timeout']:ini_get('default_socket_timeout'));\n\t\t\tif ($proxy=='socks4') {\n\t\t\t\t// SOCKS4; http://en.wikipedia.org/wiki/SOCKS#Protocol\n\t\t\t\t$packet=\"\\x04\\x01\".pack(\"n\", $parts['port']).\n\t\t\t\t\tpack(\"H*\",dechex(ip2long(gethostbyname($hostname)))).\"\\0\";\n\t\t\t\tfputs($socket, $packet, strlen($packet));\n\t\t\t\t$response=fread($socket, 9);\n\t\t\t\tif (strlen($response)==8 && (ord($response[0])==0 || ord($response[0])==4)\n\t\t\t\t\t&& ord($response[1])==90) {\n\t\t\t\t\t$options['header'][]='Host: '.$hostname;\n\t\t\t\t} else\n\t\t\t\t\t$err='Socket Status '.ord($response[1]);\n\t\t\t}\n\t\t\tfputs($socket,$options['method'].' '.$req.' HTTP/1.0'.$eol);\n\t\t\tfputs($socket,implode($eol,$options['header']).$eol.$eol);\n\t\t\tif (isset($options['content']))\n\t\t\t\tfputs($socket,$options['content'].$eol);\n\t\t\t// Get response\n\t\t\t$content='';\n\t\t\twhile (!feof($socket) &&\n\t\t\t\t($info=stream_get_meta_data($socket)) &&\n\t\t\t\t!$info['timed_out'] && !connection_aborted() &&\n\t\t\t\t$str=fgets($socket,4096))\n\t\t\t\t$content.=$str;\n\t\t\tfclose($socket);\n\t\t\t$html=explode($eol.$eol,$content,2);\n\t\t\t$body=isset($html[1])?$html[1]:'';\n\t\t\t$headers=array_merge($headers,$current=explode($eol,$html[0]));\n\t\t\t$match=NULL;\n\t\t\tforeach ($current as $header)\n\t\t\t\tif (preg_match('/Content-Encoding: (.+)/i',$header,$match))\n\t\t\t\t\tbreak;\n\t\t\tif ($match)\n\t\t\t\tswitch ($match[1]) {\n\t\t\t\t\tcase 'gzip':\n\t\t\t\t\t\t$body=gzdecode($body);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'deflate':\n\t\t\t\t\t\t$body=gzuncompress($body);\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\tif ($options['follow_location'] &&\n\t\t\t\tpreg_grep('/HTTP\\/[\\d.]{1,3} 3\\d{2}/',$headers) &&\n\t\t\t\tpreg_match('/Location: (.+?)'.preg_quote($eol).'/',\n\t\t\t\t$html[0],$loc)) {\n\t\t\t\t--$options['max_redirects'];\n\t\t\t\treturn $this->request($loc[1],$options);\n\t\t\t}\n\t\t}\n\t\treturn [\n\t\t\t'body'=>$body,\n\t\t\t'headers'=>$headers,\n\t\t\t'engine'=>'socket',\n\t\t\t'cached'=>FALSE,\n\t\t\t'error'=>$err\n\t\t];\n\t}\n\n\t/**\n\t*\tSpecify the HTTP request engine to use; If not available,\n\t*\tfall back to an applicable substitute\n\t*\t@return string\n\t*\t@param $arg string\n\t**/\n\tfunction engine($arg='curl') {\n\t\t$arg=strtolower($arg);\n\t\t$flags=[\n\t\t\t'curl'=>extension_loaded('curl'),\n\t\t\t'stream'=>ini_get('allow_url_fopen'),\n\t\t\t'socket'=>function_exists('fsockopen')\n\t\t];\n\t\tif ($flags[$arg])\n\t\t\treturn $this->wrapper=$arg;\n\t\tforeach ($flags as $key=>$val)\n\t\t\tif ($val)\n\t\t\t\treturn $this->wrapper=$key;\n        throw new \\Exception(self::E_Request);\n\t}\n\n\t/**\n\t*\tReplace old headers with new elements\n\t*\t@return NULL\n\t*\t@param $old array\n\t*\t@param $new string|array\n\t**/\n\tfunction subst(array &$old,$new) {\n\t\tif (is_string($new))\n\t\t\t$new=[$new];\n\t\tforeach ($new as $hdr) {\n\t\t\t$old=preg_grep('/'.preg_quote(strstr($hdr,':',TRUE),'/').':.+/',\n\t\t\t\t$old,PREG_GREP_INVERT);\n\t\t\tarray_push($old,$hdr);\n\t\t}\n\t}\n\n\t/**\n\t*\tSubmit HTTP request; Use HTTP context options (described in\n\t*\thttp://www.php.net/manual/en/context.http.php) if specified;\n\t*\tCache the page as instructed by remote server\n\t*\t@return array|FALSE\n\t*\t@param $url string\n\t*\t@param $options array\n\t**/\n\tfunction request($url,?array $options=NULL) {\n\t\t$fw=Base::instance();\n\t\t$parts=parse_url($url);\n\t\tif (empty($parts['scheme'])) {\n\t\t\t// Local URL\n\t\t\t$url=$fw->SCHEME.'://'.$fw->HOST.\n\t\t\t\t(in_array($fw->PORT,[80,443])?'':(':'.$fw->PORT)).\n\t\t\t\t($url[0]!='/'?($fw->BASE.'/'):'').$url;\n\t\t\t$parts=parse_url($url);\n\t\t}\n\t\telseif (!preg_match('/https?/',$parts['scheme']))\n\t\t\treturn FALSE;\n\t\tif (!is_array($options))\n\t\t\t$options=[];\n\t\tif (empty($options['header']))\n\t\t\t$options['header']=[];\n\t\telseif (is_string($options['header']))\n\t\t\t$options['header']=[$options['header']];\n\t\tif (!$this->wrapper)\n\t\t\t$this->engine();\n\t\tif ($this->wrapper!='stream') {\n\t\t\t// PHP streams can't cope with redirects when Host header is set\n\t\t\t$this->subst($options['header'],'Host: '.$parts['host']);\n\t\t}\n\t\t$this->subst($options['header'],\n\t\t\t[\n\t\t\t\t'Accept-Encoding: '.(isset($options['encoding'])?\n\t\t\t\t\t$options['encoding']:'gzip,deflate'),\n\t\t\t\t'User-Agent: '.(isset($options['user_agent'])?\n\t\t\t\t\t$options['user_agent']:\n\t\t\t\t\t'Mozilla/5.0 (compatible; '.php_uname('s').')'),\n\t\t\t\t'Connection: close'\n\t\t\t]\n\t\t);\n\t\tif (isset($options['content']) && is_string($options['content'])) {\n\t\t\tif ($options['method']=='POST' &&\n\t\t\t\t!preg_grep('/^Content-Type:/i',$options['header']))\n\t\t\t\t$this->subst($options['header'],\n\t\t\t\t\t'Content-Type: application/x-www-form-urlencoded');\n\t\t\t$this->subst($options['header'],\n\t\t\t\t'Content-Length: '.strlen($options['content']));\n\t\t}\n\t\tif (isset($parts['user'],$parts['pass']))\n\t\t\t$this->subst($options['header'],\n\t\t\t\t'Authorization: Basic '.\n\t\t\t\t\tbase64_encode($parts['user'].':'.$parts['pass'])\n\t\t\t);\n\t\t$options+=[\n\t\t\t'method'=>'GET',\n\t\t\t'header'=>$options['header'],\n\t\t\t'follow_location'=>TRUE,\n\t\t\t'max_redirects'=>20,\n\t\t\t'ignore_errors'=>FALSE\n\t\t];\n\t\t$eol=\"\\r\\n\";\n\t\tif ($fw->CACHE &&\n\t\t\tpreg_match('/GET|HEAD/',$options['method'])) {\n\t\t\t$cache=Cache::instance();\n\t\t\tif ($cache->exists(\n\t\t\t\t$hash=$fw->hash($options['method'].' '.$url).'.url',$data)) {\n\t\t\t\tif (preg_match('/Last-Modified: (.+?)'.preg_quote($eol).'/',\n\t\t\t\t\timplode($eol,$data['headers']),$mod))\n\t\t\t\t\t$this->subst($options['header'],\n\t\t\t\t\t\t'If-Modified-Since: '.$mod[1]);\n\t\t\t}\n\t\t}\n\t\t$result=$this->{'_'.$this->wrapper}($url,$options);\n\t\tif ($result && isset($cache)) {\n\t\t\tif (preg_match('/HTTP\\/[\\d.]{1,3} 304/',\n\t\t\t\timplode($eol,$result['headers']))) {\n\t\t\t\t$result=$cache->get($hash);\n\t\t\t\t$result['cached']=TRUE;\n\t\t\t}\n\t\t\telseif (preg_match('/Cache-Control:(?:.*)max-age=(\\d+)(?:,?.*'.\n\t\t\t\tpreg_quote($eol).')/i',implode($eol,$result['headers']),$exp))\n\t\t\t\t$cache->set($hash,$result,$exp[1]);\n\t\t}\n\t\t$req=[$options['method'].' '.$url];\n\t\tforeach ($options['header'] as $header)\n\t\t\tarray_push($req,$header);\n\t\treturn array_merge(['request'=>$req],$result);\n\t}\n\n\t/**\n\t*\tStrip Javascript/CSS files of extraneous whitespaces and comments;\n\t*\tReturn combined output as a minified string\n\t*\t@return string\n\t*\t@param $files string|array\n\t*\t@param $mime string\n\t*\t@param $header bool\n\t*\t@param $path string\n\t**/\n\tfunction minify($files,$mime=NULL,$header=TRUE,$path=NULL) {\n\t\t$fw=Base::instance();\n\t\tif (is_string($files))\n\t\t\t$files=$fw->split($files);\n\t\tif (!$mime)\n\t\t\t$mime=$this->mime($files[0]);\n\t\tpreg_match('/\\w+$/',$files[0],$ext);\n\t\t$cache=Cache::instance();\n\t\t$dst='';\n\t\tif (!isset($path))\n\t\t\t$path=$fw->UI.';./';\n\t\tforeach (array_unique($fw->split($path,FALSE)) as $dir)\n\t\t\tforeach ($files as $i=>$file)\n\t\t\t\tif (is_file($save=$fw->fixslashes($dir.$file)) &&\n\t\t\t\t\tis_bool(strpos($save,'../')) &&\n\t\t\t\t\tpreg_match('/\\.(css|js)$/i',$file)) {\n\t\t\t\t\tunset($files[$i]);\n\t\t\t\t\tif ($fw->CACHE &&\n\t\t\t\t\t\t($cached=$cache->exists(\n\t\t\t\t\t\t\t$hash=$fw->hash($save).'.'.$ext[0],$data)) &&\n\t\t\t\t\t\t$cached[0]>filemtime($save))\n\t\t\t\t\t\t$dst.=$data;\n\t\t\t\t\telse {\n\t\t\t\t\t\t$data='';\n\t\t\t\t\t\t$src=$fw->read($save);\n\t\t\t\t\t\tfor ($ptr=0,$len=strlen($src);$ptr<$len;) {\n\t\t\t\t\t\t\tif (preg_match('/^@import\\h+url'.\n\t\t\t\t\t\t\t\t'\\(\\h*([\\'\"])((?!(?:https?:)?\\/\\/).+?)\\1\\h*\\)[^;]*;/',\n\t\t\t\t\t\t\t\tsubstr($src,$ptr),$parts)) {\n\t\t\t\t\t\t\t\t$path=dirname($file);\n\t\t\t\t\t\t\t\t$data.=$this->minify(\n\t\t\t\t\t\t\t\t\t($path?($path.'/'):'').$parts[2],\n\t\t\t\t\t\t\t\t\t$mime,$header\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t$ptr+=strlen($parts[0]);\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif ($ext[0]=='css'&&preg_match('/^url\\(([^\\'\"].*?[^\\'\"])\\)/i',\n\t\t\t\t\t\t\t\t\tsubstr($src,$ptr),$parts)) {\n\t\t\t\t\t\t\t\t$data.=$parts[0];\n\t\t\t\t\t\t\t\t$ptr+=strlen($parts[0]);\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif ($src[$ptr]=='/') {\n\t\t\t\t\t\t\t\tif ($src[$ptr+1]=='*') {\n\t\t\t\t\t\t\t\t\t// Multiline comment\n\t\t\t\t\t\t\t\t\t$str=strstr(\n\t\t\t\t\t\t\t\t\t\tsubstr($src,$ptr+2),'*/',TRUE);\n\t\t\t\t\t\t\t\t\t$ptr+=strlen($str)+4;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\telseif ($src[$ptr+1]=='/') {\n\t\t\t\t\t\t\t\t\t// Single-line comment\n\t\t\t\t\t\t\t\t\t$str=strstr(\n\t\t\t\t\t\t\t\t\t\tsubstr($src,$ptr+2),\"\\n\",TRUE);\n\t\t\t\t\t\t\t\t\t$ptr+=(empty($str))?\n\t\t\t\t\t\t\t\t\t\tstrlen(substr($src,$ptr)):strlen($str)+2;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\t\t// Presume it's a regex pattern\n\t\t\t\t\t\t\t\t\t$regex=TRUE;\n\t\t\t\t\t\t\t\t\t// Backtrack and validate\n\t\t\t\t\t\t\t\t\tfor ($ofs=$ptr;$ofs;--$ofs) {\n\t\t\t\t\t\t\t\t\t\t// Pattern should be preceded by\n\t\t\t\t\t\t\t\t\t\t// open parenthesis, colon,\n\t\t\t\t\t\t\t\t\t\t// object property or operator\n\t\t\t\t\t\t\t\t\t\tif (preg_match(\n\t\t\t\t\t\t\t\t\t\t\t'/(return|[(:=!+\\-*&|])$/',\n\t\t\t\t\t\t\t\t\t\t\tsubstr($src,0,$ofs))) {\n\t\t\t\t\t\t\t\t\t\t\t$data.='/';\n\t\t\t\t\t\t\t\t\t\t\t++$ptr;\n\t\t\t\t\t\t\t\t\t\t\twhile ($ptr<$len) {\n\t\t\t\t\t\t\t\t\t\t\t\t$data.=$src[$ptr];\n\t\t\t\t\t\t\t\t\t\t\t\t++$ptr;\n\t\t\t\t\t\t\t\t\t\t\t\tif ($src[$ptr-1]=='\\\\') {\n\t\t\t\t\t\t\t\t\t\t\t\t\t$data.=$src[$ptr];\n\t\t\t\t\t\t\t\t\t\t\t\t\t++$ptr;\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\telseif ($src[$ptr-1]=='/')\n\t\t\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\telseif (!ctype_space($src[$ofs-1])) {\n\t\t\t\t\t\t\t\t\t\t\t// Not a regex pattern\n\t\t\t\t\t\t\t\t\t\t\t$regex=FALSE;\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif (!$regex) {\n\t\t\t\t\t\t\t\t\t\t// Division operator\n\t\t\t\t\t\t\t\t\t\t$data.=$src[$ptr];\n\t\t\t\t\t\t\t\t\t\t++$ptr;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (in_array($src[$ptr],['\\'','\"','`'])) {\n\t\t\t\t\t\t\t\t$match=$src[$ptr];\n\t\t\t\t\t\t\t\t$data.=$match;\n\t\t\t\t\t\t\t\t++$ptr;\n\t\t\t\t\t\t\t\t// String literal\n\t\t\t\t\t\t\t\twhile ($ptr<$len) {\n\t\t\t\t\t\t\t\t\t$data.=$src[$ptr];\n\t\t\t\t\t\t\t\t\t++$ptr;\n\t\t\t\t\t\t\t\t\tif ($src[$ptr-1]=='\\\\') {\n\t\t\t\t\t\t\t\t\t\t$data.=$src[$ptr];\n\t\t\t\t\t\t\t\t\t\t++$ptr;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\telseif ($src[$ptr-1]==$match)\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (ctype_space($src[$ptr])) {\n\t\t\t\t\t\t\t\tif ($ptr+1<strlen($src) &&\n\t\t\t\t\t\t\t\t\tpreg_match('/[\\w'.($ext[0]=='css'?\n\t\t\t\t\t\t\t\t\t\t'#\\.%+\\-*()\\[\\]':'\\$').']{2}|'.\n\t\t\t\t\t\t\t\t\t\t'[+\\-]{2}/',\n\t\t\t\t\t\t\t\t\t\tsubstr($data,-1).$src[$ptr+1]) ||\n\t\t\t\t\t\t\t\t\t($ext[0]=='css' && $ptr+2<strlen($src) &&\n\t\t\t\t\t\t\t\t\tpreg_match('/:\\w/',substr($src,$ptr+1,2))))\n\t\t\t\t\t\t\t\t\t$data.=' ';\n\t\t\t\t\t\t\t\t++$ptr;\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t$data.=$src[$ptr];\n\t\t\t\t\t\t\t++$ptr;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ($ext[0]=='css')\n\t\t\t\t\t\t\t$data=str_replace(';}','}',$data);\n\t\t\t\t\t\tif ($fw->CACHE)\n\t\t\t\t\t\t\t$cache->set($hash,$data);\n\t\t\t\t\t\t$dst.=$data;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\tif (PHP_SAPI!='cli' && $header)\n\t\t\theader('Content-Type: '.$mime.'; charset='.$fw->ENCODING);\n\t\treturn $dst;\n\t}\n\n\t/**\n\t*\tRetrieve RSS feed and return as an array\n\t*\t@return array|FALSE\n\t*\t@param $url string\n\t*\t@param $max int\n\t*\t@param $tags string\n\t**/\n\tfunction rss($url,$max=10,$tags=NULL) {\n\t\tif (!$data=$this->request($url))\n\t\t\treturn FALSE;\n\t\t// Suppress errors caused by invalid XML structures\n\t\tlibxml_use_internal_errors(TRUE);\n\t\t$xml=simplexml_load_string($data['body'],\n\t\t\tNULL,LIBXML_NOBLANKS|LIBXML_NOERROR);\n\t\tif (!is_object($xml))\n\t\t\treturn FALSE;\n\t\t$out=[];\n\t\tif (isset($xml->channel)) {\n\t\t\t$out['source']=(string)$xml->channel->title;\n\t\t\t$max=min($max,count($xml->channel->item));\n\t\t\tfor ($i=0;$i<$max;++$i) {\n\t\t\t\t$item=$xml->channel->item[$i];\n\t\t\t\t$list=[''=>NULL]+$item->getnamespaces(TRUE);\n\t\t\t\t$fields=[];\n\t\t\t\tforeach ($list as $ns=>$uri)\n\t\t\t\t\tforeach ($item->children($uri) as $key=>$val)\n\t\t\t\t\t\t$fields[$ns.($ns?':':'').$key]=(string)$val;\n\t\t\t\t$out['feed'][]=$fields;\n\t\t\t}\n\t\t}\n\t\telse\n\t\t\treturn FALSE;\n\t\tBase::instance()->scrub($out,$tags);\n\t\treturn $out;\n\t}\n\n\t/**\n\t*\tRetrieve information from whois server\n\t*\t@return string|FALSE\n\t*\t@param $addr string\n\t*\t@param $server string\n\t**/\n\tfunction whois($addr,$server='whois.internic.net') {\n\t\t$socket=@fsockopen($server,43,$errno,$errstr);\n\t\tif (!$socket)\n\t\t\t// Can't establish connection\n\t\t\treturn FALSE;\n\t\t// Set connection timeout parameters\n\t\tstream_set_blocking($socket,FALSE);\n\t\tstream_set_timeout($socket,ini_get('default_socket_timeout'));\n\t\t// Send request\n\t\tfputs($socket,$addr.\"\\r\\n\");\n\t\t$info=stream_get_meta_data($socket);\n\t\t// Get response\n\t\t$response='';\n\t\twhile (!feof($socket) && !$info['timed_out']) {\n\t\t\t$response.=fgets($socket,4096); // MDFK97\n\t\t\t$info=stream_get_meta_data($socket);\n\t\t}\n\t\tfclose($socket);\n\t\treturn $info['timed_out']?FALSE:trim($response);\n\t}\n\n\t/**\n\t*\tReturn preset diacritics translation table\n\t*\t@return array\n\t**/\n\tfunction diacritics() {\n\t\treturn [\n\t\t\t'Ǎ'=>'A','А'=>'A','Ā'=>'A','Ă'=>'A','Ą'=>'A','Å'=>'A',\n\t\t\t'Ǻ'=>'A','Ä'=>'Ae','Á'=>'A','À'=>'A','Ã'=>'A','Â'=>'A',\n\t\t\t'Æ'=>'AE','Ǽ'=>'AE','Б'=>'B','Ç'=>'C','Ć'=>'C','Ĉ'=>'C',\n\t\t\t'Č'=>'C','Ċ'=>'C','Ц'=>'C','Ч'=>'Ch','Ð'=>'Dj','Đ'=>'Dj',\n\t\t\t'Ď'=>'Dj','Д'=>'Dj','É'=>'E','Ę'=>'E','Ё'=>'E','Ė'=>'E',\n\t\t\t'Ê'=>'E','Ě'=>'E','Ē'=>'E','È'=>'E','Е'=>'E','Э'=>'E',\n\t\t\t'Ë'=>'E','Ĕ'=>'E','Ф'=>'F','Г'=>'G','Ģ'=>'G','Ġ'=>'G',\n\t\t\t'Ĝ'=>'G','Ğ'=>'G','Х'=>'H','Ĥ'=>'H','Ħ'=>'H','Ï'=>'I',\n\t\t\t'Ĭ'=>'I','İ'=>'I','Į'=>'I','Ī'=>'I','Í'=>'I','Ì'=>'I',\n\t\t\t'И'=>'I','Ǐ'=>'I','Ĩ'=>'I','Î'=>'I','Ĳ'=>'IJ','Ĵ'=>'J',\n\t\t\t'Й'=>'J','Я'=>'Ja','Ю'=>'Ju','К'=>'K','Ķ'=>'K','Ĺ'=>'L',\n\t\t\t'Л'=>'L','Ł'=>'L','Ŀ'=>'L','Ļ'=>'L','Ľ'=>'L','М'=>'M',\n\t\t\t'Н'=>'N','Ń'=>'N','Ñ'=>'N','Ņ'=>'N','Ň'=>'N','Ō'=>'O',\n\t\t\t'О'=>'O','Ǿ'=>'O','Ǒ'=>'O','Ơ'=>'O','Ŏ'=>'O','Ő'=>'O',\n\t\t\t'Ø'=>'O','Ö'=>'Oe','Õ'=>'O','Ó'=>'O','Ò'=>'O','Ô'=>'O',\n\t\t\t'Œ'=>'OE','П'=>'P','Ŗ'=>'R','Р'=>'R','Ř'=>'R','Ŕ'=>'R',\n\t\t\t'Ŝ'=>'S','Ş'=>'S','Š'=>'S','Ș'=>'S','Ś'=>'S','С'=>'S',\n\t\t\t'Ш'=>'Sh','Щ'=>'Shch','Ť'=>'T','Ŧ'=>'T','Ţ'=>'T','Ț'=>'T',\n\t\t\t'Т'=>'T','Ů'=>'U','Ű'=>'U','Ŭ'=>'U','Ũ'=>'U','Ų'=>'U',\n\t\t\t'Ū'=>'U','Ǜ'=>'U','Ǚ'=>'U','Ù'=>'U','Ú'=>'U','Ü'=>'Ue',\n\t\t\t'Ǘ'=>'U','Ǖ'=>'U','У'=>'U','Ư'=>'U','Ǔ'=>'U','Û'=>'U',\n\t\t\t'В'=>'V','Ŵ'=>'W','Ы'=>'Y','Ŷ'=>'Y','Ý'=>'Y','Ÿ'=>'Y',\n\t\t\t'Ź'=>'Z','З'=>'Z','Ż'=>'Z','Ž'=>'Z','Ж'=>'Zh','á'=>'a',\n\t\t\t'ă'=>'a','â'=>'a','à'=>'a','ā'=>'a','ǻ'=>'a','å'=>'a',\n\t\t\t'ä'=>'ae','ą'=>'a','ǎ'=>'a','ã'=>'a','а'=>'a','ª'=>'a',\n\t\t\t'æ'=>'ae','ǽ'=>'ae','б'=>'b','č'=>'c','ç'=>'c','ц'=>'c',\n\t\t\t'ċ'=>'c','ĉ'=>'c','ć'=>'c','ч'=>'ch','ð'=>'dj','ď'=>'dj',\n\t\t\t'д'=>'dj','đ'=>'dj','э'=>'e','é'=>'e','ё'=>'e','ë'=>'e',\n\t\t\t'ê'=>'e','е'=>'e','ĕ'=>'e','è'=>'e','ę'=>'e','ě'=>'e',\n\t\t\t'ė'=>'e','ē'=>'e','ƒ'=>'f','ф'=>'f','ġ'=>'g','ĝ'=>'g',\n\t\t\t'ğ'=>'g','г'=>'g','ģ'=>'g','х'=>'h','ĥ'=>'h','ħ'=>'h',\n\t\t\t'ǐ'=>'i','ĭ'=>'i','и'=>'i','ī'=>'i','ĩ'=>'i','į'=>'i',\n\t\t\t'ı'=>'i','ì'=>'i','î'=>'i','í'=>'i','ï'=>'i','ĳ'=>'ij',\n\t\t\t'ĵ'=>'j','й'=>'j','я'=>'ja','ю'=>'ju','ķ'=>'k','к'=>'k',\n\t\t\t'ľ'=>'l','ł'=>'l','ŀ'=>'l','ĺ'=>'l','ļ'=>'l','л'=>'l',\n\t\t\t'м'=>'m','ņ'=>'n','ñ'=>'n','ń'=>'n','н'=>'n','ň'=>'n',\n\t\t\t'ŉ'=>'n','ó'=>'o','ò'=>'o','ǒ'=>'o','ő'=>'o','о'=>'o',\n\t\t\t'ō'=>'o','º'=>'o','ơ'=>'o','ŏ'=>'o','ô'=>'o','ö'=>'oe',\n\t\t\t'õ'=>'o','ø'=>'o','ǿ'=>'o','œ'=>'oe','п'=>'p','р'=>'r',\n\t\t\t'ř'=>'r','ŕ'=>'r','ŗ'=>'r','ſ'=>'s','ŝ'=>'s','ș'=>'s',\n\t\t\t'š'=>'s','ś'=>'s','с'=>'s','ş'=>'s','ш'=>'sh','щ'=>'shch',\n\t\t\t'ß'=>'ss','ţ'=>'t','т'=>'t','ŧ'=>'t','ť'=>'t','ț'=>'t',\n\t\t\t'у'=>'u','ǘ'=>'u','ŭ'=>'u','û'=>'u','ú'=>'u','ų'=>'u',\n\t\t\t'ù'=>'u','ű'=>'u','ů'=>'u','ư'=>'u','ū'=>'u','ǚ'=>'u',\n\t\t\t'ǜ'=>'u','ǔ'=>'u','ǖ'=>'u','ũ'=>'u','ü'=>'ue','в'=>'v',\n\t\t\t'ŵ'=>'w','ы'=>'y','ÿ'=>'y','ý'=>'y','ŷ'=>'y','ź'=>'z',\n\t\t\t'ž'=>'z','з'=>'z','ż'=>'z','ж'=>'zh','ь'=>'','ъ'=>'',\n\t\t\t'њ'=>'nj','љ'=>'lj','ђ'=>'dj','џ'=>'dz','ћ'=>'c','ј'=>'j',\n\t\t\t'\\''=>'',\n\t\t];\n\t}\n\n\t/**\n\t *\tReturn a URL/filesystem-friendly version of string\n\t *\t@return string\n\t *\t@param $text string\n\t *\t@param $separator string\n\t **/\n\tfunction slug($text, $separator = '-')\n\t{\n\t\treturn trim(strtolower(preg_replace(\n\t\t\t'/([^\\pL\\pN])+/u',\n\t\t\t$separator,\n\t\t\ttrim(strtr($text, Base::instance()->DIACRITICS + $this->diacritics()))\n\t\t)), $separator);\n\t}\n\n\t/**\n\t*\tReturn chunk of text from standard Lorem Ipsum passage\n\t*\t@return string\n\t*\t@param $count int\n\t*\t@param $max int\n\t*\t@param $std bool\n\t**/\n\tfunction filler($count=1,$max=20,$std=TRUE) {\n\t\t$out='';\n\t\tif ($std)\n\t\t\t$out='Lorem ipsum dolor sit amet, consectetur adipisicing elit, '.\n\t\t\t\t'sed do eiusmod tempor incididunt ut labore et dolore magna '.\n\t\t\t\t'aliqua.';\n\t\t$rnd=explode(' ',\n\t\t\t'a ab ad accusamus adipisci alias aliquam amet animi aperiam '.\n\t\t\t'architecto asperiores aspernatur assumenda at atque aut beatae '.\n\t\t\t'blanditiis cillum commodi consequatur corporis corrupti culpa '.\n\t\t\t'cum cupiditate debitis delectus deleniti deserunt dicta '.\n\t\t\t'dignissimos distinctio dolor ducimus duis ea eaque earum eius '.\n\t\t\t'eligendi enim eos error esse est eum eveniet ex excepteur '.\n\t\t\t'exercitationem expedita explicabo facere facilis fugiat harum '.\n\t\t\t'hic id illum impedit in incidunt ipsa iste itaque iure iusto '.\n\t\t\t'laborum laudantium libero magnam maiores maxime minim minus '.\n\t\t\t'modi molestiae mollitia nam natus necessitatibus nemo neque '.\n\t\t\t'nesciunt nihil nisi nobis non nostrum nulla numquam occaecati '.\n\t\t\t'odio officia omnis optio pariatur perferendis perspiciatis '.\n\t\t\t'placeat porro possimus praesentium proident quae quia quibus '.\n\t\t\t'quo ratione recusandae reiciendis rem repellat reprehenderit '.\n\t\t\t'repudiandae rerum saepe sapiente sequi similique sint soluta '.\n\t\t\t'suscipit tempora tenetur totam ut ullam unde vel veniam vero '.\n\t\t\t'vitae voluptas');\n\t\tfor ($i=0,$add=$count-(int)$std;$i<$add;++$i) {\n\t\t\tshuffle($rnd);\n\t\t\t$words=array_slice($rnd,0,mt_rand(3,$max));\n\t\t\t$out.=(!$std&&$i==0?'':' ').ucfirst(implode(' ',$words)).'.';\n\t\t}\n\t\treturn $out;\n\t}\n\n}\n\nif (!function_exists('gzdecode')) {\n\n\t/**\n\t*\tDecode gzip-compressed string\n\t*\t@param $str string\n\t*\t@return string\n\t**/\n\tfunction gzdecode($str) {\n\t\t$fw=Base::instance();\n\t\tif (!is_dir($tmp=$fw->TEMP))\n\t\t\tmkdir($tmp,Base::MODE,TRUE);\n\t\tfile_put_contents($file=$tmp.'/'.$fw->SEED.'.'.\n\t\t\t$fw->hash(uniqid('',TRUE)).'.gz',$str,LOCK_EX);\n\t\tob_start();\n\t\treadgzfile($file);\n\t\t$out=ob_get_clean();\n\t\t@unlink($file);\n\t\treturn $out;\n\t}\n\n}\n"
  }
]