From 9cb013e9ffcb864510954a227d7ce71acac53ac3 Mon Sep 17 00:00:00 2001 From: Mark Friedrich Date: Sat, 14 Dec 2019 21:34:18 +0100 Subject: [PATCH] =?UTF-8?q?-=20BC=20Break:=20Upgraded=20required=20=5FPHP?= =?UTF-8?q?=5F=20`v7.1`=20=E2=86=92=20`v7.2`=20-=20BC=20Break:=20Core=20?= =?UTF-8?q?=5FPHP=5F=20framework=20+=20dependencies=20moved=20into=20`comp?= =?UTF-8?q?oser.json`=20and=20are=20no=20longer=20part=20of=20this=20repo?= =?UTF-8?q?=20-=20Upgraded=20some=203rd=20party=20=5FComposer=5F=20depende?= =?UTF-8?q?ncies:=20`monolog/monolog`,=20`swiftmailer/swiftmailer`,=20`lea?= =?UTF-8?q?gue/html-to-markdown`,=20`react/socket`,=20`react/promise-strea?= =?UTF-8?q?m`=20-=20Fixed=20a=20bug=20where=20`/setup`=20throws=20HTTP=20`?= =?UTF-8?q?5xx`=20error=20if=20no=20`pathfinder`=20DB=20exists=20-=20Fixed?= =?UTF-8?q?=20compatibility=20issue=20with=20PHP=20`v7.4`,=20closed=20#887?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lib/CHANGELOG.md | 915 ----- app/lib/COPYING | 621 --- app/lib/LICENSE | 621 --- app/lib/audit.php | 191 - app/lib/auth.php | 262 -- app/lib/base.php | 3496 ----------------- app/lib/basket.php | 239 -- app/lib/bcrypt.php | 96 - app/lib/changelog.txt | 509 --- app/lib/cli/ws.php | 491 --- app/lib/code.css | 1 - app/lib/composer.json | 12 - app/lib/cron.php | 259 -- app/lib/db/cortex.php | 3077 --------------- app/lib/db/cursor.php | 388 -- app/lib/db/jig.php | 175 - app/lib/db/jig/mapper.php | 541 --- app/lib/db/jig/session.php | 194 - app/lib/db/mongo.php | 145 - app/lib/db/mongo/mapper.php | 405 -- app/lib/db/mongo/session.php | 194 - app/lib/db/sql.php | 523 --- app/lib/db/sql/mapper.php | 700 ---- app/lib/db/sql/schema.php | 1320 ------- app/lib/db/sql/session.php | 221 -- app/lib/f3.php | 42 - app/lib/image.php | 616 --- app/lib/log.php | 68 - app/lib/magic.php | 139 - app/lib/markdown.php | 572 --- app/lib/matrix.php | 113 - app/lib/session.php | 196 - app/lib/sheet.php | 216 - app/lib/smtp.php | 358 -- app/lib/template.php | 353 -- app/lib/test.php | 96 - app/lib/utf.php | 199 - app/lib/web.php | 932 ----- app/lib/web/geo.php | 111 - app/lib/web/google/recaptcha.php | 58 - app/lib/web/google/staticmap.php | 65 - app/lib/web/oauth2.php | 152 - app/lib/web/openid.php | 248 -- app/lib/web/pingback.php | 176 - app/main/controller/controller.php | 2 +- app/main/lib/Cron.php | 16 +- app/main/lib/Monolog.php | 44 +- app/main/lib/logging/AbstractLog.php | 119 +- app/main/lib/logging/LogInterface.php | 46 +- .../handler/AbstractWebhookHandler.php | 2 +- .../lib/logging/handler/SocketHandler.php | 2 +- app/main/model/pathfinder/alliancemodel.php | 2 +- app/main/model/pathfinder/connectionmodel.php | 2 +- app/requirements.ini | 2 +- composer-dev.json | 24 +- composer.json | 24 +- index.php | 2 +- js/app/ui/dialog/map_info.js | 37 +- public/js/v1.5.6/app/ui/dialog/map_info.js | 37 +- public/templates/view/setup.html | 2 +- 60 files changed, 260 insertions(+), 20409 deletions(-) delete mode 100644 app/lib/CHANGELOG.md delete mode 100644 app/lib/COPYING delete mode 100644 app/lib/LICENSE delete mode 100644 app/lib/audit.php delete mode 100644 app/lib/auth.php delete mode 100644 app/lib/base.php delete mode 100644 app/lib/basket.php delete mode 100644 app/lib/bcrypt.php delete mode 100644 app/lib/changelog.txt delete mode 100644 app/lib/cli/ws.php delete mode 100644 app/lib/code.css delete mode 100644 app/lib/composer.json delete mode 100644 app/lib/cron.php delete mode 100644 app/lib/db/cortex.php delete mode 100644 app/lib/db/cursor.php delete mode 100644 app/lib/db/jig.php delete mode 100644 app/lib/db/jig/mapper.php delete mode 100644 app/lib/db/jig/session.php delete mode 100644 app/lib/db/mongo.php delete mode 100644 app/lib/db/mongo/mapper.php delete mode 100644 app/lib/db/mongo/session.php delete mode 100644 app/lib/db/sql.php delete mode 100644 app/lib/db/sql/mapper.php delete mode 100644 app/lib/db/sql/schema.php delete mode 100644 app/lib/db/sql/session.php delete mode 100644 app/lib/f3.php delete mode 100644 app/lib/image.php delete mode 100644 app/lib/log.php delete mode 100644 app/lib/magic.php delete mode 100644 app/lib/markdown.php delete mode 100644 app/lib/matrix.php delete mode 100644 app/lib/session.php delete mode 100644 app/lib/sheet.php delete mode 100644 app/lib/smtp.php delete mode 100644 app/lib/template.php delete mode 100644 app/lib/test.php delete mode 100644 app/lib/utf.php delete mode 100644 app/lib/web.php delete mode 100644 app/lib/web/geo.php delete mode 100644 app/lib/web/google/recaptcha.php delete mode 100644 app/lib/web/google/staticmap.php delete mode 100644 app/lib/web/oauth2.php delete mode 100644 app/lib/web/openid.php delete mode 100644 app/lib/web/pingback.php diff --git a/app/lib/CHANGELOG.md b/app/lib/CHANGELOG.md deleted file mode 100644 index 4960ea2c..00000000 --- a/app/lib/CHANGELOG.md +++ /dev/null @@ -1,915 +0,0 @@ -CHANGELOG - -3.6.5 (24 December 2018) -* NEW: Log, added timestamp to each line -* NEW: Auth, added support for custom compare method, [#116](https://github.com/bcosca/fatfree-core/issues/116) -* NEW: cache tag support for mongo & jig mapper, ref [#166](https://github.com/bcosca/fatfree-core/issues/116) -* NEW: Allow PHP functions as template token filters -* Web: Fix double redirect bug when running cURL with open_basedir disabled -* Web: Cope with responses from HTTP/2 servers -* Web->filler: remove very first space, when $std is false -* Web\OAuth2: Cope with HTTP/2 responses -* Web\OAuth2: take Content-Type header into account for json decoding, [#250](https://github.com/bcosca/fatfree-core/issues/250) [#251](https://github.com/bcosca/fatfree-core/issues/251) -* Web\OAuth2: fixed empty results on some endpoints [#250](https://github.com/bcosca/fatfree-core/issues/250) -* DB\SQL\Mapper: optimize mapper->count memory usage -* DB\SQL\Mapper: New table alias operator -* DB\SQL\Mapper: fix count() performance on non-grouped result sets, [bcosca/fatfree#1114](https://github.com/bcosca/fatfree/issues/1114) -* DB\SQL: Support for CTE in postgreSQL, [bcosca/fatfree#1107](https://github.com/bcosca/fatfree/issues/1107), [bcosca/fatfree#1116](https://github.com/bcosca/fatfree/issues/1116), [bcosca/fatfree#1021](https://github.com/bcosca/fatfree/issues/1021) -* DB\SQL->log: Remove extraneous whitespace -* DB\SQL: Added ability to add inline comments per SQL query -* CLI\WS, Refactoring: Streamline socket server -* CLI\WS: Add option for dropping query in OAuth2 URI -* CLI\WS: Add URL-safe base64 encoding -* CLI\WS: Detect errors in returned JSON values -* CLI\WS: Added support for Sec-WebSocket-Protocol header -* Matrix->calendar: Allow unix timestamp as date argument -* Basket: Access basket item by _id [#260](https://github.com/bcosca/fatfree-core/issues/260) -* SMTP: Added TLS 1.2 support [bcosca/fatfree#1115](https://github.com/bcosca/fatfree/issues/1115) -* SMTP->send: Respect $log argument -* Base->cast: recognize binary and octal numbers in config -* Base->cast: add awareness of hexadecimal literals -* Base->abort: Remove unnecessary Content-Encoding header -* Base->abort: Ensure headers have not been flushed -* Base->format: Differentiate between long- and full-date (with localized weekday) formats -* Base->format: Conform with intl extension's number output -* Enable route handler to override Access-Control headers in response to OPTIONS request, [#257](https://github.com/bcosca/fatfree-core/issues/257) -* Augment filters with a var_export function -* Bug fix php7.3: Fix template parse regex to be compatible with strict PCRE2 rules for hyphen placement in a character class -* Bug fix, Cache->set: update creation time when updating existing cache entries -* Bug fix: incorrect ICU date/time formatting -* Bug fix, Jig: lazy write on empty data -* Bug fix: Method uppercase to avoid route failure [#252](https://github.com/bcosca/fatfree-core/issues/252) -* Fixed error description when (PSR-11) `CONTAINER` fails to resolve a class [#253](https://github.com/bcosca/fatfree-core/issues/253) -* Mitigate CSRF predictability/vulnerability -* Expose Mapper->factory() method - -3.6.4 (19 April 2018) -* NEW: Added Dependency Injection support with CONTAINER variable [#221](https://github.com/bcosca/fatfree-core/issues/221) -* NEW: configurable LOGGABLE error codes [#1091](https://github.com/bcosca/fatfree/issues/1091#issuecomment-364674701) -* NEW: JAR.lifetime option, [#178](https://github.com/bcosca/fatfree-core/issues/178) -* Template: reduced Prefab calls -* Template: optimized reflection for better derivative support, [bcosca/fatfree#1088](https://github.com/bcosca/fatfree/issues/1088) -* Template: optimized parsing for template attributes and tokens -* DB\Mongo: fixed logging with mongodb extention -* DB\Jig: added lazy-loading [#7e1cd9b9b89](https://github.com/bcosca/fatfree-core/commit/7e1cd9b9b89c4175d0f6b86ced9d9bd49c04ac39) -* DB\Jig\Mapper: Added group feature, bcosca/fatfree#616 -* DB\SQL\Mapper: fix PostgreSQL RETURNING ID when no pkey is available, [bcosca/fatfree#1069](https://github.com/bcosca/fatfree/issues/1069), [#230](https://github.com/bcosca/fatfree-core/issues/230) -* DB\SQL\Mapper: disable order clause auto-quoting when it's already been quoted -* Web->location: add failsafe for geoip_region_name_by_code() [#GB:Bxyn9xn9AgAJ](https://groups.google.com/d/msg/f3-framework/APau4wnwNzE/Bxyn9xn9AgAJ) -* Web->request: Added proxy support [#e936361b](https://github.com/bcosca/fatfree-core/commit/e936361bc03010c4c7c38a396562e5e96a8a100d) -* Web->mime: Added JFIF format -* Markdown: handle line breaks in paragraph blocks, [bcosca/fatfree#1100](https://github.com/bcosca/fatfree/issues/1100) -* config: reduced cast calls on parsing config sections -* Patch empty SERVER_NAME [bcosca/fatfree#1084](https://github.com/bcosca/fatfree/issues/1084) -* Bugfix: unreliable request headers in Web->request() response [bcosca/fatfree#1092](https://github.com/bcosca/fatfree/issues/1092) -* Fixed, View->render: utilizing multiple UI paths, [bcosca/fatfree#1083](https://github.com/bcosca/fatfree/issues/1083) -* Fixed URL parsing with PHP 5.4 [#247](https://github.com/bcosca/fatfree-core/issues/247) -* Fixed PHP 7.2 warnings when session is active prematurely, [#238](https://github.com/bcosca/fatfree-core/issues/238) -* Fixed setcookie $expire variable type [#240](https://github.com/bcosca/fatfree-core/issues/240) -* Fixed expiration time when updating an existing cookie - -3.6.3 (31 December 2017) -* PHP7 fix: remove deprecated (unset) cast -* Web->request: restricted follow_location to 3XX responses only -* CLI mode: refactored arguments parsing -* CLI mode: fixed query string encoding -* SMTP: Refactor parsing of attachments -* SMTP: clean-up mail headers for multipart messages, [#1065](https://github.com/bcosca/fatfree/issues/1065) -* config: fixed performance issues on parsing config files -* config: cast command parameters in config entries to php type & constant, [#1030](https://github.com/bcosca/fatfree/issues/1030) -* config: reduced registry calls -* config: skip hive escaping when resolving dynamic config vars, [#1030](https://github.com/bcosca/fatfree/issues/1030) -* Bug fix: Incorrect cookie lifetime computation, [#1070](https://github.com/bcosca/fatfree/issues/1070), [#1016](https://github.com/bcosca/fatfree/issues/1016) -* DB\SQL\Mapper: use RETURNING option instead of a sequence query to get lastInsertId in PostgreSQL, [#1069](https://github.com/bcosca/fatfree/issues/1069), [#230](https://github.com/bcosca/fatfree-core/issues/230) -* DB\SQL\Session: check if _agent is too long for SQL based sessions [#236](https://github.com/bcosca/fatfree-core/issues/236) -* DB\SQL\Session: fix Session handler table creation issue on SQL Server, [#899](https://github.com/bcosca/fatfree/issues/899) -* DB\SQL: fix oracle db issue with empty error variable, [#1072](https://github.com/bcosca/fatfree/issues/1072) -* DB\SQL\Mapper: fix sorting issues on SQL Server, [#1052](https://github.com/bcosca/fatfree/issues/1052) [#225](https://github.com/bcosca/fatfree-core/issues/225) -* Prevent directory traversal attacks on filesystem based cache [#1073](https://github.com/bcosca/fatfree/issues/1073) -* Bug fix, Template: PHP constants used in include with attribute, [#983](https://github.com/bcosca/fatfree/issues/983) -* Bug fix, Template: Numeric value in expression alters PHP_EOL context -* Template: use existing linefeed instead of PHP_EOL, [#1048](https://github.com/bcosca/fatfree/issues/1048) -* Template: make newline interpolation handling configurable [#223](https://github.com/bcosca/fatfree-core/issues/223) -* Template: add beforerender to Preview -* fix custom FORMATS without modifiers -* Cache: Refactor Cache->reset for XCache -* Cache: loosen reset cache key pattern, [#1041](https://github.com/bcosca/fatfree/issues/1041) -* XCache: suffix reset only works if xcache.admin.enable_auth is disabled -* Added HTTP 103 as recently approved by the IETF -* LDAP changes to for AD flexibility [#227](https://github.com/bcosca/fatfree-core/issues/227) -* Hide debug trace from ajax errors when DEBUG=0 [#1071](https://github.com/bcosca/fatfree/issues/1071) -* fix View->render using potentially wrong cache entry - -3.6.2 (26 June 2017) -* Return a status code > 0 when dying on error [#220](https://github.com/bcosca/fatfree-core/issues/220) -* fix SMTP line width [#215](https://github.com/bcosca/fatfree-core/issues/215) -* Allow using a custom field for ldap user id checking [#217](https://github.com/bcosca/fatfree-core/issues/217) -* NEW: DB\SQL->exists: generic method to check if SQL table exists -* Pass handler to route handler and hooks [#1035](https://github.com/bcosca/fatfree/issues/1035) -* pass carriage return of multiline dictionary keys -* Better Web->slug customization -* fix incorrect header issue [#211](https://github.com/bcosca/fatfree-core/issues/211) -* fix schema issue on databases with case-sensitive collation, fixes [#209](https://github.com/bcosca/fatfree-core/issues/209) -* Add filter for deriving C-locale equivalent of a number -* Bug fix: @LANGUAGE remains unchanged after override -* abort: added Header pre-check -* Assemble URL after ONREROUTE -* Add reroute argument to skip script termination -* Invoke ONREROUTE after headers are sent -* SQLite switch to backtick as quote -* Bug fix: Incorrect timing in SQL query logs -* DB\SQL\Mapper: Cast return value of count to integer -* Patched $_SERVER['REQUEST_URI'] to ensure it contains a relative URI -* Tweak debug verbosity -* fix php carriage return issue in preview->build [#205](https://github.com/bcosca/fatfree-core/pull/205) -* fixed template string resolution [#205](https://github.com/bcosca/fatfree-core/pull/205) -* Fixed unexpected default seed on CACHE set [#1028](https://github.com/bcosca/fatfree/issues/1028) -* DB\SQL\Mapper: Optimized field escaping on options -* Optimize template conversion to PHP file - -3.6.1 (2 April 2017) -* NEW: Recaptcha plugin [#194](https://github.com/bcosca/fatfree-core/pull/194) -* NEW: MB variable for detecting multibyte support -* NEW: DB\SQL: Cache parsed schema for the TTL duration -* NEW: quick erase flag on Jig/Mongo/SQL mappers [#193](https://github.com/bcosca/fatfree-core/pull/193) -* NEW: Allow OPTIONS method to return a response body [#171](https://github.com/bcosca/fatfree-core/pull/171) -* NEW: Add support for Memcached (bcosca/fatfree#997) -* NEW: Rudimentary preload resource (HTTP2 server) support via template push() -* NEW: Add support for new MongoDB driver [#177](https://github.com/bcosca/fatfree-core/pull/177) -* Changed: template filter are all lowercase now -* Changed: Fix template lookup inconsistency: removed base dir from UI on render -* Changed: count() method now has an options argument [#192](https://github.com/bcosca/fatfree-core/pull/192) -* Changed: SMTP, Spit out error message if any -* \DB\SQL\Mapper: refactored row count strategy -* DB\SQL\Mapper: Allow non-scalar values to be assigned as mapper property -* DB\SQL::PARAM_FLOAT: remove cast to float (#106 and bcosca/fatfree#984) (#191) -* DB\SQL\mapper->erase: allow empty string -* DB\SQL\mapper->insert: fields reset after successful INSERT -* Add option to debounce Cursor->paginate subset [#195](https://github.com/bcosca/fatfree-core/pull/195) -* View: Don't delete sandboxed variables (#198) -* Preview: Optimize compilation of template expressions -* Preview: Use shorthand tag for direct rendering -* Preview->resolve(): new tweak to allow template persistence as option -* Web: Expose diacritics translation table -* SMTP: Enable logging of message body only when $log argument is 'verbose' -* SMTP: Convert headers to camelcase for consistency -* make cache seed more flexible, #164 -* Improve trace details for DEBUG>2 -* Enable config() to read from an array of input files -* Improved alias and reroute regex -* Make camelCase and snakeCase Unicode-aware -* format: Provision for optional whitespaces -* Break APCu-BC dependence -* Old PHP 5.3 cleanup -* Debug log must include HTTP query -* Recognize X-Forwarded-Port header (bcosca/fatfree#1002) -* Avoid use of deprecated mcrypt module -* Return only the client's IP when using the `X-Forwarded-For` header to deduce an IP address -* Remove orphan mutex locks on termination (#157) -* Use 80 as default port number to avoid issues when `$_SERVER['SERVER_PORT']` is not existing -* fread replaced with readfile() for simple send() usecase -* Bug fix: request URI with multiple leading slashes, #203 -* Bug fix: Query generates wrong adhoc field value -* Bug fix: SMTP stream context issue #200 -* Bug fix: child pseudo class selector in minify, bcosca/fatfree#1008 -* Bug fix: "Undefined index: CLI" error (#197) -* Bug fix: cast Cache-Control expire time to int, bcosca/fatfree#1004 -* Bug fix: Avoid issuance of multiple Content-Type headers for nested templates -* Bug fix: wildcard token issue with digits (bcosca/fatfree#996) -* Bug fix: afterupdate ignored when row does not change -* Bug fix: session handler read() method for PHP7 (need strict string) #184 #185 -* Bug fix: reroute mocking in CLI mode (#183) -* Bug fix: Reroute authoritative relative references (#181) -* Bug fix: locales order and charset hyphen -* Bug fix: base stripped twice in router (#176) - -3.6.0 (19 November 2016) -* NEW: [cli] request type -* NEW: console-friendly CLI mode -* NEW: lexicon caching -* NEW: Silent operator skips startup error check (#125) -* NEW: DB\SQL->trans() -* NEW: custom config section parser, i.e. [conf > Foo::bar] -* NEW: support for cache tags in SQL -* NEW: custom FORMATS -* NEW: Mongo mapper fields whitelist -* NEW: WebSocket server -* NEW: Base->extend method (#158) -* NEW: Implement framework variable caching via config, i.e. FOO = "bar" | 3600 -* NEW: Lightweight OAuth2 client -* NEW: SEED variable, configurable app-specific hashing prefix (#149, bcosca/fatfree#951, bcosca/fatfree#884, bcosca/fatfree#629) -* NEW: CLI variable -* NEW: Web->send, specify custom filename (#124) -* NEW: Web->send, added flushing flag (#131) -* NEW: Indexed route wildcards, now exposed in PARAMS['*'] -* Changed: PHP 5.4 is now the minimum version requirement -* Changed: Prevent database wrappers from being cloned -* Changed: Router works on PATH instead of URI (#126) NB: PARAMS.0 no longer contains the query string -* Changed: Removed ALIASES autobuilding (#118) -* Changed: Route wildcards match empty strings (#119) -* Changed: Disable default debug highlighting, HIGHLIGHT is false now -* General PHP 5.4 optimizations -* Optimized config parsing -* Optimized Base->recursive -* Optimized header extraction -* Optimized cache/expire headers -* Optimized session_start behaviour (bcosca/fatfree#673) -* Optimized reroute regex -* Tweaked cookie removal -* Better route precedence order -* Performance tweak: reduced cache calls -* Refactored lexicon (LOCALES) build-up, much faster now -* Added turkish locale bug workaround -* Geo->tzinfo Update to UTC -* Added Xcache reset (bcosca/fatfree#928) -* Redis cache: allow db name in dsn -* SMTP: Improve server emulation responses -* SMTP: Optimize transmission envelope -* SMTP: Implement mock transmission -* SMTP: Various bug fixes and feature improvements -* SMTP: quit on failed authentication -* Geo->weather: force metric units -* Base->until: Implement CLI interoperability -* Base->format: looser plural syntax -* Base->format: Force decimal as default number format -* Base->merge: Added $keep flag to save result to the hive key -* Base->reroute: Allow array as URL argument for aliasing -* Base->alias: Allow query string (or array) to be appended to alias -* Permit reroute to named routes with URL query segment -* Sync COOKIE global on set() -* Permit non-hive variables to use JS dot notation -* RFC2616: Use absolute URIs for Location header -* Matrix->calendar: Check if calendar extension is loaded -* Markdown: require start of line/whitespace for text processing (#136) -* DB\[SQL|Jig|Mongo]->log(FALSE) disables logging -* DB\SQL->exec: Added timestamp toggle to db log -* DB\SQL->schema: Remove unnecessary line terminators -* DB\SQL\Mapper: allow array filter with empty string -* DB\SQL\Mapper: optimized handling for key-less tables -* DB\SQL\Mapper: added float support (#106) -* DB\SQL\Session: increased default column sizes (#148, bcosca/fatfree#931, bcosca/fatfree#950) -* Web: Catch cURL errors -* Optimize Web->receive (bcosca/fatfree#930) -* Web->minify: fix arbitrary file download vulnerability -* Web->request: fix cache control max-age detection (bcosca/fatfree#908) -* Web->request: Add request headers & error message to return value (bcosca/fatfree#737) -* Web->request: Refactored response to HTTP request -* Web->send flush while sending big files -* Image->rgb: allow hex strings -* Image->captcha: Check if GD module supports TrueType -* Image->load: Return FALSE on load failure -* Image->resize: keep aspect ratio when only width or height was given -* Updated OpenID lib (bcosca/fatfree#965) -* Audit->card: add new mastercard "2" BIN range (bcosca/fatfree#954) -* Deprecated: Bcrypt class -* Preview->render: optimized detection to remove short open PHP tags and allow xml tags (#133) -* Display file and line number in exception handler (bcosca/fatfree#967) -* Added error reporting level to Base->error and ERROR.level (bcosca/fatfree#957) -* Added optional custom cache instance to Session (#141) -* CLI-aware mock() -* XFRAME and PACKAGE can be switched off now (#128) -* Bug fix: wrong time calculation on memcache reset (#170) -* Bug fix: encode CLI parameters -* Bug fix: Close connection on abort explicitly (#162) -* Bug fix: Image->identicon, Avoid double-size sprite rotation (and possible segfault) -* Bug fix: Image->render and Image->dump, removed unnecessary 2nd argument (#146) -* Bug fix: Magic->offsetset, access property as array element (#147) -* Bug fix: multi-line custom template tag parsing (bcosca/fatfree#935) -* Bug fix: cache headers on errors (bcosca/fatfree#885) -* Bug fix: Web, deprecated CURLOPT_SSL_VERIFYHOST in curl -* Bug fix: Web, Invalid user error constant (bcosca/fatfree#962) -* Bug fix: Web->request, redirections for domain-less location (#135) -* Bug fix: DB\SQL\Mapper, reset changed flag after update (#142, #152) -* Bug fix: DB\SQL\Mapper, fix changed flag when using assignment operator #143 #150 #151 -* Bug fix: DB\SQL\Mapper, revival of the HAVING clause -* Bug fix: DB\SQL\Mapper, pgsql with non-integer primary keys (bcosca/fatfree#916) -* Bug fix: DB\SQL\Session, quote table name (bcosca/fatfree#977) -* Bug fix: snakeCase returns word starting with underscore (bcosca/fatfree#927) -* Bug fix: mock does not populate PATH variable -* Bug fix: Geo->weather API key (#129) -* Bug fix: Incorrect compilation of array element with zero index -* Bug fix: Compilation of array construct is incorrect -* Bug fix: Trailing slash redirection on UTF-8 paths (#121) - -3.5.1 (31 December 2015) -* NEW: ttl attribute in template tag -* NEW: allow anonymous function for template filter -* NEW: format modifier for international and custom currency symbol -* NEW: Image->data() returns image resource -* NEW: extract() get prefixed array keys from an assoc array -* NEW: Optimized and faster Template parser with full support for HTML5 empty tags -* NEW: Added support for {@token} encapsulation syntax in routes definition -* NEW: DB\SQL->exec(), automatically shift to 1-based query arguments -* NEW: abort() flush output -* Added referenced value to devoid() -* Template token filters are now resolved within Preview->token() -* Web->_curl: restrict redirections to HTTP -* Web->minify(), skip importing of external files -* Improved session and error handling in until() -* Get the error trace array with the new $format parameter -* Better support for unicode URLs -* Optimized TZ detection with date_default_timezone_get() -* format() Provide default decimal places -* Optimize code: remove redundant TTL checks -* Optimized timeout handling in Web->request() -* Improved PHPDoc hints -* Added missing russian DIACRITICS letters -* DB\Cursor: allow child implementation of reset() -* DB\Cursor: Copyfrom now does an internal call to set() -* DB\SQL: Provide the ability to disable SQL logging -* DB\SQL: improved query analysis to trigger fetchAll -* DB\SQL\Mapper: added support for binary table columns -* SQL,JIG,MONGO,CACHE Session handlers refactored and optimized -* SMTP Refactoring and optimization -* Bug fix: SMTP, Align quoted_printable_encode() with SMTP specs (dot-stuffing) -* Bug fix: SMTP, Send buffered optional headers to output -* Bug fix: SMTP, Content-Transfer-Encoding for non-TLS connections -* Bug fix: SMTP, Single attachment error -* Bug fix: Cursor->load not always mapping to first record -* Bug fix: dry SQL mapper should not trigger 'load' -* Bug fix: Code highlighting on empty text -* Bug fix: Image->resize, round dimensions instead of cast -* Bug fix: whitespace handling in $f3->compile() -* Bug fix: TTL of `View` and `Preview` (`Template`) -* Bug fix: token filter regex -* Bug fix: Template, empty attributes -* Bug fix: Preview->build() greedy regex -* Bug fix: Web->minify() single-line comment on last line -* Bug fix: Web->request(), follow_location with cURL and open_basedir -* Bug fix: Web->send() Single quotes around filename not interpreted correctly by some browsers - -3.5.0 (2 June 2015) -* NEW: until() method for long polling -* NEW: abort() to disconnect HTTP client (and continue execution) -* NEW: SQL Mapper->required() returns TRUE if field is not nullable -* NEW: PREMAP variable for allowing prefixes to handlers named after HTTP verbs -* NEW: [configs] section to allow config includes -* NEW: Test->passed() returns TRUE if no test failed -* NEW: SQL mapper changed() function -* NEW: fatfree-core composer support -* NEW: constants() method to expose constants -* NEW: Preview->filter() for configurable token filters -* NEW: CORS variable for Cross-Origin Resource Sharing support, #731 -* Change in behavior: Switch to htmlspecialchars for escaping -* Change in behavior: No movement in cursor position after erase(), #797 -* Change in behavior: ERROR.trace is a multiline string now -* Change in behavior: Strict token recognition in href attribute -* Router fix: loose method search -* Better route precedence order, #12 -* Preserve contents of ROUTES, #723 -* Alias: allow array of parameters -* Improvements on reroute method -* Fix for custom Jig session files -* Audit: better mobile detection -* Audit: add argument to test string as browser agent -* DB mappers: abort insert/update/erase from hooks, #684 -* DB mappers: Allow array inputs in copyfrom() -* Cache,SQL,Jig,Mongo Session: custom callback for suspect sessions -* Fix for unexpected HIVE values when defining an empty HIVE array -* SQL mapper: check for results from CALL and EXEC queries, #771 -* SQL mapper: consider SQL schema prefix, #820 -* SQL mapper: write to log before execution to - enable tracking of PDOStatement error -* Add SQL Mapper->table() to return table name -* Allow override of the schema in SQL Mapper->schema() -* Improvement: Keep JIG table as reference, #758 -* Expand regex to include whitespaces in SQL DB dsn, #817 -* View: Removed reserved variables $fw and $implicit -* Add missing newlines after template expansion -* Web->receive: fix for complex field names, #806 -* Web: Improvements in socket engine -* Web: customizable user_agent for all engines, #822 -* SMTP: Provision for Content-ID in attachments -* Image + minify: allow absolute paths -* Promote framework error to E_USER_ERROR -* Geo->weather switch to OpenWeather -* Expose mask() and grab() methods for routing -* Expose trace() method to expose the debug backtrace -* Implement recursion strategy using IteratorAggregate, #714 -* Exempt whitespace between % and succeeding operator from being minified, #773 -* Optimized error detection and ONERROR handler, fatfree-core#18 -* Tweak error log output -* Optimized If-Modified-Since cache header usage -* Improved APCu compatibility, #724 -* Bug fix: Web::send fails on filename with spaces, #810 -* Bug fix: overwrite limit in findone() -* Bug fix: locale-specific edge cases affecting SQL schema, #772 -* Bug fix: Newline stripping in config() -* Bug fix: bracket delimited identifier for sybase and dblib driver -* Bug fix: Mongo mapper collection->count driver compatibility -* Bug fix: SQL Mapper->set() forces adhoc value if already defined -* Bug fix: Mapper ignores HAVING clause -* Bug fix: Constructor invocation in call() -* Bug fix: Wrong element returned by ajax/sync request -* Bug fix: handling of non-consecutive compound key members -* Bug fix: Virtual fields not retrieved when group option is present, #757 -* Bug fix: group option generates incorrect SQL query, #757 -* Bug fix: ONERROR does not receive PARAMS on fatal error - -3.4.0 (1 January 2015) -* NEW: [redirects] section -* NEW: Custom config sections -* NEW: User-defined AUTOLOAD function -* NEW: ONREROUTE variable -* NEW: Provision for in-memory Jig database (#727) -* Return run() result (#687) -* Pass result of run() to mock() (#687) -* Add port suffix to REALM variable -* New attribute in tag to extend hive -* Adjust unit tests and clean up templates -* Expose header-related methods -* Web->request: allow content array -* Preserve contents of ROUTES (#723) -* Smart detection of PHP functions in template expressions -* Add afterrender() hook to View class -* Implement ArrayAccess and magic properties on hive -* Improvement on mocking of superglobals and request body -* Fix table creation for pgsql handled sessions -* Add QUERY to hive -* Exempt E_NOTICE from default error_reporting() -* Add method to build alias routes from template, fixes #693 -* Fix dangerous caching of cookie values -* Fix multiple encoding in nested templates -* Fix node attribute parsing for empty/zero values -* Apply URL encoding on BASE to emulate v2 behavior (#123) -* Improve Base->map performance (#595) -* Add simple backtrace for fatal errors -* Count Cursor->load() results (#581) -* Add form field name to Web->receive() callback arguments -* Fix missing newlines after template expansion -* Fix overwrite of ENCODING variable -* limit & offset workaround for SQL Server, fixes #671 -* SQL Mapper->find: GROUP BY SQL compliant statement -* Bug fix: Missing abstract method fields() -* Bug fix: Auto escaping does not work with mapper objects (#710) -* Bug fix: 'with' attribute in tag raise error when no token - inside -* View rendering: optional Content-Type header -* Bug fix: Undefined variable: cache (#705) -* Bug fix: Routing does not work if project base path includes valid - special URI character (#704) -* Bug fix: Template hash collision (#702) -* Bug fix: Property visibility is incorrect (#697) -* Bug fix: Missing Allow header on HTTP 405 response -* Bug fix: Double quotes in lexicon files (#681) -* Bug fix: Space should not be mandatory in ICU pluralization format string -* Bug fix: Incorrect log entry when SQL query contains a question mark -* Bug fix: Error stack trace -* Bug fix: Cookie expiration (#665) -* Bug fix: OR operator (||) parsed incorrectly -* Bug fix: Routing treatment of * wildcard character -* Bug fix: Mapper copyfrom() method doesn't allow class/object callbacks - (#590) -* Bug fix: exists() creates elements/properties (#591) -* Bug fix: Wildcard in routing pattern consumes entire query string (#592) -* Bug fix: Workaround bug in latest MongoDB driver -* Bug fix: Default error handler silently fails for AJAX request with - DEBUG>0 (#599) -* Bug fix: Mocked BODY overwritten (#601) -* Bug fix: Undefined pkey (#607) - -3.3.0 (8 August 2014) -* NEW: Attribute in tag to extend hive -* NEW: Image overlay with transparency and alignment control -* NEW: Allow redirection of specified route patterns to a URL -* Bug fix: Missing AND operator in SQL Server schema query (Issue #576) -* Count Cursor->load() results (Feature request #581) -* Mapper copyfrom() method doesn't allow class/object callbacks (Issue #590) -* Bug fix: exists() creates elements/properties (Issue #591) -* Bug fix: Wildcard in routing pattern consumes entire query string - (Issue #592) -* Tweak Base->map performance (Issue #595) -* Bug fix: Default error handler silently fails for AJAX request with - DEBUG>0 (Issue #599) -* Bug fix: Mocked BODY overwritten (Issue #601) -* Bug fix: Undefined pkey (Issue #607) -* Bug fix: beforeupdate() position (Issue #633) -* Bug fix: exists() return value for cached keys -* Bug fix: Missing error code in UNLOAD handler -* Bug fix: OR operator (||) parsed incorrectly -* Add input name parameter to custom slug function -* Apply URL encoding on BASE to emulate v2 behavior (Issue #123) -* Reduce mapper update() iterations -* Bug fix: Routing treatment of * wildcard character -* SQL Mapper->find: GROUP BY SQL compliant statement -* Work around bug in latest MongoDB driver -* Work around probable race condition and optimize cache access -* View rendering: Optional Content-Type header -* Fix missing newlines after template expansion -* Add form field name to Web->receive() callback arguments -* Quick reference: add RAW variable - -3.2.2 (19 March 2014) -* NEW: Locales set automatically (Feature request #522) -* NEW: Mapper dbtype() -* NEW: before- and after- triggers for all mappers -* NEW: Decode HTML5 entities if PHP>5.3 detected (Feature request #552) -* NEW: Send credentials only if AUTH is present in the SMTP extension - response (Feature request #545) -* NEW: BITMASK variable to allow ENT_COMPAT override -* NEW: Redis support for caching -* Enable SMTP feature detection -* Enable extended ICU custom date format (Feature request #555) -* Enable custom time ICU format -* Add option to turn off session table creation (Feature request #557) -* Enhanced template token rendering and custom filters (Feature request - #550) -* Avert multiple loads in DB-managed sessions (Feature request #558) -* Add EXEC to associative fetch -* Bug fix: Building template tokens breaks on inline OR condition (Issue - #573) -* Bug fix: SMTP->send does not use the $log parameter (Issue #571) -* Bug fix: Allow setting sqlsrv primary keys on insert (Issue #570) -* Bug fix: Generated query for obtaining table schema in sqlsrv incorrect - (Bug #565) -* Bug fix: SQL mapper flag set even when value has not changed (Bug #562) -* Bug fix: Add XFRAME config option (Feature request #546) -* Bug fix: Incorrect parsing of comments (Issue #541) -* Bug fix: Multiple Set-Cookie headers (Issue #533) -* Bug fix: Mapper is dry after save() -* Bug fix: Prevent infinite loop when error handler is triggered - (Issue #361) -* Bug fix: Mapper tweaks not passing primary keys as arguments -* Bug fix: Zero indexes in dot-notated arrays fail to compile -* Bug fix: Prevent GROUP clause double-escaping -* Bug fix: Regression of zlib compression bug -* Bug fix: Method copyto() does not include ad hoc fields -* Check existence of OpenID mode (Issue #529) -* Generate a 404 when a tokenized class doesn't exist -* Fix SQLite quotes (Issue #521) -* Bug fix: BASE is incorrect on Windows - -3.2.1 (7 January 2014) -* NEW: EMOJI variable, UTF->translate(), UTF->emojify(), and UTF->strrev() -* Allow empty strings in config() -* Add support for turning off php://input buffering via RAW - (FALSE by default) -* Add Cursor->load() and Cursor->find() TTL support -* Support Web->receive() large file downloads via PUT -* ONERROR safety check -* Fix session CSRF cookie detection -* Framework object now passed to route handler contructors -* Allow override of DIACRITICS -* Various code optimizations -* Support log disabling (Issue #483) -* Implicit mapper load() on authentication -* Declare abstract methods for Cursor derivatives -* Support single-quoted HTML/XML attributes (Feature request #503) -* Relax property visibility of mappers and derivatives -* Deprecated: {{~ ~}} instructions and {{* *}} comments; Use {~ ~} and - {* *} instead -* Minor fix: Audit->ipv4() return value -* Bug fix: Backslashes in BASE not converted on Windows -* Bug fix: UTF->substr() with negative offset and specified length -* Bug fix: Replace named URL tokens on render() -* Bug fix: BASE is not empty when run from document root -* Bug fix: stringify() recursion - -3.2.0 (18 December 2013) -* NEW: Automatic CSRF protection (with IP and User-Agent checks) for - sessions mapped to SQL-, Jig-, Mongo- and Cache-based backends -* NEW: Named routes -* NEW: PATH variable; returns the URL relative to BASE -* NEW: Image->captcha() color parameters -* NEW: Ability to access MongoCuror thru the cursor() method -* NEW: Mapper->fields() method returns array of field names -* NEW: Mapper onload(), oninsert(), onupdate(), and onerase() event - listeners/triggers -* NEW: Preview class (a lightweight template engine) -* NEW: rel() method derives path from URL relative to BASE; useful for - rerouting -* NEW: PREFIX variable for prepending a string to a dictionary term; - Enable support for prefixed dictionary arrays and .ini files (Feature - request #440) -* NEW: Google static map plugin -* NEW: devoid() method -* Introduce clean(); similar to scrub(), except that arg is passed by - value -* Use $ttl for cookie expiration (Issue #457) -* Fix needs_rehash() cost comparison -* Add pass-by-reference argument to exists() so if method returns TRUE, - a subsequent get() is unnecessary -* Improve MySQL support -* Move esc(), raw(), and dupe() to View class where they more - appropriately belong -* Allow user-defined fields in SQL mapper constructor (Feature request - #450) -* Re-implement the pre-3.0 template resolve() feature -* Remove redundant instances of session_commit() -* Add support for input filtering in Mapper->copyfrom() -* Prevent intrusive behavior of Mapper->copyfrom() -* Support multiple SQL primary keys -* Support custom tag attributes/inline tokens defined at runtime - (Feature request #438) -* Broader support for HTTP basic auth -* Prohibit Jig _id clear() -* Add support for detailed stringify() output -* Add base directory to UI path as fallback -* Support Test->expect() chaining -* Support __tostring() in stringify() -* Trigger error on invalid CAPTCHA length (Issue #458) -* Bug fix: exists() pass-by-reference argument returns incorrect value -* Bug fix: DB Exec does not return affected row if query contains a - sub-SELECT (Issue #437) -* Improve seed generator and add code for detecting of acceptable - limits in Image->captcha() (Feature request #460) -* Add decimal format ICU extension -* Bug fix: 404-reported URI contains HTTP query -* Bug fix: Data type detection in DB->schema() -* Bug fix: TZ initialization -* Bug fix: paginate() passes incorrect argument to count() -* Bug fix: Incorrect query when reloading after insert() -* Bug fix: SQL preg_match error in pdo_type matching (Issue #447) -* Bug fix: Missing merge() function (Issue #444) -* Bug fix: BASE misdefined in command line mode -* Bug fix: Stringifying hive may run infinite (Issue #436) -* Bug fix: Incomplete stringify() when DEBUG<3 (Issue #432) -* Bug fix: Redirection of basic auth (Issue #430) -* Bug fix: Filter only PHP code (including short tags) in templates -* Bug fix: Markdown paragraph parser does not convert PHP code blocks - properly -* Bug fix: identicon() colors on same keys are randomized -* Bug fix: quotekey() fails on aliased keys -* Bug fix: Missing _id in Jig->find() return value -* Bug fix: LANGUAGE/LOCALES handling -* Bug fix: Loose comparison in stringify() - -3.1.2 (5 November 2013) -* Abandon .chm help format; Package API documentation in plain HTML; - (Launch lib/api/index.html in your browser) -* Deprecate BAIL in favor of HALT (default: TRUE) -* Revert to 3.1.0 autoload behavior; Add support for lowercase folder - names -* Allow Spring-style HTTP method overrides -* Add support for SQL Server-based sessions -* Capture full X-Forwarded-For header -* Add protection against malicious scripts; Extra check if file was really - uploaded -* Pass-thru page limit in return value of Cursor->paginate() -* Optimize code: Implement single-pass escaping -* Short circuit Jig->find() if source file is empty -* Bug fix: PHP globals passed by reference in hive() result (Issue #424) -* Bug fix: ZIP mime type incorrect behavior -* Bug fix: Jig->erase() filter malfunction -* Bug fix: Mongo->select() group -* Bug fix: Unknown bcrypt constant - -3.1.1 (13 October 2013) -* NEW: Support OpenID attribute exchange -* NEW: BAIL variable enables/disables continuance of execution on non-fatal - errors -* Deprecate BAIL in favor of HALT (default: FALSE) -* Add support for Oracle -* Mark cached queries in log (Feature Request #405) -* Implement Bcrypt->needs_reshash() -* Add entropy to SQL cache hash; Add uuid() method to DB backends -* Find real document root; Simplify debug paths -* Permit OpenID required fields to be declared as comma-separated string or - array -* Pass modified filename as argument to user-defined function in - Web->receive() -* Quote keys in optional SQL clauses (Issue #408) -* Allow UNLOAD to override fatal error detection (Issue #404) -* Mutex operator precedence error (Issue #406) -* Bug fix: exists() malfunction (Issue #401) -* Bug fix: Jig mapper triggers error when loading from CACHE (Issue #403) -* Bug fix: Array index check -* Bug fix: OpenID verified() return value -* Bug fix: Basket->find() should return a set of results (Issue #407); - Also implemented findone() for consistency with mappers -* Bug fix: PostgreSQL last insert ID (Issue #410) -* Bug fix: $port component URL overwritten by _socket() -* Bug fix: Calculation of elapsed time - -3.1.0 (20 August 2013) -* NEW: Web->filler() returns a chunk of text from the standard - Lorem Ipsum passage -* Change in behavior: Drop support for JSON serialization -* SQL->exec() now returns value of RETURNING clause -* Add support for $ttl argument in count() (Issue #393) -* Allow UI to be overridden by custom $path -* Return result of PDO primitives: begintransaction(), rollback(), and - commit() -* Full support for PHP 5.5 -* Flush buffers only when DEBUG=0 -* Support class->method, class::method, and lambda functions as - Web->basic() arguments -* Commit session on Basket->save() -* Optional enlargement in Image->resize() -* Support authentication on hosts running PHP-CGI -* Change visibility level of Cache properties -* Prevent ONERROR recursion -* Work around Apache pre-2.4 VirtualDocumentRoot bug -* Prioritize cURL in HTTP engine detection -* Bug fix: Minify tricky JS -* Bug fix: desktop() detection -* Bug fix: Double-slash on TEMP-relative path -* Bug fix: Cursor mapping of first() and last() records -* Bug fix: Premature end of Web->receive() on multiple files -* Bug fix: German umlaute to its corresponding grammatically-correct - equivalent - -3.0.9 (12 June 2013) -* NEW: Web->whois() -* NEW: Template tags -* Improve CACHE consistency -* Case-insensitive MIME type detection -* Support pre-PHP 5.3.4 in Prefab->instance() -* Refactor isdesktop() and ismobile(); Add isbot() -* Add support for Markdown strike-through -* Work around ODBC's lack of quote() support -* Remove useless Prefab destructor -* Support multiple cache instances -* Bug fix: Underscores in OpenId keys mangled -* Refactor format() -* Numerous tweaks -* Bug fix: MongoId object not preserved -* Bug fix: Double-quotes included in lexicon() string (Issue #341) -* Bug fix: UTF-8 formatting mangled on Windows (Issue #342) -* Bug fix: Cache->load() error when CACHE is FALSE (Issue #344) -* Bug fix: send() ternary expression -* Bug fix: Country code constants - -3.0.8 (17 May 2013) -* NEW: Bcrypt lightweight hashing library\ -* Return total number of records in superset in Cursor->paginate() -* ONERROR short-circuit (Enhancement #334) -* Apply quotes/backticks on DB identifiers -* Allow enabling/disabling of SQL log -* Normalize glob() behavior (Issue #330) -* Bug fix: mbstring 2-byte text truncation (Issue #325) -* Bug fix: Unsupported operand types (Issue #324) - -3.0.7 (2 May 2013) -* NEW: route() now allows an array of routing patterns as first argument; - support array as first argument of map() -* NEW: entropy() for calculating password strength (NIST 800-63) -* NEW: AGENT variable containing auto-detected HTTP user agent string -* NEW: ismobile() and isdesktop() methods -* NEW: Prefab class and descendants now accept constructor arguments -* Change in behavior: Cache->exists() now returns timestamp and TTL of - cache entry or FALSE if not found (Feature request #315) -* Preserve timestamp and TTL when updating cache entry (Feature request - #316) -* Improved currency formatting with C99 compliance -* Suppress unnecessary program halt at startup caused by misconfigured - server -* Add support for dashes in custom attribute names in templates -* Bug fix: Routing precedene (Issue #313) -* Bug fix: Remove Jig _id element from document property -* Bug fix: Web->rss() error when not enough items in the feed (Issue #299) -* Bug fix: Web engine fallback (Issue #300) -* Bug fix: and formatting -* Bug fix: Text rendering of text with trailing punctuation (Issue #303) -* Bug fix: Incorrect regex in SMTP - -3.0.6 (31 Mar 2013) -* NEW: Image->crop() -* Modify documentation blocks for PHPDoc interoperability -* Allow user to control whether Base->rerouet() uses a permanent or - temporary redirect -* Allow JAR elements to be set individually -* Refactor DB\SQL\Mapper->insert() to cope with autoincrement fields -* Trigger error when captcha() font is missing -* Remove unnecessary markdown regex recursion -* Check for scalars instead of DB\SQL strings -* Implement more comprehensive diacritics table -* Add option for disabling 401 errors when basic auth() fails -* Add markdown syntax highlighting for Apache configuration -* Markdown->render() deprecated to remove dependency on UI variable; - Feature replaced by Markdown->convert() to enable translation from - markdown string to HTML -* Optimize factory() code of all data mappers -* Apply backticks on MySQL table names -* Bug fix: Routing failure when directory path contains a tilde (Issue #291) -* Bug fix: Incorrect markdown parsing of strong/em sequences and inline HTML -* Bug fix: Cached page not echoed (Issue #278) -* Bug fix: Object properties not escaped when rendering -* Bug fix: OpenID error response ignored -* Bug fix: memcache_get_extended_stats() timeout -* Bug fix: Base->set() doesn't pass TTL to Cache->set() -* Bug fix: Base->scrub() ignores pass-thru * argument (Issue #274) - -3.0.5 (16 Feb 2013) -* NEW: Markdown class with PHP, HTML, and .ini syntax highlighting support -* NEW: Options for caching of select() and find() results -* NEW: Web->acceptable() -* Add send() argument for forcing downloads -* Provide read() option for applying Unix LF as standard line ending -* Bypass lexicon() call if LANGUAGE is undefined -* Load fallback language dictionary if LANGUAGE is undefined -* map() now checks existence of class/methods for non-tokenized URLs -* Improve error reporting of non-existent Template methods -* Address output buffer issues on some servers -* Bug fix: Setting DEBUG to 0 won't suppress the stack trace when the - content type is application/json (Issue #257) -* Bug fix: Image dump/render additional arguments shifted -* Bug fix: ob_clean() causes buffer issues with zlib compression -* Bug fix: minify() fails when commenting CSS @ rules (Issue #251) -* Bug fix: Handling of commas inside quoted strings -* Bug fix: Glitch in stringify() handling of closures -* Bug fix: dry() in mappers returns TRUE despite being hydrated by - factory() (Issue #265) -* Bug fix: expect() not handling flags correctly -* Bug fix: weather() fails when server is unreachable - -3.0.4 (29 Jan 2013) -* NEW: Support for ICU/CLDR pluralization -* NEW: User-defined FALLBACK language -* NEW: minify() now recognizes CSS @import directives -* NEW: UTF->bom() returns byte order mark for UTF-8 encoding -* Expose SQL\Mapper->schema() -* Change in behavior: Send error response as JSON string if AJAX request is - detected -* Deprecated: afind*() methods -* Discard output buffer in favor of debug output -* Make _id available to Jig queries -* Magic class now implements ArrayAccess -* Abort execution on startup errors -* Suppress stack trace on DEBUG level 0 -* Allow single = as equality operator in Jig query expressions -* Abort OpenID discovery if Web->request() fails -* Mimic PHP *RECURSION* in stringify() -* Modify Jig parser to allow wildcard-search using preg_match() -* Abort execution after error() execution -* Concatenate cached/uncached minify() iterations; Prevent spillover - caching of previous minify() result -* Work around obscure PHP session id regeneration bug -* Revise algorithm for Jig filter involving undefined fields (Issue #230) -* Use checkdnsrr() instead of gethostbyname() in DNSBL check -* Auto-adjust pagination to cursor boundaries -* Add Romanian diacritics -* Bug fix: Root namespace reference and sorting with undefined Jig fields -* Bug fix: Greedy receive() regex -* Bug fix: Default LANGUAGE always 'en' -* Bug fix: minify() hammers cache backend -* Bug fix: Previous values of primary keys not saved during factory() - instantiation -* Bug fix: Jig find() fails when search key is not present in all records -* Bug fix: Jig SORT_DESC (Issue #233) -* Bug fix: Error reporting (Issue #225) -* Bug fix: language() return value - -3.0.3 (29 Dec 2013) -* NEW: [ajax] and [sync] routing pattern modifiers -* NEW: Basket class (session-based pseudo-mapper, shopping cart, etc.) -* NEW: Test->message() method -* NEW: DB profiling via DB->log() -* NEW: Matrix->calendar() -* NEW: Audit->card() and Audit->mod10() for credit card verification -* NEW: Geo->weather() -* NEW: Base->relay() accepts comma-separated callbacks; but unlike - Base->chain(), result of previous callback becomes argument of the next -* Numerous performance tweaks -* Interoperability with new MongoClient class -* Web->request() now recognizes gzip and deflate encoding -* Differences in behavior of Web->request() engines rectified -* mutex() now uses an ID as argument (instead of filename to make it clear - that specified file is not the target being locked, but a primitive - cross-platform semaphore) -* DB\SQL\Mapper field _id now returned even in the absence of any - auto-increment field -* Magic class spinned off as a separate file -* ISO 3166-1 alpha-2 table updated -* Apache redirect emulation for PHP 5.4 CLI server mode -* Framework instance now passed as argument to any user-defined shutdown - function -* Cache engine now used as storage for Web->minify() output -* Flag added for enabling/disabling Image class filter history -* Bug fix: Trailing routing token consumes HTTP query -* Bug fix: LANGUAGE spills over to LOCALES setting -* Bug fix: Inconsistent dry() return value -* Bug fix: URL-decoding - -3.0.2 (23 Dec 2013) -* NEW: Syntax-highlighted stack traces via Base->highlight(); boolean - HIGHLIGHT global variable can be used to enable/disable this feature -* NEW: Template engine tag -* NEW: Image->captcha() -* NEW: DNSBL-based spammer detection (ported from 2.x) -* NEW: paginate(), first(), and last() methods for data mappers -* NEW: X-HTTP-Method-Override header now recognized -* NEW: Base->chain() method for executing callbacks in succession -* NEW: HOST global variable; derived from either $_SERVER['SERVER_NAME'] or - gethostname() -* NEW: REALM global variable representing full canonical URI -* NEW: Auth plug-in -* NEW: Pingback plug-in (implements both Pingback 1.0 protocol client and - server) -* NEW: DEBUG verbosity can now reach up to level 3; Base->stringify() drills - down to object properties at this setting -* NEW: HTTP PATCH method added to recognized HTTP ReST methods -* Web->slug() now trims trailing dashes -* Web->request() now allows relative local URLs as argument -* Use of PARAMS in route handlers now unnecessary; framework now passes two - arguments to route handlers: the framework object instance and an array - containing the captured values of tokens in route patterns -* Standardized timeout settings among Web->request() backends -* Session IDs regenerated for additional security -* Automatic HTTP 404 responses by Base->call() now restricted to route - handlers -* Empty comments in ini-style files now parsed properly -* Use file_get_contents() in methods that don't involve high concurrency - -3.0.1 (14 Dec 2013) -* Major rewrite of much of the framework's core features diff --git a/app/lib/COPYING b/app/lib/COPYING deleted file mode 100644 index 3c7236c8..00000000 --- a/app/lib/COPYING +++ /dev/null @@ -1,621 +0,0 @@ -GNU GENERAL PUBLIC LICENSE -Version 3, 29 June 2007 - -Copyright (C) 2007 Free Software Foundation, Inc. -Everyone is permitted to copy and distribute verbatim copies -of this license document, but changing it is not allowed. - -Preamble - -The GNU General Public License is a free, copyleft license for -software and other kinds of works. - -The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - -When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - -To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - -For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - -Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - -Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - -Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - -The precise terms and conditions for copying, distribution and -modification follow. - -TERMS AND CONDITIONS - -0. Definitions. - -"This License" refers to version 3 of the GNU General Public License. - -"Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - -"The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - -To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - -A "covered work" means either the unmodified Program or a work based -on the Program. - -To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - -To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - -An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - -1. Source Code. - -The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - -A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - -The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - -The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - -The Corresponding Source for a work in source code form is that -same work. - -2. Basic Permissions. - -All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - -3. Protecting Users' Legal Rights From Anti-Circumvention Law. - -No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - -When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - -4. Conveying Verbatim Copies. - -You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - -5. Conveying Modified Source Versions. - -You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - -a) The work must carry prominent notices stating that you modified -it, and giving a relevant date. - -b) The work must carry prominent notices stating that it is -released under this License and any conditions added under section -7. This requirement modifies the requirement in section 4 to -"keep intact all notices". - -c) You must license the entire work, as a whole, under this -License to anyone who comes into possession of a copy. This -License will therefore apply, along with any applicable section 7 -additional terms, to the whole of the work, and all its parts, -regardless of how they are packaged. This License gives no -permission to license the work in any other way, but it does not -invalidate such permission if you have separately received it. - -d) If the work has interactive user interfaces, each must display -Appropriate Legal Notices; however, if the Program has interactive -interfaces that do not display Appropriate Legal Notices, your -work need not make them do so. - -A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - -6. Conveying Non-Source Forms. - -You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - -a) Convey the object code in, or embodied in, a physical product -(including a physical distribution medium), accompanied by the -Corresponding Source fixed on a durable physical medium -customarily used for software interchange. - -b) Convey the object code in, or embodied in, a physical product -(including a physical distribution medium), accompanied by a -written offer, valid for at least three years and valid for as -long as you offer spare parts or customer support for that product -model, to give anyone who possesses the object code either (1) a -copy of the Corresponding Source for all the software in the -product that is covered by this License, on a durable physical -medium customarily used for software interchange, for a price no -more than your reasonable cost of physically performing this -conveying of source, or (2) access to copy the -Corresponding Source from a network server at no charge. - -c) Convey individual copies of the object code with a copy of the -written offer to provide the Corresponding Source. This -alternative is allowed only occasionally and noncommercially, and -only if you received the object code with such an offer, in accord -with subsection 6b. - -d) Convey the object code by offering access from a designated -place (gratis or for a charge), and offer equivalent access to the -Corresponding Source in the same way through the same place at no -further charge. You need not require recipients to copy the -Corresponding Source along with the object code. If the place to -copy the object code is a network server, the Corresponding Source -may be on a different server (operated by you or a third party) -that supports equivalent copying facilities, provided you maintain -clear directions next to the object code saying where to find the -Corresponding Source. Regardless of what server hosts the -Corresponding Source, you remain obligated to ensure that it is -available for as long as needed to satisfy these requirements. - -e) Convey the object code using peer-to-peer transmission, provided -you inform other peers where the object code and Corresponding -Source of the work are being offered to the general public at no -charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - -A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - -"Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - -If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - -The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - -Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - -7. Additional Terms. - -"Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - -Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - -a) Disclaiming warranty or limiting liability differently from the -terms of sections 15 and 16 of this License; or - -b) Requiring preservation of specified reasonable legal notices or -author attributions in that material or in the Appropriate Legal -Notices displayed by works containing it; or - -c) Prohibiting misrepresentation of the origin of that material, or -requiring that modified versions of such material be marked in -reasonable ways as different from the original version; or - -d) Limiting the use for publicity purposes of names of licensors or -authors of the material; or - -e) Declining to grant rights under trademark law for use of some -trade names, trademarks, or service marks; or - -f) Requiring indemnification of licensors and authors of that -material by anyone who conveys the material (or modified versions of -it) with contractual assumptions of liability to the recipient, for -any liability that these contractual assumptions directly impose on -those licensors and authors. - -All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - -8. Termination. - -You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - -However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - -Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - -Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - -9. Acceptance Not Required for Having Copies. - -You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - -10. Automatic Licensing of Downstream Recipients. - -Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - -An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - -You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - -11. Patents. - -A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - -A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - -In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - -If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - -A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - -12. No Surrender of Others' Freedom. - -If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - -13. Use with the GNU Affero General Public License. - -Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - -14. Revised Versions of this License. - -The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - -If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - -Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - -15. Disclaimer of Warranty. - -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. Limitation of Liability. - -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - -17. Interpretation of Sections 15 and 16. - -If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - -END OF TERMS AND CONDITIONS diff --git a/app/lib/LICENSE b/app/lib/LICENSE deleted file mode 100644 index 3c7236c8..00000000 --- a/app/lib/LICENSE +++ /dev/null @@ -1,621 +0,0 @@ -GNU GENERAL PUBLIC LICENSE -Version 3, 29 June 2007 - -Copyright (C) 2007 Free Software Foundation, Inc. -Everyone is permitted to copy and distribute verbatim copies -of this license document, but changing it is not allowed. - -Preamble - -The GNU General Public License is a free, copyleft license for -software and other kinds of works. - -The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - -When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - -To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - -For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - -Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - -Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - -Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - -The precise terms and conditions for copying, distribution and -modification follow. - -TERMS AND CONDITIONS - -0. Definitions. - -"This License" refers to version 3 of the GNU General Public License. - -"Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - -"The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - -To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - -A "covered work" means either the unmodified Program or a work based -on the Program. - -To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - -To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - -An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - -1. Source Code. - -The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - -A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - -The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - -The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - -The Corresponding Source for a work in source code form is that -same work. - -2. Basic Permissions. - -All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - -3. Protecting Users' Legal Rights From Anti-Circumvention Law. - -No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - -When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - -4. Conveying Verbatim Copies. - -You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - -5. Conveying Modified Source Versions. - -You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - -a) The work must carry prominent notices stating that you modified -it, and giving a relevant date. - -b) The work must carry prominent notices stating that it is -released under this License and any conditions added under section -7. This requirement modifies the requirement in section 4 to -"keep intact all notices". - -c) You must license the entire work, as a whole, under this -License to anyone who comes into possession of a copy. This -License will therefore apply, along with any applicable section 7 -additional terms, to the whole of the work, and all its parts, -regardless of how they are packaged. This License gives no -permission to license the work in any other way, but it does not -invalidate such permission if you have separately received it. - -d) If the work has interactive user interfaces, each must display -Appropriate Legal Notices; however, if the Program has interactive -interfaces that do not display Appropriate Legal Notices, your -work need not make them do so. - -A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - -6. Conveying Non-Source Forms. - -You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - -a) Convey the object code in, or embodied in, a physical product -(including a physical distribution medium), accompanied by the -Corresponding Source fixed on a durable physical medium -customarily used for software interchange. - -b) Convey the object code in, or embodied in, a physical product -(including a physical distribution medium), accompanied by a -written offer, valid for at least three years and valid for as -long as you offer spare parts or customer support for that product -model, to give anyone who possesses the object code either (1) a -copy of the Corresponding Source for all the software in the -product that is covered by this License, on a durable physical -medium customarily used for software interchange, for a price no -more than your reasonable cost of physically performing this -conveying of source, or (2) access to copy the -Corresponding Source from a network server at no charge. - -c) Convey individual copies of the object code with a copy of the -written offer to provide the Corresponding Source. This -alternative is allowed only occasionally and noncommercially, and -only if you received the object code with such an offer, in accord -with subsection 6b. - -d) Convey the object code by offering access from a designated -place (gratis or for a charge), and offer equivalent access to the -Corresponding Source in the same way through the same place at no -further charge. You need not require recipients to copy the -Corresponding Source along with the object code. If the place to -copy the object code is a network server, the Corresponding Source -may be on a different server (operated by you or a third party) -that supports equivalent copying facilities, provided you maintain -clear directions next to the object code saying where to find the -Corresponding Source. Regardless of what server hosts the -Corresponding Source, you remain obligated to ensure that it is -available for as long as needed to satisfy these requirements. - -e) Convey the object code using peer-to-peer transmission, provided -you inform other peers where the object code and Corresponding -Source of the work are being offered to the general public at no -charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - -A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - -"Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - -If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - -The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - -Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - -7. Additional Terms. - -"Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - -Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - -a) Disclaiming warranty or limiting liability differently from the -terms of sections 15 and 16 of this License; or - -b) Requiring preservation of specified reasonable legal notices or -author attributions in that material or in the Appropriate Legal -Notices displayed by works containing it; or - -c) Prohibiting misrepresentation of the origin of that material, or -requiring that modified versions of such material be marked in -reasonable ways as different from the original version; or - -d) Limiting the use for publicity purposes of names of licensors or -authors of the material; or - -e) Declining to grant rights under trademark law for use of some -trade names, trademarks, or service marks; or - -f) Requiring indemnification of licensors and authors of that -material by anyone who conveys the material (or modified versions of -it) with contractual assumptions of liability to the recipient, for -any liability that these contractual assumptions directly impose on -those licensors and authors. - -All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - -8. Termination. - -You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - -However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - -Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - -Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - -9. Acceptance Not Required for Having Copies. - -You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - -10. Automatic Licensing of Downstream Recipients. - -Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - -An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - -You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - -11. Patents. - -A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - -A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - -In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - -If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - -A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - -12. No Surrender of Others' Freedom. - -If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - -13. Use with the GNU Affero General Public License. - -Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - -14. Revised Versions of this License. - -The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - -If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - -Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - -15. Disclaimer of Warranty. - -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. Limitation of Liability. - -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - -17. Interpretation of Sections 15 and 16. - -If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - -END OF TERMS AND CONDITIONS diff --git a/app/lib/audit.php b/app/lib/audit.php deleted file mode 100644 index 0e69b56a..00000000 --- a/app/lib/audit.php +++ /dev/null @@ -1,191 +0,0 @@ -. - -*/ - -//! Data validator -class Audit extends Prefab { - - //@{ User agents - const - UA_Mobile='android|blackberry|phone|ipod|palm|windows\s+ce', - UA_Desktop='bsd|linux|os\s+[x9]|solaris|windows', - UA_Bot='bot|crawl|slurp|spider'; - //@} - - /** - * Return TRUE if string is a valid URL - * @return bool - * @param $str string - **/ - function url($str) { - return is_string(filter_var($str,FILTER_VALIDATE_URL)); - } - - /** - * Return TRUE if string is a valid e-mail address; - * Check DNS MX records if specified - * @return bool - * @param $str string - * @param $mx boolean - **/ - function email($str,$mx=TRUE) { - $hosts=[]; - return is_string(filter_var($str,FILTER_VALIDATE_EMAIL)) && - (!$mx || getmxrr(substr($str,strrpos($str,'@')+1),$hosts)); - } - - /** - * Return TRUE if string is a valid IPV4 address - * @return bool - * @param $addr string - **/ - function ipv4($addr) { - return (bool)filter_var($addr,FILTER_VALIDATE_IP,FILTER_FLAG_IPV4); - } - - /** - * Return TRUE if string is a valid IPV6 address - * @return bool - * @param $addr string - **/ - function ipv6($addr) { - return (bool)filter_var($addr,FILTER_VALIDATE_IP,FILTER_FLAG_IPV6); - } - - /** - * Return TRUE if IP address is within private range - * @return bool - * @param $addr string - **/ - function isprivate($addr) { - return !(bool)filter_var($addr,FILTER_VALIDATE_IP, - FILTER_FLAG_IPV4|FILTER_FLAG_IPV6|FILTER_FLAG_NO_PRIV_RANGE); - } - - /** - * Return TRUE if IP address is within reserved range - * @return bool - * @param $addr string - **/ - function isreserved($addr) { - return !(bool)filter_var($addr,FILTER_VALIDATE_IP, - FILTER_FLAG_IPV4|FILTER_FLAG_IPV6|FILTER_FLAG_NO_RES_RANGE); - } - - /** - * Return TRUE if IP address is neither private nor reserved - * @return bool - * @param $addr string - **/ - function ispublic($addr) { - return (bool)filter_var($addr,FILTER_VALIDATE_IP, - FILTER_FLAG_IPV4|FILTER_FLAG_IPV6| - FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE); - } - - /** - * Return TRUE if user agent is a desktop browser - * @return bool - * @param $agent string - **/ - function isdesktop($agent=NULL) { - if (!isset($agent)) - $agent=Base::instance()->AGENT; - return (bool)preg_match('/('.self::UA_Desktop.')/i',$agent) && - !$this->ismobile($agent); - } - - /** - * Return TRUE if user agent is a mobile device - * @return bool - * @param $agent string - **/ - function ismobile($agent=NULL) { - if (!isset($agent)) - $agent=Base::instance()->AGENT; - return (bool)preg_match('/('.self::UA_Mobile.')/i',$agent); - } - - /** - * Return TRUE if user agent is a Web bot - * @return bool - * @param $agent string - **/ - function isbot($agent=NULL) { - if (!isset($agent)) - $agent=Base::instance()->AGENT; - return (bool)preg_match('/('.self::UA_Bot.')/i',$agent); - } - - /** - * Return TRUE if specified ID has a valid (Luhn) Mod-10 check digit - * @return bool - * @param $id string - **/ - function mod10($id) { - if (!ctype_digit($id)) - return FALSE; - $id=strrev($id); - $sum=0; - for ($i=0,$l=strlen($id);$i<$l;$i++) - $sum+=$id[$i]+$i%2*(($id[$i]>4)*-4+$id[$i]%5); - return !($sum%10); - } - - /** - * Return credit card type if number is valid - * @return string|FALSE - * @param $id string - **/ - function card($id) { - $id=preg_replace('/[^\d]/','',$id); - if ($this->mod10($id)) { - if (preg_match('/^3[47][0-9]{13}$/',$id)) - return 'American Express'; - if (preg_match('/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/',$id)) - return 'Diners Club'; - if (preg_match('/^6(?:011|5[0-9][0-9])[0-9]{12}$/',$id)) - return 'Discover'; - if (preg_match('/^(?:2131|1800|35\d{3})\d{11}$/',$id)) - return 'JCB'; - if (preg_match('/^5[1-5][0-9]{14}$|'. - '^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)\d{12}$/',$id)) - return 'MasterCard'; - if (preg_match('/^4[0-9]{12}(?:[0-9]{3})?$/',$id)) - return 'Visa'; - } - return FALSE; - } - - /** - * Return entropy estimate of a password (NIST 800-63) - * @return int|float - * @param $str string - **/ - function entropy($str) { - $len=strlen($str); - return 4*min($len,1)+($len>1?(2*(min($len,8)-1)):0)+ - ($len>8?(1.5*(min($len,20)-8)):0)+($len>20?($len-20):0)+ - 6*(bool)(preg_match( - '/[A-Z].*?[0-9[:punct:]]|[0-9[:punct:]].*?[A-Z]/',$str)); - } - -} diff --git a/app/lib/auth.php b/app/lib/auth.php deleted file mode 100644 index d043cf22..00000000 --- a/app/lib/auth.php +++ /dev/null @@ -1,262 +0,0 @@ -. - -*/ - -//! Authorization/authentication plug-in -class Auth { - - //@{ Error messages - const - E_LDAP='LDAP connection failure', - E_SMTP='SMTP connection failure'; - //@} - - protected - //! Auth storage - $storage, - //! Mapper object - $mapper, - //! Storage options - $args, - //! Custom compare function - $func; - - /** - * Jig storage handler - * @return bool - * @param $id string - * @param $pw string - * @param $realm string - **/ - protected function _jig($id,$pw,$realm) { - $success = (bool) - call_user_func_array( - [$this->mapper,'load'], - [ - array_merge( - [ - '@'.$this->args['id'].'==?'. - ($this->func?'':' AND @'.$this->args['pw'].'==?'). - (isset($this->args['realm'])? - (' AND @'.$this->args['realm'].'==?'):''), - $id - ], - ($this->func?[]:[$pw]), - (isset($this->args['realm'])?[$realm]:[]) - ) - ] - ); - if ($success && $this->func) - $success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw'])); - return $success; - } - - /** - * MongoDB storage handler - * @return bool - * @param $id string - * @param $pw string - * @param $realm string - **/ - protected function _mongo($id,$pw,$realm) { - $success = (bool) - $this->mapper->load( - [$this->args['id']=>$id]+ - ($this->func?[]:[$this->args['pw']=>$pw])+ - (isset($this->args['realm'])? - [$this->args['realm']=>$realm]:[]) - ); - if ($success && $this->func) - $success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw'])); - return $success; - } - - /** - * SQL storage handler - * @return bool - * @param $id string - * @param $pw string - * @param $realm string - **/ - protected function _sql($id,$pw,$realm) { - $success = (bool) - call_user_func_array( - [$this->mapper,'load'], - [ - array_merge( - [ - $this->args['id'].'=?'. - ($this->func?'':' AND '.$this->args['pw'].'=?'). - (isset($this->args['realm'])? - (' AND '.$this->args['realm'].'=?'):''), - $id - ], - ($this->func?[]:[$pw]), - (isset($this->args['realm'])?[$realm]:[]) - ) - ] - ); - if ($success && $this->func) - $success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw'])); - return $success; - } - - /** - * LDAP storage handler - * @return bool - * @param $id string - * @param $pw string - **/ - protected function _ldap($id,$pw) { - $port=(int)($this->args['port']?:389); - $filter=$this->args['filter']=$this->args['filter']?:"uid=".$id; - $this->args['attr']=$this->args['attr']?:["uid"]; - array_walk($this->args['attr'], - function($attr)use(&$filter,$id) { - $filter=str_ireplace($attr."=*",$attr."=".$id,$filter);}); - $dc=@ldap_connect($this->args['dc'],$port); - if ($dc && - ldap_set_option($dc,LDAP_OPT_PROTOCOL_VERSION,3) && - ldap_set_option($dc,LDAP_OPT_REFERRALS,0) && - ldap_bind($dc,$this->args['rdn'],$this->args['pw']) && - ($result=ldap_search($dc,$this->args['base_dn'], - $filter,$this->args['attr'])) && - ldap_count_entries($dc,$result) && - ($info=ldap_get_entries($dc,$result)) && - $info['count']==1 && - @ldap_bind($dc,$info[0]['dn'],$pw) && - @ldap_close($dc)) { - return in_array($id,(array_map(function($value){return $value[0];}, - array_intersect_key($info[0], - array_flip($this->args['attr'])))),TRUE); - } - user_error(self::E_LDAP,E_USER_ERROR); - } - - /** - * SMTP storage handler - * @return bool - * @param $id string - * @param $pw string - **/ - protected function _smtp($id,$pw) { - $socket=@fsockopen( - (strtolower($this->args['scheme'])=='ssl'? - 'ssl://':'').$this->args['host'], - $this->args['port']); - $dialog=function($cmd=NULL) use($socket) { - if (!is_null($cmd)) - fputs($socket,$cmd."\r\n"); - $reply=''; - while (!feof($socket) && - ($info=stream_get_meta_data($socket)) && - !$info['timed_out'] && $str=fgets($socket,4096)) { - $reply.=$str; - if (preg_match('/(?:^|\n)\d{3} .+\r\n/s', - $reply)) - break; - } - return $reply; - }; - if ($socket) { - stream_set_blocking($socket,TRUE); - $dialog(); - $fw=Base::instance(); - $dialog('EHLO '.$fw->HOST); - if (strtolower($this->args['scheme'])=='tls') { - $dialog('STARTTLS'); - stream_socket_enable_crypto( - $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); - $dialog('EHLO '.$fw->HOST); - } - // Authenticate - $dialog('AUTH LOGIN'); - $dialog(base64_encode($id)); - $reply=$dialog(base64_encode($pw)); - $dialog('QUIT'); - fclose($socket); - return (bool)preg_match('/^235 /',$reply); - } - user_error(self::E_SMTP,E_USER_ERROR); - } - - /** - * Login auth mechanism - * @return bool - * @param $id string - * @param $pw string - * @param $realm string - **/ - function login($id,$pw,$realm=NULL) { - return $this->{'_'.$this->storage}($id,$pw,$realm); - } - - /** - * HTTP basic auth mechanism - * @return bool - * @param $func callback - **/ - function basic($func=NULL) { - $fw=Base::instance(); - $realm=$fw->REALM; - $hdr=NULL; - if (isset($_SERVER['HTTP_AUTHORIZATION'])) - $hdr=$_SERVER['HTTP_AUTHORIZATION']; - elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) - $hdr=$_SERVER['REDIRECT_HTTP_AUTHORIZATION']; - if (!empty($hdr)) - list($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW'])= - explode(':',base64_decode(substr($hdr,6))); - if (isset($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']) && - $this->login( - $_SERVER['PHP_AUTH_USER'], - $func? - $fw->call($func,$_SERVER['PHP_AUTH_PW']): - $_SERVER['PHP_AUTH_PW'], - $realm - )) - return TRUE; - if (PHP_SAPI!='cli') - header('WWW-Authenticate: Basic realm="'.$realm.'"'); - $fw->status(401); - return FALSE; - } - - /** - * Instantiate class - * @return object - * @param $storage string|object - * @param $args array - * @param $func callback - **/ - function __construct($storage,array $args=NULL,$func=NULL) { - if (is_object($storage) && is_a($storage,'DB\Cursor')) { - $this->storage=$storage->dbtype(); - $this->mapper=$storage; - unset($ref); - } - else - $this->storage=$storage; - $this->args=$args; - $this->func=$func; - } - -} diff --git a/app/lib/base.php b/app/lib/base.php deleted file mode 100644 index 8990c299..00000000 --- a/app/lib/base.php +++ /dev/null @@ -1,3496 +0,0 @@ -. - -*/ - -//! Factory class for single-instance objects -abstract class Prefab { - - /** - * Return class instance - * @return static - **/ - static function instance() { - if (!Registry::exists($class=get_called_class())) { - $ref=new Reflectionclass($class); - $args=func_get_args(); - Registry::set($class, - $args?$ref->newinstanceargs($args):new $class); - } - return Registry::get($class); - } - -} - -//! Base structure -final class Base extends Prefab implements ArrayAccess { - - //@{ Framework details - const - PACKAGE='Fat-Free Framework', - VERSION='3.6.5-Release'; - //@} - - //@{ HTTP status codes (RFC 2616) - const - HTTP_100='Continue', - HTTP_101='Switching Protocols', - HTTP_103='Early Hints', - HTTP_200='OK', - HTTP_201='Created', - HTTP_202='Accepted', - HTTP_203='Non-Authorative Information', - HTTP_204='No Content', - HTTP_205='Reset Content', - HTTP_206='Partial Content', - HTTP_300='Multiple Choices', - HTTP_301='Moved Permanently', - HTTP_302='Found', - HTTP_303='See Other', - HTTP_304='Not Modified', - HTTP_305='Use Proxy', - HTTP_307='Temporary Redirect', - HTTP_400='Bad Request', - HTTP_401='Unauthorized', - HTTP_402='Payment Required', - HTTP_403='Forbidden', - HTTP_404='Not Found', - HTTP_405='Method Not Allowed', - HTTP_406='Not Acceptable', - HTTP_407='Proxy Authentication Required', - HTTP_408='Request Timeout', - HTTP_409='Conflict', - HTTP_410='Gone', - HTTP_411='Length Required', - HTTP_412='Precondition Failed', - HTTP_413='Request Entity Too Large', - HTTP_414='Request-URI Too Long', - HTTP_415='Unsupported Media Type', - HTTP_416='Requested Range Not Satisfiable', - HTTP_417='Expectation Failed', - HTTP_500='Internal Server Error', - HTTP_501='Not Implemented', - HTTP_502='Bad Gateway', - HTTP_503='Service Unavailable', - HTTP_504='Gateway Timeout', - HTTP_505='HTTP Version Not Supported'; - //@} - - const - //! Mapped PHP globals - GLOBALS='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV', - //! HTTP verbs - VERBS='GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS', - //! Default directory permissions - MODE=0755, - //! Syntax highlighting stylesheet - CSS='code.css'; - - //@{ Request types - const - REQ_SYNC=1, - REQ_AJAX=2, - REQ_CLI=4; - //@} - - //@{ Error messages - const - E_Pattern='Invalid routing pattern: %s', - E_Named='Named route does not exist: %s', - E_Fatal='Fatal error: %s', - E_Open='Unable to open %s', - E_Routes='No routes specified', - E_Class='Invalid class %s', - E_Method='Invalid method %s', - E_Hive='Invalid hive key %s'; - //@} - - private - //! Globals - $hive, - //! Initial settings - $init, - //! Language lookup sequence - $languages, - //! Mutex locks - $locks=[], - //! Default fallback language - $fallback='en'; - - /** - * Sync PHP global with corresponding hive key - * @return array - * @param $key string - **/ - function sync($key) { - return $this->hive[$key]=&$GLOBALS['_'.$key]; - } - - /** - * Return the parts of specified hive key - * @return array - * @param $key string - **/ - private function cut($key) { - return preg_split('/\[\h*[\'"]?(.+?)[\'"]?\h*\]|(->)|\./', - $key,NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE); - } - - /** - * Replace tokenized URL with available token values - * @return string - * @param $url array|string - * @param $args array - **/ - function build($url,$args=[]) { - $args+=$this->hive['PARAMS']; - if (is_array($url)) - foreach ($url as &$var) { - $var=$this->build($var,$args); - unset($var); - } - else { - $i=0; - $url=preg_replace_callback('/@(\w+)|(\*)/', - function($match) use(&$i,$args) { - if (isset($match[1]) && - array_key_exists($match[1],$args)) - return $args[$match[1]]; - if (isset($match[2]) && - array_key_exists($match[2],$args)) { - if (!is_array($args[$match[2]])) - return $args[$match[2]]; - $i++; - return $args[$match[2]][$i-1]; - } - return $match[0]; - },$url); - } - return $url; - } - - /** - * Parse string containing key-value pairs - * @return array - * @param $str string - **/ - function parse($str) { - preg_match_all('/(\w+|\*)\h*=\h*(?:\[(.+?)\]|(.+?))(?=,|$)/', - $str,$pairs,PREG_SET_ORDER); - $out=[]; - foreach ($pairs as $pair) - if ($pair[2]) { - $out[$pair[1]]=[]; - foreach (explode(',',$pair[2]) as $val) - array_push($out[$pair[1]],$val); - } - else - $out[$pair[1]]=trim($pair[3]); - return $out; - } - - /** - * Cast string variable to PHP type or constant - * @param $val - * @return mixed - */ - function cast($val) { - if (preg_match('/^(?:0x[0-9a-f]+|0[0-7]+|0b[01]+)$/i',$val)) - return intval($val,0); - if (is_numeric($val)) - return $val+0; - $val=trim($val); - if (preg_match('/^\w+$/i',$val) && defined($val)) - return constant($val); - return $val; - } - - /** - * Convert JS-style token to PHP expression - * @return string - * @param $str string - **/ - function compile($str) { - return preg_replace_callback( - '/(?|::)\w+)?)'. - '((?:\.\w+|\[(?:(?:[^\[\]]*|(?R))*)\]|(?:\->|::)\w+|\()*)/', - function($expr) { - $str='$'.$expr[1]; - if (isset($expr[2])) - $str.=preg_replace_callback( - '/\.(\w+)(\()?|\[((?:[^\[\]]*|(?R))*)\]/', - function($sub) { - if (empty($sub[2])) { - if (ctype_digit($sub[1])) - $sub[1]=(int)$sub[1]; - $out='['. - (isset($sub[3])? - $this->compile($sub[3]): - $this->export($sub[1])). - ']'; - } - else - $out=function_exists($sub[1])? - $sub[0]: - ('['.$this->export($sub[1]).']'.$sub[2]); - return $out; - }, - $expr[2] - ); - return $str; - }, - $str - ); - } - - /** - * Get hive key reference/contents; Add non-existent hive keys, - * array elements, and object properties by default - * @return mixed - * @param $key string - * @param $add bool - * @param $var mixed - **/ - function &ref($key,$add=TRUE,&$var=NULL) { - $null=NULL; - $parts=$this->cut($key); - if ($parts[0]=='SESSION') { - if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) - session_start(); - $this->sync('SESSION'); - } - elseif (!preg_match('/^\w+$/',$parts[0])) - user_error(sprintf(self::E_Hive,$this->stringify($key)), - E_USER_ERROR); - if (is_null($var)) { - if ($add) - $var=&$this->hive; - else - $var=$this->hive; - } - $obj=FALSE; - foreach ($parts as $part) - if ($part=='->') - $obj=TRUE; - elseif ($obj) { - $obj=FALSE; - if (!is_object($var)) - $var=new stdclass; - if ($add || property_exists($var,$part)) - $var=&$var->$part; - else { - $var=&$null; - break; - } - } - else { - if (!is_array($var)) - $var=[]; - if ($add || array_key_exists($part,$var)) - $var=&$var[$part]; - else { - $var=&$null; - break; - } - } - return $var; - } - - /** - * Return TRUE if hive key is set - * (or return timestamp and TTL if cached) - * @return bool - * @param $key string - * @param $val mixed - **/ - function exists($key,&$val=NULL) { - $val=$this->ref($key,FALSE); - return isset($val)? - TRUE: - (Cache::instance()->exists($this->hash($key).'.var',$val)?:FALSE); - } - - /** - * Return TRUE if hive key is empty and not cached - * @param $key string - * @param $val mixed - * @return bool - **/ - function devoid($key,&$val=NULL) { - $val=$this->ref($key,FALSE); - return empty($val) && - (!Cache::instance()->exists($this->hash($key).'.var',$val) || - !$val); - } - - /** - * Bind value to hive key - * @return mixed - * @param $key string - * @param $val mixed - * @param $ttl int - **/ - function set($key,$val,$ttl=0) { - $time=(int)$this->hive['TIME']; - if (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) { - $this->set('REQUEST'.$expr[2],$val); - if ($expr[1]=='COOKIE') { - $parts=$this->cut($key); - $jar=$this->unserialize($this->serialize($this->hive['JAR'])); - unset($jar['lifetime']); - if (isset($_COOKIE[$parts[1]])) - call_user_func_array('setcookie', - array_merge([$parts[1],NULL],['expire'=>0]+$jar)); - if ($ttl) - $jar['expire']=$time+$ttl; - call_user_func_array('setcookie',[$parts[1],$val]+$jar); - $_COOKIE[$parts[1]]=$val; - return $val; - } - } - else switch ($key) { - case 'CACHE': - $val=Cache::instance()->load($val); - break; - case 'ENCODING': - ini_set('default_charset',$val); - if (extension_loaded('mbstring')) - mb_internal_encoding($val); - break; - case 'FALLBACK': - $this->fallback=$val; - $lang=$this->language($this->hive['LANGUAGE']); - case 'LANGUAGE': - if (!isset($lang)) - $val=$this->language($val); - $lex=$this->lexicon($this->hive['LOCALES'],$ttl); - case 'LOCALES': - if (isset($lex) || $lex=$this->lexicon($val,$ttl)) - foreach ($lex as $dt=>$dd) { - $ref=&$this->ref($this->hive['PREFIX'].$dt); - $ref=$dd; - unset($ref); - } - break; - case 'TZ': - date_default_timezone_set($val); - break; - } - $ref=&$this->ref($key); - $ref=$val; - if (preg_match('/^JAR\b/',$key)) { - if ($key=='JAR.lifetime') - $this->set('JAR.expire',$val==0?0: - (is_int($val)?$time+$val:strtotime($val))); - else { - if ($key=='JAR.expire') - $this->hive['JAR']['lifetime']=max(0,$val-$time); - $jar=$this->unserialize($this->serialize($this->hive['JAR'])); - unset($jar['expire']); - if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) - call_user_func_array('session_set_cookie_params',$jar); - } - } - if ($ttl) - // Persist the key-value pair - Cache::instance()->set($this->hash($key).'.var',$val,$ttl); - return $ref; - } - - /** - * Retrieve contents of hive key - * @return mixed - * @param $key string - * @param $args string|array - **/ - function get($key,$args=NULL) { - if (is_string($val=$this->ref($key,FALSE)) && !is_null($args)) - return call_user_func_array( - [$this,'format'], - array_merge([$val],is_array($args)?$args:[$args]) - ); - if (is_null($val)) { - // Attempt to retrieve from cache - if (Cache::instance()->exists($this->hash($key).'.var',$data)) - return $data; - } - return $val; - } - - /** - * Unset hive key - * @return NULL - * @param $key string - **/ - function clear($key) { - // Normalize array literal - $cache=Cache::instance(); - $parts=$this->cut($key); - if ($key=='CACHE') - // Clear cache contents - $cache->reset(); - elseif (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) { - $this->clear('REQUEST'.$expr[2]); - if ($expr[1]=='COOKIE') { - $parts=$this->cut($key); - $jar=$this->hive['JAR']; - unset($jar['lifetime']); - $jar['expire']=0; - call_user_func_array('setcookie', - array_merge([$parts[1],NULL],$jar)); - unset($_COOKIE[$parts[1]]); - } - } - elseif ($parts[0]=='SESSION') { - if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) - session_start(); - if (empty($parts[1])) { - // End session - session_unset(); - session_destroy(); - $this->clear('COOKIE.'.session_name()); - } - $this->sync('SESSION'); - } - if (!isset($parts[1]) && array_key_exists($parts[0],$this->init)) - // Reset global to default value - $this->hive[$parts[0]]=$this->init[$parts[0]]; - else { - eval('unset('.$this->compile('@this->hive.'.$key).');'); - if ($parts[0]=='SESSION') { - session_commit(); - session_start(); - } - if ($cache->exists($hash=$this->hash($key).'.var')) - // Remove from cache - $cache->clear($hash); - } - } - - /** - * Return TRUE if hive variable is 'on' - * @return bool - * @param $key string - **/ - function checked($key) { - $ref=&$this->ref($key); - return $ref=='on'; - } - - /** - * Return TRUE if property has public visibility - * @return bool - * @param $obj object - * @param $key string - **/ - function visible($obj,$key) { - if (property_exists($obj,$key)) { - $ref=new ReflectionProperty(get_class($obj),$key); - $out=$ref->ispublic(); - unset($ref); - return $out; - } - return FALSE; - } - - /** - * Multi-variable assignment using associative array - * @return NULL - * @param $vars array - * @param $prefix string - * @param $ttl int - **/ - function mset(array $vars,$prefix='',$ttl=0) { - foreach ($vars as $key=>$val) - $this->set($prefix.$key,$val,$ttl); - } - - /** - * Publish hive contents - * @return array - **/ - function hive() { - return $this->hive; - } - - /** - * Copy contents of hive variable to another - * @return mixed - * @param $src string - * @param $dst string - **/ - function copy($src,$dst) { - $ref=&$this->ref($dst); - return $ref=$this->ref($src,FALSE); - } - - /** - * Concatenate string to hive string variable - * @return string - * @param $key string - * @param $val string - **/ - function concat($key,$val) { - $ref=&$this->ref($key); - $ref.=$val; - return $ref; - } - - /** - * Swap keys and values of hive array variable - * @return array - * @param $key string - * @public - **/ - function flip($key) { - $ref=&$this->ref($key); - return $ref=array_combine(array_values($ref),array_keys($ref)); - } - - /** - * Add element to the end of hive array variable - * @return mixed - * @param $key string - * @param $val mixed - **/ - function push($key,$val) { - $ref=&$this->ref($key); - $ref[]=$val; - return $val; - } - - /** - * Remove last element of hive array variable - * @return mixed - * @param $key string - **/ - function pop($key) { - $ref=&$this->ref($key); - return array_pop($ref); - } - - /** - * Add element to the beginning of hive array variable - * @return mixed - * @param $key string - * @param $val mixed - **/ - function unshift($key,$val) { - $ref=&$this->ref($key); - array_unshift($ref,$val); - return $val; - } - - /** - * Remove first element of hive array variable - * @return mixed - * @param $key string - **/ - function shift($key) { - $ref=&$this->ref($key); - return array_shift($ref); - } - - /** - * Merge array with hive array variable - * @return array - * @param $key string - * @param $src string|array - * @param $keep bool - **/ - function merge($key,$src,$keep=FALSE) { - $ref=&$this->ref($key); - if (!$ref) - $ref=[]; - $out=array_merge($ref,is_string($src)?$this->hive[$src]:$src); - if ($keep) - $ref=$out; - return $out; - } - - /** - * Extend hive array variable with default values from $src - * @return array - * @param $key string - * @param $src string|array - * @param $keep bool - **/ - function extend($key,$src,$keep=FALSE) { - $ref=&$this->ref($key); - if (!$ref) - $ref=[]; - $out=array_replace_recursive( - is_string($src)?$this->hive[$src]:$src,$ref); - if ($keep) - $ref=$out; - return $out; - } - - /** - * Convert backslashes to slashes - * @return string - * @param $str string - **/ - function fixslashes($str) { - return $str?strtr($str,'\\','/'):$str; - } - - /** - * Split comma-, semi-colon, or pipe-separated string - * @return array - * @param $str string - * @param $noempty bool - **/ - function split($str,$noempty=TRUE) { - return array_map('trim', - preg_split('/[,;|]/',$str,0,$noempty?PREG_SPLIT_NO_EMPTY:0)); - } - - /** - * Convert PHP expression/value to compressed exportable string - * @return string - * @param $arg mixed - * @param $stack array - **/ - function stringify($arg,array $stack=NULL) { - if ($stack) { - foreach ($stack as $node) - if ($arg===$node) - return '*RECURSION*'; - } - else - $stack=[]; - switch (gettype($arg)) { - case 'object': - $str=''; - foreach (get_object_vars($arg) as $key=>$val) - $str.=($str?',':''). - $this->export($key).'=>'. - $this->stringify($val, - array_merge($stack,[$arg])); - return get_class($arg).'::__set_state(['.$str.'])'; - case 'array': - $str=''; - $num=isset($arg[0]) && - ctype_digit(implode('',array_keys($arg))); - foreach ($arg as $key=>$val) - $str.=($str?',':''). - ($num?'':($this->export($key).'=>')). - $this->stringify($val,array_merge($stack,[$arg])); - return '['.$str.']'; - default: - return $this->export($arg); - } - } - - /** - * Flatten array values and return as CSV string - * @return string - * @param $args array - **/ - function csv(array $args) { - return implode(',',array_map('stripcslashes', - array_map([$this,'stringify'],$args))); - } - - /** - * Convert snakecase string to camelcase - * @return string - * @param $str string - **/ - function camelcase($str) { - return preg_replace_callback( - '/_(\pL)/u', - function($match) { - return strtoupper($match[1]); - }, - $str - ); - } - - /** - * Convert camelcase string to snakecase - * @return string - * @param $str string - **/ - function snakecase($str) { - return strtolower(preg_replace('/(?!^)\p{Lu}/u','_\0',$str)); - } - - /** - * Return -1 if specified number is negative, 0 if zero, - * or 1 if the number is positive - * @return int - * @param $num mixed - **/ - function sign($num) { - return $num?($num/abs($num)):0; - } - - /** - * Extract values of array whose keys start with the given prefix - * @return array - * @param $arr array - * @param $prefix string - **/ - function extract($arr,$prefix) { - $out=[]; - foreach (preg_grep('/^'.preg_quote($prefix,'/').'/',array_keys($arr)) - as $key) - $out[substr($key,strlen($prefix))]=$arr[$key]; - return $out; - } - - /** - * Convert class constants to array - * @return array - * @param $class object|string - * @param $prefix string - **/ - function constants($class,$prefix='') { - $ref=new ReflectionClass($class); - return $this->extract($ref->getconstants(),$prefix); - } - - /** - * Generate 64bit/base36 hash - * @return string - * @param $str - **/ - function hash($str) { - return str_pad(base_convert( - substr(sha1($str),-16),16,36),11,'0',STR_PAD_LEFT); - } - - /** - * Return Base64-encoded equivalent - * @return string - * @param $data string - * @param $mime string - **/ - function base64($data,$mime) { - return 'data:'.$mime.';base64,'.base64_encode($data); - } - - /** - * Convert special characters to HTML entities - * @return string - * @param $str string - **/ - function encode($str) { - return @htmlspecialchars($str,$this->hive['BITMASK'], - $this->hive['ENCODING'])?:$this->scrub($str); - } - - /** - * Convert HTML entities back to characters - * @return string - * @param $str string - **/ - function decode($str) { - return htmlspecialchars_decode($str,$this->hive['BITMASK']); - } - - /** - * Invoke callback recursively for all data types - * @return mixed - * @param $arg mixed - * @param $func callback - * @param $stack array - **/ - function recursive($arg,$func,$stack=[]) { - if ($stack) { - foreach ($stack as $node) - if ($arg===$node) - return $arg; - } - switch (gettype($arg)) { - case 'object': - $ref=new ReflectionClass($arg); - if ($ref->iscloneable()) { - $arg=clone($arg); - $cast=is_a($arg,'IteratorAggregate')? - iterator_to_array($arg):get_object_vars($arg); - foreach ($cast as $key=>$val) - $arg->$key=$this->recursive( - $val,$func,array_merge($stack,[$arg])); - } - return $arg; - case 'array': - $copy=[]; - foreach ($arg as $key=>$val) - $copy[$key]=$this->recursive($val,$func, - array_merge($stack,[$arg])); - return $copy; - } - return $func($arg); - } - - /** - * Remove HTML tags (except those enumerated) and non-printable - * characters to mitigate XSS/code injection attacks - * @return mixed - * @param $arg mixed - * @param $tags string - **/ - function clean($arg,$tags=NULL) { - return $this->recursive($arg, - function($val) use($tags) { - if ($tags!='*') - $val=trim(strip_tags($val, - '<'.implode('><',$this->split($tags)).'>')); - return trim(preg_replace( - '/[\x00-\x08\x0B\x0C\x0E-\x1F]/','',$val)); - } - ); - } - - /** - * Similar to clean(), except that variable is passed by reference - * @return mixed - * @param $var mixed - * @param $tags string - **/ - function scrub(&$var,$tags=NULL) { - return $var=$this->clean($var,$tags); - } - - /** - * Return locale-aware formatted string - * @return string - **/ - function format() { - $args=func_get_args(); - $val=array_shift($args); - // Get formatting rules - $conv=localeconv(); - return preg_replace_callback( - '/\{\s*(?P\d+)\s*(?:,\s*(?P\w+)\s*'. - '(?:,\s*(?P(?:\w+(?:\s*\{.+?\}\s*,?\s*)?)*)'. - '(?:,\s*(?P.+?))?)?)?\s*\}/', - function($expr) use($args,$conv) { - extract($expr); - extract($conv); - if (!array_key_exists($pos,$args)) - return $expr[0]; - if (isset($type)) { - if (isset($this->hive['FORMATS'][$type])) - return $this->call( - $this->hive['FORMATS'][$type], - [ - $args[$pos], - isset($mod)?$mod:null, - isset($prop)?$prop:null - ] - ); - switch ($type) { - case 'plural': - preg_match_all('/(?\w+)'. - '(?:\s*\{\s*(?.+?)\s*\})/', - $mod,$matches,PREG_SET_ORDER); - $ord=['zero','one','two']; - foreach ($matches as $match) { - extract($match); - if (isset($ord[$args[$pos]]) && - $tag==$ord[$args[$pos]] || $tag=='other') - return str_replace('#',$args[$pos],$data); - } - case 'number': - if (isset($mod)) - switch ($mod) { - case 'integer': - return number_format( - $args[$pos],0,'',$thousands_sep); - case 'currency': - $int=$cstm=FALSE; - if (isset($prop) && - $cstm=!$int=($prop=='int')) - $currency_symbol=$prop; - if (!$cstm && - function_exists('money_format')) - return money_format( - '%'.($int?'i':'n'),$args[$pos]); - $fmt=[ - 0=>'(nc)',1=>'(n c)', - 2=>'(nc)',10=>'+nc', - 11=>'+n c',12=>'+ nc', - 20=>'nc+',21=>'n c+', - 22=>'nc +',30=>'n+c', - 31=>'n +c',32=>'n+ c', - 40=>'nc+',41=>'n c+', - 42=>'nc +',100=>'(cn)', - 101=>'(c n)',102=>'(cn)', - 110=>'+cn',111=>'+c n', - 112=>'+ cn',120=>'cn+', - 121=>'c n+',122=>'cn +', - 130=>'+cn',131=>'+c n', - 132=>'+ cn',140=>'c+n', - 141=>'c+ n',142=>'c +n' - ]; - if ($args[$pos]<0) { - $sgn=$negative_sign; - $pre='n'; - } - else { - $sgn=$positive_sign; - $pre='p'; - } - return str_replace( - ['+','n','c'], - [$sgn,number_format( - abs($args[$pos]), - $frac_digits, - $decimal_point, - $thousands_sep), - $int?$int_curr_symbol - :$currency_symbol], - $fmt[(int)( - (${$pre.'_cs_precedes'}%2). - (${$pre.'_sign_posn'}%5). - (${$pre.'_sep_by_space'}%3) - )] - ); - case 'percent': - return number_format( - $args[$pos]*100,0,$decimal_point, - $thousands_sep).'%'; - } - $frac=$args[$pos]-(int)$args[$pos]; - return number_format( - $args[$pos], - isset($prop)? - $prop: - $frac?strlen($frac)-2:0, - $decimal_point,$thousands_sep); - case 'date': - $prop='%d %B %Y'; - if (empty($mod) || $mod=='short') - $prop='%x'; - elseif ($mod=='full') - $prop='%A, '.$prop; - return strftime($prop,$args[$pos]); - case 'time': - $prop='%r'; - if (empty($mod) || $mod=='short') - $prop='%X'; - return strftime($prop,$args[$pos]); - default: - return $expr[0]; - } - } - return $args[$pos]; - }, - $val - ); - } - - /** - * Return string representation of expression - * @return string - * @param $expr mixed - **/ - function export($expr) { - return var_export($expr,TRUE); - } - - /** - * Assign/auto-detect language - * @return string - * @param $code string - **/ - function language($code) { - $code=preg_replace('/\h+|;q=[0-9.]+/','',$code); - $code.=($code?',':'').$this->fallback; - $this->languages=[]; - foreach (array_reverse(explode(',',$code)) as $lang) - if (preg_match('/^(\w{2})(?:-(\w{2}))?\b/i',$lang,$parts)) { - // Generic language - array_unshift($this->languages,$parts[1]); - if (isset($parts[2])) { - // Specific language - $parts[0]=$parts[1].'-'.($parts[2]=strtoupper($parts[2])); - array_unshift($this->languages,$parts[0]); - } - } - $this->languages=array_unique($this->languages); - $locales=[]; - $windows=preg_match('/^win/i',PHP_OS); - // Work around PHP's Turkish locale bug - foreach (preg_grep('/^(?!tr)/i',$this->languages) as $locale) { - if ($windows) { - $parts=explode('-',$locale); - $locale=@constant('ISO::LC_'.$parts[0]); - if (isset($parts[1]) && - $country=@constant('ISO::CC_'.strtolower($parts[1]))) - $locale.='-'.$country; - } - $locale=str_replace('-','_',$locale); - $locales[]=$locale.'.'.ini_get('default_charset'); - $locales[]=$locale; - } - setlocale(LC_ALL,$locales); - return $this->hive['LANGUAGE']=implode(',',$this->languages); - } - - /** - * Return lexicon entries - * @return array - * @param $path string - * @param $ttl int - **/ - function lexicon($path,$ttl=0) { - $languages=$this->languages?:explode(',',$this->fallback); - $cache=Cache::instance(); - if ($cache->exists( - $hash=$this->hash(implode(',',$languages)).'.dic',$lex)) - return $lex; - $lex=[]; - foreach ($languages as $lang) - foreach ($this->split($path) as $dir) - if ((is_file($file=($base=$dir.$lang).'.php') || - is_file($file=$base.'.php')) && - is_array($dict=require($file))) - $lex+=$dict; - elseif (is_file($file=$base.'.ini')) { - preg_match_all( - '/(?<=^|\n)(?:'. - '\[(?.+?)\]|'. - '(?[^\h\r\n;].*?)\h*=\h*'. - '(?(?:\\\\\h*\r?\n|.+?)*)'. - ')(?=\r?\n|$)/', - $this->read($file),$matches,PREG_SET_ORDER); - if ($matches) { - $prefix=''; - foreach ($matches as $match) - if ($match['prefix']) - $prefix=$match['prefix'].'.'; - elseif (!array_key_exists( - $key=$prefix.$match['lval'],$lex)) - $lex[$key]=trim(preg_replace( - '/\\\\\h*\r?\n/',"\n",$match['rval'])); - } - } - if ($ttl) - $cache->set($hash,$lex,$ttl); - return $lex; - } - - /** - * Return string representation of PHP value - * @return string - * @param $arg mixed - **/ - function serialize($arg) { - switch (strtolower($this->hive['SERIALIZER'])) { - case 'igbinary': - return igbinary_serialize($arg); - default: - return serialize($arg); - } - } - - /** - * Return PHP value derived from string - * @return string - * @param $arg mixed - **/ - function unserialize($arg) { - switch (strtolower($this->hive['SERIALIZER'])) { - case 'igbinary': - return igbinary_unserialize($arg); - default: - return unserialize($arg); - } - } - - /** - * Send HTTP status header; Return text equivalent of status code - * @return string - * @param $code int - **/ - function status($code) { - $reason=@constant('self::HTTP_'.$code); - if (!$this->hive['CLI'] && !headers_sent()) - header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason); - return $reason; - } - - /** - * Send cache metadata to HTTP client - * @return NULL - * @param $secs int - **/ - function expire($secs=0) { - if (!$this->hive['CLI'] && !headers_sent()) { - $secs=(int)$secs; - if ($this->hive['PACKAGE']) - header('X-Powered-By: '.$this->hive['PACKAGE']); - if ($this->hive['XFRAME']) - header('X-Frame-Options: '.$this->hive['XFRAME']); - header('X-XSS-Protection: 1; mode=block'); - header('X-Content-Type-Options: nosniff'); - if ($this->hive['VERB']=='GET' && $secs) { - $time=microtime(TRUE); - header_remove('Pragma'); - header('Cache-Control: max-age='.$secs); - header('Expires: '.gmdate('r',$time+$secs)); - header('Last-Modified: '.gmdate('r')); - } - else { - header('Pragma: no-cache'); - header('Cache-Control: no-cache, no-store, must-revalidate'); - header('Expires: '.gmdate('r',0)); - } - } - } - - /** - * Return HTTP user agent - * @return string - **/ - function agent() { - $headers=$this->hive['HEADERS']; - return isset($headers['X-Operamini-Phone-UA'])? - $headers['X-Operamini-Phone-UA']: - (isset($headers['X-Skyfire-Phone'])? - $headers['X-Skyfire-Phone']: - (isset($headers['User-Agent'])? - $headers['User-Agent']:'')); - } - - /** - * Return TRUE if XMLHttpRequest detected - * @return bool - **/ - function ajax() { - $headers=$this->hive['HEADERS']; - return isset($headers['X-Requested-With']) && - $headers['X-Requested-With']=='XMLHttpRequest'; - } - - /** - * Sniff IP address - * @return string - **/ - function ip() { - $headers=$this->hive['HEADERS']; - return isset($headers['Client-IP'])? - $headers['Client-IP']: - (isset($headers['X-Forwarded-For'])? - explode(',',$headers['X-Forwarded-For'])[0]: - (isset($_SERVER['REMOTE_ADDR'])? - $_SERVER['REMOTE_ADDR']:'')); - } - - /** - * Return filtered stack trace as a formatted string (or array) - * @return string|array - * @param $trace array|NULL - * @param $format bool - **/ - function trace(array $trace=NULL,$format=TRUE) { - if (!$trace) { - $trace=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $frame=$trace[0]; - if (isset($frame['file']) && $frame['file']==__FILE__) - array_shift($trace); - } - $debug=$this->hive['DEBUG']; - $trace=array_filter( - $trace, - function($frame) use($debug) { - return isset($frame['file']) && - ($debug>1 || - ($frame['file']!=__FILE__ || $debug) && - (empty($frame['function']) || - !preg_match('/^(?:(?:trigger|user)_error|'. - '__call|call_user_func)/',$frame['function']))); - } - ); - if (!$format) - return $trace; - $out=''; - $eol="\n"; - // Analyze stack trace - foreach ($trace as $frame) { - $line=''; - if (isset($frame['class'])) - $line.=$frame['class'].$frame['type']; - if (isset($frame['function'])) - $line.=$frame['function'].'('. - ($debug>2 && isset($frame['args'])? - $this->csv($frame['args']):'').')'; - $src=$this->fixslashes(str_replace($_SERVER['DOCUMENT_ROOT']. - '/','',$frame['file'])).':'.$frame['line']; - $out.='['.$src.'] '.$line.$eol; - } - return $out; - } - - /** - * Log error; Execute ONERROR handler if defined, else display - * default error page (HTML for synchronous requests, JSON string - * for AJAX requests) - * @return NULL - * @param $code int - * @param $text string - * @param $trace array - * @param $level int - **/ - function error($code,$text='',array $trace=NULL,$level=0) { - $prior=$this->hive['ERROR']; - $header=$this->status($code); - $req=$this->hive['VERB'].' '.$this->hive['PATH']; - if ($this->hive['QUERY']) - $req.='?'.$this->hive['QUERY']; - if (!$text) - $text='HTTP '.$code.' ('.$req.')'; - $trace=$this->trace($trace); - $loggable=$this->hive['LOGGABLE']; - if (!is_array($loggable)) - $loggable=$this->split($loggable); - foreach ($loggable as $status) - if ($status=='*' || - preg_match('/^'.preg_replace('/\D/','\d',$status).'$/',$code)) { - error_log($text); - foreach (explode("\n",$trace) as $nexus) - if ($nexus) - error_log($nexus); - break; - } - if ($highlight=!$this->hive['CLI'] && !$this->hive['AJAX'] && - $this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS)) - $trace=$this->highlight($trace); - $this->hive['ERROR']=[ - 'status'=>$header, - 'code'=>$code, - 'text'=>$text, - 'trace'=>$trace, - 'level'=>$level - ]; - $this->expire(-1); - $handler=$this->hive['ONERROR']; - $this->hive['ONERROR']=NULL; - $eol="\n"; - if ((!$handler || - $this->call($handler,[$this,$this->hive['PARAMS']], - 'beforeroute,afterroute')===FALSE) && - !$prior && !$this->hive['CLI'] && !$this->hive['QUIET']) - echo $this->hive['AJAX']? - json_encode( - array_diff_key( - $this->hive['ERROR'], - $this->hive['DEBUG']? - []: - ['trace'=>1] - ) - ): - (''.$eol. - ''.$eol. - ''. - ''.$code.' '.$header.''. - ($highlight? - (''):''). - ''.$eol. - ''.$eol. - '

'.$header.'

'.$eol. - '

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

'.$eol. - ($this->hive['DEBUG']?('
'.$trace.'
'.$eol):''). - ''.$eol. - ''); - if ($this->hive['HALT']) - die(1); - } - - /** - * Mock HTTP request - * @return mixed - * @param $pattern string - * @param $args array - * @param $headers array - * @param $body string - **/ - function mock($pattern, - array $args=NULL,array $headers=NULL,$body=NULL) { - if (!$args) - $args=[]; - $types=['sync','ajax','cli']; - preg_match('/([\|\w]+)\h+(?:@(\w+)(?:(\(.+?)\))*|([^\h]+))'. - '(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts); - $verb=strtoupper($parts[1]); - if ($parts[2]) { - if (empty($this->hive['ALIASES'][$parts[2]])) - user_error(sprintf(self::E_Named,$parts[2]),E_USER_ERROR); - $parts[4]=$this->hive['ALIASES'][$parts[2]]; - $parts[4]=$this->build($parts[4], - isset($parts[3])?$this->parse($parts[3]):[]); - } - if (empty($parts[4])) - user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR); - $url=parse_url($parts[4]); - parse_str(@$url['query'],$GLOBALS['_GET']); - if (preg_match('/GET|HEAD/',$verb)) - $GLOBALS['_GET']=array_merge($GLOBALS['_GET'],$args); - $GLOBALS['_POST']=$verb=='POST'?$args:[]; - $GLOBALS['_REQUEST']=array_merge($GLOBALS['_GET'],$GLOBALS['_POST']); - foreach ($headers?:[] as $key=>$val) - $_SERVER['HTTP_'.strtr(strtoupper($key),'-','_')]=$val; - $this->hive['VERB']=$verb; - $this->hive['PATH']=$url['path']; - $this->hive['URI']=$this->hive['BASE'].$url['path']; - if ($GLOBALS['_GET']) - $this->hive['URI'].='?'.http_build_query($GLOBALS['_GET']); - $this->hive['BODY']=''; - if (!preg_match('/GET|HEAD/',$verb)) - $this->hive['BODY']=$body?:http_build_query($args); - $this->hive['AJAX']=isset($parts[5]) && - preg_match('/ajax/i',$parts[5]); - $this->hive['CLI']=isset($parts[5]) && - preg_match('/cli/i',$parts[5]); - return $this->run(); - } - - /** - * Assemble url from alias name - * @return string - * @param $name string - * @param $params array|string - * @param $query string|array - **/ - function alias($name,$params=[],$query=NULL) { - if (!is_array($params)) - $params=$this->parse($params); - if (empty($this->hive['ALIASES'][$name])) - user_error(sprintf(self::E_Named,$name),E_USER_ERROR); - $url=$this->build($this->hive['ALIASES'][$name],$params); - if (is_array($query)) - $query=http_build_query($query); - return $url.($query?('?'.$query):''); - } - - /** - * Bind handler to route pattern - * @return NULL - * @param $pattern string|array - * @param $handler callback - * @param $ttl int - * @param $kbps int - **/ - function route($pattern,$handler,$ttl=0,$kbps=0) { - $types=['sync','ajax','cli']; - $alias=null; - if (is_array($pattern)) { - foreach ($pattern as $item) - $this->route($item,$handler,$ttl,$kbps); - return; - } - preg_match('/([\|\w]+)\h+(?:(?:@?(.+?)\h*:\h*)?(@(\w+)|[^\h]+))'. - '(?:\h+\[('.implode('|',$types).')\])?/u',$pattern,$parts); - if (isset($parts[2]) && $parts[2]) - $this->hive['ALIASES'][$alias=$parts[2]]=$parts[3]; - elseif (!empty($parts[4])) { - if (empty($this->hive['ALIASES'][$parts[4]])) - user_error(sprintf(self::E_Named,$parts[4]),E_USER_ERROR); - $parts[3]=$this->hive['ALIASES'][$alias=$parts[4]]; - } - if (empty($parts[3])) - user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR); - $type=empty($parts[5])?0:constant('self::REQ_'.strtoupper($parts[5])); - foreach ($this->split($parts[1]) as $verb) { - if (!preg_match('/'.self::VERBS.'/',$verb)) - $this->error(501,$verb.' '.$this->hive['URI']); - $this->hive['ROUTES'][$parts[3]][$type][strtoupper($verb)]= - [$handler,$ttl,$kbps,$alias]; - } - } - - /** - * Reroute to specified URI - * @return NULL - * @param $url array|string - * @param $permanent bool - * @param $die bool - **/ - function reroute($url=NULL,$permanent=FALSE,$die=TRUE) { - if (!$url) - $url=$this->hive['REALM']; - if (is_array($url)) - $url=call_user_func_array([$this,'alias'],$url); - elseif (preg_match('/^(?:@?([^\/()?]+)(?:(\(.+?)\))*(\?.+)*)/', - $url,$parts) && - isset($this->hive['ALIASES'][$parts[1]])) - $url=$this->hive['ALIASES'][$parts[1]]; - $url=$this->build($url,isset($parts[2])?$this->parse($parts[2]):[]). - (isset($parts[3])?$parts[3]:''); - if (($handler=$this->hive['ONREROUTE']) && - $this->call($handler,[$url,$permanent])!==FALSE) - return; - if ($url[0]=='/' && (empty($url[1]) || $url[1]!='/')) { - $port=$this->hive['PORT']; - $port=in_array($port,[80,443])?'':(':'.$port); - $url=$this->hive['SCHEME'].'://'. - $this->hive['HOST'].$port.$this->hive['BASE'].$url; - } - if ($this->hive['CLI']) - $this->mock('GET '.$url.' [cli]'); - else { - header('Location: '.$url); - $this->status($permanent?301:302); - if ($die) - die; - } - } - - /** - * Provide ReST interface by mapping HTTP verb to class method - * @return NULL - * @param $url string - * @param $class string|object - * @param $ttl int - * @param $kbps int - **/ - function map($url,$class,$ttl=0,$kbps=0) { - if (is_array($url)) { - foreach ($url as $item) - $this->map($item,$class,$ttl,$kbps); - return; - } - foreach (explode('|',self::VERBS) as $method) - $this->route($method.' '.$url,is_string($class)? - $class.'->'.$this->hive['PREMAP'].strtolower($method): - [$class,$this->hive['PREMAP'].strtolower($method)], - $ttl,$kbps); - } - - /** - * Redirect a route to another URL - * @return NULL - * @param $pattern string|array - * @param $url string - * @param $permanent bool - */ - function redirect($pattern,$url,$permanent=TRUE) { - if (is_array($pattern)) { - foreach ($pattern as $item) - $this->redirect($item,$url,$permanent); - return; - } - $this->route($pattern,function($fw) use($url,$permanent) { - $fw->reroute($url,$permanent); - }); - } - - /** - * Return TRUE if IPv4 address exists in DNSBL - * @return bool - * @param $ip string - **/ - function blacklisted($ip) { - if ($this->hive['DNSBL'] && - !in_array($ip, - is_array($this->hive['EXEMPT'])? - $this->hive['EXEMPT']: - $this->split($this->hive['EXEMPT']))) { - // Reverse IPv4 dotted quad - $rev=implode('.',array_reverse(explode('.',$ip))); - foreach (is_array($this->hive['DNSBL'])? - $this->hive['DNSBL']: - $this->split($this->hive['DNSBL']) as $server) - // DNSBL lookup - if (checkdnsrr($rev.'.'.$server,'A')) - return TRUE; - } - return FALSE; - } - - /** - * Applies the specified URL mask and returns parameterized matches - * @return $args array - * @param $pattern string - * @param $url string|NULL - **/ - function mask($pattern,$url=NULL) { - if (!$url) - $url=$this->rel($this->hive['URI']); - $case=$this->hive['CASELESS']?'i':''; - $wild=preg_quote($pattern,'/'); - $i=0; - while (is_int($pos=strpos($wild,'\*'))) { - $wild=substr_replace($wild,'(?P<_'.$i.'>[^\?]*)',$pos,2); - $i++; - } - preg_match('/^'. - preg_replace( - '/((\\\{)?@(\w+\b)(?(2)\\\}))/', - '(?P<\3>[^\/\?]+)', - $wild).'\/?$/'.$case.'um',$url,$args); - foreach (array_keys($args) as $key) { - if (preg_match('/^_\d+$/',$key)) { - if (empty($args['*'])) - $args['*']=$args[$key]; - else { - if (is_string($args['*'])) - $args['*']=[$args['*']]; - array_push($args['*'],$args[$key]); - } - unset($args[$key]); - } - elseif (is_numeric($key) && $key) - unset($args[$key]); - } - return $args; - } - - /** - * Match routes against incoming URI - * @return mixed - **/ - function run() { - if ($this->blacklisted($this->hive['IP'])) - // Spammer detected - $this->error(403); - if (!$this->hive['ROUTES']) - // No routes defined - user_error(self::E_Routes,E_USER_ERROR); - // Match specific routes first - $paths=[]; - foreach ($keys=array_keys($this->hive['ROUTES']) as $key) { - $path=preg_replace('/@\w+/','*@',$key); - if (substr($path,-1)!='*') - $path.='+'; - $paths[]=$path; - } - $vals=array_values($this->hive['ROUTES']); - array_multisort($paths,SORT_DESC,$keys,$vals); - $this->hive['ROUTES']=array_combine($keys,$vals); - // Convert to BASE-relative URL - $req=urldecode($this->hive['PATH']); - $preflight=FALSE; - if ($cors=(isset($this->hive['HEADERS']['Origin']) && - $this->hive['CORS']['origin'])) { - $cors=$this->hive['CORS']; - header('Access-Control-Allow-Origin: '.$cors['origin']); - header('Access-Control-Allow-Credentials: '. - $this->export($cors['credentials'])); - $preflight= - isset($this->hive['HEADERS']['Access-Control-Request-Method']); - } - $allowed=[]; - foreach ($this->hive['ROUTES'] as $pattern=>$routes) { - if (!$args=$this->mask($pattern,$req)) - continue; - ksort($args); - $route=NULL; - $ptr=$this->hive['CLI']?self::REQ_CLI:$this->hive['AJAX']+1; - if (isset($routes[$ptr][$this->hive['VERB']]) || - isset($routes[$ptr=0])) - $route=$routes[$ptr]; - if (!$route) - continue; - if (isset($route[$this->hive['VERB']]) && !$preflight) { - if ($this->hive['VERB']=='GET' && - preg_match('/.+\/$/',$this->hive['PATH'])) - $this->reroute(substr($this->hive['PATH'],0,-1). - ($this->hive['QUERY']?('?'.$this->hive['QUERY']):'')); - list($handler,$ttl,$kbps,$alias)=$route[$this->hive['VERB']]; - // Capture values of route pattern tokens - $this->hive['PARAMS']=$args; - // Save matching route - $this->hive['ALIAS']=$alias; - $this->hive['PATTERN']=$pattern; - if ($cors && $cors['expose']) - header('Access-Control-Expose-Headers: '. - (is_array($cors['expose'])? - implode(',',$cors['expose']):$cors['expose'])); - if (is_string($handler)) { - // Replace route pattern tokens in handler if any - $handler=preg_replace_callback('/({)?@(\w+\b)(?(1)})/', - function($id) use($args) { - $pid=count($id)>2?2:1; - return isset($args[$id[$pid]])? - $args[$id[$pid]]: - $id[0]; - }, - $handler - ); - if (preg_match('/(.+)\h*(?:->|::)/',$handler,$match) && - !class_exists($match[1])) - $this->error(404); - } - // Process request - $result=NULL; - $body=''; - $now=microtime(TRUE); - if (preg_match('/GET|HEAD/',$this->hive['VERB']) && $ttl) { - // Only GET and HEAD requests are cacheable - $headers=$this->hive['HEADERS']; - $cache=Cache::instance(); - $cached=$cache->exists( - $hash=$this->hash($this->hive['VERB'].' '. - $this->hive['URI']).'.url',$data); - if ($cached) { - if (isset($headers['If-Modified-Since']) && - strtotime($headers['If-Modified-Since'])+ - $ttl>$now) { - $this->status(304); - die; - } - // Retrieve from cache backend - list($headers,$body,$result)=$data; - if (!$this->hive['CLI']) - array_walk($headers,'header'); - $this->expire($cached[0]+$ttl-$now); - } - else - // Expire HTTP client-cached page - $this->expire($ttl); - } - else - $this->expire(0); - if (!strlen($body)) { - if (!$this->hive['RAW'] && !$this->hive['BODY']) - $this->hive['BODY']=file_get_contents('php://input'); - ob_start(); - // Call route handler - $result=$this->call($handler,[$this,$args,$handler], - 'beforeroute,afterroute'); - $body=ob_get_clean(); - if (isset($cache) && !error_get_last()) { - // Save to cache backend - $cache->set($hash,[ - // Remove cookies - preg_grep('/Set-Cookie\:/',headers_list(), - PREG_GREP_INVERT),$body,$result],$ttl); - } - } - $this->hive['RESPONSE']=$body; - if (!$this->hive['QUIET']) { - if ($kbps) { - $ctr=0; - foreach (str_split($body,1024) as $part) { - // Throttle output - $ctr++; - if ($ctr/$kbps>($elapsed=microtime(TRUE)-$now) && - !connection_aborted()) - usleep(1e6*($ctr/$kbps-$elapsed)); - echo $part; - } - } - else - echo $body; - } - if ($result || $this->hive['VERB']!='OPTIONS') - return $result; - } - $allowed=array_merge($allowed,array_keys($route)); - } - if (!$allowed) - // URL doesn't match any route - $this->error(404); - elseif (!$this->hive['CLI']) { - if (!preg_grep('/Allow:/',$headers_send=headers_list())) - // Unhandled HTTP method - header('Allow: '.implode(',',array_unique($allowed))); - if ($cors) { - if (!preg_grep('/Access-Control-Allow-Methods:/',$headers_send)) - header('Access-Control-Allow-Methods: OPTIONS,'. - implode(',',$allowed)); - if ($cors['headers'] && - !preg_grep('/Access-Control-Allow-Headers:/',$headers_send)) - header('Access-Control-Allow-Headers: '. - (is_array($cors['headers'])? - implode(',',$cors['headers']): - $cors['headers'])); - if ($cors['ttl']>0) - header('Access-Control-Max-Age: '.$cors['ttl']); - } - if ($this->hive['VERB']!='OPTIONS') - $this->error(405); - } - return FALSE; - } - - /** - * Loop until callback returns TRUE (for long polling) - * @return mixed - * @param $func callback - * @param $args array - * @param $timeout int - **/ - function until($func,$args=NULL,$timeout=60) { - if (!$args) - $args=[]; - $time=time(); - $max=ini_get('max_execution_time'); - $limit=max(0,($max?min($timeout,$max):$timeout)-1); - $out=''; - // Turn output buffering on - ob_start(); - // Not for the weak of heart - while ( - // No error occurred - !$this->hive['ERROR'] && - // Got time left? - time()-$time+1<$limit && - // Still alive? - !connection_aborted() && - // Restart session - !headers_sent() && - (session_status()==PHP_SESSION_ACTIVE || session_start()) && - // CAUTION: Callback will kill host if it never becomes truthy! - !$out=$this->call($func,$args)) { - if (!$this->hive['CLI']) - session_commit(); - // Hush down - sleep(1); - } - ob_flush(); - flush(); - return $out; - } - - /** - * Disconnect HTTP client; - * Set FcgidOutputBufferSize to zero if server uses mod_fcgid; - * Disable mod_deflate when rendering text/html output - **/ - function abort() { - if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) - session_start(); - $out=''; - while (ob_get_level()) - $out=ob_get_clean().$out; - if (!headers_sent()) { - header('Content-Length: '.strlen($out)); - header('Connection: close'); - } - session_commit(); - echo $out; - flush(); - if (function_exists('fastcgi_finish_request')) - fastcgi_finish_request(); - } - - /** - * Grab the real route handler behind the string expression - * @return string|array - * @param $func string - * @param $args array - **/ - function grab($func,$args=NULL) { - if (preg_match('/(.+)\h*(->|::)\h*(.+)/s',$func,$parts)) { - // Convert string to executable PHP callback - if (!class_exists($parts[1])) - user_error(sprintf(self::E_Class,$parts[1]),E_USER_ERROR); - if ($parts[2]=='->') { - if (is_subclass_of($parts[1],'Prefab')) - $parts[1]=call_user_func($parts[1].'::instance'); - elseif ($container=$this->get('CONTAINER')) { - if (is_object($container) && is_callable([$container,'has']) - && $container->has($parts[1])) // PSR11 - $parts[1]=call_user_func([$container,'get'],$parts[1]); - elseif (is_callable($container)) - $parts[1]=call_user_func($container,$parts[1],$args); - elseif (is_string($container) && - is_subclass_of($container,'Prefab')) - $parts[1]=call_user_func($container.'::instance')-> - get($parts[1]); - else - user_error(sprintf(self::E_Class, - $this->stringify($parts[1])), - E_USER_ERROR); - } - else { - $ref=new ReflectionClass($parts[1]); - $parts[1]=method_exists($parts[1],'__construct') && $args? - $ref->newinstanceargs($args): - $ref->newinstance(); - } - } - $func=[$parts[1],$parts[3]]; - } - return $func; - } - - /** - * Execute callback/hooks (supports 'class->method' format) - * @return mixed|FALSE - * @param $func callback - * @param $args mixed - * @param $hooks string - **/ - function call($func,$args=NULL,$hooks='') { - if (!is_array($args)) - $args=[$args]; - // Grab the real handler behind the string representation - if (is_string($func)) - $func=$this->grab($func,$args); - // Execute function; abort if callback/hook returns FALSE - if (!is_callable($func)) - // No route handler - if ($hooks=='beforeroute,afterroute') { - $allowed=[]; - if (is_array($func)) - $allowed=array_intersect( - array_map('strtoupper',get_class_methods($func[0])), - explode('|',self::VERBS) - ); - header('Allow: '.implode(',',$allowed)); - $this->error(405); - } - else - user_error(sprintf(self::E_Method, - is_string($func)?$func:$this->stringify($func)), - E_USER_ERROR); - $obj=FALSE; - if (is_array($func)) { - $hooks=$this->split($hooks); - $obj=TRUE; - } - // Execute pre-route hook if any - if ($obj && $hooks && in_array($hook='beforeroute',$hooks) && - method_exists($func[0],$hook) && - call_user_func_array([$func[0],$hook],$args)===FALSE) - return FALSE; - // Execute callback - $out=call_user_func_array($func,$args?:[]); - if ($out===FALSE) - return FALSE; - // Execute post-route hook if any - if ($obj && $hooks && in_array($hook='afterroute',$hooks) && - method_exists($func[0],$hook) && - call_user_func_array([$func[0],$hook],$args)===FALSE) - return FALSE; - return $out; - } - - /** - * Execute specified callbacks in succession; Apply same arguments - * to all callbacks - * @return array - * @param $funcs array|string - * @param $args mixed - **/ - function chain($funcs,$args=NULL) { - $out=[]; - foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func) - $out[]=$this->call($func,$args); - return $out; - } - - /** - * Execute specified callbacks in succession; Relay result of - * previous callback as argument to the next callback - * @return array - * @param $funcs array|string - * @param $args mixed - **/ - function relay($funcs,$args=NULL) { - foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func) - $args=[$this->call($func,$args)]; - return array_shift($args); - } - - /** - * Configure framework according to .ini-style file settings; - * If optional 2nd arg is provided, template strings are interpreted - * @return object - * @param $source string|array - * @param $allow bool - **/ - function config($source,$allow=FALSE) { - if (is_string($source)) - $source=$this->split($source); - if ($allow) - $preview=Preview::instance(); - foreach ($source as $file) { - preg_match_all( - '/(?<=^|\n)(?:'. - '\[(?
.+?)\]|'. - '(?[^\h\r\n;].*?)\h*=\h*'. - '(?(?:\\\\\h*\r?\n|.+?)*)'. - ')(?=\r?\n|$)/', - $this->read($file), - $matches,PREG_SET_ORDER); - if ($matches) { - $sec='globals'; - $cmd=[]; - foreach ($matches as $match) { - if ($match['section']) { - $sec=$match['section']; - if (preg_match( - '/^(?!(?:global|config|route|map|redirect)s\b)'. - '((?:\.?\w)+)/i',$sec,$msec) && - !$this->exists($msec[0])) - $this->set($msec[0],NULL); - preg_match('/^(config|route|map|redirect)s\b|'. - '^((?:\.?\w)+)\s*\>\s*(.*)/i',$sec,$cmd); - continue; - } - if ($allow) - foreach (['lval','rval'] as $ndx) - $match[$ndx]=$preview-> - resolve($match[$ndx],NULL,0,FALSE,FALSE); - if (!empty($cmd)) { - isset($cmd[3])? - $this->call($cmd[3], - [$match['lval'],$match['rval'],$cmd[2]]): - call_user_func_array( - [$this,$cmd[1]], - array_merge([$match['lval']], - str_getcsv($cmd[1]=='config'? - $this->cast($match['rval']): - $match['rval'])) - ); - } - else { - $rval=preg_replace( - '/\\\\\h*(\r?\n)/','\1',$match['rval']); - $ttl=NULL; - if (preg_match('/^(.+)\|\h*(\d+)$/',$rval,$tmp)) { - array_shift($tmp); - list($rval,$ttl)=$tmp; - } - $args=array_map( - function($val) { - $val=$this->cast($val); - if (is_string($val)) - $val=strlen($val)? - preg_replace('/\\\\"/','"',$val): - NULL; - return $val; - }, - // Mark quoted strings with 0x00 whitespace - str_getcsv(preg_replace( - '/(?[^:]+)(?:\:(?.+))?/', - $sec,$parts); - $func=isset($parts['func'])?$parts['func']:NULL; - $custom=(strtolower($parts['section'])!='globals'); - if ($func) - $args=[$this->call($func,$args)]; - if (count($args)>1) - $args=[$args]; - if (isset($ttl)) - $args=array_merge($args,[$ttl]); - call_user_func_array( - [$this,'set'], - array_merge( - [ - ($custom?($parts['section'].'.'):''). - $match['lval'] - ], - $args - ) - ); - } - } - } - } - return $this; - } - - /** - * Create mutex, invoke callback then drop ownership when done - * @return mixed - * @param $id string - * @param $func callback - * @param $args mixed - **/ - function mutex($id,$func,$args=NULL) { - if (!is_dir($tmp=$this->hive['TEMP'])) - mkdir($tmp,self::MODE,TRUE); - // Use filesystem lock - if (is_file($lock=$tmp. - $this->get('SEED').'.'.$this->hash($id).'.lock') && - filemtime($lock)+ini_get('max_execution_time')locks[$id]=$lock; - $out=$this->call($func,$args); - fclose($handle); - @unlink($lock); - unset($this->locks[$id]); - return $out; - } - - /** - * Read file (with option to apply Unix LF as standard line ending) - * @return string - * @param $file string - * @param $lf bool - **/ - function read($file,$lf=FALSE) { - $out=@file_get_contents($file); - return $lf?preg_replace('/\r\n|\r/',"\n",$out):$out; - } - - /** - * Exclusive file write - * @return int|FALSE - * @param $file string - * @param $data mixed - * @param $append bool - **/ - function write($file,$data,$append=FALSE) { - return file_put_contents($file,$data,LOCK_EX|($append?FILE_APPEND:0)); - } - - /** - * Apply syntax highlighting - * @return string - * @param $text string - **/ - function highlight($text) { - $out=''; - $pre=FALSE; - $text=trim($text); - if ($text && !preg_match('/^<\?php/',$text)) { - $text=''. - $this->encode($token[1]).''): - ('>'.$this->encode($token))). - ''; - return $out?(''.$out.''):$text; - } - - /** - * Dump expression with syntax highlighting - * @return NULL - * @param $expr mixed - **/ - function dump($expr) { - echo $this->highlight($this->stringify($expr)); - } - - /** - * Return path (and query parameters) relative to the base directory - * @return string - * @param $url string - **/ - function rel($url) { - return preg_replace('/^(?:https?:\/\/)?'. - preg_quote($this->hive['BASE'],'/').'(\/.*|$)/','\1',$url); - } - - /** - * Namespace-aware class autoloader - * @return mixed - * @param $class string - **/ - protected function autoload($class) { - $class=$this->fixslashes(ltrim($class,'\\')); - $func=NULL; - if (is_array($path=$this->hive['AUTOLOAD']) && - isset($path[1]) && is_callable($path[1])) - list($path,$func)=$path; - foreach ($this->split($this->hive['PLUGINS'].';'.$path) as $auto) - if ($func && is_file($file=$func($auto.$class).'.php') || - is_file($file=$auto.$class.'.php') || - is_file($file=$auto.strtolower($class).'.php') || - is_file($file=strtolower($auto.$class).'.php')) - return require($file); - } - - /** - * Execute framework/application shutdown sequence - * @param $cwd string - **/ - function unload($cwd) { - chdir($cwd); - if (!($error=error_get_last()) && - session_status()==PHP_SESSION_ACTIVE) - session_commit(); - foreach ($this->locks as $lock) - @unlink($lock); - $handler=$this->hive['UNLOAD']; - if ((!$handler || $this->call($handler,$this)===FALSE) && - $error && in_array($error['type'], - [E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR])) - // Fatal error detected - $this->error(500, - sprintf(self::E_Fatal,$error['message']),[$error]); - } - - /** - * Convenience method for checking hive key - * @return mixed - * @param $key string - **/ - function offsetexists($key) { - return $this->exists($key); - } - - /** - * Convenience method for assigning hive value - * @return mixed - * @param $key string - * @param $val scalar - **/ - function offsetset($key,$val) { - return $this->set($key,$val); - } - - /** - * Convenience method for retrieving hive value - * @return mixed - * @param $key string - **/ - function &offsetget($key) { - $val=&$this->ref($key); - return $val; - } - - /** - * Convenience method for removing hive key - * @return NULL - * @param $key string - **/ - function offsetunset($key) { - $this->clear($key); - } - - /** - * Alias for offsetexists() - * @return mixed - * @param $key string - **/ - function __isset($key) { - return $this->offsetexists($key); - } - - /** - * Alias for offsetset() - * @return mixed - * @param $key string - * @param $val mixed - **/ - function __set($key,$val) { - return $this->offsetset($key,$val); - } - - /** - * Alias for offsetget() - * @return mixed - * @param $key string - **/ - function &__get($key) { - $val=&$this->offsetget($key); - return $val; - } - - /** - * Alias for offsetunset() - * @return mixed - * @param $key string - **/ - function __unset($key) { - $this->offsetunset($key); - } - - /** - * Call function identified by hive key - * @return mixed - * @param $key string - * @param $args array - **/ - function __call($key,array $args) { - if ($this->exists($key,$val)) - return call_user_func_array($val,$args); - user_error(sprintf(self::E_Method,$key),E_USER_ERROR); - } - - //! Prohibit cloning - private function __clone() { - } - - //! Bootstrap - function __construct() { - // Managed directives - ini_set('default_charset',$charset='UTF-8'); - if (extension_loaded('mbstring')) - mb_internal_encoding($charset); - ini_set('display_errors',0); - // Deprecated directives - @ini_set('magic_quotes_gpc',0); - @ini_set('register_globals',0); - // Intercept errors/exceptions; PHP5.3-compatible - $check=error_reporting((E_ALL|E_STRICT)&~(E_NOTICE|E_USER_NOTICE)); - set_exception_handler( - function($obj) { - $this->hive['EXCEPTION']=$obj; - $this->error(500, - $obj->getmessage().' '. - '['.$obj->getFile().':'.$obj->getLine().']', - $obj->gettrace()); - } - ); - set_error_handler( - function($level,$text,$file,$line) { - if ($level & error_reporting()) - $this->error(500,$text,NULL,$level); - } - ); - if (!isset($_SERVER['SERVER_NAME']) || $_SERVER['SERVER_NAME']==='') - $_SERVER['SERVER_NAME']=gethostname(); - $headers=[]; - if ($cli=PHP_SAPI=='cli') { - // Emulate HTTP request - $_SERVER['REQUEST_METHOD']='GET'; - if (!isset($_SERVER['argv'][1])) { - $_SERVER['argc']++; - $_SERVER['argv'][1]='/'; - } - $req=$query=''; - if (substr($_SERVER['argv'][1],0,1)=='/') { - $req=$_SERVER['argv'][1]; - $query=parse_url($req,PHP_URL_QUERY); - } else { - foreach($_SERVER['argv'] as $i=>$arg) { - if (!$i) continue; - if (preg_match('/^\-(\-)?(\w+)(?:\=(.*))?$/',$arg,$m)) { - foreach($m[1]?[$m[2]]:str_split($m[2]) as $k) - $query.=($query?'&':'').urlencode($k).'='; - if (isset($m[3])) - $query.=urlencode($m[3]); - } else - $req.='/'.$arg; - } - if (!$req) - $req='/'; - if ($query) - $req.='?'.$query; - } - $_SERVER['REQUEST_URI']=$req; - parse_str($query,$GLOBALS['_GET']); - } - elseif (function_exists('getallheaders')) { - foreach (getallheaders() as $key=>$val) { - $tmp=strtoupper(strtr($key,'-','_')); - // TODO: use ucwords delimiters for php 5.4.32+ & 5.5.16+ - $key=strtr(ucwords(strtolower(strtr($key,'-',' '))),' ','-'); - $headers[$key]=$val; - if (isset($_SERVER['HTTP_'.$tmp])) - $headers[$key]=&$_SERVER['HTTP_'.$tmp]; - } - } - else { - if (isset($_SERVER['CONTENT_LENGTH'])) - $headers['Content-Length']=&$_SERVER['CONTENT_LENGTH']; - if (isset($_SERVER['CONTENT_TYPE'])) - $headers['Content-Type']=&$_SERVER['CONTENT_TYPE']; - foreach (array_keys($_SERVER) as $key) - if (substr($key,0,5)=='HTTP_') - $headers[strtr(ucwords(strtolower(strtr( - substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key]; - } - if (isset($headers['X-HTTP-Method-Override'])) - $_SERVER['REQUEST_METHOD']=$headers['X-HTTP-Method-Override']; - elseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method'])) - $_SERVER['REQUEST_METHOD']=strtoupper($_POST['_method']); - $scheme=isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' || - isset($headers['X-Forwarded-Proto']) && - $headers['X-Forwarded-Proto']=='https'?'https':'http'; - // Create hive early on to expose header methods - $this->hive=['HEADERS'=>&$headers]; - if (function_exists('apache_setenv')) { - // Work around Apache pre-2.4 VirtualDocumentRoot bug - $_SERVER['DOCUMENT_ROOT']=str_replace($_SERVER['SCRIPT_NAME'],'', - $_SERVER['SCRIPT_FILENAME']); - apache_setenv("DOCUMENT_ROOT",$_SERVER['DOCUMENT_ROOT']); - } - $_SERVER['DOCUMENT_ROOT']=realpath($_SERVER['DOCUMENT_ROOT']); - $base=''; - if (!$cli) - $base=rtrim($this->fixslashes( - dirname($_SERVER['SCRIPT_NAME'])),'/'); - $uri=parse_url((preg_match('/^\w+:\/\//',$_SERVER['REQUEST_URI'])?'': - $scheme.'://'.$_SERVER['SERVER_NAME']).$_SERVER['REQUEST_URI']); - $_SERVER['REQUEST_URI']=$uri['path']. - (isset($uri['query'])?'?'.$uri['query']:''). - (isset($uri['fragment'])?'#'.$uri['fragment']:''); - $path=preg_replace('/^'.preg_quote($base,'/').'/','',$uri['path']); - $jar=[ - 'expire'=>0, - 'lifetime'=>0, - 'path'=>$base?:'/', - 'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) && - !filter_var($_SERVER['SERVER_NAME'],FILTER_VALIDATE_IP)? - $_SERVER['SERVER_NAME']:'', - 'secure'=>($scheme=='https'), - 'httponly'=>TRUE - ]; - $port=80; - if (isset($headers['X-Forwarded-Port'])) - $port=$headers['X-Forwarded-Port']; - elseif (isset($_SERVER['SERVER_PORT'])) - $port=$_SERVER['SERVER_PORT']; - // Default configuration - $this->hive+=[ - 'AGENT'=>$this->agent(), - 'AJAX'=>$this->ajax(), - 'ALIAS'=>NULL, - 'ALIASES'=>[], - 'AUTOLOAD'=>'./', - 'BASE'=>$base, - 'BITMASK'=>ENT_COMPAT, - 'BODY'=>NULL, - 'CACHE'=>FALSE, - 'CASELESS'=>TRUE, - 'CLI'=>$cli, - 'CORS'=>[ - 'headers'=>'', - 'origin'=>FALSE, - 'credentials'=>FALSE, - 'expose'=>FALSE, - 'ttl'=>0 - ], - 'DEBUG'=>0, - 'DIACRITICS'=>[], - 'DNSBL'=>'', - 'EMOJI'=>[], - 'ENCODING'=>$charset, - 'ERROR'=>NULL, - 'ESCAPE'=>TRUE, - 'EXCEPTION'=>NULL, - 'EXEMPT'=>NULL, - 'FALLBACK'=>$this->fallback, - 'FORMATS'=>[], - 'FRAGMENT'=>isset($uri['fragment'])?$uri['fragment']:'', - 'HALT'=>TRUE, - 'HIGHLIGHT'=>FALSE, - 'HOST'=>$_SERVER['SERVER_NAME'], - 'IP'=>$this->ip(), - 'JAR'=>$jar, - 'LANGUAGE'=>isset($headers['Accept-Language'])? - $this->language($headers['Accept-Language']): - $this->fallback, - 'LOCALES'=>'./', - 'LOGGABLE'=>'*', - 'LOGS'=>'./', - 'MB'=>extension_loaded('mbstring'), - 'ONERROR'=>NULL, - 'ONREROUTE'=>NULL, - 'PACKAGE'=>self::PACKAGE, - 'PARAMS'=>[], - 'PATH'=>$path, - 'PATTERN'=>NULL, - 'PLUGINS'=>$this->fixslashes(__DIR__).'/', - 'PORT'=>$port, - 'PREFIX'=>NULL, - 'PREMAP'=>'', - 'QUERY'=>isset($uri['query'])?$uri['query']:'', - 'QUIET'=>FALSE, - 'RAW'=>FALSE, - 'REALM'=>$scheme.'://'.$_SERVER['SERVER_NAME']. - ($port && !in_array($port,[80,443])?(':'.$port):''). - $_SERVER['REQUEST_URI'], - 'RESPONSE'=>'', - 'ROOT'=>$_SERVER['DOCUMENT_ROOT'], - 'ROUTES'=>[], - 'SCHEME'=>$scheme, - 'SEED'=>$this->hash($_SERVER['SERVER_NAME'].$base), - 'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php', - 'TEMP'=>'tmp/', - 'TIME'=>&$_SERVER['REQUEST_TIME_FLOAT'], - 'TZ'=>@date_default_timezone_get(), - 'UI'=>'./', - 'UNLOAD'=>NULL, - 'UPLOADS'=>'./', - 'URI'=>&$_SERVER['REQUEST_URI'], - 'VERB'=>&$_SERVER['REQUEST_METHOD'], - 'VERSION'=>self::VERSION, - 'XFRAME'=>'SAMEORIGIN' - ]; - if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) { - unset($jar['expire']); - session_cache_limiter(''); - call_user_func_array('session_set_cookie_params',$jar); - } - if (PHP_SAPI=='cli-server' && - preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI'])) - $this->reroute('/'); - if (ini_get('auto_globals_jit')) - // Override setting - $GLOBALS+=['_ENV'=>$_ENV,'_REQUEST'=>$_REQUEST]; - // Sync PHP globals with corresponding hive keys - $this->init=$this->hive; - foreach (explode('|',self::GLOBALS) as $global) { - $sync=$this->sync($global); - $this->init+=[ - $global=>preg_match('/SERVER|ENV/',$global)?$sync:[] - ]; - } - if ($check && $error=error_get_last()) - // Error detected - $this->error(500, - sprintf(self::E_Fatal,$error['message']),[$error]); - date_default_timezone_set($this->hive['TZ']); - // Register framework autoloader - spl_autoload_register([$this,'autoload']); - // Register shutdown handler - register_shutdown_function([$this,'unload'],getcwd()); - } - -} - -//! Cache engine -class Cache extends Prefab { - - protected - //! Cache DSN - $dsn, - //! Prefix for cache entries - $prefix, - //! MemCache or Redis object - $ref; - - /** - * Return timestamp and TTL of cache entry or FALSE if not found - * @return array|FALSE - * @param $key string - * @param $val mixed - **/ - function exists($key,&$val=NULL) { - $fw=Base::instance(); - if (!$this->dsn) - return FALSE; - $ndx=$this->prefix.'.'.$key; - $parts=explode('=',$this->dsn,2); - switch ($parts[0]) { - case 'apc': - case 'apcu': - $raw=call_user_func($parts[0].'_fetch',$ndx); - break; - case 'redis': - $raw=$this->ref->get($ndx); - break; - case 'memcache': - $raw=memcache_get($this->ref,$ndx); - break; - case 'memcached': - $raw=$this->ref->get($ndx); - break; - case 'wincache': - $raw=wincache_ucache_get($ndx); - break; - case 'xcache': - $raw=xcache_get($ndx); - break; - case 'folder': - $raw=$fw->read($parts[1].$ndx); - break; - } - if (!empty($raw)) { - list($val,$time,$ttl)=(array)$fw->unserialize($raw); - if ($ttl===0 || $time+$ttl>microtime(TRUE)) - return [$time,$ttl]; - $val=null; - $this->clear($key); - } - return FALSE; - } - - /** - * Store value in cache - * @return mixed|FALSE - * @param $key string - * @param $val mixed - * @param $ttl int - **/ - function set($key,$val,$ttl=0) { - $fw=Base::instance(); - if (!$this->dsn) - return TRUE; - $ndx=$this->prefix.'.'.$key; - if ($cached=$this->exists($key)) - $ttl=$cached[1]; - $data=$fw->serialize([$val,microtime(TRUE),$ttl]); - $parts=explode('=',$this->dsn,2); - switch ($parts[0]) { - case 'apc': - case 'apcu': - return call_user_func($parts[0].'_store',$ndx,$data,$ttl); - case 'redis': - return $this->ref->set($ndx,$data,$ttl?['ex'=>$ttl]:[]); - case 'memcache': - return memcache_set($this->ref,$ndx,$data,0,$ttl); - case 'memcached': - return $this->ref->set($ndx,$data,$ttl); - case 'wincache': - return wincache_ucache_set($ndx,$data,$ttl); - case 'xcache': - return xcache_set($ndx,$data,$ttl); - case 'folder': - return $fw->write($parts[1]. - str_replace(['/','\\'],'',$ndx),$data); - } - return FALSE; - } - - /** - * Retrieve value of cache entry - * @return mixed|FALSE - * @param $key string - **/ - function get($key) { - return $this->dsn && $this->exists($key,$data)?$data:FALSE; - } - - /** - * Delete cache entry - * @return bool - * @param $key string - **/ - function clear($key) { - if (!$this->dsn) - return; - $ndx=$this->prefix.'.'.$key; - $parts=explode('=',$this->dsn,2); - switch ($parts[0]) { - case 'apc': - case 'apcu': - return call_user_func($parts[0].'_delete',$ndx); - case 'redis': - return $this->ref->del($ndx); - case 'memcache': - return memcache_delete($this->ref,$ndx); - case 'memcached': - return $this->ref->delete($ndx); - case 'wincache': - return wincache_ucache_delete($ndx); - case 'xcache': - return xcache_unset($ndx); - case 'folder': - return @unlink($parts[1].$ndx); - } - return FALSE; - } - - /** - * Clear contents of cache backend - * @return bool - * @param $suffix string - **/ - function reset($suffix=NULL) { - if (!$this->dsn) - return TRUE; - $regex='/'.preg_quote($this->prefix.'.','/').'.*'. - preg_quote($suffix,'/').'/'; - $parts=explode('=',$this->dsn,2); - switch ($parts[0]) { - case 'apc': - case 'apcu': - $info=call_user_func($parts[0].'_cache_info', - $parts[0]=='apcu'?FALSE:'user'); - if (!empty($info['cache_list'])) { - $key=array_key_exists('info', - $info['cache_list'][0])?'info':'key'; - foreach ($info['cache_list'] as $item) - if (preg_match($regex,$item[$key])) - call_user_func($parts[0].'_delete',$item[$key]); - } - return TRUE; - case 'redis': - $keys=$this->ref->keys($this->prefix.'.*'.$suffix); - foreach($keys as $key) - $this->ref->del($key); - return TRUE; - case 'memcache': - foreach (memcache_get_extended_stats( - $this->ref,'slabs') as $slabs) - foreach (array_filter(array_keys($slabs),'is_numeric') - as $id) - foreach (memcache_get_extended_stats( - $this->ref,'cachedump',$id) as $data) - if (is_array($data)) - foreach (array_keys($data) as $key) - if (preg_match($regex,$key)) - memcache_delete($this->ref,$key); - return TRUE; - case 'memcached': - foreach ($this->ref->getallkeys()?:[] as $key) - if (preg_match($regex,$key)) - $this->ref->delete($key); - return TRUE; - case 'wincache': - $info=wincache_ucache_info(); - foreach ($info['ucache_entries'] as $item) - if (preg_match($regex,$item['key_name'])) - wincache_ucache_delete($item['key_name']); - return TRUE; - case 'xcache': - if ($suffix && !ini_get('xcache.admin.enable_auth')) { - $cnt=xcache_count(XC_TYPE_VAR); - for ($i=0;$i<$cnt;$i++) { - $list=xcache_list(XC_TYPE_VAR,$i); - foreach ($list['cache_list'] as $item) - if (preg_match($regex,$item['name'])) - xcache_unset($item['name']); - } - } else - xcache_unset_by_prefix($this->prefix.'.'); - return TRUE; - case 'folder': - if ($glob=@glob($parts[1].'*')) - foreach ($glob as $file) - if (preg_match($regex,basename($file))) - @unlink($file); - return TRUE; - } - return FALSE; - } - - /** - * Load/auto-detect cache backend - * @return string - * @param $dsn bool|string - * @param $seed bool|string - **/ - function load($dsn,$seed=NULL) { - $fw=Base::instance(); - if ($dsn=trim($dsn)) { - if (preg_match('/^redis=(.+)/',$dsn,$parts) && - extension_loaded('redis')) { - list($host,$port,$db)=explode(':',$parts[1])+[1=>6379,2=>NULL]; - $this->ref=new Redis; - if(!$this->ref->connect($host,$port,2)) - $this->ref=NULL; - if(isset($db)) - $this->ref->select($db); - } - elseif (preg_match('/^memcache=(.+)/',$dsn,$parts) && - extension_loaded('memcache')) - foreach ($fw->split($parts[1]) as $server) { - list($host,$port)=explode(':',$server)+[1=>11211]; - if (empty($this->ref)) - $this->ref=@memcache_connect($host,$port)?:NULL; - else - memcache_add_server($this->ref,$host,$port); - } - elseif (preg_match('/^memcached=(.+)/',$dsn,$parts) && - extension_loaded('memcached')) - foreach ($fw->split($parts[1]) as $server) { - list($host,$port)=explode(':',$server)+[1=>11211]; - if (empty($this->ref)) - $this->ref=new Memcached(); - $this->ref->addServer($host,$port); - } - if (empty($this->ref) && !preg_match('/^folder\h*=/',$dsn)) - $dsn=($grep=preg_grep('/^(apc|wincache|xcache)/', - array_map('strtolower',get_loaded_extensions())))? - // Auto-detect - current($grep): - // Use filesystem as fallback - ('folder='.$fw->TEMP.'cache/'); - if (preg_match('/^folder\h*=\h*(.+)/',$dsn,$parts) && - !is_dir($parts[1])) - mkdir($parts[1],Base::MODE,TRUE); - } - $this->prefix=$seed?:$fw->SEED; - return $this->dsn=$dsn; - } - - /** - * Class constructor - * @param $dsn bool|string - **/ - function __construct($dsn=FALSE) { - if ($dsn) - $this->load($dsn); - } - -} - -//! View handler -class View extends Prefab { - - private - //! Temporary hive - $temp; - - protected - //! Template file - $file, - //! Post-rendering handler - $trigger, - //! Nesting level - $level=0; - - /** @var \Base Framework instance */ - protected $fw; - - function __construct() { - $this->fw=\Base::instance(); - } - - /** - * Encode characters to equivalent HTML entities - * @return string - * @param $arg mixed - **/ - function esc($arg) { - return $this->fw->recursive($arg, - function($val) { - return is_string($val)?$this->fw->encode($val):$val; - } - ); - } - - /** - * Decode HTML entities to equivalent characters - * @return string - * @param $arg mixed - **/ - function raw($arg) { - return $this->fw->recursive($arg, - function($val) { - return is_string($val)?$this->fw->decode($val):$val; - } - ); - } - - /** - * Create sandbox for template execution - * @return string - * @param $hive array - * @param $mime string - **/ - protected function sandbox(array $hive=NULL,$mime=NULL) { - $fw=$this->fw; - $implicit=FALSE; - if (is_null($hive)) { - $implicit=TRUE; - $hive=$fw->hive(); - } - if ($this->level<1 || $implicit) { - if (!$fw->CLI && $mime && !headers_sent() && - !preg_grep ('/^Content-Type:/',headers_list())) - header('Content-Type: '.$mime.'; '. - 'charset='.$fw->ENCODING); - if ($fw->ESCAPE) - $hive=$this->esc($hive); - if (isset($hive['ALIASES'])) - $hive['ALIASES']=$fw->build($hive['ALIASES']); - } - $this->temp=$hive; - unset($fw,$hive,$implicit,$mime); - extract($this->temp); - $this->temp=NULL; - $this->level++; - ob_start(); - require($this->file); - $this->level--; - return ob_get_clean(); - } - - /** - * Render template - * @return string - * @param $file string - * @param $mime string - * @param $hive array - * @param $ttl int - **/ - function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { - $fw=$this->fw; - $cache=Cache::instance(); - foreach ($fw->split($fw->UI) as $dir) { - if ($cache->exists($hash=$fw->hash($dir.$file),$data)) - return $data; - if (is_file($this->file=$fw->fixslashes($dir.$file))) { - if (isset($_COOKIE[session_name()]) && - !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) - session_start(); - $fw->sync('SESSION'); - $data=$this->sandbox($hive,$mime); - if (isset($this->trigger['afterrender'])) - foreach($this->trigger['afterrender'] as $func) - $data=$fw->call($func,[$data, $dir.$file]); - if ($ttl) - $cache->set($hash,$data,$ttl); - return $data; - } - } - user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); - } - - /** - * post rendering handler - * @param $func callback - */ - function afterrender($func) { - $this->trigger['afterrender'][]=$func; - } - -} - -//! Lightweight template engine -class Preview extends View { - - protected - //! token filter - $filter=[ - 'c'=>'$this->c', - 'esc'=>'$this->esc', - 'raw'=>'$this->raw', - 'export'=>'Base::instance()->export', - 'alias'=>'Base::instance()->alias', - 'format'=>'Base::instance()->format' - ]; - - protected - //! newline interpolation - $interpolation=true; - - /** - * Enable/disable markup parsing interpolation - * mainly used for adding appropriate newlines - * @param $bool bool - */ - function interpolation($bool) { - $this->interpolation=$bool; - } - - /** - * Return C-locale equivalent of number - * @return string - * @param $val int|float - **/ - function c($val) { - $locale=setlocale(LC_NUMERIC,0); - setlocale(LC_NUMERIC,'C'); - $out=(string)(float)$val; - $locale=setlocale(LC_NUMERIC,$locale); - return $out; - } - - /** - * Convert token to variable - * @return string - * @param $str string - **/ - function token($str) { - $fw=$this->fw; - $str=trim(preg_replace('/\{\{(.+?)\}\}/s',trim('\1'), - $fw->compile($str))); - if (preg_match('/^(.+)(?split(trim($parts[2],"\xC2\xA0")) as $func) - $str=((empty($this->filter[$cmd=$func]) && - function_exists($cmd)) || - is_string($cmd=$this->filter($func)))? - $cmd.'('.$str.')': - 'Base::instance()->'. - 'call($this->filter(\''.$func.'\'),['.$str.'])'; - } - return $str; - } - - /** - * Register or get (one specific or all) token filters - * @param string $key - * @param string|closure $func - * @return array|closure|string - */ - function filter($key=NULL,$func=NULL) { - if (!$key) - return array_keys($this->filter); - $key=strtolower($key); - if (!$func) - return $this->filter[$key]; - $this->filter[$key]=$func; - } - - /** - * Assemble markup - * @return string - * @param $node string - **/ - protected function build($node) { - return preg_replace_callback( - '/\{~(.+?)~\}|\{\*(.+?)\*\}|\{\-(.+?)\-\}|'. - '\{\{(.+?)\}\}((\r?\n)*)/s', - function($expr) { - if ($expr[1]) - $str='token($expr[1]).' ?>'; - elseif ($expr[2]) - return ''; - elseif ($expr[3]) - $str=$expr[3]; - else { - $str='token($expr[4])).')'. - ($this->interpolation? - (!empty($expr[6])?'."'.$expr[6].'"':''):'').' ?>'; - if (isset($expr[5])) - $str.=$expr[5]; - } - return $str; - }, - $node - ); - } - - /** - * Render template string - * @return string - * @param $node string|array - * @param $hive array - * @param $ttl int - * @param $persist bool - * @param $escape bool - **/ - function resolve($node,array $hive=NULL,$ttl=0,$persist=FALSE,$escape=NULL) { - $fw=$this->fw; - $cache=Cache::instance(); - if ($escape!==NULL) { - $esc=$fw->ESCAPE; - $fw->ESCAPE=$escape; - } - if ($ttl || $persist) - $hash=$fw->hash($fw->serialize($node)); - if ($ttl && $cache->exists($hash,$data)) - return $data; - if ($persist) { - if (!is_dir($tmp=$fw->TEMP)) - mkdir($tmp,Base::MODE,TRUE); - if (!is_file($this->file=($tmp. - $fw->SEED.'.'.$hash.'.php'))) - $fw->write($this->file,$this->build($node)); - if (isset($_COOKIE[session_name()]) && - !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) - session_start(); - $fw->sync('SESSION'); - $data=$this->sandbox($hive); - } - else { - if (!$hive) - $hive=$fw->hive(); - if ($fw->ESCAPE) - $hive=$this->esc($hive); - extract($hive); - unset($hive); - ob_start(); - eval(' ?>'.$this->build($node).'set($hash,$data,$ttl); - if ($escape!==NULL) - $fw->ESCAPE=$esc; - return $data; - } - - /** - * Parse template string - * @return string - * @param $text string - **/ - function parse($text) { - // Remove PHP code and comments - return preg_replace( - '/\h*<\?(?!xml)(?:php|\s*=)?.+?\?>\h*|'. - '\{\*.+?\*\}/is','', $text); - } - - /** - * Render template - * @return string - * @param $file string - * @param $mime string - * @param $hive array - * @param $ttl int - **/ - function render($file,$mime='text/html',array $hive=NULL,$ttl=0) { - $fw=$this->fw; - $cache=Cache::instance(); - if (!is_dir($tmp=$fw->TEMP)) - mkdir($tmp,Base::MODE,TRUE); - foreach ($fw->split($fw->UI) as $dir) { - if ($cache->exists($hash=$fw->hash($dir.$file),$data)) - return $data; - if (is_file($view=$fw->fixslashes($dir.$file))) { - if (!is_file($this->file=($tmp. - $fw->SEED.'.'.$fw->hash($view).'.php')) || - filemtime($this->file)read($view); - if (isset($this->trigger['beforerender'])) - foreach ($this->trigger['beforerender'] as $func) - $contents=$fw->call($func, [$contents, $view]); - $text=$this->parse($contents); - $fw->write($this->file,$this->build($text)); - } - if (isset($_COOKIE[session_name()]) && - !headers_sent() && session_status()!=PHP_SESSION_ACTIVE) - session_start(); - $fw->sync('SESSION'); - $data=$this->sandbox($hive,$mime); - if(isset($this->trigger['afterrender'])) - foreach ($this->trigger['afterrender'] as $func) - $data=$fw->call($func, [$data, $view]); - if ($ttl) - $cache->set($hash,$data,$ttl); - return $data; - } - } - user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); - } - - /** - * post rendering handler - * @param $func callback - */ - function beforerender($func) { - $this->trigger['beforerender'][]=$func; - } - -} - -//! ISO language/country codes -class ISO extends Prefab { - - //@{ ISO 3166-1 country codes - const - CC_af='Afghanistan', - CC_ax='Åland Islands', - CC_al='Albania', - CC_dz='Algeria', - CC_as='American Samoa', - CC_ad='Andorra', - CC_ao='Angola', - CC_ai='Anguilla', - CC_aq='Antarctica', - CC_ag='Antigua and Barbuda', - CC_ar='Argentina', - CC_am='Armenia', - CC_aw='Aruba', - CC_au='Australia', - CC_at='Austria', - CC_az='Azerbaijan', - CC_bs='Bahamas', - CC_bh='Bahrain', - CC_bd='Bangladesh', - CC_bb='Barbados', - CC_by='Belarus', - CC_be='Belgium', - CC_bz='Belize', - CC_bj='Benin', - CC_bm='Bermuda', - CC_bt='Bhutan', - CC_bo='Bolivia', - CC_bq='Bonaire, Sint Eustatius and Saba', - CC_ba='Bosnia and Herzegovina', - CC_bw='Botswana', - CC_bv='Bouvet Island', - CC_br='Brazil', - CC_io='British Indian Ocean Territory', - CC_bn='Brunei Darussalam', - CC_bg='Bulgaria', - CC_bf='Burkina Faso', - CC_bi='Burundi', - CC_kh='Cambodia', - CC_cm='Cameroon', - CC_ca='Canada', - CC_cv='Cape Verde', - CC_ky='Cayman Islands', - CC_cf='Central African Republic', - CC_td='Chad', - CC_cl='Chile', - CC_cn='China', - CC_cx='Christmas Island', - CC_cc='Cocos (Keeling) Islands', - CC_co='Colombia', - CC_km='Comoros', - CC_cg='Congo', - CC_cd='Congo, The Democratic Republic of', - CC_ck='Cook Islands', - CC_cr='Costa Rica', - CC_ci='Côte d\'ivoire', - CC_hr='Croatia', - CC_cu='Cuba', - CC_cw='Curaçao', - CC_cy='Cyprus', - CC_cz='Czech Republic', - CC_dk='Denmark', - CC_dj='Djibouti', - CC_dm='Dominica', - CC_do='Dominican Republic', - CC_ec='Ecuador', - CC_eg='Egypt', - CC_sv='El Salvador', - CC_gq='Equatorial Guinea', - CC_er='Eritrea', - CC_ee='Estonia', - CC_et='Ethiopia', - CC_fk='Falkland Islands (Malvinas)', - CC_fo='Faroe Islands', - CC_fj='Fiji', - CC_fi='Finland', - CC_fr='France', - CC_gf='French Guiana', - CC_pf='French Polynesia', - CC_tf='French Southern Territories', - CC_ga='Gabon', - CC_gm='Gambia', - CC_ge='Georgia', - CC_de='Germany', - CC_gh='Ghana', - CC_gi='Gibraltar', - CC_gr='Greece', - CC_gl='Greenland', - CC_gd='Grenada', - CC_gp='Guadeloupe', - CC_gu='Guam', - CC_gt='Guatemala', - CC_gg='Guernsey', - CC_gn='Guinea', - CC_gw='Guinea-Bissau', - CC_gy='Guyana', - CC_ht='Haiti', - CC_hm='Heard Island and McDonald Islands', - CC_va='Holy See (Vatican City State)', - CC_hn='Honduras', - CC_hk='Hong Kong', - CC_hu='Hungary', - CC_is='Iceland', - CC_in='India', - CC_id='Indonesia', - CC_ir='Iran, Islamic Republic of', - CC_iq='Iraq', - CC_ie='Ireland', - CC_im='Isle of Man', - CC_il='Israel', - CC_it='Italy', - CC_jm='Jamaica', - CC_jp='Japan', - CC_je='Jersey', - CC_jo='Jordan', - CC_kz='Kazakhstan', - CC_ke='Kenya', - CC_ki='Kiribati', - CC_kp='Korea, Democratic People\'s Republic of', - CC_kr='Korea, Republic of', - CC_kw='Kuwait', - CC_kg='Kyrgyzstan', - CC_la='Lao People\'s Democratic Republic', - CC_lv='Latvia', - CC_lb='Lebanon', - CC_ls='Lesotho', - CC_lr='Liberia', - CC_ly='Libya', - CC_li='Liechtenstein', - CC_lt='Lithuania', - CC_lu='Luxembourg', - CC_mo='Macao', - CC_mk='Macedonia, The Former Yugoslav Republic of', - CC_mg='Madagascar', - CC_mw='Malawi', - CC_my='Malaysia', - CC_mv='Maldives', - CC_ml='Mali', - CC_mt='Malta', - CC_mh='Marshall Islands', - CC_mq='Martinique', - CC_mr='Mauritania', - CC_mu='Mauritius', - CC_yt='Mayotte', - CC_mx='Mexico', - CC_fm='Micronesia, Federated States of', - CC_md='Moldova, Republic of', - CC_mc='Monaco', - CC_mn='Mongolia', - CC_me='Montenegro', - CC_ms='Montserrat', - CC_ma='Morocco', - CC_mz='Mozambique', - CC_mm='Myanmar', - CC_na='Namibia', - CC_nr='Nauru', - CC_np='Nepal', - CC_nl='Netherlands', - CC_nc='New Caledonia', - CC_nz='New Zealand', - CC_ni='Nicaragua', - CC_ne='Niger', - CC_ng='Nigeria', - CC_nu='Niue', - CC_nf='Norfolk Island', - CC_mp='Northern Mariana Islands', - CC_no='Norway', - CC_om='Oman', - CC_pk='Pakistan', - CC_pw='Palau', - CC_ps='Palestinian Territory, Occupied', - CC_pa='Panama', - CC_pg='Papua New Guinea', - CC_py='Paraguay', - CC_pe='Peru', - CC_ph='Philippines', - CC_pn='Pitcairn', - CC_pl='Poland', - CC_pt='Portugal', - CC_pr='Puerto Rico', - CC_qa='Qatar', - CC_re='Réunion', - CC_ro='Romania', - CC_ru='Russian Federation', - CC_rw='Rwanda', - CC_bl='Saint Barthélemy', - CC_sh='Saint Helena, Ascension and Tristan da Cunha', - CC_kn='Saint Kitts and Nevis', - CC_lc='Saint Lucia', - CC_mf='Saint Martin (French Part)', - CC_pm='Saint Pierre and Miquelon', - CC_vc='Saint Vincent and The Grenadines', - CC_ws='Samoa', - CC_sm='San Marino', - CC_st='Sao Tome and Principe', - CC_sa='Saudi Arabia', - CC_sn='Senegal', - CC_rs='Serbia', - CC_sc='Seychelles', - CC_sl='Sierra Leone', - CC_sg='Singapore', - CC_sk='Slovakia', - CC_sx='Sint Maarten (Dutch Part)', - CC_si='Slovenia', - CC_sb='Solomon Islands', - CC_so='Somalia', - CC_za='South Africa', - CC_gs='South Georgia and The South Sandwich Islands', - CC_ss='South Sudan', - CC_es='Spain', - CC_lk='Sri Lanka', - CC_sd='Sudan', - CC_sr='Suriname', - CC_sj='Svalbard and Jan Mayen', - CC_sz='Swaziland', - CC_se='Sweden', - CC_ch='Switzerland', - CC_sy='Syrian Arab Republic', - CC_tw='Taiwan, Province of China', - CC_tj='Tajikistan', - CC_tz='Tanzania, United Republic of', - CC_th='Thailand', - CC_tl='Timor-Leste', - CC_tg='Togo', - CC_tk='Tokelau', - CC_to='Tonga', - CC_tt='Trinidad and Tobago', - CC_tn='Tunisia', - CC_tr='Turkey', - CC_tm='Turkmenistan', - CC_tc='Turks and Caicos Islands', - CC_tv='Tuvalu', - CC_ug='Uganda', - CC_ua='Ukraine', - CC_ae='United Arab Emirates', - CC_gb='United Kingdom', - CC_us='United States', - CC_um='United States Minor Outlying Islands', - CC_uy='Uruguay', - CC_uz='Uzbekistan', - CC_vu='Vanuatu', - CC_ve='Venezuela', - CC_vn='Viet Nam', - CC_vg='Virgin Islands, British', - CC_vi='Virgin Islands, U.S.', - CC_wf='Wallis and Futuna', - CC_eh='Western Sahara', - CC_ye='Yemen', - CC_zm='Zambia', - CC_zw='Zimbabwe'; - //@} - - //@{ ISO 639-1 language codes (Windows-compatibility subset) - const - LC_af='Afrikaans', - LC_am='Amharic', - LC_ar='Arabic', - LC_as='Assamese', - LC_ba='Bashkir', - LC_be='Belarusian', - LC_bg='Bulgarian', - LC_bn='Bengali', - LC_bo='Tibetan', - LC_br='Breton', - LC_ca='Catalan', - LC_co='Corsican', - LC_cs='Czech', - LC_cy='Welsh', - LC_da='Danish', - LC_de='German', - LC_dv='Divehi', - LC_el='Greek', - LC_en='English', - LC_es='Spanish', - LC_et='Estonian', - LC_eu='Basque', - LC_fa='Persian', - LC_fi='Finnish', - LC_fo='Faroese', - LC_fr='French', - LC_gd='Scottish Gaelic', - LC_gl='Galician', - LC_gu='Gujarati', - LC_he='Hebrew', - LC_hi='Hindi', - LC_hr='Croatian', - LC_hu='Hungarian', - LC_hy='Armenian', - LC_id='Indonesian', - LC_ig='Igbo', - LC_is='Icelandic', - LC_it='Italian', - LC_ja='Japanese', - LC_ka='Georgian', - LC_kk='Kazakh', - LC_km='Khmer', - LC_kn='Kannada', - LC_ko='Korean', - LC_lb='Luxembourgish', - LC_lo='Lao', - LC_lt='Lithuanian', - LC_lv='Latvian', - LC_mi='Maori', - LC_ml='Malayalam', - LC_mr='Marathi', - LC_ms='Malay', - LC_mt='Maltese', - LC_ne='Nepali', - LC_nl='Dutch', - LC_no='Norwegian', - LC_oc='Occitan', - LC_or='Oriya', - LC_pl='Polish', - LC_ps='Pashto', - LC_pt='Portuguese', - LC_qu='Quechua', - LC_ro='Romanian', - LC_ru='Russian', - LC_rw='Kinyarwanda', - LC_sa='Sanskrit', - LC_si='Sinhala', - LC_sk='Slovak', - LC_sl='Slovenian', - LC_sq='Albanian', - LC_sv='Swedish', - LC_ta='Tamil', - LC_te='Telugu', - LC_th='Thai', - LC_tk='Turkmen', - LC_tr='Turkish', - LC_tt='Tatar', - LC_uk='Ukrainian', - LC_ur='Urdu', - LC_vi='Vietnamese', - LC_wo='Wolof', - LC_yo='Yoruba', - LC_zh='Chinese'; - //@} - - /** - * Return list of languages indexed by ISO 639-1 language code - * @return array - **/ - function languages() { - return \Base::instance()->constants($this,'LC_'); - } - - /** - * Return list of countries indexed by ISO 3166-1 country code - * @return array - **/ - function countries() { - return \Base::instance()->constants($this,'CC_'); - } - -} - -//! Container for singular object instances -final class Registry { - - private static - //! Object catalog - $table; - - /** - * Return TRUE if object exists in catalog - * @return bool - * @param $key string - **/ - static function exists($key) { - return isset(self::$table[$key]); - } - - /** - * Add object to catalog - * @return object - * @param $key string - * @param $obj object - **/ - static function set($key,$obj) { - return self::$table[$key]=$obj; - } - - /** - * Retrieve object from catalog - * @return object - * @param $key string - **/ - static function get($key) { - return self::$table[$key]; - } - - /** - * Delete object from catalog - * @return NULL - * @param $key string - **/ - static function clear($key) { - self::$table[$key]=NULL; - unset(self::$table[$key]); - } - - //! Prohibit cloning - private function __clone() { - } - - //! Prohibit instantiation - private function __construct() { - } - -} - -return Base::instance(); diff --git a/app/lib/basket.php b/app/lib/basket.php deleted file mode 100644 index 08515ee2..00000000 --- a/app/lib/basket.php +++ /dev/null @@ -1,239 +0,0 @@ -. - -*/ - -//! Session-based pseudo-mapper -class Basket extends Magic { - - //@{ Error messages - const - E_Field='Undefined field %s'; - //@} - - protected - //! Session key - $key, - //! Current item identifier - $id, - //! Current item contents - $item=[]; - - /** - * Return TRUE if field is defined - * @return bool - * @param $key string - **/ - function exists($key) { - return array_key_exists($key,$this->item); - } - - /** - * Assign value to field - * @return scalar|FALSE - * @param $key string - * @param $val scalar - **/ - function set($key,$val) { - return ($key=='_id')?FALSE:($this->item[$key]=$val); - } - - /** - * Retrieve value of field - * @return scalar|FALSE - * @param $key string - **/ - function &get($key) { - if ($key=='_id') - return $this->id; - if (array_key_exists($key,$this->item)) - return $this->item[$key]; - user_error(sprintf(self::E_Field,$key),E_USER_ERROR); - return FALSE; - } - - /** - * Delete field - * @return NULL - * @param $key string - **/ - function clear($key) { - unset($this->item[$key]); - } - - /** - * Return items that match key/value pair; - * If no key/value pair specified, return all items - * @return array - * @param $key string - * @param $val mixed - **/ - function find($key=NULL,$val=NULL) { - $out=[]; - if (isset($_SESSION[$this->key])) { - foreach ($_SESSION[$this->key] as $id=>$item) - if (!isset($key) || - array_key_exists($key,$item) && $item[$key]==$val || - $key=='_id' && $id==$val) { - $obj=clone($this); - $obj->id=$id; - $obj->item=$item; - $out[]=$obj; - } - } - return $out; - } - - /** - * Return first item that matches key/value pair - * @return object|FALSE - * @param $key string - * @param $val mixed - **/ - function findone($key,$val) { - return ($data=$this->find($key,$val))?$data[0]:FALSE; - } - - /** - * Map current item to matching key/value pair - * @return array - * @param $key string - * @param $val mixed - **/ - function load($key,$val) { - if ($found=$this->find($key,$val)) { - $this->id=$found[0]->id; - return $this->item=$found[0]->item; - } - $this->reset(); - return []; - } - - /** - * Return TRUE if current item is empty/undefined - * @return bool - **/ - function dry() { - return !$this->item; - } - - /** - * Return number of items in basket - * @return int - **/ - function count() { - return isset($_SESSION[$this->key])?count($_SESSION[$this->key]):0; - } - - /** - * Save current item - * @return array - **/ - function save() { - if (!$this->id) - $this->id=uniqid(NULL,TRUE); - $_SESSION[$this->key][$this->id]=$this->item; - return $this->item; - } - - /** - * Erase item matching key/value pair - * @return bool - * @param $key string - * @param $val mixed - **/ - function erase($key,$val) { - $found=$this->find($key,$val); - if ($found && $id=$found[0]->id) { - unset($_SESSION[$this->key][$id]); - if ($id==$this->id) - $this->reset(); - return TRUE; - } - return FALSE; - } - - /** - * Reset cursor - * @return NULL - **/ - function reset() { - $this->id=NULL; - $this->item=[]; - } - - /** - * Empty basket - * @return NULL - **/ - function drop() { - unset($_SESSION[$this->key]); - } - - /** - * Hydrate item using hive array variable - * @return NULL - * @param $var array|string - **/ - function copyfrom($var) { - if (is_string($var)) - $var=\Base::instance()->$var; - foreach ($var as $key=>$val) - $this->set($key,$val); - } - - /** - * Populate hive array variable with item contents - * @return NULL - * @param $key string - **/ - function copyto($key) { - $var=&\Base::instance()->ref($key); - foreach ($this->item as $key=>$field) - $var[$key]=$field; - } - - /** - * Check out basket contents - * @return array - **/ - function checkout() { - if (isset($_SESSION[$this->key])) { - $out=$_SESSION[$this->key]; - unset($_SESSION[$this->key]); - return $out; - } - return []; - } - - /** - * Instantiate class - * @return void - * @param $key string - **/ - function __construct($key='basket') { - $this->key=$key; - if (session_status()!=PHP_SESSION_ACTIVE) - session_start(); - Base::instance()->sync('SESSION'); - $this->reset(); - } - -} diff --git a/app/lib/bcrypt.php b/app/lib/bcrypt.php deleted file mode 100644 index 23554719..00000000 --- a/app/lib/bcrypt.php +++ /dev/null @@ -1,96 +0,0 @@ -. -* -**/ - -/** -* Lightweight password hashing library (PHP 5.5+ only) -* @deprecated Use http://php.net/manual/en/ref.password.php instead -**/ -class Bcrypt extends Prefab { - - //@{ Error messages - const - E_CostArg='Invalid cost parameter', - E_SaltArg='Salt must be at least 22 alphanumeric characters'; - //@} - - //! Default cost - const - COST=10; - - /** - * Generate bcrypt hash of string - * @return string|FALSE - * @param $pw string - * @param $salt string - * @param $cost int - **/ - function hash($pw,$salt=NULL,$cost=self::COST) { - if ($cost<4 || $cost>31) - user_error(self::E_CostArg,E_USER_ERROR); - $len=22; - if ($salt) { - if (!preg_match('/^[[:alnum:]\.\/]{'.$len.',}$/',$salt)) - user_error(self::E_SaltArg,E_USER_ERROR); - } - else { - $raw=16; - $iv=''; - if (!$iv && extension_loaded('openssl')) - $iv=openssl_random_pseudo_bytes($raw); - if (!$iv) - for ($i=0;$i<$raw;$i++) - $iv.=chr(mt_rand(0,255)); - $salt=str_replace('+','.',base64_encode($iv)); - } - $salt=substr($salt,0,$len); - $hash=crypt($pw,sprintf('$2y$%02d$',$cost).$salt); - return strlen($hash)>13?$hash:FALSE; - } - - /** - * Check if password is still strong enough - * @return bool - * @param $hash string - * @param $cost int - **/ - function needs_rehash($hash,$cost=self::COST) { - list($pwcost)=sscanf($hash,"$2y$%d$"); - return $pwcost<$cost; - } - - /** - * Verify password against hash using timing attack resistant approach - * @return bool - * @param $pw string - * @param $hash string - **/ - function verify($pw,$hash) { - $val=crypt($pw,$hash); - $len=strlen($val); - if ($len!=strlen($hash) || $len<14) - return FALSE; - $out=0; - for ($i=0;$i<$len;$i++) - $out|=(ord($val[$i])^ord($hash[$i])); - return $out===0; - } - -} diff --git a/app/lib/changelog.txt b/app/lib/changelog.txt deleted file mode 100644 index 6a4ddd29..00000000 --- a/app/lib/changelog.txt +++ /dev/null @@ -1,509 +0,0 @@ -CHANGELOG - -3.4.0 (1 January 2015) -* NEW: [redirects] section -* NEW: Custom config sections -* NEW: User-defined AUTOLOAD function -* NEW: ONREROUTE variable -* NEW: Provision for in-memory Jig database (#727) -* Return run() result (#687) -* Pass result of run() to mock() (#687) -* Add port suffix to REALM variable -* New attribute in tag to extend hive -* Adjust unit tests and clean up templates -* Expose header-related methods -* Web->request: allow content array -* Preserve contents of ROUTES (#723) -* Smart detection of PHP functions in template expressions -* Add afterrender() hook to View class -* Implement ArrayAccess and magic properties on hive -* Improvement on mocking of superglobals and request body -* Fix table creation for pgsql handled sessions -* Add QUERY to hive -* Exempt E_NOTICE from default error_reporting() -* Add method to build alias routes from template, fixes #693 -* Fix dangerous caching of cookie values -* Fix multiple encoding in nested templates -* Fix node attribute parsing for empty/zero values -* Apply URL encoding on BASE to emulate v2 behavior (#123) -* Improve Base->map performance (#595) -* Add simple backtrace for fatal errors -* Count Cursor->load() results (#581) -* Add form field name to Web->receive() callback arguments -* Fix missing newlines after template expansion -* Fix overwrite of ENCODING variable -* limit & offset workaround for SQL Server, fixes #671 -* SQL Mapper->find: GROUP BY SQL compliant statement -* Bug fix: Missing abstract method fields() -* Bug fix: Auto escaping does not work with mapper objects (#710) -* Bug fix: 'with' attribute in tag raise error when no token - inside -* View rendering: optional Content-Type header -* Bug fix: Undefined variable: cache (#705) -* Bug fix: Routing does not work if project base path includes valid - special URI character (#704) -* Bug fix: Template hash collision (#702) -* Bug fix: Property visibility is incorrect (#697) -* Bug fix: Missing Allow header on HTTP 405 response -* Bug fix: Double quotes in lexicon files (#681) -* Bug fix: Space should not be mandatory in ICU pluralization format string -* Bug fix: Incorrect log entry when SQL query contains a question mark -* Bug fix: Error stack trace -* Bug fix: Cookie expiration (#665) -* Bug fix: OR operator (||) parsed incorrectly -* Bug fix: Routing treatment of * wildcard character -* Bug fix: Mapper copyfrom() method doesn't allow class/object callbacks - (#590) -* Bug fix: exists() creates elements/properties (#591) -* Bug fix: Wildcard in routing pattern consumes entire query string (#592) -* Bug fix: Workaround bug in latest MongoDB driver -* Bug fix: Default error handler silently fails for AJAX request with - DEBUG>0 (#599) -* Bug fix: Mocked BODY overwritten (#601) -* Bug fix: Undefined pkey (#607) - -3.3.0 (8 August 2014) -* NEW: Attribute in tag to extend hive -* NEW: Image overlay with transparency and alignment control -* NEW: Allow redirection of specified route patterns to a URL -* Bug fix: Missing AND operator in SQL Server schema query (Issue #576) -* Count Cursor->load() results (Feature request #581) -* Mapper copyfrom() method doesn't allow class/object callbacks (Issue #590) -* Bug fix: exists() creates elements/properties (Issue #591) -* Bug fix: Wildcard in routing pattern consumes entire query string - (Issue #592) -* Tweak Base->map performance (Issue #595) -* Bug fix: Default error handler silently fails for AJAX request with - DEBUG>0 (Issue #599) -* Bug fix: Mocked BODY overwritten (Issue #601) -* Bug fix: Undefined pkey (Issue #607) -* Bug fix: beforeupdate() position (Issue #633) -* Bug fix: exists() return value for cached keys -* Bug fix: Missing error code in UNLOAD handler -* Bug fix: OR operator (||) parsed incorrectly -* Add input name parameter to custom slug function -* Apply URL encoding on BASE to emulate v2 behavior (Issue #123) -* Reduce mapper update() iterations -* Bug fix: Routing treatment of * wildcard character -* SQL Mapper->find: GROUP BY SQL compliant statement -* Work around bug in latest MongoDB driver -* Work around probable race condition and optimize cache access -* View rendering: Optional Content-Type header -* Fix missing newlines after template expansion -* Add form field name to Web->receive() callback arguments -* Quick reference: add RAW variable - -3.2.2 (19 March 2014) -* NEW: Locales set automatically (Feature request #522) -* NEW: Mapper dbtype() -* NEW: before- and after- triggers for all mappers -* NEW: Decode HTML5 entities if PHP>5.3 detected (Feature request #552) -* NEW: Send credentials only if AUTH is present in the SMTP extension - response (Feature request #545) -* NEW: BITMASK variable to allow ENT_COMPAT override -* NEW: Redis support for caching -* Enable SMTP feature detection -* Enable extended ICU custom date format (Feature request #555) -* Enable custom time ICU format -* Add option to turn off session table creation (Feature request #557) -* Enhanced template token rendering and custom filters (Feature request - #550) -* Avert multiple loads in DB-managed sessions (Feature request #558) -* Add EXEC to associative fetch -* Bug fix: Building template tokens breaks on inline OR condition (Issue - #573) -* Bug fix: SMTP->send does not use the $log parameter (Issue #571) -* Bug fix: Allow setting sqlsrv primary keys on insert (Issue #570) -* Bug fix: Generated query for obtaining table schema in sqlsrv incorrect - (Bug #565) -* Bug fix: SQL mapper flag set even when value has not changed (Bug #562) -* Bug fix: Add XFRAME config option (Feature request #546) -* Bug fix: Incorrect parsing of comments (Issue #541) -* Bug fix: Multiple Set-Cookie headers (Issue #533) -* Bug fix: Mapper is dry after save() -* Bug fix: Prevent infinite loop when error handler is triggered - (Issue #361) -* Bug fix: Mapper tweaks not passing primary keys as arguments -* Bug fix: Zero indexes in dot-notated arrays fail to compile -* Bug fix: Prevent GROUP clause double-escaping -* Bug fix: Regression of zlib compression bug -* Bug fix: Method copyto() does not include ad hoc fields -* Check existence of OpenID mode (Issue #529) -* Generate a 404 when a tokenized class doesn't exist -* Fix SQLite quotes (Issue #521) -* Bug fix: BASE is incorrect on Windows - -3.2.1 (7 January 2014) -* NEW: EMOJI variable, UTF->translate(), UTF->emojify(), and UTF->strrev() -* Allow empty strings in config() -* Add support for turning off php://input buffering via RAW - (FALSE by default) -* Add Cursor->load() and Cursor->find() TTL support -* Support Web->receive() large file downloads via PUT -* ONERROR safety check -* Fix session CSRF cookie detection -* Framework object now passed to route handler contructors -* Allow override of DIACRITICS -* Various code optimizations -* Support log disabling (Issue #483) -* Implicit mapper load() on authentication -* Declare abstract methods for Cursor derivatives -* Support single-quoted HTML/XML attributes (Feature request #503) -* Relax property visibility of mappers and derivatives -* Deprecated: {{~ ~}} instructions and {{* *}} comments; Use {~ ~} and - {* *} instead -* Minor fix: Audit->ipv4() return value -* Bug fix: Backslashes in BASE not converted on Windows -* Bug fix: UTF->substr() with negative offset and specified length -* Bug fix: Replace named URL tokens on render() -* Bug fix: BASE is not empty when run from document root -* Bug fix: stringify() recursion - -3.2.0 (18 December 2013) -* NEW: Automatic CSRF protection (with IP and User-Agent checks) for - sessions mapped to SQL-, Jig-, Mongo- and Cache-based backends -* NEW: Named routes -* NEW: PATH variable; returns the URL relative to BASE -* NEW: Image->captcha() color parameters -* NEW: Ability to access MongoCuror thru the cursor() method -* NEW: Mapper->fields() method returns array of field names -* NEW: Mapper onload(), oninsert(), onupdate(), and onerase() event - listeners/triggers -* NEW: Preview class (a lightweight template engine) -* NEW: rel() method derives path from URL relative to BASE; useful for - rerouting -* NEW: PREFIX variable for prepending a string to a dictionary term; - Enable support for prefixed dictionary arrays and .ini files (Feature - request #440) -* NEW: Google static map plugin -* NEW: devoid() method -* Introduce clean(); similar to scrub(), except that arg is passed by - value -* Use $ttl for cookie expiration (Issue #457) -* Fix needs_rehash() cost comparison -* Add pass-by-reference argument to exists() so if method returns TRUE, - a subsequent get() is unnecessary -* Improve MySQL support -* Move esc(), raw(), and dupe() to View class where they more - appropriately belong -* Allow user-defined fields in SQL mapper constructor (Feature request - #450) -* Re-implement the pre-3.0 template resolve() feature -* Remove redundant instances of session_commit() -* Add support for input filtering in Mapper->copyfrom() -* Prevent intrusive behavior of Mapper->copyfrom() -* Support multiple SQL primary keys -* Support custom tag attributes/inline tokens defined at runtime - (Feature request #438) -* Broader support for HTTP basic auth -* Prohibit Jig _id clear() -* Add support for detailed stringify() output -* Add base directory to UI path as fallback -* Support Test->expect() chaining -* Support __tostring() in stringify() -* Trigger error on invalid CAPTCHA length (Issue #458) -* Bug fix: exists() pass-by-reference argument returns incorrect value -* Bug fix: DB Exec does not return affected row if query contains a - sub-SELECT (Issue #437) -* Improve seed generator and add code for detecting of acceptable - limits in Image->captcha() (Feature request #460) -* Add decimal format ICU extension -* Bug fix: 404-reported URI contains HTTP query -* Bug fix: Data type detection in DB->schema() -* Bug fix: TZ initialization -* Bug fix: paginate() passes incorrect argument to count() -* Bug fix: Incorrect query when reloading after insert() -* Bug fix: SQL preg_match error in pdo_type matching (Issue #447) -* Bug fix: Missing merge() function (Issue #444) -* Bug fix: BASE misdefined in command line mode -* Bug fix: Stringifying hive may run infinite (Issue #436) -* Bug fix: Incomplete stringify() when DEBUG<3 (Issue #432) -* Bug fix: Redirection of basic auth (Issue #430) -* Bug fix: Filter only PHP code (including short tags) in templates -* Bug fix: Markdown paragraph parser does not convert PHP code blocks - properly -* Bug fix: identicon() colors on same keys are randomized -* Bug fix: quotekey() fails on aliased keys -* Bug fix: Missing _id in Jig->find() return value -* Bug fix: LANGUAGE/LOCALES handling -* Bug fix: Loose comparison in stringify() - -3.1.2 (5 November 2013) -* Abandon .chm help format; Package API documentation in plain HTML; - (Launch lib/api/index.html in your browser) -* Deprecate BAIL in favor of HALT (default: TRUE) -* Revert to 3.1.0 autoload behavior; Add support for lowercase folder - names -* Allow Spring-style HTTP method overrides -* Add support for SQL Server-based sessions -* Capture full X-Forwarded-For header -* Add protection against malicious scripts; Extra check if file was really - uploaded -* Pass-thru page limit in return value of Cursor->paginate() -* Optimize code: Implement single-pass escaping -* Short circuit Jig->find() if source file is empty -* Bug fix: PHP globals passed by reference in hive() result (Issue #424) -* Bug fix: ZIP mime type incorrect behavior -* Bug fix: Jig->erase() filter malfunction -* Bug fix: Mongo->select() group -* Bug fix: Unknown bcrypt constant - -3.1.1 (13 October 2013) -* NEW: Support OpenID attribute exchange -* NEW: BAIL variable enables/disables continuance of execution on non-fatal - errors -* Deprecate BAIL in favor of HALT (default: FALSE) -* Add support for Oracle -* Mark cached queries in log (Feature Request #405) -* Implement Bcrypt->needs_reshash() -* Add entropy to SQL cache hash; Add uuid() method to DB backends -* Find real document root; Simplify debug paths -* Permit OpenID required fields to be declared as comma-separated string or - array -* Pass modified filename as argument to user-defined function in - Web->receive() -* Quote keys in optional SQL clauses (Issue #408) -* Allow UNLOAD to override fatal error detection (Issue #404) -* Mutex operator precedence error (Issue #406) -* Bug fix: exists() malfunction (Issue #401) -* Bug fix: Jig mapper triggers error when loading from CACHE (Issue #403) -* Bug fix: Array index check -* Bug fix: OpenID verified() return value -* Bug fix: Basket->find() should return a set of results (Issue #407); - Also implemented findone() for consistency with mappers -* Bug fix: PostgreSQL last insert ID (Issue #410) -* Bug fix: $port component URL overwritten by _socket() -* Bug fix: Calculation of elapsed time - -3.1.0 (20 August 2013) -* NEW: Web->filler() returns a chunk of text from the standard - Lorem Ipsum passage -* Change in behavior: Drop support for JSON serialization -* SQL->exec() now returns value of RETURNING clause -* Add support for $ttl argument in count() (Issue #393) -* Allow UI to be overridden by custom $path -* Return result of PDO primitives: begintransaction(), rollback(), and - commit() -* Full support for PHP 5.5 -* Flush buffers only when DEBUG=0 -* Support class->method, class::method, and lambda functions as - Web->basic() arguments -* Commit session on Basket->save() -* Optional enlargement in Image->resize() -* Support authentication on hosts running PHP-CGI -* Change visibility level of Cache properties -* Prevent ONERROR recursion -* Work around Apache pre-2.4 VirtualDocumentRoot bug -* Prioritize cURL in HTTP engine detection -* Bug fix: Minify tricky JS -* Bug fix: desktop() detection -* Bug fix: Double-slash on TEMP-relative path -* Bug fix: Cursor mapping of first() and last() records -* Bug fix: Premature end of Web->receive() on multiple files -* Bug fix: German umlaute to its corresponding grammatically-correct - equivalent - -3.0.9 (12 June 2013) -* NEW: Web->whois() -* NEW: Template tags -* Improve CACHE consistency -* Case-insensitive MIME type detection -* Support pre-PHP 5.3.4 in Prefab->instance() -* Refactor isdesktop() and ismobile(); Add isbot() -* Add support for Markdown strike-through -* Work around ODBC's lack of quote() support -* Remove useless Prefab destructor -* Support multiple cache instances -* Bug fix: Underscores in OpenId keys mangled -* Refactor format() -* Numerous tweaks -* Bug fix: MongoId object not preserved -* Bug fix: Double-quotes included in lexicon() string (Issue #341) -* Bug fix: UTF-8 formatting mangled on Windows (Issue #342) -* Bug fix: Cache->load() error when CACHE is FALSE (Issue #344) -* Bug fix: send() ternary expression -* Bug fix: Country code constants - -3.0.8 (17 May 2013) -* NEW: Bcrypt lightweight hashing library\ -* Return total number of records in superset in Cursor->paginate() -* ONERROR short-circuit (Enhancement #334) -* Apply quotes/backticks on DB identifiers -* Allow enabling/disabling of SQL log -* Normalize glob() behavior (Issue #330) -* Bug fix: mbstring 2-byte text truncation (Issue #325) -* Bug fix: Unsupported operand types (Issue #324) - -3.0.7 (2 May 2013) -* NEW: route() now allows an array of routing patterns as first argument; - support array as first argument of map() -* NEW: entropy() for calculating password strength (NIST 800-63) -* NEW: AGENT variable containing auto-detected HTTP user agent string -* NEW: ismobile() and isdesktop() methods -* NEW: Prefab class and descendants now accept constructor arguments -* Change in behavior: Cache->exists() now returns timestamp and TTL of - cache entry or FALSE if not found (Feature request #315) -* Preserve timestamp and TTL when updating cache entry (Feature request - #316) -* Improved currency formatting with C99 compliance -* Suppress unnecessary program halt at startup caused by misconfigured - server -* Add support for dashes in custom attribute names in templates -* Bug fix: Routing precedene (Issue #313) -* Bug fix: Remove Jig _id element from document property -* Bug fix: Web->rss() error when not enough items in the feed (Issue #299) -* Bug fix: Web engine fallback (Issue #300) -* Bug fix: and formatting -* Bug fix: Text rendering of text with trailing punctuation (Issue #303) -* Bug fix: Incorrect regex in SMTP - -3.0.6 (31 Mar 2013) -* NEW: Image->crop() -* Modify documentation blocks for PHPDoc interoperability -* Allow user to control whether Base->rerouet() uses a permanent or - temporary redirect -* Allow JAR elements to be set individually -* Refactor DB\SQL\Mapper->insert() to cope with autoincrement fields -* Trigger error when captcha() font is missing -* Remove unnecessary markdown regex recursion -* Check for scalars instead of DB\SQL strings -* Implement more comprehensive diacritics table -* Add option for disabling 401 errors when basic auth() fails -* Add markdown syntax highlighting for Apache configuration -* Markdown->render() deprecated to remove dependency on UI variable; - Feature replaced by Markdown->convert() to enable translation from - markdown string to HTML -* Optimize factory() code of all data mappers -* Apply backticks on MySQL table names -* Bug fix: Routing failure when directory path contains a tilde (Issue #291) -* Bug fix: Incorrect markdown parsing of strong/em sequences and inline HTML -* Bug fix: Cached page not echoed (Issue #278) -* Bug fix: Object properties not escaped when rendering -* Bug fix: OpenID error response ignored -* Bug fix: memcache_get_extended_stats() timeout -* Bug fix: Base->set() doesn't pass TTL to Cache->set() -* Bug fix: Base->scrub() ignores pass-thru * argument (Issue #274) - -3.0.5 (16 Feb 2013) -* NEW: Markdown class with PHP, HTML, and .ini syntax highlighting support -* NEW: Options for caching of select() and find() results -* NEW: Web->acceptable() -* Add send() argument for forcing downloads -* Provide read() option for applying Unix LF as standard line ending -* Bypass lexicon() call if LANGUAGE is undefined -* Load fallback language dictionary if LANGUAGE is undefined -* map() now checks existence of class/methods for non-tokenized URLs -* Improve error reporting of non-existent Template methods -* Address output buffer issues on some servers -* Bug fix: Setting DEBUG to 0 won't suppress the stack trace when the - content type is application/json (Issue #257) -* Bug fix: Image dump/render additional arguments shifted -* Bug fix: ob_clean() causes buffer issues with zlib compression -* Bug fix: minify() fails when commenting CSS @ rules (Issue #251) -* Bug fix: Handling of commas inside quoted strings -* Bug fix: Glitch in stringify() handling of closures -* Bug fix: dry() in mappers returns TRUE despite being hydrated by - factory() (Issue #265) -* Bug fix: expect() not handling flags correctly -* Bug fix: weather() fails when server is unreachable - -3.0.4 (29 Jan 2013) -* NEW: Support for ICU/CLDR pluralization -* NEW: User-defined FALLBACK language -* NEW: minify() now recognizes CSS @import directives -* NEW: UTF->bom() returns byte order mark for UTF-8 encoding -* Expose SQL\Mapper->schema() -* Change in behavior: Send error response as JSON string if AJAX request is - detected -* Deprecated: afind*() methods -* Discard output buffer in favor of debug output -* Make _id available to Jig queries -* Magic class now implements ArrayAccess -* Abort execution on startup errors -* Suppress stack trace on DEBUG level 0 -* Allow single = as equality operator in Jig query expressions -* Abort OpenID discovery if Web->request() fails -* Mimic PHP *RECURSION* in stringify() -* Modify Jig parser to allow wildcard-search using preg_match() -* Abort execution after error() execution -* Concatenate cached/uncached minify() iterations; Prevent spillover - caching of previous minify() result -* Work around obscure PHP session id regeneration bug -* Revise algorithm for Jig filter involving undefined fields (Issue #230) -* Use checkdnsrr() instead of gethostbyname() in DNSBL check -* Auto-adjust pagination to cursor boundaries -* Add Romanian diacritics -* Bug fix: Root namespace reference and sorting with undefined Jig fields -* Bug fix: Greedy receive() regex -* Bug fix: Default LANGUAGE always 'en' -* Bug fix: minify() hammers cache backend -* Bug fix: Previous values of primary keys not saved during factory() - instantiation -* Bug fix: Jig find() fails when search key is not present in all records -* Bug fix: Jig SORT_DESC (Issue #233) -* Bug fix: Error reporting (Issue #225) -* Bug fix: language() return value - -3.0.3 (29 Dec 2013) -* NEW: [ajax] and [sync] routing pattern modifiers -* NEW: Basket class (session-based pseudo-mapper, shopping cart, etc.) -* NEW: Test->message() method -* NEW: DB profiling via DB->log() -* NEW: Matrix->calendar() -* NEW: Audit->card() and Audit->mod10() for credit card verification -* NEW: Geo->weather() -* NEW: Base->relay() accepts comma-separated callbacks; but unlike - Base->chain(), result of previous callback becomes argument of the next -* Numerous performance tweaks -* Interoperability with new MongoClient class -* Web->request() now recognizes gzip and deflate encoding -* Differences in behavior of Web->request() engines rectified -* mutex() now uses an ID as argument (instead of filename to make it clear - that specified file is not the target being locked, but a primitive - cross-platform semaphore) -* DB\SQL\Mapper field _id now returned even in the absence of any - auto-increment field -* Magic class spinned off as a separate file -* ISO 3166-1 alpha-2 table updated -* Apache redirect emulation for PHP 5.4 CLI server mode -* Framework instance now passed as argument to any user-defined shutdown - function -* Cache engine now used as storage for Web->minify() output -* Flag added for enabling/disabling Image class filter history -* Bug fix: Trailing routing token consumes HTTP query -* Bug fix: LANGUAGE spills over to LOCALES setting -* Bug fix: Inconsistent dry() return value -* Bug fix: URL-decoding - -3.0.2 (23 Dec 2013) -* NEW: Syntax-highlighted stack traces via Base->highlight(); boolean - HIGHLIGHT global variable can be used to enable/disable this feature -* NEW: Template engine tag -* NEW: Image->captcha() -* NEW: DNSBL-based spammer detection (ported from 2.x) -* NEW: paginate(), first(), and last() methods for data mappers -* NEW: X-HTTP-Method-Override header now recognized -* NEW: Base->chain() method for executing callbacks in succession -* NEW: HOST global variable; derived from either $_SERVER['SERVER_NAME'] or - gethostname() -* NEW: REALM global variable representing full canonical URI -* NEW: Auth plug-in -* NEW: Pingback plug-in (implements both Pingback 1.0 protocol client and - server) -* NEW: DEBUG verbosity can now reach up to level 3; Base->stringify() drills - down to object properties at this setting -* NEW: HTTP PATCH method added to recognized HTTP ReST methods -* Web->slug() now trims trailing dashes -* Web->request() now allows relative local URLs as argument -* Use of PARAMS in route handlers now unnecessary; framework now passes two - arguments to route handlers: the framework object instance and an array - containing the captured values of tokens in route patterns -* Standardized timeout settings among Web->request() backends -* Session IDs regenerated for additional security -* Automatic HTTP 404 responses by Base->call() now restricted to route - handlers -* Empty comments in ini-style files now parsed properly -* Use file_get_contents() in methods that don't involve high concurrency - -3.0.1 (14 Dec 2013) -* Major rewrite of much of the framework's core features diff --git a/app/lib/cli/ws.php b/app/lib/cli/ws.php deleted file mode 100644 index b9c575bb..00000000 --- a/app/lib/cli/ws.php +++ /dev/null @@ -1,491 +0,0 @@ -. - -*/ - -namespace CLI; - -//! RFC6455 WebSocket server -class WS { - - const - //! UUID magic string - Magic='258EAFA5-E914-47DA-95CA-C5AB0DC85B11', - //! Max packet size - Packet=65536; - - //@{ Mask bits for first byte of header - const - Text=0x01, - Binary=0x02, - Close=0x08, - Ping=0x09, - Pong=0x0a, - OpCode=0x0f, - Finale=0x80; - //@} - - //@{ Mask bits for second byte of header - const - Length=0x7f; - //@} - - protected - $addr, - $ctx, - $wait, - $sockets, - $protocol, - $agents=[], - $events=[]; - - /** - * Allocate stream socket - * @return NULL - * @param $socket resource - **/ - function alloc($socket) { - if (is_bool($buf=$this->read($socket))) - return; - // Get WebSocket headers - $hdrs=[]; - $EOL="\r\n"; - $verb=NULL; - $uri=NULL; - foreach (explode($EOL,trim($buf)) as $line) - if (preg_match('/^(\w+)\s(.+)\sHTTP\/1\.\d$/', - trim($line),$match)) { - $verb=$match[1]; - $uri=$match[2]; - } - else - if (preg_match('/^(.+): (.+)/',trim($line),$match)) - // Standardize header - $hdrs[ - strtr( - ucwords( - strtolower( - strtr($match[1],'-',' ') - ) - ),' ','-' - ) - ]=$match[2]; - else { - $this->close($socket); - return; - } - if (empty($hdrs['Upgrade']) && - empty($hdrs['Sec-Websocket-Key'])) { - // Not a WebSocket request - if ($verb && $uri) - $this->write( - $socket, - 'HTTP/1.1 400 Bad Request'.$EOL. - 'Connection: close'.$EOL.$EOL - ); - $this->close($socket); - return; - } - // Handshake - $buf='HTTP/1.1 101 Switching Protocols'.$EOL. - 'Upgrade: websocket'.$EOL. - 'Connection: Upgrade'.$EOL; - if (isset($hdrs['Sec-Websocket-Protocol'])) - $buf.='Sec-WebSocket-Protocol: '. - $hdrs['Sec-Websocket-Protocol'].$EOL; - $buf.='Sec-WebSocket-Accept: '. - base64_encode( - sha1($hdrs['Sec-Websocket-Key'].WS::Magic,TRUE) - ).$EOL.$EOL; - if ($this->write($socket,$buf)) { - // Connect agent to server - $this->sockets[(int)$socket]=$socket; - $this->agents[(int)$socket]= - new Agent($this,$socket,$verb,$uri,$hdrs); - } - } - - /** - * Close stream socket - * @return NULL - * @param $socket resource - **/ - function close($socket) { - if (isset($this->agents[(int)$socket])) - unset($this->sockets[(int)$socket],$this->agents[(int)$socket]); - stream_socket_shutdown($socket,STREAM_SHUT_WR); - @fclose($socket); - } - - /** - * Read from stream socket - * @return string|FALSE - * @param $socket resource - **/ - function read($socket) { - if (is_string($buf=@fread($socket,WS::Packet)) && - strlen($buf) && - strlen($buf)events['error']) && - is_callable($func=$this->events['error'])) - $func($this); - $this->close($socket); - return FALSE; - } - - /** - * Write to stream socket - * @return int|FALSE - * @param $socket resource - * @param $buf string - **/ - function write($socket,$buf) { - for ($i=0,$bytes=0;$ievents['error']) && - is_callable($func=$this->events['error'])) - $func($this); - $this->close($socket); - return FALSE; - } - return $bytes; - } - - /** - * Return socket agents - * @return array - * @param $uri string - ***/ - function agents($uri=NULL) { - return array_filter( - $this->agents, - function($val) use($uri) { - return $uri?($val->uri()==$uri):TRUE; - } - ); - } - - /** - * Return event handlers - * @return array - **/ - function events() { - return $this->events; - } - - /** - * Bind function to event handler - * @return object - * @param $event string - * @param $func callable - **/ - function on($event,$func) { - $this->events[$event]=$func; - return $this; - } - - /** - * Terminate server - * @return NULL - * @param $signal int - **/ - function kill($signal) { - die; - } - - /** - * Execute the server process - * @return object - **/ - function run() { - $fw=\Base::instance(); - // Assign signal handlers - declare(ticks=1); - pcntl_signal(SIGINT,[$this,'kill']); - pcntl_signal(SIGTERM,[$this,'kill']); - gc_enable(); - // Activate WebSocket listener - $listen=stream_socket_server( - $this->addr,$errno,$errstr, - STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, - $this->ctx - ); - $socket=socket_import_stream($listen); - register_shutdown_function(function() use($listen) { - foreach ($this->sockets as $socket) - if ($socket!=$listen) - $this->close($socket); - $this->close($listen); - if (isset($this->events['stop']) && - is_callable($func=$this->events['stop'])) - $func($this); - }); - if ($errstr) - user_error($errstr,E_USER_ERROR); - if (isset($this->events['start']) && - is_callable($func=$this->events['start'])) - $func($this); - $this->sockets=[(int)$listen=>$listen]; - $empty=[]; - $wait=$this->wait; - while (TRUE) { - $active=$this->sockets; - $mark=microtime(TRUE); - $count=@stream_select( - $active,$empty,$empty,(int)$wait,round(1e6*($wait-(int)$wait)) - ); - if (is_bool($count) && $wait) { - if (isset($this->events['error']) && - is_callable($func=$this->events['error'])) - $func($this); - die; - } - if ($count) { - // Process active connections - foreach ($active as $socket) { - if (!is_resource($socket)) - continue; - if ($socket==$listen) { - if ($socket=@stream_socket_accept($listen,0)) - $this->alloc($socket); - else - if (isset($this->events['error']) && - is_callable($func=$this->events['error'])) - $func($this); - } - else { - $id=(int)$socket; - if (isset($this->agents[$id])) - $this->agents[$id]->fetch(); - } - } - $wait-=microtime(TRUE)-$mark; - while ($wait<1e-6) { - $wait+=$this->wait; - $count=0; - } - } - if (!$count) { - $mark=microtime(TRUE); - foreach ($this->sockets as $id=>$socket) { - if (!is_resource($socket)) - continue; - if ($socket!=$listen && - isset($this->agents[$id]) && - isset($this->events['idle']) && - is_callable($func=$this->events['idle'])) - $func($this->agents[$id]); - } - $wait=$this->wait-microtime(TRUE)+$mark; - } - gc_collect_cycles(); - } - } - - /** - * Instantiate object - * @return object - * @param $addr string - * @param $ctx resource - * @param $wait int - **/ - function __construct($addr,$ctx=NULL,$wait=60) { - $this->addr=$addr; - $this->ctx=$ctx?:stream_context_create(); - $this->wait=$wait; - $this->events=[]; - } - -} - -//! RFC6455 remote socket -class Agent { - - protected - $server, - $id, - $socket, - $flag, - $verb, - $uri, - $headers, - $events; - - /** - * Return server instance - * @return object - **/ - function server() { - return $this->server; - } - - /** - * Return socket ID - * @return string - **/ - function id() { - return $this->id; - } - - /** - * Return socket - * @return object - **/ - function socket() { - return $this->socket; - } - - /** - * Return request method - * @return string - **/ - function verb() { - return $this->verb; - } - - /** - * Return request URI - * @return string - **/ - function uri() { - return $this->uri; - } - - /** - * Return socket headers - * @return string - **/ - function headers() { - return $this->headers; - } - - /** - * Frame and transmit payload - * @return string|FALSE - * @param $socket resource - * @param $op int - * @param $payload string - **/ - function send($op,$data='') { - $server=$this->server; - $mask=WS::Finale | $op & WS::OpCode; - $len=strlen($data); - $buf=''; - if ($len>0xffff) - $buf=pack('CCNN',$mask,0x7f,$len); - else - if ($len>0x7d) - $buf=pack('CCn',$mask,0x7e,$len); - else - $buf=pack('CC',$mask,$len); - $buf.=$data; - if (is_bool($server->write($this->socket,$buf))) - return FALSE; - if (!in_array($op,[WS::Pong,WS::Close]) && - isset($this->events['send']) && - is_callable($func=$this->events['send'])) - $func($this,$op,$data); - return $data; - } - - /** - * Retrieve and unmask payload - * @return array|FALSE - **/ - function fetch() { - // Unmask payload - $server=$this->server; - if (is_bool($buf=$server->read($this->socket))) - return FALSE; - $op=ord($buf[0]) & WS::OpCode; - $len=ord($buf[1]) & WS::Length; - $pos=2; - if ($len==0x7e) { - $len=ord($buf[2])*256+ord($buf[3]); - $pos+=2; - } - else - if ($len==0x7f) { - for ($i=0,$len=0;$i<8;$i++) - $len=$len*256+ord($buf[$i+2]); - $pos+=8; - } - for ($i=0,$mask=[];$i<4;$i++) - $mask[$i]=ord($buf[$pos+$i]); - $pos+=4; - if (strlen($buf)<$len+$pos) - return FALSE; - for ($i=0,$data='';$i<$len;$i++) - $data.=chr(ord($buf[$pos+$i])^$mask[$i%4]); - // Dispatch - switch ($op & WS::OpCode) { - case WS::Ping: - $this->send(WS::Pong); - break; - case WS::Close: - $server->close($this->socket); - break; - case WS::Text: - $data=trim($data); - case WS::Binary: - if (isset($this->events['receive']) && - is_callable($func=$this->events['receive'])) - $func($this,$op,$data); - break; - } - return [$op,$data]; - } - - /** - * Destroy object - * @return NULL - **/ - function __destruct() { - if (isset($this->events['disconnect']) && - is_callable($func=$this->events['disconnect'])) - $func($this); - } - - /** - * Instantiate object - * @return object - * @param $server object - * @param $socket resource - * @param $verb string - * @param $uri string - * @param $hdrs array - **/ - function __construct($server,$socket,$verb,$uri,array $hdrs) { - $this->server=$server; - $this->id=stream_socket_get_name($socket,TRUE); - $this->socket=$socket; - $this->verb=$verb; - $this->uri=$uri; - $this->headers=$hdrs; - $this->events=$server->events(); - if (isset($this->events['connect']) && - is_callable($func=$this->events['connect'])) - $func($this); - } - -} diff --git a/app/lib/code.css b/app/lib/code.css deleted file mode 100644 index 618703f9..00000000 --- a/app/lib/code.css +++ /dev/null @@ -1 +0,0 @@ -code{word-wrap:break-word;color:black}.comment,.doc_comment,.ml_comment{color:dimgray;font-style:italic}.variable{color:blueviolet}.const,.constant_encapsed_string,.class_c,.dir,.file,.func_c,.halt_compiler,.line,.method_c,.lnumber,.dnumber{color:crimson}.string,.and_equal,.boolean_and,.boolean_or,.concat_equal,.dec,.div_equal,.inc,.is_equal,.is_greater_or_equal,.is_identical,.is_not_equal,.is_not_identical,.is_smaller_or_equal,.logical_and,.logical_or,.logical_xor,.minus_equal,.mod_equal,.mul_equal,.ns_c,.ns_separator,.or_equal,.plus_equal,.sl,.sl_equal,.sr,.sr_equal,.xor_equal,.start_heredoc,.end_heredoc,.object_operator,.paamayim_nekudotayim{color:black}.abstract,.array,.array_cast,.as,.break,.case,.catch,.class,.clone,.continue,.declare,.default,.do,.echo,.else,.elseif,.empty.enddeclare,.endfor,.endforach,.endif,.endswitch,.endwhile,.eval,.exit,.extends,.final,.for,.foreach,.function,.global,.goto,.if,.implements,.include,.include_once,.instanceof,.interface,.isset,.list,.namespace,.new,.print,.private,.public,.protected,.require,.require_once,.return,.static,.switch,.throw,.try,.unset,.use,.var,.while{color:royalblue}.open_tag,.open_tag_with_echo,.close_tag{color:orange}.ini_section{color:black}.ini_key{color:royalblue}.ini_value{color:crimson}.xml_tag{color:dodgerblue}.xml_attr{color:blueviolet}.xml_data{color:red}.section{color:black}.directive{color:blue}.data{color:dimgray} diff --git a/app/lib/composer.json b/app/lib/composer.json deleted file mode 100644 index 01bdca12..00000000 --- a/app/lib/composer.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "bcosca/fatfree-core", - "description": "A powerful yet easy-to-use PHP micro-framework designed to help you build dynamic and robust Web applications - fast!", - "homepage": "http://fatfreeframework.com/", - "license": "GPL-3.0", - "require": { - "php": ">=5.4" - }, - "autoload": { - "classmap": ["."] - } -} diff --git a/app/lib/cron.php b/app/lib/cron.php deleted file mode 100644 index 563783c2..00000000 --- a/app/lib/cron.php +++ /dev/null @@ -1,259 +0,0 @@ -'0 0 1 1 *', - 'annually'=>'0 0 1 1 *', - 'monthly'=>'0 0 1 * *', - 'weekly'=>'0 0 * * 0', - 'daily'=>'0 0 * * *', - 'hourly'=>'0 * * * *', - ); - - /** @var bool */ - private $windows; - - /** - * Set binary path after checking that it can be executed and is CLI - * @param string $path - * @return string - */ - function binary($path) { - if (function_exists('exec')) { - exec($path.' -v 2>&1',$out,$ret); - if ($ret==0 && preg_match('/cli/',@$out[0],$out)) - $this->binary=$path; - } - return $this->binary; - } - - /** - * Schedule a job - * @param string $job - * @param string $handler - * @param string $expr - */ - function set($job,$handler,$expr) { - if (!preg_match('/^[\w\-]+$/',$job)) - user_error(sprintf(self::E_Invalid,$job),E_USER_ERROR); - $this->jobs[$job]=array($handler,$expr); - } - - /** - * Define a schedule preset - * @param string $name - * @param string $expr - */ - function preset($name,$expr) { - $this->presets[$name]=$expr; - } - - /** - * Returns TRUE if the requested job is due at the given time - * @param string $job - * @param int $time - * @return bool - */ - function isDue($job,$time) { - if (!isset($this->jobs[$job]) || !$parts=$this->parseExpr($this->jobs[$job][1])) - return FALSE; - foreach($this->parseTimestamp($time) as $i=>$k) - if (!in_array($k,$parts[$i])) - return FALSE; - return TRUE; - } - - /** - * Execute a job - * @param string $job - * @param bool $async - * @return bool TRUE = job has been executed / FALSE = job has been delegated to a background process - */ - function execute($job,$async=TRUE) { - if (!isset($this->jobs[$job])) - user_error(sprintf(self::E_NotFound,$job),E_USER_ERROR); - $f3=\Base::instance(); - if (is_string($func=$this->jobs[$job][0])) - $func=$f3->grab($func); - if (!is_callable($func)) - user_error(sprintf(self::E_Callable,$job),E_USER_ERROR); - if ($async && isset($this->binary)) { - // PHP docs: If a program is started with this function, in order for it to continue running in the background, - // the output of the program must be redirected to a file or another output stream. - // Failing to do so will cause PHP to hang until the execution of the program ends. - $dir=dirname($this->script); - $file=basename($this->script); - if (!preg_match($this->windows?'/^[A-Z]:\\\\/i':'/^\//',$dir)) - $dir=getcwd().'/'.$dir; - if ($this->windows) { - pclose(popen(sprintf('start /b "cron" "%s" "%s\\%s" "/cron/%s"',$this->binary,$dir,$file,$job),'r')); - } else { - exec(sprintf('cd "%s" && %s %s /cron/%s >/dev/null 2>/dev/null &',$dir,$this->binary,$file,$job)); - } - return FALSE; - } - $start=microtime(TRUE); - call_user_func_array($func,array($f3)); - if ($this->log) { - $log=new Log('cron.log'); - $log->write(sprintf(self::L_Execution,$job,microtime(TRUE)-$start)); - } - return TRUE; - } - - /** - * Run scheduler, i.e executes all due jobs at a given time - * @param int $time - * @param bool $async - * @return array List of executed jobs - */ - function run($time=NULL,$async=TRUE) { - if (!isset($time)) - $time=time(); - $exec=array(); - foreach(array_keys($this->jobs) as $job) - if ($this->isDue($job,$time)) - $exec[$job]=$this->execute($job,$async); - return $exec; - } - - /** - * Route controller code - * @param \Base $f3 - * @param array $params - */ - function route($f3,$params) { - if (PHP_SAPI!='cli' && !$this->web) - $f3->error(404); - $exec=isset($params['job'])? - array($params['job']=>$this->execute($params['job'],FALSE)): - $this->run(); - if (!$this->silent) { - if (PHP_SAPI!='cli') - header('Content-Type: text/plain'); - if (!$exec) - die('Nothing to do'); - foreach($exec as $job=>$ok) - echo sprintf('%s [%s]',$job,$ok?'OK':'async')."\r\n"; - } - } - - /** - * Parse a timestamp - * @param int $time - * @return array - */ - function parseTimestamp($time) { - return array( - (int)date('i',$time),//minute - (int)date('H',$time),//hour - (int)date('d',$time),//day of month - (int)date('m',$time),//month - (int)date('w',$time),//day of week - ); - } - - /** - * Parse a cron expression - * @param string $expr - * @return array|FALSE - */ - function parseExpr($expr) { - $parts=array(); - if (preg_match('/^@(\w+)$/',$expr,$m)) { - if (!isset($this->presets[$m[1]])) - return FALSE; - $expr=$this->presets[$m[1]]; - } - $expr=preg_split('/\s+/',$expr,-1,PREG_SPLIT_NO_EMPTY); - $ranges=array( - 0=>59,//minute - 1=>23,//hour - 2=>31,//day of month - 3=>12,//month - 4=>6,//day of week - ); - foreach($ranges as $i=>$max) - if (isset($expr[$i]) && preg_match_all('/(?<=,|^)\h*(?:(\d+)(?:-(\d+))?|(\*))(?:\/(\d+))?\h*(?=,|$)/', - $expr[$i],$matches,PREG_SET_ORDER)) { - $parts[$i]=array(); - foreach($matches as $m) { - if (!$range=@range(@$m[3]?0:$m[1],@$m[3]?$max:(@$m[2]?:$m[1]),@$m[4]?:1)) - return FALSE;//step exceeds specified range - $parts[$i]=array_merge($parts[$i],$range); - } - } else - return FALSE; - return $parts; - } - - //! Read-only public properties - function __get($name) { - if (in_array($name,array('binary','jobs','presets'))) - return $this->$name; - if ($name=='clipath') // alias for script [deprecated] - return $this->script; - trigger_error(sprintf(self::E_Undefined,__CLASS__,$name)); - } - - //! Constructor - function __construct() { - $f3=\Base::instance(); - $config=(array)$f3->get('CRON'); - foreach(array('log','web','script','silent') as $k) - if (isset($config[$k])) { - settype($config[$k],gettype($this->$k)); - $this->$k=$config[$k]; - } - if (isset($config['binary'])) - $this->binary($config['binary']); - if (isset($config['jobs'])) - foreach($config['jobs'] as $job=>$arr) { - $handler=array_shift($arr); - $this->set($job,$handler,implode(',',$arr)); - } - if (isset($config['presets'])) - foreach($config['presets'] as $name=>$expr) - $this->preset($name,is_array($expr)?implode(',',$expr):$expr); - if (!isset($this->binary)) - foreach(array('php','php-cli') as $path) // try to guess the binary name - if ($this->binary($path)) - break; - $this->windows=(bool)preg_match('/^win/i',PHP_OS); - $f3->route(array('GET /cron','GET /cron/@job'),array($this,'route')); - } - -} \ No newline at end of file diff --git a/app/lib/db/cortex.php b/app/lib/db/cortex.php deleted file mode 100644 index a8e5b5e0..00000000 --- a/app/lib/db/cortex.php +++ /dev/null @@ -1,3077 +0,0 @@ - - * https://github.com/ikkez/F3-Sugar/ - * - * @package DB - * @version 1.6.0 - * @date 03.02.2019 - * @since 24.04.2012 - */ - -namespace DB; -use DB\SQL\Schema; - -class Cortex extends Cursor { - - protected - // config - $db, // DB object [ \DB\SQL, \DB\Jig, \DB\Mongo ] - $table, // selected table, string - $fluid, // fluid sql schema mode, boolean - $fieldConf, // field configuration, array - $ttl, // default mapper schema ttl - $rel_ttl, // default mapper rel ttl - $primary, // SQL table primary key - // behaviour - $smartLoading, // intelligent lazy eager loading, boolean - $standardiseID, // return standardized '_id' field for SQL when casting - // internals - $dbsType, // mapper engine type [jig, sql, mongo] - $fieldsCache, // relation field cache - $saveCsd, // mm rel save cascade - $collection, // collection - $relFilter, // filter for loading related models - $hasCond, // IDs of records the next find should have - $whitelist, // restrict to these fields - $relWhitelist, // restrict relations to these fields - $grp_stack, // stack of group conditions - $countFields, // relational counter buffer - $preBinds, // bind values to be prepended to $filter - $vFields, // virtual fields buffer - $_ttl, // rel_ttl overwrite - $charset; // sql collation charset - - /** @var Cursor */ - protected $mapper; - - /** @var CortexQueryParser */ - protected $queryParser; - - /** @var bool initialization flag */ - static $init = false; - - /** @var array sql table schema cache */ - static $schema_cache = []; - - const - // special datatypes - DT_SERIALIZED = 'SERIALIZED', - DT_JSON = 'JSON', - - // error messages - E_ARRAY_DATATYPE = 'Unable to save an Array in field %s. Use DT_SERIALIZED or DT_JSON.', - E_CONNECTION = 'No valid DB Connection given.', - E_NO_TABLE = 'No table specified.', - E_UNKNOWN_DB_ENGINE = 'This unknown DB system is not supported: %s', - E_FIELD_SETUP = 'No field setup defined', - E_UNKNOWN_FIELD = 'Field %s does not exist in %s.', - E_INVALID_RELATION_OBJECT = 'You can only save hydrated mapper objects', - E_NULLABLE_COLLISION = 'Unable to set NULL to the NOT NULLABLE field: %s', - E_WRONG_RELATION_CLASS = 'Relations only works with Cortex objects', - E_MM_REL_VALUE = 'Invalid value for many field "%s". Expecting null, split-able string, hydrated mapper object, or array of mapper objects.', - E_MM_REL_CLASS = 'Mismatching m:m relation config from class `%s` to `%s`.', - E_MM_REL_FIELD = 'Mismatching m:m relation keys from `%s` to `%s`.', - E_REL_CONF_INC = 'Incomplete relation config for `%s`. Linked key is missing.', - E_MISSING_REL_CONF = 'Cannot create related model. Specify a model name or relConf array.', - E_HAS_COND = 'Cannot use a "has"-filter on a non-bidirectional relation field'; - - /** - * init the ORM, based on given DBS - * @param null|object $db - * @param string $table - * @param null|bool $fluid - * @param int $ttl - */ - public function __construct($db = NULL, $table = NULL, $fluid = NULL, $ttl = 0) { - if (!is_null($fluid)) - $this->fluid = $fluid; - if (!is_object($this->db=(is_string($db=($db?:$this->db)) - ? \Base::instance()->get($db):$db)) && !static::$init) - trigger_error(self::E_CONNECTION,E_USER_ERROR); - if ($this->db instanceof Jig) - $this->dbsType = 'jig'; - elseif ($this->db instanceof SQL) - $this->dbsType = 'sql'; - elseif ($this->db instanceof Mongo) - $this->dbsType = 'mongo'; - if ($table) - $this->table = $table; - if ($this->dbsType != 'sql') - $this->primary = '_id'; - elseif (!$this->primary) - $this->primary = 'id'; - $this->table = $this->getTable(); - if (!$this->table) - trigger_error(self::E_NO_TABLE,E_USER_ERROR); - $this->ttl = $ttl ?: ($this->ttl ?: 60); - if (!$this->rel_ttl) - $this->rel_ttl = 0; - $this->_ttl = $this->rel_ttl ?: 0; - if (static::$init == TRUE) return; - if ($this->fluid) - static::setup($this->db,$this->table,array()); - $this->initMapper(); - } - - /** - * create mapper instance - */ - public function initMapper() { - switch ($this->dbsType) { - case 'jig': - $this->mapper = new Jig\Mapper($this->db, $this->table); - break; - case 'sql': - // ensure to load full table schema, so we can work with it at runtime - $this->mapper = new SQL\Mapper($this->db, $this->table, null, - ($this->fluid)?0:$this->ttl); - $this->applyWhitelist(); - break; - case 'mongo': - $this->mapper = new Mongo\Mapper($this->db, $this->table); - break; - default: - trigger_error(sprintf(self::E_UNKNOWN_DB_ENGINE,$this->dbsType),E_USER_ERROR); - } - $this->queryParser = CortexQueryParser::instance(); - $this->reset(); - $this->clearFilter(); - $f3 = \Base::instance(); - $this->smartLoading = $f3->exists('CORTEX.smartLoading') ? - $f3->get('CORTEX.smartLoading') : TRUE; - $this->standardiseID = $f3->exists('CORTEX.standardiseID') ? - $f3->get('CORTEX.standardiseID') : TRUE; - if(!empty($this->fieldConf)) - foreach($this->fieldConf as &$conf) { - $conf=static::resolveRelationConf($conf,$this->primary); - unset($conf); - } - } - - /** - * return raw mapper instance - * @return Cursor - */ - public function getMapper() { - return $this->mapper; - } - - /** - * get fields or set whitelist / blacklist of fields - * @param array $fields - * @param bool $exclude - * @return array - */ - public function fields(array $fields=array(), $exclude=false) { - $addInc=[]; - if ($fields) - // collect & set restricted fields for related mappers - foreach($fields as $i=>$val) - if(is_int(strpos($val,'.'))) { - list($key, $relField) = explode('.',$val,2); - $this->relWhitelist[$key][(int)$exclude][] = $relField; - unset($fields[$i]); - $addInc[] = $key; - } - $fields = array_unique($fields); - $schema = $this->whitelist ?: $this->mapper->fields(); - if (!$schema && $this->dbsType != 'sql' && $this->dry()) { - $this->load(); - $schema = $this->mapper->fields(); - $this->reset(); - } - // include relation linkage fields to $fields (if $fields is a whitelist) - if (!$exclude && !empty($fields) && !empty($addInc)) - $fields=array_unique(array_merge($fields,$addInc)); - // include relation linkage fields to existing whitelist (if $fields is a blacklist or there's nothing else to whitelist) - elseif (!empty($addInc) && $this->whitelist) - $this->whitelist=array_unique(array_merge($this->whitelist,$addInc)); - // initially merge configured fields into schema (add virtual/rel fields to schema) - if (!$this->whitelist && $this->fieldConf) - $schema=array_unique(array_merge($schema, - array_keys($this->fieldConf),array_keys($this->vFields?:[]))); - // skip if there's nothing to set for own model - if (!$fields || empty($fields)) - return $schema; - elseif ($exclude) { - $this->whitelist=array_diff($schema,$fields); - } else - $this->whitelist=$fields; - $id=$this->dbsType=='sql'?$this->primary:'_id'; - if (!in_array($id,$this->whitelist) && !($exclude && in_array($id,$fields))) - $this->whitelist[]=$id; - $this->applyWhitelist(); - return $this->whitelist; - } - - /** - * apply whitelist to active mapper schema - */ - protected function applyWhitelist() { - if ($this->dbsType == 'sql') { - // fetch full schema - if (!$this->fluid && isset(self::$schema_cache[$key=$this->table.$this->db->uuid()])) - $schema = self::$schema_cache[$key]; - else { - $schema = $this->mapper->schema(); - self::$schema_cache[$this->table.$this->db->uuid()] = $schema; - } - // apply reduced fields schema - if ($this->whitelist) - $schema = array_intersect_key($schema, array_flip($this->whitelist)); - $this->mapper->schema($schema); - $this->mapper->reset(); - } - } - - /** - * set model definition - * config example: - * array('title' => array( - * 'type' => \DB\SQL\Schema::DT_TEXT, - * 'default' => 'new record title', - * 'nullable' => true - * ) - * '...' => ... - * ) - * @param array $config - */ - function setFieldConfiguration(array $config) { - $this->fieldConf = $config; - $this->reset(); - } - - /** - * returns model field conf array - * @return array|null - */ - public function getFieldConfiguration() { - return $this->fieldConf; - } - - /** - * kick start to just fetch the config - * @return array - */ - static public function resolveConfiguration() { - static::$init=true; - $self = new static(); - static::$init=false; - $conf = array ( - 'table'=>$self->getTable(), - 'fieldConf'=>$self->getFieldConfiguration(), - 'db'=>$self->db, - 'fluid'=>$self->fluid, - 'primary'=>$self->primary, - 'charset'=>$self->charset, - ); - unset($self); - return $conf; - } - - /** - * give this model a reference to the collection it is part of - * @param CortexCollection $cx - */ - public function addToCollection($cx) { - $this->collection = $cx; - } - - /** - * returns the collection where this model lives in - * @return CortexCollection - */ - protected function getCollection() { - return ($this->collection && $this->smartLoading) - ? $this->collection : false; - } - - /** - * returns model table name - * @return string - */ - public function getTable() { - if (!$this->table && ($this->fluid || static::$init)) - $this->table = strtolower(get_class($this)); - return $this->table; - } - - /** - * setup / update table schema - * @static - * @param $db - * @param $table - * @param $fields - * @return bool - */ - static public function setup($db=null, $table=null, $fields=null) { - /** @var Cortex $self */ - $self = get_called_class(); - $self::$schema_cache=[]; - if (is_null($db) || is_null($table) || is_null($fields)) - $df = $self::resolveConfiguration(); - if (!is_object($db=(is_string($db=($db?:$df['db']))?\Base::instance()->get($db):$db))) - trigger_error(self::E_CONNECTION,E_USER_ERROR); - if (strlen($table=$table?:$df['table'])==0) - trigger_error(self::E_NO_TABLE,E_USER_ERROR); - if (is_null($fields)) - if (!empty($df['fieldConf'])) - $fields = $df['fieldConf']; - elseif(!$df['fluid']) { - trigger_error(self::E_FIELD_SETUP,E_USER_ERROR); - return false; - } else - $fields = array(); - if ($db instanceof SQL) { - $schema = new Schema($db); - // prepare field configuration - foreach($fields as $key => &$field) { - // fetch relation field types - $field = static::resolveRelationConf($field); - // check m:m relation - if (array_key_exists('has-many', $field)) { - // m:m relation conf [class,to-key,from-key] - if (is_array($relConf = $field['has-many'])) { - $rel = $relConf[0]::resolveConfiguration(); - // check if foreign conf matches m:m - if (array_key_exists($relConf[1],$rel['fieldConf']) - && !is_null($rel['fieldConf'][$relConf[1]]) - && $relConf['hasRel'] == 'has-many') { - // compute mm table name - $mmTable = isset($relConf[2]) ? $relConf[2] : - static::getMMTableName($rel['table'], $relConf['relField'], - $table, $key, $rel['fieldConf'][$relConf[1]]['has-many']); - if (!in_array($mmTable,$schema->getTables())) { - $mmt = $schema->createTable($mmTable); - $relField = $relConf['relField'].($relConf['isSelf']?'_ref':''); - $mmt->addColumn($relField)->type($relConf['relFieldType']); - $mmt->addColumn($key)->type($field['type']); - $index = array($relField,$key); - sort($index); - $mmt->addIndex($index); - $mmt->build(); - } - } - } - unset($fields[$key]); - continue; - } - // skip virtual fields with no type - if (!array_key_exists('type', $field)) { - unset($fields[$key]); - continue; - } - // transform array fields - if (in_array($field['type'], array(self::DT_JSON, self::DT_SERIALIZED))) - $field['type']=$schema::DT_TEXT; - // defaults values - if (!array_key_exists('nullable', $field)) - $field['nullable'] = true; - unset($field); - } - if (!in_array($table, $schema->getTables())) { - // create table - $table = $schema->createTable($table); - if (isset($df) && $df['charset']) - $table->setCharset($df['charset']); - foreach ($fields as $field_key => $field_conf) - $table->addColumn($field_key, $field_conf); - if(isset($df) && $df['primary'] != 'id') { - $table->addColumn($df['primary'])->type_int(); - $table->primary($df['primary']); - } - $table->build(); - } else { - // add missing fields - $table = $schema->alterTable($table); - $existingCols = $table->getCols(); - foreach ($fields as $field_key => $field_conf) - if (!in_array($field_key, $existingCols)) - $table->addColumn($field_key, $field_conf); - // remove unused fields - // foreach ($existingCols as $col) - // if (!in_array($col, array_keys($fields)) && $col!='id') - // $table->dropColumn($col); - $table->build(); - } - } - return true; - } - - /** - * erase all model data, handle with care - * @param null $db - * @param null $table - */ - static public function setdown($db=null, $table=null) { - $self = get_called_class(); - if (is_null($db) || is_null($table)) - $df = $self::resolveConfiguration(); - if (!is_object($db=(is_string($db=($db?:$df['db']))?\Base::instance()->get($db):$db))) - trigger_error(self::E_CONNECTION,E_USER_ERROR); - if (strlen($table=strtolower($table?:$df['table']))==0) - trigger_error(self::E_NO_TABLE,E_USER_ERROR); - if (isset($df) && !empty($df['fieldConf'])) - $fields = $df['fieldConf']; - else - $fields = array(); - $deletable = array(); - $deletable[] = $table; - foreach ($fields as $key => $field) { - $field = static::resolveRelationConf($field); - if (array_key_exists('has-many',$field)) { - if (!is_array($relConf = $field['has-many'])) - continue; - $rel = $relConf[0]::resolveConfiguration(); - // check if foreign conf matches m:m - if (array_key_exists($relConf[1],$rel['fieldConf']) && !is_null($relConf[1]) - && key($rel['fieldConf'][$relConf[1]]) == 'has-many') { - // compute mm table name - $deletable[] = isset($relConf[2]) ? $relConf[2] : - static::getMMTableName( - $rel['table'], $relConf[1], $table, $key, - $rel['fieldConf'][$relConf[1]]['has-many']); - } - } - } - - if($db instanceof Jig) { - /** @var Jig $db */ - $dir = $db->dir(); - foreach ($deletable as $item) - if(file_exists($dir.$item)) - unlink($dir.$item); - } elseif($db instanceof SQL) { - /** @var SQL $db */ - $schema = new Schema($db); - $tables = $schema->getTables(); - foreach ($deletable as $item) - if(in_array($item, $tables)) - $schema->dropTable($item); - } elseif($db instanceof Mongo) { - /** @var Mongo $db */ - foreach ($deletable as $item) - $db->selectCollection($item)->drop(); - } - } - - /** - * computes the m:m table name - * @param string $ftable foreign table - * @param string $fkey foreign key - * @param string $ptable own table - * @param string $pkey own key - * @param null|array $fConf foreign conf [class,key] - * @return string - */ - static protected function getMMTableName($ftable, $fkey, $ptable, $pkey, $fConf=null) { - if ($fConf) { - list($fclass, $pfkey) = $fConf; - $self = get_called_class(); - // check for a matching config - if ($pfkey != $pkey) - trigger_error(sprintf(self::E_MM_REL_FIELD, - $fclass.'.'.$pfkey, $self.'.'.$pkey),E_USER_ERROR); - } - $mmTable = array($ftable.'__'.$fkey, $ptable.'__'.$pkey); - natcasesort($mmTable); - // shortcut for self-referencing mm tables - if ($mmTable[0] == $mmTable[1] || - ($fConf && isset($fConf['isSelf']) && $fConf['isSelf']==true)) - return array_shift($mmTable); - $return = strtolower(str_replace('\\', '_', implode('_mm_', $mmTable))); - return $return; - } - - /** - * get mm table name from config - * @param array $conf own relation config - * @param string $key relation field - * @param null|array $fConf optional foreign config - * @return string - */ - protected function mmTable($conf, $key, $fConf=null) { - if (!isset($conf['refTable'])) { - // compute mm table name - $mmTable = isset($conf[2]) ? $conf[2] : - static::getMMTableName($conf['relTable'], - $conf['relField'], $this->table, $key, $fConf); - $this->fieldConf[$key]['has-many']['refTable'] = $mmTable; - } else - $mmTable = $conf['refTable']; - return $mmTable; - } - - /** - * resolve relation field types - * @param array $field - * @param string $pkey - * @return array - */ - protected static function resolveRelationConf($field,$pkey=NULL) { - if (array_key_exists('belongs-to-one', $field)) { - // find primary field definition - if (!is_array($relConf = $field['belongs-to-one'])) - $relConf = array($relConf, '_id'); - // set field type - if ($relConf[1] == '_id') - $field['type'] = Schema::DT_INT4; - else { - // find foreign field type - $fc = $relConf[0]::resolveConfiguration(); - $field['belongs-to-one']['relPK'] = $fc['primary']; - $field['type'] = $fc['fieldConf'][$relConf[1]]['type']; - } - $field['nullable'] = true; - $field['relType'] = 'belongs-to-one'; - } - elseif (array_key_exists('belongs-to-many', $field)){ - $field['type'] = self::DT_JSON; - $field['nullable'] = true; - $field['relType'] = 'belongs-to-many'; - } - elseif (array_key_exists('has-many', $field)){ - $field['relType'] = 'has-many'; - if (!isset($field['type'])) - $field['type'] = Schema::DT_INT; - $relConf = $field['has-many']; - if(!is_array($relConf)) - return $field; - $rel = $relConf[0]::resolveConfiguration(); - if(array_key_exists('has-many',$rel['fieldConf'][$relConf[1]])) { - // has-many <> has-many (m:m) - $field['has-many']['hasRel'] = 'has-many'; - $field['has-many']['isSelf'] = (ltrim($relConf[0],'\\')==get_called_class()); - $field['has-many']['relTable'] = $rel['table']; - $field['has-many']['relField'] = $relConf[1]; - $field['has-many']['relFieldType'] = isset($rel['fieldConf'][$relConf[1]]['type']) ? - $rel['fieldConf'][$relConf[1]]['type'] : Schema::DT_INT; - $field['has-many']['relPK'] = isset($relConf['relPK'])? - $relConf['relPK']:$rel['primary']; - $field['has-many']['localKey'] = isset($relConf['localKey'])? - $relConf['localKey']:($pkey?:'_id'); - } else { - // has-many <> belongs-to-one (m:1) - $field['has-many']['hasRel'] = 'belongs-to-one'; - $toConf=$rel['fieldConf'][$relConf[1]]['belongs-to-one']; - $field['has-many']['relField'] = is_array($toConf) ? - $toConf[1] : $rel['primary']; - } - } elseif(array_key_exists('has-one', $field)) - $field['relType'] = 'has-one'; - return $field; - } - - /** - * Return an array of result arrays matching criteria - * @param null $filter - * @param array $options - * @param int $ttl - * @param int $rel_depths - * @return array - */ - public function afind($filter = NULL, array $options = NULL, $ttl = 0, $rel_depths = 1) { - $result = $this->find($filter, $options, $ttl); - return $result ? $result->castAll($rel_depths): NULL; - } - - /** - * Return an array of objects matching criteria - * @param array|null $filter - * @param array|null $options - * @param int $ttl - * @return CortexCollection|false - */ - public function find($filter = NULL, array $options = NULL, $ttl = 0) { - $sort=false; - if ($this->dbsType!='sql') { - // see if reordering is needed - foreach($this->countFields?:[] as $counter) { - if ($options && isset($options['order']) && - preg_match('/count_'.$counter.'\h+(asc|desc)/i',$options['order'],$match)) - $sort=true; - } - if ($sort) { - // backup slice settings - if (isset($options['limit'])) { - $limit = $options['limit']; - unset($options['limit']); - } - if (isset($options['offset'])) { - $offset = $options['offset']; - unset($options['offset']); - } - } - } - $this->_ttl=$ttl?:$this->rel_ttl; - $result = $this->filteredFind($filter,$options,$ttl); - if (empty($result)) - return false; - foreach($result as &$record) { - $record = $this->factory($record); - unset($record); - } - // add counter for NoSQL engines - foreach($this->countFields?:[] as $counter) - foreach($result as &$mapper) { - $cr=$mapper->get($counter); - $mapper->virtual('count_'.$counter,$cr?count($cr):null); - unset($mapper); - } - $cc = new CortexCollection(); - $cc->setModels($result); - if($sort) { - $cc->orderBy($options['order']); - $cc->slice(isset($offset)?$offset:0,isset($limit)?$limit:NULL); - } - $this->clearFilter(); - return $cc; - } - - /** - * wrapper for custom find queries - * @param array $filter - * @param array $options - * @param int $ttl - * @param bool $count - * @return array|int|false array of underlying cursor objects - */ - protected function filteredFind($filter = NULL, array $options = NULL, $ttl = 0, $count=false) { - if ($this->grp_stack) { - if ($this->dbsType == 'mongo') { - $group = array( - 'keys' => $this->grp_stack['keys'], - 'reduce' => 'function (obj, prev) {'.$this->grp_stack['reduce'].'}', - 'initial' => $this->grp_stack['initial'], - 'finalize' => $this->grp_stack['finalize'], - ); - if ($options && isset($options['group'])) { - if(is_array($options['group'])) - $options['group'] = array_merge($options['group'],$group); - else { - $keys = explode(',',$options['group']); - $keys = array_combine($keys,array_fill(0,count($keys),1)); - $group['keys'] = array_merge($group['keys'],$keys); - $options['group'] = $group; - } - } else - $options = array('group'=>$group); - } - if($this->dbsType == 'sql') { - if ($options && isset($options['group'])) - $options['group'].= ','.$this->grp_stack; - else - $options['group'] = $this->grp_stack; - } - // Jig can't group yet, but pending enhancement https://github.com/bcosca/fatfree/pull/616 - } - if ($this->dbsType == 'sql' && !$count) { - $m_refl=new \ReflectionObject($this->mapper); - $m_ad_prop=$m_refl->getProperty('adhoc'); - $m_ad_prop->setAccessible(true); - $m_refl_adhoc=$m_ad_prop->getValue($this->mapper); - $m_ad_prop->setAccessible(false); - unset($m_ad_prop,$m_refl); - } - $hasJoin = array(); - if ($this->hasCond) { - foreach($this->hasCond as $key => $hasCond) { - $addToFilter = null; - if ($deep = is_int(strpos($key,'.'))) { - $key = rtrim($key,'.'); - $hasCond = array(null,null); - } - list($has_filter,$has_options) = $hasCond; - $type = $this->fieldConf[$key]['relType']; - $fromConf = $this->fieldConf[$key][$type]; - switch($type) { - case 'has-one': - case 'has-many': - if (!is_array($fromConf)) - trigger_error(sprintf(self::E_REL_CONF_INC, $key),E_USER_ERROR); - $id = $this->dbsType == 'sql' ? $this->primary : '_id'; - if ($type=='has-many' && isset($fromConf['relField']) - && $fromConf['hasRel'] == 'belongs-to-one') - $id=$fromConf['relField']; - // many-to-many - if ($type == 'has-many' && $fromConf['hasRel'] == 'has-many') { - if (!$deep && $this->dbsType == 'sql' - && !isset($has_options['limit']) && !isset($has_options['offset'])) { - $hasJoin = array_merge($hasJoin, - $this->_hasJoinMM_sql($key,$hasCond,$filter,$options)); - if (!isset($options['group'])) - $options['group'] = ''; - $groupFields = explode(',', preg_replace('/"/','',$options['group'])); - if (!in_array($this->table.'.'.$this->primary,$groupFields)) { - $options['group'] = ($options['group']?',':'').$this->table.'.'.$this->primary; - $groupFields[]=$this->table.'.'.$this->primary; - } - // all non-aggregated fields need to be present in the GROUP BY clause - if (isset($m_refl_adhoc) && preg_match('/sybase|dblib|odbc|sqlsrv/i',$this->db->driver())) - foreach (array_diff($this->mapper->fields(),array_keys($m_refl_adhoc)) as $field) - if (!in_array($this->table.'.'.$field,$groupFields)) - $options['group'] .= ', '.$this->table.'.'.$field; - } - elseif ($result = $this->_hasRefsInMM($key,$has_filter,$has_options,$ttl)) - $addToFilter = array($id.' IN ?', $result); - } - // *-to-one - elseif (!$deep && $this->dbsType == 'sql') { - // use sub-query inclusion - $has_filter=$this->mergeFilter([$has_filter, - [$this->rel($key)->getTable().'.'.$fromConf[1].'='.$this->getTable().'.'.$id]]); - $result = $this->_refSubQuery($key,$has_filter,$has_options); - $addToFilter = array_merge(['exists('.$result[0].')'],$result[1]); - } - elseif ($result = $this->_hasRefsIn($key,$has_filter,$has_options,$ttl)) - $addToFilter = array($id.' IN ?', $result); - break; - // one-to-* - case 'belongs-to-one': - if (!$deep && $this->dbsType == 'sql' - && !isset($has_options['limit']) && !isset($has_options['offset'])) { - if (!is_array($fromConf)) - $fromConf = array($fromConf, '_id'); - $rel = $fromConf[0]::resolveConfiguration(); - if ($this->dbsType == 'sql' && $fromConf[1] == '_id') - $fromConf[1] = $rel['primary']; - $hasJoin[] = $this->_hasJoin_sql($key,$rel['table'],$hasCond,$filter,$options); - } elseif ($result = $this->_hasRefsIn($key,$has_filter,$has_options,$ttl)) - $addToFilter = array($key.' IN ?', $result); - break; - default: - trigger_error(self::E_HAS_COND,E_USER_ERROR); - } - if (isset($result) && !isset($addToFilter)) - return false; - elseif (isset($addToFilter)) { - if (!$filter) - $filter = array(''); - if (!empty($filter[0])) - $filter[0] .= ' and '; - $cond = array_shift($addToFilter); - if ($this->dbsType=='sql') - $cond = $this->queryParser->sql_prependTableToFields($cond,$this->table); - $filter[0] .= '('.$cond.')'; - $filter = array_merge($filter, $addToFilter); - } - } - $this->hasCond = null; - } - $filter = $this->queryParser->prepareFilter($filter, $this->dbsType, $this->db, $this->fieldConf); - if ($this->dbsType=='sql') { - $qtable = $this->db->quotekey($this->table); - if (isset($options['order']) && $this->db->driver() == 'pgsql') - // PostgreSQLism: sort NULL values to the end of a table - $options['order'] = preg_replace('/\h+DESC(?=\s*(?:$|,))/i',' DESC NULLS LAST',$options['order']); - // assemble full sql query for joined queries - if ($hasJoin) { - $adhoc=[]; - // when in count-mode and grouping is active, wrap the query later - // otherwise add a an adhoc counter field here - if (!($subquery_mode=($options && !empty($options['group']))) && $count) - $adhoc[]='(COUNT(*)) as _rows'; - if (!$count) - // add bind parameters for filters in adhoc fields - if ($this->preBinds) { - $crit = array_shift($filter); - $filter = array_merge($this->preBinds,$filter); - array_unshift($filter,$crit); - } - if (!empty($m_refl_adhoc)) - // add adhoc field expressions - foreach ($m_refl_adhoc as $key=>$val) - $adhoc[]=$val['expr'].' AS '.$this->db->quotekey($key); - $fields=implode(',',$adhoc); - if ($count && $subquery_mode) { - if (empty($fields)) - // Select at least one field, ideally the grouping fields or sqlsrv fails - $fields=preg_replace('/HAVING.+$/i','',$options['group']); - if (preg_match('/mssql|dblib|sqlsrv/',$this->engine)) - $fields='TOP 100 PERCENT '.$fields; - } - if (!$count) - // add only selected fields to field list - $fields.=($fields?', ':'').implode(', ',array_map(function($field) use($qtable){ - return $qtable.'.'.$this->db->quotekey($field); - },array_diff($this->mapper->fields(),array_keys($m_refl_adhoc)))); - // assemble query - $sql = 'SELECT '.$fields.' FROM '.$qtable.' ' - .implode(' ',$hasJoin).' WHERE '.$filter[0]; - $db=$this->db; - // add grouping in both, count & selection mode - if (isset($options['group'])) - $sql.=' GROUP BY '.preg_replace_callback('/\w+[._\-\w]*/i', - function($match) use($db) { - return $db->quotekey($match[0]); - }, $options['group']); - if (!$count) { - if (isset($options['order'])) - $sql.=' ORDER BY '.implode(',',array_map( - function($str) use($db) { - return preg_match('/^\h*(\w+[._\-\w]*)(?:\h+((?:ASC|DESC)[\w\h]*))?\h*$/i', - $str,$parts)? - ($db->quotekey($parts[1]). - (isset($parts[2])?(' '.$parts[2]):'')):$str; - }, - explode(',',$options['order']))); - // SQL Server fixes - if (preg_match('/mssql|sqlsrv|odbc/', $this->db->driver()) && - (isset($options['limit']) || isset($options['offset']))) { - $ofs=isset($options['offset'])?(int)$options['offset']:0; - $lmt=isset($options['limit'])?(int)$options['limit']:0; - if (strncmp($this->db->version(),'11',2)>=0) { - // SQL Server >= 2012 - if (!isset($options['order'])) - $sql.=' ORDER BY '.$this->db->quotekey($this->primary); - $sql.=' OFFSET '.$ofs.' ROWS'.($lmt?' FETCH NEXT '.$lmt.' ROWS ONLY':''); - } else { - // SQL Server 2008 - $order=(!isset($options['order'])) - ?($this->db->quotekey($this->table.'.'.$this->primary)):$options['order']; - $sql=str_replace('SELECT','SELECT '.($lmt>0?'TOP '.($ofs+$lmt):'').' ROW_NUMBER() '. - 'OVER (ORDER BY '.$order.') AS rnum,',$sql); - $sql='SELECT * FROM ('.$sql.') x WHERE rnum > '.($ofs); - } - } else { - if (isset($options['limit'])) - $sql.=' LIMIT '.(int)$options['limit']; - if (isset($options['offset'])) - $sql.=' OFFSET '.(int)$options['offset']; - } - } elseif ($subquery_mode) - // wrap count query if necessary - $sql='SELECT COUNT(*) AS '.$this->db->quotekey('_rows').' '. - 'FROM ('.$sql.') AS '.$this->db->quotekey('_temp'); - unset($filter[0]); - $result = $this->db->exec($sql, $filter, $ttl); - if ($count) - return $result[0]['_rows']; - foreach ($result as &$record) { - // factory new mappers - $mapper = clone($this->mapper); - $mapper->reset(); - $mapper->query= array($record); - foreach ($record as $key=>$val) - $mapper->set($key, $val); - $record = $mapper; - unset($record, $mapper); - } - return $result; - } elseif (!empty($this->preBinds)) { - // bind values to adhoc queries - if (!$filter) - // we (PDO) need any filter to bind values - $filter = array('1=1'); - $crit = array_shift($filter); - $filter = array_merge($this->preBinds,$filter); - array_unshift($filter,$crit); - } - } - if ($options) { - $options = $this->queryParser->prepareOptions($options,$this->dbsType,$this->db); - if ($count) - unset($options['order']); - } - return ($count) - ? $this->mapper->count($filter,$options,$ttl) - : $this->mapper->find($filter,$options,$ttl); - } - - /** - * Retrieve first object that satisfies criteria - * @param null $filter - * @param array $options - * @param int $ttl - * @return bool - */ - public function load($filter = NULL, array $options = NULL, $ttl = 0) { - $this->reset(); - $this->_ttl=$ttl?:$this->rel_ttl; - $res = $this->filteredFind($filter, $options, $ttl); - if ($res) { - $this->mapper->query = $res; - $this->first(); - } else - $this->mapper->reset(); - $this->emit('load'); - return $this->valid(); - } - - /** - * add has-conditional filter to next find call - * @param string $key - * @param array $filter - * @param null $options - * @return $this - */ - public function has($key, $filter, $options = null) { - if (is_string($filter)) - $filter=array($filter); - if (is_int(strpos($key,'.'))) { - list($key,$fkey) = explode('.',$key,2); - if (!isset($this->hasCond[$key.'.'])) - $this->hasCond[$key.'.'] = array(); - $this->hasCond[$key.'.'][$fkey] = array($filter,$options); - } else { - if (!isset($this->fieldConf[$key])) - trigger_error(sprintf(self::E_UNKNOWN_FIELD,$key,get_called_class()),E_USER_ERROR); - if (!isset($this->fieldConf[$key]['relType'])) - trigger_error(self::E_HAS_COND,E_USER_ERROR); - $this->hasCond[$key] = array($filter,$options); - } - return $this; - } - - /** - * return IDs of records that has a linkage to this mapper - * @param string $key relation field - * @param array $filter condition for foreign records - * @param array $options filter options for foreign records - * @param int $ttl - * @return array|false - */ - protected function _hasRefsIn($key, $filter, $options, $ttl = 0) { - $type = $this->fieldConf[$key]['relType']; - $fieldConf = $this->fieldConf[$key][$type]; - // one-to-many shortcut - $rel = $this->getRelFromConf($fieldConf,$key); - $hasSet = $rel->find($filter, $options, $ttl); - if (!$hasSet) - return false; - $hasSetByRelId = array_unique($hasSet->getAll($fieldConf[1], true)); - return empty($hasSetByRelId) ? false : $hasSetByRelId; - } - - /** - * build sub query on relation - * @param $key - * @param $filter - * @param $options - * @return mixed - */ - protected function _refSubQuery($key, $filter, $options,$fields=null) { - $type = $this->fieldConf[$key]['relType']; - $fieldConf = $this->fieldConf[$key][$type]; - $rel = $this->getRelFromConf($fieldConf,$key); - $filter[0]=$this->queryParser->sql_quoteCondition($filter[0],$this->db); - return $rel->mapper->stringify(implode(',',array_map([$this->db,'quotekey'], - $fields?:[$rel->primary])),$filter,$options); - } - - /** - * return IDs of own mappers that match the given relation filter on pivot tables - * @param string $key - * @param array $filter - * @param array $options - * @param int $ttl - * @return array|false - */ - protected function _hasRefsInMM($key, $filter, $options, $ttl=0) { - $fieldConf = $this->fieldConf[$key]['has-many']; - $rel = $this->getRelInstance($fieldConf[0],null,$key,true); - $hasSet = $rel->find($filter,$options,$ttl); - $result = false; - if ($hasSet) { - $hasIDs = $hasSet->getAll('_id',true); - $mmTable = $this->mmTable($fieldConf,$key); - $pivot = $this->getRelInstance(null,array('db'=>$this->db,'table'=>$mmTable)); - $filter = [$key.' IN ?',$hasIDs]; - if ($fieldConf['isSelf']) { - $filter[0].= ' OR '.$key.'_ref IN ?'; - $filter[] = $hasIDs; - } - $pivotSet = $pivot->find($filter,null,$ttl); - if ($pivotSet) { - $result = $pivotSet->getAll($fieldConf['relField'],true); - if ($fieldConf['isSelf']) - $result = array_merge($result, - $pivotSet->getAll($fieldConf['relField'].'_ref',true)); - $result = array_diff(array_unique($result),$hasIDs); - } - } - return $result; - } - - /** - * build query for SQL pivot table join and merge conditions - */ - protected function _hasJoinMM_sql($key, $hasCond, &$filter, &$options) { - $fieldConf = $this->fieldConf[$key]['has-many']; - $relTable = $fieldConf['relTable']; - $hasJoin = array(); - $mmTable = $this->mmTable($fieldConf,$key); - if ($fieldConf['isSelf']) { - $relTable .= '_ref'; - $hasJoin[] = $this->_sql_left_join($this->primary,$this->table,$fieldConf['relField'].'_ref',$mmTable); - $hasJoin[] = $this->_sql_left_join($key,$mmTable,$fieldConf['relPK'], - [$fieldConf['relTable'],$relTable]); - // cross-linked - $hasJoin[] = $this->_sql_left_join($this->primary,$this->table, - $fieldConf['relField'],[$mmTable,$mmTable.'_c']); - $hasJoin[] = $this->_sql_left_join($key.'_ref',$mmTable.'_c',$fieldConf['relPK'], - [$fieldConf['relTable'],$relTable.'_c']); - $this->_sql_mergeRelCondition($hasCond,$relTable,$filter,$options); - $this->_sql_mergeRelCondition($hasCond,$relTable.'_c',$filter,$options,'OR'); - } else { - $hasJoin[] = $this->_sql_left_join($this->primary,$this->table,$fieldConf['relField'],$mmTable); - $hasJoin[] = $this->_sql_left_join($key,$mmTable,$fieldConf['relPK'],$relTable); - $this->_sql_mergeRelCondition($hasCond,$relTable,$filter,$options); - } - return $hasJoin; - } - - /** - * build query for single SQL table join and merge conditions - */ - protected function _hasJoin_sql($key, $table, $cond, &$filter, &$options) { - $relConf = $this->fieldConf[$key]['belongs-to-one']; - $relModel = is_array($relConf)?$relConf[0]:$relConf; - $rel = $this->getRelInstance($relModel,null,$key); - $fkey = is_array($this->fieldConf[$key]['belongs-to-one']) ? - $this->fieldConf[$key]['belongs-to-one'][1] : $rel->primary; - $alias = $table.'__'.$key; - $query = $this->_sql_left_join($key,$this->table,$fkey,[$table,$alias]); - $this->_sql_mergeRelCondition($cond,$alias,$filter,$options); - return $query; - } - - /** - * assemble SQL join query string - * @param string $skey - * @param string $sTable - * @param string $fkey - * @param string|array $fTable - * @return string - */ - protected function _sql_left_join($skey, $sTable, $fkey, $fTable) { - if (is_array($fTable)) - list($fTable,$fTable_alias) = $fTable; - $skey = $this->db->quotekey($skey); - $sTable = $this->db->quotekey($sTable); - $fkey = $this->db->quotekey($fkey); - $fTable = $this->db->quotekey($fTable); - if (isset($fTable_alias)) { - $fTable_alias = $this->db->quotekey($fTable_alias); - return 'LEFT JOIN '.$fTable.' AS '.$fTable_alias.' ON '.$sTable.'.'.$skey.' = '.$fTable_alias.'.'.$fkey; - } else - return 'LEFT JOIN '.$fTable.' ON '.$sTable.'.'.$skey.' = '.$fTable.'.'.$fkey; - } - - /** - * merge condition of relation with current condition - * @param array $cond condition of related model - * @param string $table table of related model - * @param array $filter current filter to merge with - * @param array $options current options to merge with - * @param string $glue - */ - protected function _sql_mergeRelCondition($cond, $table, &$filter, &$options, $glue='AND') { - if (!empty($cond[0])) { - $whereClause = '('.array_shift($cond[0]).')'; - $whereClause = $this->queryParser->sql_prependTableToFields($whereClause,$table); - if (!$filter) - $filter = array($whereClause); - elseif (!empty($filter[0])) - $filter[0] = '('.$this->queryParser->sql_prependTableToFields($filter[0],$this->table) - .') '.$glue.' '.$whereClause; - $filter = array_merge($filter, $cond[0]); - } - if ($cond[1] && isset($cond[1]['group'])) { - $hasGroup = preg_replace('/(\w+)/i', $table.'.$1', $cond[1]['group']); - $options['group'] .= ','.$hasGroup; - } - } - - /** - * add filter for loading related models - * @param string $key - * @param array $filter - * @param array $option - * @return $this - */ - public function filter($key, $filter=null, $option=null) { - if (is_int(strpos($key,'.'))) { - list($key,$fkey) = explode('.',$key,2); - if (!isset($this->relFilter[$key.'.'])) - $this->relFilter[$key.'.'] = array(); - $this->relFilter[$key.'.'][$fkey] = array($filter,$option); - } else - $this->relFilter[$key] = array($filter,$option); - return $this; - } - - /** - * removes one or all relation filter - * @param null|string $key - */ - public function clearFilter($key = null) { - if (!$key) - $this->relFilter = array(); - elseif(isset($this->relFilter[$key])) - unset($this->relFilter[$key]); - } - - /** - * merge the relation filter to the query criteria if it exists - * @param string $key - * @param array $crit - * @return array - */ - protected function mergeWithRelFilter($key, $crit) { - if (array_key_exists($key, $this->relFilter) && - !empty($this->relFilter[$key][0])) - $crit=$this->mergeFilter(array($this->relFilter[$key][0],$crit)); - return $crit; - } - - /** - * merge multiple filters - * @param array $filters - * @param string $glue - * @return array - */ - public function mergeFilter($filters, $glue='and') { - $crit = array(); - $params = array(); - if ($filters) { - foreach($filters as $filter) { - $crit[] = array_shift($filter); - $params = array_merge($params,$filter); - } - array_unshift($params,'( '.implode(' ) '.$glue.' ( ',$crit).' )'); - } - return $params; - } - - /** - * returns the option condition for a relation filter, if defined - * @param string $key - * @return array null - */ - protected function getRelFilterOption($key) { - return (array_key_exists($key, $this->relFilter) && - !empty($this->relFilter[$key][1])) - ? $this->relFilter[$key][1] : null; - } - - /** - * Delete object/s and reset ORM - * @param $filter - * @return bool - */ - public function erase($filter = null) { - $filter = $this->queryParser->prepareFilter($filter, $this->dbsType, $this->db); - if (!$filter) { - if ($this->emit('beforeerase')===false) - return false; - if ($this->fieldConf) { - // clear all m:m references - foreach($this->fieldConf as $key => $conf) - if (isset($conf['has-many']) && - $conf['has-many']['hasRel']=='has-many') { - $rel = $this->getRelInstance(null, array( - 'db'=>$this->db, - 'table'=>$this->mmTable($conf['has-many'],$key))); - $id = $this->get($conf['has-many']['relPK'],true); - $rel->erase(array($conf['has-many']['relField'].' = ?', $id)); - } - } - $this->mapper->erase(); - $this->emit('aftererase'); - } elseif($filter) - $this->mapper->erase($filter); - return true; - } - - /** - * Save mapped record - * @return mixed - **/ - function save() { - // update changed collections - $fields = $this->fieldConf; - if ($fields) - foreach($fields as $key=>$conf) - if (!empty($this->fieldsCache[$key]) && $this->fieldsCache[$key] instanceof CortexCollection - && $this->fieldsCache[$key]->hasChanged()) - $this->set($key,$this->fieldsCache[$key]->getAll('_id',true)); - // perform event & save operations - if ($new = $this->dry()) { - if ($this->emit('beforeinsert')===false) - return false; - $result=$this->insert(); - } else { - if ($this->emit('beforeupdate')===false) - return false; - $result=$this->update(); - } - // m:m save cascade - if (!empty($this->saveCsd)) { - foreach($this->saveCsd as $key => $val) { - if ($fields[$key]['relType'] == 'has-many') { - $relConf = $fields[$key]['has-many']; - if ($relConf['hasRel'] == 'has-many') { - $mmTable = $this->mmTable($relConf,$key); - $mm = $this->getRelInstance(null, array('db'=>$this->db, 'table'=>$mmTable)); - $id = $this->get($relConf['localKey'],true); - $filter = [$relConf['relField'].' = ?',$id]; - if ($relConf['isSelf']) { - $filter[0].= ' OR '.$relConf['relField'].'_ref = ?'; - $filter[] = $id; - } - // delete all refs - if (empty($val)) - $mm->erase($filter); - // update refs - elseif (is_array($val)) { - $mm->erase($filter); - foreach(array_unique($val) as $v) { - if ($relConf['isSelf'] && $v==$id) - continue; - $mm->set($key,$v); - $mm->set($relConf['relField'].($relConf['isSelf']?'_ref':''),$id); - $mm->save(); - $mm->reset(); - } - } - unset($mm); - } - elseif($relConf['hasRel'] == 'belongs-to-one') { - $rel = $this->getRelInstance($relConf[0],$relConf,$key); - // find existing relations - $refs = $rel->find([$relConf[1].' = ?',$this->getRaw($relConf['relField'])]); - if (empty($val)) { - foreach ($refs?:[] as $model) { - $model->set($relConf[1],NULL); - $model->save(); - } - $this->fieldsCache[$key] = NULL; - } else { - if ($refs) { - $ref_ids = $refs->getAll('_id'); - // unlink removed relations - $remove_refs = array_diff($ref_ids,$val); - foreach ($refs as $model) - if (in_array($model->getRaw($relConf['relField']),$remove_refs)) { - $model->set($relConf[1],null); - $model->save(); - } - // get new relation keys - $val = array_diff($val,$ref_ids); - } else - $refs = new CortexCollection(); - if (!empty($val)) { - // find models that need to be linked - $new_refs = $rel->find([$relConf['relField'].' IN ?',$val]); - foreach ($new_refs?:[] as $model) { - // set relation to new models - $model->set($relConf[1],$this->getRaw($relConf['relField'])); - $model->save(); - $refs->add($model); - } - } - $this->fieldsCache[$key] = $refs; - } - } - } elseif($fields[$key]['relType'] == 'has-one') { - $val->save(); - } - } - $this->saveCsd = array(); - } - $this->emit($new?'afterinsert':'afterupdate'); - return $result; - } - - /** - * Count records that match criteria - * @param null $filter - * @param array $options - * @param int $ttl - * @return mixed - */ - public function count($filter=NULL, array $options=NULL, $ttl=60) { - $has=$this->hasCond; - $count=$this->filteredFind($filter,$options,$ttl,true); - $this->hasCond=$has; - return $count; - } - - /** - * Count records that are currently loaded - * @return int - */ - public function loaded() { - return count($this->mapper->query); - } - - /** - * add a virtual field that counts occurring relations - * @param $key - */ - public function countRel($key, $alias=null, $filter=null, $option=null) { - if (!$alias) - $alias = 'count_'.$key; - $filter_bak = null; - if ($filter || $option) { - $filter_bak = isset($this->relFilter[$key]) ? $this->relFilter[$key] : false; - $this->filter($key,$filter,$option); - } - if (isset($this->fieldConf[$key])){ - // one-to-one, one-to-many - if ($this->fieldConf[$key]['relType'] == 'belongs-to-one') { - if ($this->dbsType == 'sql') { - $this->mapper->set($alias,'count('.$this->db->quotekey($key).')'); - $this->grp_stack=(!$this->grp_stack)?$key:$this->grp_stack.','.$key; - if ($this->whitelist && !in_array($alias,$this->whitelist)) - $this->whitelist[] = $alias; - } elseif ($this->dbsType == 'mongo') - $this->_mongo_addGroup(array( - 'keys'=>array($key=>1), - 'reduce' => 'prev.'.$alias.'++;', - "initial" => array($alias => 0) - )); - else - trigger_error('Cannot add direct relational counter.',E_USER_ERROR); - } elseif($this->fieldConf[$key]['relType'] == 'has-many') { - $relConf=$this->fieldConf[$key]['has-many']; - if ($relConf['hasRel']=='has-many') { - // many-to-many - if ($this->dbsType == 'sql') { - $mmTable = $this->mmTable($relConf,$key); - $filter = array($mmTable.'.'.$relConf['relField'] - .' = '.$this->table.'.'.$this->primary); - $from = $this->db->quotekey($mmTable); - if (array_key_exists($key, $this->relFilter) && - !empty($this->relFilter[$key][0])) { - $options=array(); - $from = $this->db->quotekey($mmTable).' '. - $this->_sql_left_join($key,$mmTable,$relConf['relPK'],$relConf['relTable']); - $relFilter = $this->relFilter[$key]; - $this->_sql_mergeRelCondition($relFilter,$relConf['relTable'], - $filter,$options); - } - $filter = $this->queryParser->prepareFilter($filter, - $this->dbsType, $this->db, $this->fieldConf); - $crit = array_shift($filter); - if (count($filter)>0) - $this->preBinds=array_merge($this->preBinds,$filter); - $this->mapper->set($alias, - '(select count('.$this->db->quotekey($mmTable.'.'.$relConf['relField']).')'. - ' from '.$from.' where '.$crit. - ' group by '.$this->db->quotekey($mmTable.'.'.$relConf['relField']).')'); - if ($this->whitelist && !in_array($alias,$this->whitelist)) - $this->whitelist[] = $alias; - } else { - // count rel - $this->countFields[]=$key; - } - } elseif($this->fieldConf[$key]['has-many']['hasRel']=='belongs-to-one') { - // many-to-one - if ($this->dbsType == 'sql') { - $fConf=$relConf[0]::resolveConfiguration(); - $fTable=$fConf['table']; - $fAlias=$fTable.'__count'; - $rKey=$relConf[1]; - $crit = $fAlias.'.'.$rKey.' = '.$this->table.'.'.$relConf['relField']; - $filter = $this->mergeWithRelFilter($key,array($crit)); - $filter[0] = $this->queryParser->sql_prependTableToFields($filter[0],$fAlias); - $filter = $this->queryParser->prepareFilter($filter, - $this->dbsType, $this->db, $this->fieldConf); - $crit = array_shift($filter); - if (count($filter)>0) - $this->preBinds=array_merge($this->preBinds,$filter); - $this->mapper->set($alias, - '(select count('.$this->db->quotekey($fAlias.'.'.$fConf['primary']).') from '. - $this->db->quotekey($fTable).' AS '.$this->db->quotekey($fAlias).' where '. - $crit.' group by '.$this->db->quotekey($fAlias.'.'.$rKey).')'); - if ($this->whitelist && !in_array($alias,$this->whitelist)) - $this->whitelist[] = $alias; - } else { - // count rel - $this->countFields[]=$key; - } - } - } - } - if ($filter_bak!==null) { - if ($filter_bak) - $this->relFilter[$key] = $filter_bak; - else - $this->clearFilter($key); - } - } - - /** - * merge mongo group options array - * @param $opt - */ - protected function _mongo_addGroup($opt) { - if (!$this->grp_stack) - $this->grp_stack = array('keys'=>array(),'initial'=>array(),'reduce'=>'','finalize'=>''); - if (isset($opt['keys'])) - $this->grp_stack['keys']+=$opt['keys']; - if (isset($opt['reduce'])) - $this->grp_stack['reduce'].=$opt['reduce']; - if (isset($opt['initial'])) - $this->grp_stack['initial']+=$opt['initial']; - if (isset($opt['finalize'])) - $this->grp_stack['finalize'].=$opt['finalize']; - } - - /** - * update a given date or time field with the current time - * @param string $key - */ - public function touch($key) { - if (isset($this->fieldConf[$key]) - && isset($this->fieldConf[$key]['type'])) { - $type = $this->fieldConf[$key]['type']; - $date = ($this->dbsType=='sql' && preg_match('/mssql|sybase|dblib|odbc|sqlsrv/', - $this->db->driver())) ? 'Ymd' : 'Y-m-d'; - if ($type == Schema::DT_DATETIME || $type == Schema::DT_TIMESTAMP) - $this->set($key,date($date.' H:i:s')); - elseif ($type == Schema::DT_DATE) - $this->set($key,date($date)); - elseif ($type == Schema::DT_INT4) - $this->set($key,time()); - } - } - - /** - * Bind value to key - * @return mixed - * @param $key string - * @param $val mixed - */ - function set($key, $val) { - if ($key == '_id' && $this->dbsType == 'sql') - $key = $this->primary; - $fields = $this->fieldConf; - unset($this->fieldsCache[$key]); - // pre-process if field config available - if (!empty($fields) && isset($fields[$key]) && is_array($fields[$key])) { - // handle relations - if (isset($fields[$key]['belongs-to-one'])) { - // one-to-many, one-to-one - if (empty($val)) - $val = NULL; - elseif (is_object($val) && - !($this->dbsType=='mongo' && ( - ($this->db->legacy() && $val instanceof \MongoId) || - (!$this->db->legacy() && $val instanceof \MongoDB\BSON\ObjectId)))) { - // fetch fkey from mapper - if (!$val instanceof Cortex || $val->dry()) - trigger_error(self::E_INVALID_RELATION_OBJECT,E_USER_ERROR); - else { - $relConf = $fields[$key]['belongs-to-one']; - $rel_field = (is_array($relConf) ? $relConf[1] : '_id'); - $val = $val->get($rel_field,true); - } - } elseif ($this->dbsType == 'mongo' && (($this->db->legacy() && !$val instanceof \MongoId) - || (!$this->db->legacy() && !$val instanceof \MongoDB\BSON\ObjectId))) - $val = $this->db->legacy() ? new \MongoId($val) : new \MongoDB\BSON\ObjectId($val); - } elseif (isset($fields[$key]['has-one'])){ - $relConf = $fields[$key]['has-one']; - if (empty($val)) { - $val = $this->get($key); - $val->set($relConf[1],NULL); - } else { - if (!$val instanceof Cortex) { - $rel = $this->getRelInstance($relConf[0],null,$key); - $rel->load(array('_id = ?', $val)); - $val = $rel; - } - $val->set($relConf[1], $this->_id); - } - $this->saveCsd[$key] = $val; - return $val; - } elseif (isset($fields[$key]['belongs-to-many'])) { - // many-to-many, unidirectional - $fields[$key]['type'] = self::DT_JSON; - $relConf = $fields[$key]['belongs-to-many']; - $rel_field = (is_array($relConf) ? $relConf[1] : '_id'); - $val = $this->getForeignKeysArray($val, $rel_field, $key); - } - elseif (isset($fields[$key]['has-many'])) { - $relConf = $fields[$key]['has-many']; - // many-to-many, bidirectional - // many-to-one, inverse - if ($relConf['hasRel'] == 'has-many' - || $relConf['hasRel'] == 'belongs-to-one') { - // custom setter - $val = $this->emit('set_'.$key, $val); - $val = $this->getForeignKeysArray($val,'_id',$key); - if (empty($val) && is_array($val)) - $val=new CortexCollection(); - $this->saveCsd[$key] = $val; // array of keys - $this->fieldsCache[$key] = $val; - return $val; - } - } - // add nullable polyfill - if ($val === NULL && ($this->dbsType == 'jig' || $this->dbsType == 'mongo') - && array_key_exists('nullable', $fields[$key]) && $fields[$key]['nullable'] === false) - trigger_error(sprintf(self::E_NULLABLE_COLLISION,$key),E_USER_ERROR); - // MongoId shorthand - if ($this->dbsType == 'mongo' && (($this->db->legacy() && !$val instanceof \MongoId) - || (!$this->db->legacy() && !$val instanceof \MongoDB\BSON\ObjectId))) { - if ($key == '_id') - $val = $this->db->legacy() ? new \MongoId($val) : new \MongoDB\BSON\ObjectId($val); - elseif (preg_match('/INT/i',$fields[$key]['type']) - && !isset($fields[$key]['relType'])) - $val = (int) $val; - } - // cast boolean - if (preg_match('/BOOL/i',$fields[$key]['type'])) { - $val = !$val || $val==='false' ? false : (bool) $val; - if ($this->dbsType == 'sql') - $val = (int) $val; - } - // custom setter - $val = $this->emit('set_'.$key, $val); - // clean datetime - if (isset($fields[$key]['type']) && empty($val) && - in_array($fields[$key]['type'], [Schema::DT_DATE,Schema::DT_DATETIME]) - ) - $val=NULL; - // convert array content - if (is_array($val) && $this->dbsType == 'sql') { - if ($fields[$key]['type']==self::DT_SERIALIZED) - $val=serialize($val); - elseif ($fields[$key]['type']==self::DT_JSON) - $val=json_encode($val); - else - trigger_error(sprintf(self::E_ARRAY_DATATYPE,$key),E_USER_ERROR); - } - } else { - // custom setter - $val = $this->emit('set_'.$key, $val); - } - // fluid SQL - if ($this->fluid && $this->dbsType == 'sql') { - $schema = new Schema($this->db); - $table = $schema->alterTable($this->table); - // add missing field - if (!in_array($key,$table->getCols())) { - // determine data type - if (isset($this->fieldConf[$key]) && isset($this->fieldConf[$key]['type'])) - $type = $this->fieldConf[$key]['type']; - elseif (is_int($val)) $type = $schema::DT_INT; - elseif (is_double($val)) $type = $schema::DT_DOUBLE; - elseif (is_float($val)) $type = $schema::DT_FLOAT; - elseif (is_bool($val)) $type = $schema::DT_BOOLEAN; - elseif (strlen($val)>10 && strtotime($val)) $type = $schema::DT_DATETIME; - elseif (date('Y-m-d H:i:s', strtotime($val)) == $val) $type = $schema::DT_DATETIME; - elseif (date('Y-m-d', strtotime($val)) == $val) $type = $schema::DT_DATE; - elseif (\UTF::instance()->strlen($val)<=255) $type = $schema::DT_VARCHAR256; - else $type = $schema::DT_TEXT; - $table->addColumn($key)->type($type); - $table->build(); - // update mapper fields - $newField = $table->getCols(true); - $newField = $newField[$key]; - $fields = $this->mapper->schema(); - $fields[$key] = $newField + array('value'=>NULL,'initial'=>NULL,'changed'=>NULL); - $this->mapper->schema($fields); - } - } - return $this->mapper->set($key, $val); - } - - /** - * call custom field handlers - * @param $event - * @param $val - * @return mixed - */ - protected function emit($event, $val=null) { - if (isset($this->trigger[$event])) { - if (preg_match('/^[sg]et_/',$event)) { - $val = (is_string($f=$this->trigger[$event]) - && preg_match('/^[sg]et_/',$f)) - ? call_user_func(array($this,$event),$val) - : \Base::instance()->call($f,array($this,$val)); - } else - $val = \Base::instance()->call($this->trigger[$event],array($this,$val)); - } elseif (preg_match('/^[sg]et_/',$event) && method_exists($this,$event)) { - $this->trigger[] = $event; - $val = call_user_func(array($this,$event),$val); - } - return $val; - } - - /** - * Define a custom field setter - * @param $key - * @param $func - */ - public function onset($key, $func) { - $this->trigger['set_'.$key] = $func; - } - - /** - * Define a custom field getter - * @param $key - * @param $func - */ - public function onget($key, $func) { - $this->trigger['get_'.$key] = $func; - } - - /** - * virtual mapper field setter - * @param string $key - * @param mixed|callback $val - */ - public function virtual($key, $val) { - $this->vFields[$key]=$val; - if (!empty($this->whitelist)) { - $this->whitelist[] = $key; - $this->whitelist = array_unique($this->whitelist); - } - } - - /** - * reset virtual fields - * @param string $key - */ - public function clearVirtual($key=NULL) { - if ($key) - unset($this->vFields[$key]); - else - $this->vFields=[]; - } - - /** - * Retrieve contents of key - * @return mixed - * @param string $key - * @param bool $raw - */ - function &get($key, $raw = false) { - // handle virtual fields - if (isset($this->vFields[$key])) { - $out = (is_callable($this->vFields[$key])) - ? call_user_func($this->vFields[$key], $this) : $this->vFields[$key]; - return $out; - } - $fields = $this->fieldConf; - $id = $this->primary; - if ($key == '_id' && $this->dbsType == 'sql') - $key = $id; - if ($this->whitelist && !in_array($key,$this->whitelist)) { - $out = null; - return $out; - } - if ($raw) { - $out = $this->exists($key) ? $this->mapper->{$key} : NULL; - if ($this->dbsType == 'mongo' && !$this->db->legacy() && $out instanceof \MongoDB\Model\BSONArray) - $out = (array) $out; - return $out; - } - if (!empty($fields) && isset($fields[$key]) && is_array($fields[$key]) - && !array_key_exists($key,$this->fieldsCache)) { - // load relations - if (isset($fields[$key]['belongs-to-one'])) { - // one-to-X, bidirectional, direct way - if (!$this->exists($key) || is_null($this->mapper->{$key})) - $this->fieldsCache[$key] = null; - else { - // get config for this field - $relConf = $fields[$key]['belongs-to-one']; - // fetch related model - $rel = $this->getRelFromConf($relConf,$key); - // am i part of a result collection? - if ($cx = $this->getCollection()) { - // does the collection has cached results for this key? - if (!$cx->hasRelSet($key)) { - // build the cache, find all values of current key - $relKeys = array_unique($cx->getAll($key,true)); - // find related models - $crit = array($relConf[1].' IN ?', $relKeys); - $relSet = $rel->find($this->mergeWithRelFilter($key, $crit), - $this->getRelFilterOption($key),$this->_ttl); - // cache relSet, sorted by ID - $cx->setRelSet($key, $relSet ? $relSet->getBy($relConf[1]) : NULL); - } - // get a subset of the preloaded set - $result = $cx->getSubset($key,(string) $this->get($key,true)); - $this->fieldsCache[$key] = $result ? $result[0] : NULL; - } else { - $crit = array($relConf[1].' = ?', $this->get($key, true)); - $crit = $this->mergeWithRelFilter($key, $crit); - $this->fieldsCache[$key] = $rel->findone($crit, - $this->getRelFilterOption($key),$this->_ttl); - } - } - } - elseif (($type = isset($fields[$key]['has-one'])) - || isset($fields[$key]['has-many'])) { - $type = $type ? 'has-one' : 'has-many'; - $fromConf = $fields[$key][$type]; - if (!is_array($fromConf)) - trigger_error(sprintf(self::E_REL_CONF_INC, $key),E_USER_ERROR); - $rel = $this->getRelInstance($fromConf[0],null,$key,true); - $relFieldConf = $rel->getFieldConfiguration(); - $relType = isset($relFieldConf[$fromConf[1]]['belongs-to-one']) ? - 'belongs-to-one' : 'has-many'; - // one-to-*, bidirectional, inverse way - if ($relType == 'belongs-to-one') { - $toConf = $relFieldConf[$fromConf[1]]['belongs-to-one']; - if (!is_array($toConf)) - $toConf = array($toConf, $id); - if ($toConf[1] != $id && (!$this->exists($toConf[1]) - || is_null($this->mapper->get($toConf[1])))) - $this->fieldsCache[$key] = null; - elseif ($cx=$this->getCollection()) { - // part of a result set - if (!$cx->hasRelSet($key)) { - // emit eager loading - $relKeys = $cx->getAll($toConf[1],true); - $crit = array($fromConf[1].' IN ?', $relKeys); - $relSet = $rel->find($this->mergeWithRelFilter($key,$crit), - $this->getRelFilterOption($key),$this->_ttl); - $cx->setRelSet($key, $relSet ? $relSet->getBy($fromConf[1],true) : NULL); - } - $result = $cx->getSubset($key, array($this->get($toConf[1]))); - $this->fieldsCache[$key] = $result ? (($type == 'has-one') - ? $result[0][0] : CortexCollection::factory($result[0])) : NULL; - } // no collection - elseif (($val=$this->getRaw($toConf[1])) && $val!==NULL) { - $crit=[$fromConf[1].' = ?',$val]; - $crit=$this->mergeWithRelFilter($key,$crit); - $opt=$this->getRelFilterOption($key); - $this->fieldsCache[$key]=(($type=='has-one') - ?$rel->findone($crit,$opt,$this->_ttl) - :$rel->find($crit,$opt,$this->_ttl))?:NULL; - } else - $this->fieldsCache[$key] = NULL; - } - // many-to-many, bidirectional - elseif ($relType == 'has-many') { - $toConf = $relFieldConf[$fromConf[1]]['has-many']; - $mmTable = $this->mmTable($fromConf,$key,$toConf); - // create mm table mapper - if (!$this->get($id,true)) { - $this->fieldsCache[$key] = null; - return $this->fieldsCache[$key]; - } - $id = $toConf['relPK']; - $rel = $this->getRelInstance(null,array('db'=>$this->db,'table'=>$mmTable)); - if ($cx = $this->getCollection()) { - if (!$cx->hasRelSet($key)) { - // get IDs of all results - $relKeys = $cx->getAll($id,true); - // get all pivot IDs - $filter = [$fromConf['relField'].' IN ?',$relKeys]; - if ($fromConf['isSelf']) { - $filter[0].= ' OR '.$fromConf['relField'].'_ref IN ?'; - $filter[] = $relKeys; - } - $mmRes = $rel->find($filter,null,$this->_ttl); - if (!$mmRes) - $cx->setRelSet($key, NULL); - else { - $pivotRel = array(); - $pivotKeys = array(); - foreach($mmRes as $model) { - $val = $model->get($key,true); - if ($fromConf['isSelf']) { - $refVal = $model->get($fromConf['relField'].'_ref',true); - $pivotRel[(string) $refVal][] = $val; - $pivotRel[(string) $val][] = $refVal; - $pivotKeys[] = $val; - $pivotKeys[] = $refVal; - } else { - $pivotRel[ (string) $model->get($fromConf['relField'])][] = $val; - $pivotKeys[] = $val; - } - } - // cache pivot keys - $cx->setRelSet($key.'_pivot', $pivotRel); - // preload all rels - $pivotKeys = array_unique($pivotKeys); - $fRel = $this->getRelInstance($fromConf[0],null,$key,true); - $crit = array($id.' IN ?', $pivotKeys); - $relSet = $fRel->find($this->mergeWithRelFilter($key, $crit), - $this->getRelFilterOption($key),$this->_ttl); - $cx->setRelSet($key, $relSet ? $relSet->getBy($id) : NULL); - unset($fRel); - } - } - // fetch subset from preloaded rels using cached pivot keys - $fkeys = $cx->getSubset($key.'_pivot', array($this->get($id))); - $this->fieldsCache[$key] = $fkeys ? - CortexCollection::factory($cx->getSubset($key, $fkeys[0])) : NULL; - } // no collection - else { - // find foreign keys - $fId=$this->get($fromConf['localKey'],true); - $filter = [$fromConf['relField'].' = ?',$fId]; - if ($fromConf['isSelf']) { - $filter = [$fromConf['relField'].' = ?',$fId]; - $filter[0].= ' OR '.$fromConf['relField'].'_ref = ?'; - $filter[] = $filter[1]; - } - $results = $rel->find($filter,null,$this->_ttl); - if (!$results) - $this->fieldsCache[$key] = NULL; - else { - $fkeys = $results->getAll($key,true); - if ($fromConf['isSelf']) { - // merge both rel sides and remove itself - $fkeys = array_diff(array_merge($fkeys, - $results->getAll($key.'_ref',true)),[$fId]); - } - // create foreign table mapper - unset($rel); - $rel = $this->getRelInstance($fromConf[0],null,$key,true); - // load foreign models - $filter = array($fromConf['relPK'].' IN ?', $fkeys); - $filter = $this->mergeWithRelFilter($key, $filter); - $this->fieldsCache[$key] = $rel->find($filter, - $this->getRelFilterOption($key),$this->_ttl); - } - } - } - } - elseif (isset($fields[$key]['belongs-to-many'])) { - // many-to-many, unidirectional - $fields[$key]['type'] = self::DT_JSON; - $result = $this->getRaw($key); - if ($this->dbsType == 'sql') - $result = json_decode($result, true); - if (!is_array($result)) - $this->fieldsCache[$key] = $result; - else { - // create foreign table mapper - $relConf = $fields[$key]['belongs-to-many']; - $rel = $this->getRelFromConf($relConf,$key); - $fkeys = array(); - foreach ($result as $el) - $fkeys[] = (is_int($el)||ctype_digit($el))?(int)$el:(string)$el; - // if part of a result set - if ($cx = $this->getCollection()) { - if (!$cx->hasRelSet($key)) { - // find all keys - $relKeys = ($cx->getAll($key,true)); - if ($this->dbsType == 'sql'){ - foreach ($relKeys as &$val) { - $val = substr($val, 1, -1); - unset($val); - } - $relKeys = json_decode('['.implode(',',$relKeys).']'); - } else - $relKeys = call_user_func_array('array_merge', $relKeys); - // get related models - if (!empty($relKeys)) { - $crit = array($relConf[1].' IN ?', array_unique($relKeys)); - $relSet = $rel->find($this->mergeWithRelFilter($key, $crit), - $this->getRelFilterOption($key),$this->_ttl); - // cache relSet, sorted by ID - $cx->setRelSet($key, $relSet ? $relSet->getBy($relConf[1]) : NULL); - } else - $cx->setRelSet($key, NULL); - } - // get a subset of the preloaded set - $this->fieldsCache[$key] = CortexCollection::factory($cx->getSubset($key, $fkeys)); - } else { - // load foreign models - $filter = array($relConf[1].' IN ?', $fkeys); - $filter = $this->mergeWithRelFilter($key, $filter); - $this->fieldsCache[$key] = $rel->find($filter, - $this->getRelFilterOption($key),$this->_ttl); - } - } - } - // resolve array fields - elseif (isset($fields[$key]['type'])) { - if ($this->dbsType == 'sql') { - if ($fields[$key]['type'] == self::DT_SERIALIZED) - $this->fieldsCache[$key] = unserialize($this->mapper->{$key}); - elseif ($fields[$key]['type'] == self::DT_JSON) - $this->fieldsCache[$key] = json_decode($this->mapper->{$key},true); - } - if ($this->exists($key) && preg_match('/BOOL/i',$fields[$key]['type'])) { - $this->fieldsCache[$key] = (bool) $this->mapper->{$key}; - } - } - } - // fetch cached value, if existing - // TODO: fix array key reference editing, #71 -// if (array_key_exists($key,$this->fieldsCache)) -// $val = $this->fieldsCache[$key]; -// elseif ($this->exists($key)) { -// $val =& $this->mapper->{$key}; -// } else -// $val = NULL; - $val = array_key_exists($key,$this->fieldsCache) ? $this->fieldsCache[$key] - : (($this->exists($key)) ? $this->mapper->{$key} : null); - if ($this->dbsType == 'mongo' && (($this->db->legacy() && $val instanceof \MongoId) || - (!$this->db->legacy() && $val instanceof \MongoDB\BSON\ObjectId))) { - // conversion to string makes further processing in template, etc. much easier - $val = (string) $val; - } - // custom getter - $out = $this->emit('get_'.$key, $val); - return $out; - } - - /** - * return raw value of a field - * @param $key - * @return mixed - */ - function &getRaw($key) { - return $this->get($key, true); - } - - /** - * find the ID values of given relation collection - * @param $val string|array|object|bool - * @param $rel_field string - * @param $key string - * @return array|Cortex|null|object - */ - protected function getForeignKeysArray($val, $rel_field, $key) { - if (is_null($val)) - return NULL; - if (is_object($val) && $val instanceof CortexCollection) - $val = $val->getAll($rel_field,true); - elseif (is_string($val)) - // split-able string of collection IDs - $val = \Base::instance()->split($val); - elseif (!is_array($val) && !(is_object($val) - && ($val instanceof Cortex && !$val->dry()))) - trigger_error(sprintf(self::E_MM_REL_VALUE, $key),E_USER_ERROR); - // hydrated mapper as collection - if (is_object($val)) { - $nval = array(); - while (!$val->dry()) { - $nval[] = $val->get($rel_field,true); - $val->next(); - } - $val = $nval; - } - elseif (is_array($val)) { - // array of single hydrated mappers, raw ID value or mixed - $isMongo = ($this->dbsType == 'mongo'); - foreach ($val as &$item) { - if (is_object($item) && - !($isMongo && (($this->db->legacy() && $item instanceof \MongoId) || - (!$this->db->legacy() && $item instanceof \MongoDB\BSON\ObjectId)))) { - if (!$item instanceof Cortex || $item->dry()) - trigger_error(self::E_INVALID_RELATION_OBJECT,E_USER_ERROR); - else $item = $item->get($rel_field,true); - } - if ($isMongo && $rel_field == '_id' && is_string($item)) - $item = $this->db->legacy() ? new \MongoId($item) : new \MongoDB\BSON\ObjectId($item); - if (is_numeric($item)) - $item = (int) $item; - unset($item); - } - } - return $val; - } - - /** - * creates and caches related mapper objects - * @param string $model - * @param array $relConf - * @param string $key - * @param bool $pushFilter - * @return Cortex - */ - protected function getRelInstance($model=null, $relConf=null, $key='', $pushFilter=false) { - if (!$model && !$relConf) - trigger_error(self::E_MISSING_REL_CONF,E_USER_ERROR); - $relConf = $model ? $model::resolveConfiguration() : $relConf; - $relName = ($model?:'Cortex').'\\'.$relConf['db']->uuid(). - '\\'.$relConf['table'].'\\'.$key; - if (\Registry::exists($relName)) { - $rel = \Registry::get($relName); - $rel->reset(); - } else { - $rel = $model ? new $model : new Cortex($relConf['db'], $relConf['table']); - if (!$rel instanceof Cortex) - trigger_error(self::E_WRONG_RELATION_CLASS,E_USER_ERROR); - \Registry::set($relName, $rel); - } - // restrict fields of related mapper - if(!empty($key) && isset($this->relWhitelist[$key])) { - if (isset($this->relWhitelist[$key][0])) - $rel->fields($this->relWhitelist[$key][0],false); - if (isset($this->relWhitelist[$key][1])) - $rel->fields($this->relWhitelist[$key][1],true); - } - if ($pushFilter && !empty($key)) { - if (isset($this->relFilter[$key.'.'])) { - foreach($this->relFilter[$key.'.'] as $fkey=>$conf) - $rel->filter($fkey,$conf[0],$conf[1]); - } - if (isset($this->hasCond[$key.'.'])) { - foreach($this->hasCond[$key.'.'] as $fkey=>$conf) - $rel->has($fkey,$conf[0],$conf[1]); - } - } - return $rel; - } - - /** - * get relation model from config - * @param $relConf - * @param $key - * @return Cortex - */ - protected function getRelFromConf(&$relConf, $key) { - if (!is_array($relConf)) - $relConf = array($relConf, '_id'); - $rel = $this->getRelInstance($relConf[0],null,$key,true); - if($this->dbsType=='sql' && $relConf[1] == '_id') - $relConf[1] = $rel->primary; - return $rel; - } - - /** - * returns a clean/dry model from a relation - * @param string $key - * @return Cortex - */ - public function rel($key) { - $rt = $this->fieldConf[$key]['relType']; - $rc = $this->fieldConf[$key][$rt]; - if (!is_array($rc)) - $rc = array($rc,'_id'); - return $this->getRelInstance($rc[0],null,$key); - } - - /** - * Return fields of mapper object as an associative array - * @return array - * @param bool|Cortex $obj - * @param int|array $rel_depths depths to resolve relations - */ - public function cast($obj = NULL, $rel_depths = 1) { - $fields = $this->mapper->cast( ($obj) ? $obj->mapper : null ); - if (!empty($this->vFields)) - foreach(array_keys($this->vFields) as $key) - $fields[$key]=$this->get($key); - if (is_int($rel_depths)) - $rel_depths = array('*'=>$rel_depths-1); - elseif (is_array($rel_depths)) - $rel_depths['*'] = isset($rel_depths['*'])?--$rel_depths['*']:-1; - if ($this->fieldConf) { - $fields += array_fill_keys(array_keys($this->fieldConf),NULL); - if ($this->whitelist) - $fields = array_intersect_key($fields, array_flip($this->whitelist)); - $mp = $obj ? : $this; - foreach ($fields as $key => &$val) { - // post process configured fields - if (isset($this->fieldConf[$key]) && is_array($this->fieldConf[$key])) { - // handle relations - $rd = isset($rel_depths[$key]) ? $rel_depths[$key] : $rel_depths['*']; - if ((is_array($rd) || $rd >= 0) && $type=preg_grep('/[belongs|has]-(to-)*[one|many]/', - array_keys($this->fieldConf[$key]))) { - $relType=current($type); - // cast relations - $val = (($relType == 'belongs-to-one' || $relType == 'belongs-to-many') - && !$mp->exists($key)) ? NULL : $mp->get($key); - if ($val instanceof Cortex) - $val = $val->cast(null, $rd); - elseif ($val instanceof CortexCollection) - $val = $val->castAll($rd); - } - // extract array fields - elseif (isset($this->fieldConf[$key]['type'])) { - if ($this->dbsType == 'sql') { - if ($this->fieldConf[$key]['type'] == self::DT_SERIALIZED) - $val=unserialize($mp->mapper->{$key}); - elseif ($this->fieldConf[$key]['type'] == self::DT_JSON) - $val=json_decode($mp->mapper->{$key}, true); - } - if ($this->exists($key) - && preg_match('/BOOL/i',$this->fieldConf[$key]['type'])) { - $val = (bool) $mp->mapper->{$key}; - } - } - } - if ($this->dbsType == 'mongo' && $key == '_id') - $val = (string) $val; - if ($this->dbsType == 'sql' && $key == 'id' && $this->standardiseID) { - $fields['_id'] = $val; - unset($fields[$key]); - } - unset($val); - } - } - // custom getter - foreach ($fields as $key => &$val) { - $val = $this->emit('get_'.$key, $val); - unset($val); - } - return $fields; - } - - /** - * cast a related collection of mappers - * @param string $key field name - * @param int $rel_depths depths to resolve relations - * @return array array of associative arrays - */ - function castField($key, $rel_depths=0) { - if (!$key) - return NULL; - $mapper_arr = $this->get($key); - if(!$mapper_arr) - return NULL; - $out = array(); - foreach ($mapper_arr as $mp) - $out[] = $mp->cast(null,$rel_depths); - return $out; - } - - /** - * wrap result mapper - * @param Cursor|array $mapper - * @return Cortex - */ - protected function factory($mapper) { - if (is_array($mapper)) { - $mp = clone($this->mapper); - $mp->reset(); - $cx = $this->factory($mp); - $cx->copyfrom($mapper); - } else { - $cx = clone($this); - $cx->reset(false); - $cx->mapper = $mapper; - } - $cx->emit('load'); - return $cx; - } - - public function dry() { - return $this->mapper->dry(); - } - - /** - * hydrate the mapper from hive key or given array - * @param string|array $key - * @param callback|array|string $fields - * @return NULL - */ - public function copyfrom($key, $fields = null) { - $f3 = \Base::instance(); - $srcfields = is_array($key) ? $key : $f3->get($key); - if ($fields) - if (is_callable($fields)) - $srcfields = $fields($srcfields); - else { - if (is_string($fields)) - $fields = $f3->split($fields); - $srcfields = array_intersect_key($srcfields, array_flip($fields)); - } - foreach ($srcfields as $key => $val) { - if (isset($this->fieldConf[$key]) && isset($this->fieldConf[$key]['type'])) { - if ($this->fieldConf[$key]['type'] == self::DT_JSON && is_string($val)) - $val = json_decode($val); - elseif ($this->fieldConf[$key]['type'] == self::DT_SERIALIZED && is_string($val)) - $val = unserialize($val); - } - $this->set($key, $val); - } - } - - /** - * copy mapper values into hive key - * @param string $key the hive key to copy into - * @param int $relDepth the depth of relations to resolve - * @return NULL|void - */ - public function copyto($key, $relDepth=0) { - \Base::instance()->set($key, $this->cast(null,$relDepth)); - } - - /** - * copy to hive key with relations being simple arrays of keys - * @param $key - */ - function copyto_flat($key) { - /** @var \Base $f3 */ - $f3 = \Base::instance(); - $this->copyto($key); - foreach ($this->fields() as $field) { - if (isset($this->fieldConf[$field]) && isset($this->fieldConf[$field]['relType']) - && $this->fieldConf[$field]['relType']=='has-many' - && $f3->devoid($key.'.'.$field)) { - $val = $this->get($field); - if ($val instanceof CortexCollection) - $f3->set($key.'.'.$field,$val->getAll('_id')); - elseif (is_array($val)) - $f3->set($key.'.'.$field,$val); - else - $f3->clear($key.'.'.$field); - } - } - } - - public function skip($ofs = 1) { - $this->reset(false); - if ($this->mapper->skip($ofs)) - return $this; - else - $this->reset(false); - } - - public function first() { - $this->reset(false); - $this->mapper->first(); - return $this; - } - - public function last() { - $this->reset(false); - $this->mapper->last(); - return $this; - } - - /** - * reset and re-initialize the mapper - * @param bool $mapper - * @return NULL|void - */ - public function reset($mapper = true) { - if ($mapper) - $this->mapper->reset(); - $this->fieldsCache=[]; - $this->saveCsd=[]; - $this->countFields=[]; - $this->preBinds=[]; - $this->grp_stack=null; - // set default values - if (($this->dbsType == 'jig' || $this->dbsType == 'mongo') - && !empty($this->fieldConf)) - foreach($this->fieldConf as $field_key => $field_conf) - if (array_key_exists('default',$field_conf)) { - $val = ($field_conf['default'] === \DB\SQL\Schema::DF_CURRENT_TIMESTAMP) - ? date('Y-m-d H:i:s') : $field_conf['default']; - $this->set($field_key, $val); - } - } - - /** - * reset only specific fields and return to their default values - * @param array $fields - */ - public function resetFields(array $fields) { - $defaults = $this->defaults(); - foreach ($fields as $field) { - unset($this->fieldsCache[$field]); - unset($this->saveCsd[$field]); - if (isset($defaults[$field])) - $this->set($field,$defaults[$field]); - else { - $this->set($field,NULL); - } - } - } - - /** - * return default values from schema configuration - * @param bool $set set default values to mapper - * @return array - */ - function defaults($set=false) { - $out = []; - $fields = $this->fieldConf; - if ($this->dbsType == 'sql') - $fields = array_replace_recursive($this->mapper->schema(),$fields); - foreach($fields as $field_key => $field_conf) - if (array_key_exists('default',$field_conf)) { - $val = ($field_conf['default'] === \DB\SQL\Schema::DF_CURRENT_TIMESTAMP) - ? date('Y-m-d H:i:s') : $field_conf['default']; - if ($val!==NULL) { - $out[$field_key]=$val; - if ($set) - $this->set($field_key, $val); - } - } - return $out; - } - - /** - * check if a certain field exists in the mapper or - * or is a virtual relation field - * @param string $key - * @param bool $relField - * @return bool - */ - function exists($key, $relField = false) { - if (!$this->dry() && $key == '_id') return true; - return $this->mapper->exists($key) || - ($relField && isset($this->fieldConf[$key]['relType'])); - } - - /** - * return TRUE if any/specified field value has changed - * @param string $key - * @return mixed - */ - public function changed($key=null) { - if ($key=='_id') - $key = $this->primary; - if (method_exists($this->mapper,'changed')) - return $this->mapper->changed($key); - else - trigger_error('method does not exist on mapper',E_USER_ERROR); - } - - /** - * clear any mapper field or relation - * @param string $key - * @return NULL|void - */ - function clear($key) { - unset($this->fieldsCache[$key]); - if (isset($this->fieldConf[$key]['relType'])) - $this->set($key,null); - $this->mapper->clear($key); - } - - function insert() { - $res = $this->mapper->insert(); - if (is_array($res)) - $res = $this->mapper; - if (is_object($res)) - $res = $this->factory($res); - return is_int($res) ? $this : $res; - } - - function update() { - $res = $this->mapper->update(); - if (is_array($res)) - $res = $this->mapper; - if (is_object($res)) - $res = $this->factory($res); - return is_int($res) ? $this : $res; - } - - function dbtype() { - return $this->mapper->dbtype(); - } - - public function __clone() { - $this->mapper = clone($this->mapper); - } - - function getiterator() { -// return new \ArrayIterator($this->cast(null,false)); - return new \ArrayIterator(array()); - } -} - - -class CortexQueryParser extends \Prefab { - - const - E_BRACKETS = 'Invalid query: unbalanced brackets found', - E_INBINDVALUE = 'Bind value for IN operator must be a populated array', - E_ENGINEERROR = 'Engine not supported', - E_MISSINGBINDKEY = 'Named bind parameter `%s` does not exist in filter arguments'; - - protected - $queryCache = array(); - - /** - * converts the given filter array to fit the used DBS - * - * example filter: - * array('text = ? AND num = ?','bar',5) - * array('num > ? AND num2 <= ?',5,10) - * array('num1 > num2') - * array('text like ?','%foo%') - * array('(text like ? OR text like ?) AND num != ?','foo%','%bar',23) - * - * @param array $cond - * @param string $engine - * @param object $db - * @param null $fieldConf - * @return array|bool|null - */ - public function prepareFilter($cond, $engine, $db, $fieldConf=null) { - if (is_null($cond)) return $cond; - if (is_string($cond)) - $cond = array($cond); - $f3 = \Base::instance(); - $cacheHash = $f3->hash($f3->stringify($cond)).'.'.$engine; - if ($engine=='sql') - $cacheHash.='-'.$db->driver(); - if (isset($this->queryCache[$cacheHash])) - // load from memory - return $this->queryCache[$cacheHash]; - elseif ($f3->exists('CORTEX.queryParserCache') - && ($ttl = (int) $f3->get('CORTEX.queryParserCache'))) { - $cache = \Cache::instance(); - // load from cache - if ($f3->get('CACHE') && $ttl && ($cached = $cache->exists($cacheHash, $ncond)) - && $cached[0] + $ttl > microtime(TRUE)) { - $this->queryCache[$cacheHash] = $ncond; - return $ncond; - } - } - $where = array_shift($cond); - $args = $cond; - $where = str_replace(array('&&', '||'), array('AND', 'OR'), $where); - // prepare IN condition - $where = preg_replace('/\bIN\b\s*\(\s*(\?|:\w+)?\s*\)/i', 'IN $1', $where); - switch ($engine) { - case 'jig': - $ncond = $this->_jig_parse_filter($where, $args); - break; - case 'mongo': - $parts = $this->splitLogical($where); - if (is_int(strpos($where, ':'))) - list($parts, $args) = $this->convertNamedParams($parts, $args); - foreach ($parts as &$part) { - $part = $this->_mongo_parse_relational_op($part, $args, $db, $fieldConf); - unset($part); - } - $ncond = $this->_mongo_parse_logical_op($parts); - break; - case 'sql': - if (!$f3->exists('CORTEX.quoteConditions',$qc) || $qc) - $where = $this->sql_quoteCondition($where,$db); - // preserve identifier - $where = preg_replace('/(?!\B)_id/', 'id', $where); - if ($db->driver() == 'pgsql') - $where = preg_replace('/\s+like\s+/i', ' ILIKE ', $where); - $parts = $this->splitLogical($where); - // ensure positional bind params - if (is_int(strpos($where, ':'))) - list($parts, $args) = $this->convertNamedParams($parts, $args); - $ncond = array(); - foreach ($parts as &$part) { - // enhanced IN handling - if (is_int(strpos($part, '?'))) { - $val = array_shift($args); - if (is_int($pos = strpos($part, ' IN ?'))) { - if ($val instanceof CortexCollection) - $val = $val->getAll('_id',TRUE); - if (!is_array($val) || empty($val)) - trigger_error(self::E_INBINDVALUE,E_USER_ERROR); - $bindMarks = str_repeat('?,',count($val) - 1).'?'; - $part = substr($part, 0, $pos).' IN ('.$bindMarks.') '; - $ncond = array_merge($ncond, $val); - } elseif($val === null && - preg_match('/((?:\S[\w\-]+\S.?)+)\s*'. - '(!?==?)\s*(?:\?|:\w+)/i',$part,$match)) { - $part = ' '.$match[1].' IS '.($match[2][0]=='!'?'NOT ':'').'NULL '; - } else - $ncond[] = $val; - } - unset($part); - } - array_unshift($ncond, implode($parts)); - break; - default: - trigger_error(self::E_ENGINEERROR,E_USER_ERROR); - } - $this->queryCache[$cacheHash] = $ncond; - if(isset($ttl) && $f3->get('CACHE')) { - // save to cache - $cache = \Cache::instance(); - $cache->set($cacheHash,$ncond,$ttl); - } - return $ncond; - } - - /** - * split where criteria string into logical chunks - * @param $cond - * @return array - */ - protected function splitLogical($cond) { - return preg_split('/(\s*(?[^()]+)|\((?:(?>[^()]+)|^(?R))*\))*\)|'. // exclude SQL function names "foo(" - '(?:(\b(?=!)]|$))/i', // only when part of condition or within brackets - function($match) use($db) { - if (!isset($match[1])) - return $match[0]; - if (preg_match('/\b(AND|OR|IN|LIKE|NOT|HAVING|SELECT|FROM|WHERE)\b/i',$match[1])) - return $match[1]; - return $db->quotekey($match[1]); - }, $cond); - return $out ?: $cond; - } - - /** - * add table prefix to identifiers which do not have a table prefix yet - * @param string $cond - * @param string $table - * @return string - */ - public function sql_prependTableToFields($cond, $table) { - $out = preg_replace_callback('/'. - '(\w+\((?:[^)(]+|\((?:[^)(]+|(?R))*\))*\))|'. - '(?:(\s)|^|(?<=[(]))'. - '([a-zA-Z_](?:[\w\-_]+))'. - '(?=[\s<>=!)]|$)/i', - function($match) use($table) { - if (!isset($match[3])) - return $match[1]; - if (preg_match('/\b(AND|OR|IN|LIKE|NOT|HAVING|SELECT|FROM|WHERE)\b/i',$match[3])) - return $match[0]; - return $match[2].$table.'.'.$match[3]; - }, $cond); - return $out ?: $cond; - } - - /** - * convert filter array to jig syntax - * @param $where - * @param $args - * @return array - */ - protected function _jig_parse_filter($where, $args) { - $parts = $this->splitLogical($where); - if (is_int(strpos($where, ':'))) - list($parts, $args) = $this->convertNamedParams($parts, $args); - $ncond = array(); - foreach ($parts as &$part) { - if (preg_match('/\s*\b(AND|OR)\b\s*/i',$part)) - continue; - // prefix field names - $part = preg_replace('/([a-z_-]+(?:[\w-]+))/i', '@$1', $part, -1, $count); - // value comparison - if (is_int(strpos($part, '?'))) { - $val = array_shift($args); - preg_match('/(@\w+)/i', $part, $match); - $skipVal=false; - // find like operator - if (is_int(strpos($upart = strtoupper($part), ' @LIKE '))) { - if ($not = is_int($npos = strpos($upart, '@NOT'))) - $pos = $npos; - $val = '/'.$this->_likeValueToRegEx($val).'/iu'; - $part = ($not ? '!' : '').'preg_match(?,'.$match[0].')'; - } // find IN operator - elseif (is_int($pos = strpos($upart, ' @IN '))) { - if ($val instanceof CortexCollection) - $val = $val->getAll('_id',TRUE); - if ($not = is_int($npos = strpos($upart, '@NOT'))) - $pos = $npos; - $part = ($not ? '!' : '').'in_array('.substr($part, 0, $pos). - ',array(\''.implode('\',\'', $val).'\'))'; - $skipVal=true; - } - elseif($val===null && preg_match('/(\w+)\s*([!=<>]+)\s*\?/i',$part,$nmatch) - && ($nmatch[2]=='=' || $nmatch[2]=='==')){ - $kval=ltrim($nmatch[1],'@'); - $part = '(!array_key_exists(\''.$kval.'\',$_row) || '. - '(array_key_exists(\''.$kval.'\',$_row) && $_row[\''.$kval.'\']===NULL))'; - unset($part); - continue; - } - // add existence check - $part = ($val===null && !$skipVal) - ? '(array_key_exists(\''.ltrim($match[0],'@').'\',$_row) && '.$part.')' - : '(isset('.$match[0].') && '.$part.')'; - if (!$skipVal) - $ncond[] = $val; - } elseif ($count >= 1) { - // field comparison - preg_match_all('/(@\w+)/i', $part, $matches); - $chks = array(); - foreach ($matches[0] as $field) - $chks[] = 'isset('.$field.')'; - $part = '('.implode(' && ',$chks).' && ('.$part.'))'; - } - unset($part); - } - array_unshift($ncond, implode(' ', $parts)); - return $ncond; - } - - /** - * find and wrap logical operators AND, OR, (, ) - * @param $parts - * @return array - */ - protected function _mongo_parse_logical_op($parts) { - $b_offset = 0; - $ncond = array(); - $child = array(); - for ($i = 0, $max = count($parts); $i < $max; $i++) { - $part = $parts[$i]; - if (is_string($part)) - $part = trim($part); - if ($part == '(') { - // add sub-bracket to parse array - if ($b_offset > 0) - $child[] = $part; - $b_offset++; - } elseif ($part == ')') { - $b_offset--; - // found closing bracket - if ($b_offset == 0) { - $ncond[] = ($this->_mongo_parse_logical_op($child)); - $child = array(); - } elseif ($b_offset < 0) - trigger_error(self::E_BRACKETS,E_USER_ERROR); - else - // add sub-bracket to parse array - $child[] = $part; - } - elseif ($b_offset > 0) { - // add to parse array - $child[]=$part; - // condition type - } elseif (!is_array($part)) { - if (strtoupper($part) == 'AND') - $add = true; - elseif (strtoupper($part) == 'OR') - $or = true; - } else // skip - $ncond[] = $part; - } - if ($b_offset > 0) - trigger_error(self::E_BRACKETS,E_USER_ERROR); - if (isset($add)) - return array('$and' => $ncond); - elseif (isset($or)) - return array('$or' => $ncond); - else - return $ncond[0]; - } - - /** - * find and convert relational operators - * @param $part - * @param $args - * @param \DB\Mongo $db - * @param null $fieldConf - * @return array|null - */ - protected function _mongo_parse_relational_op($part, &$args, \DB\Mongo $db, $fieldConf=null) { - if (is_null($part)) - return $part; - if (preg_match('/\<\=|\>\=|\<\>|\<|\>|\!\=|\=\=|\=|like|not like|in|not in/i', $part, $match)) { - $var = is_int(strpos($part, '?')) ? array_shift($args) : null; - $exp = explode($match[0], $part); - $key = trim($exp[0]); - // unbound value - if (is_numeric($exp[1])) - $var = $exp[1]; - // field comparison - elseif (!is_int(strpos($exp[1], '?'))) - return array('$where' => 'this.'.$key.' '.$match[0].' this.'.trim($exp[1])); - $upart = strtoupper($match[0]); - // MongoID shorthand - if ($key == '_id' || (isset($fieldConf[$key]) && isset($fieldConf[$key]['relType']))) { - if (is_array($var)) - foreach ($var as &$id) { - if ($db->legacy() && !$id instanceof \MongoId) - $id = new \MongoId($id); - elseif (!$db->legacy() && !$id instanceof \MongoDB\BSON\ObjectId) - $id = new \MongoDB\BSON\ObjectId($id); - unset($id); - } - elseif($db->legacy() && !$var instanceof \MongoId) - $var = new \MongoId($var); - elseif(!$db->legacy() && !$var instanceof \MongoDB\BSON\ObjectId) - $var = new \MongoDB\BSON\ObjectId($var); - } - // find LIKE operator - if (in_array($upart, array('LIKE','NOT LIKE'))) { - $rgx = $this->_likeValueToRegEx($var); - $var = $db->legacy() ? new \MongoRegex('/'.$rgx.'/iu') : new \MongoDB\BSON\Regex($rgx,'iu'); - if ($upart == 'NOT LIKE') - $var = array('$not' => $var); - } // find IN operator - elseif (in_array($upart, array('IN','NOT IN'))) { - if ($var instanceof CortexCollection) - $var = $var->getAll('_id',true); - $var = array(($upart=='NOT IN')?'$nin':'$in' => array_values($var)); - } // translate operators - elseif (!in_array($match[0], array('==', '='))) { - $opr = str_replace(array('<>', '<', '>', '!', '='), - array('$ne', '$lt', '$gt', '$n', 'e'), $match[0]); - $var = array($opr => (strtolower($var) == 'null') ? null : - (is_object($var) ? $var : (is_numeric($var) ? $var + 0 : $var))); - } - return array($key => $var); - } - return $part; - } - - /** - * @param string $var - * @return string - */ - protected function _likeValueToRegEx($var) { - $lC = substr($var, -1, 1); - // %var% -> /var/ - if ($var[0] == '%' && $lC == '%') - $var = substr($var, 1, -1); - // var% -> /^var/ - elseif ($lC == '%') - $var = '^'.substr($var, 0, -1); - // %var -> /var$/ - elseif ($var[0] == '%') - $var = substr($var, 1).'$'; - return $var; - } - - /** - * convert options array syntax to given engine type - * - * example: - * array('order'=>'location') // default direction is ASC - * array('order'=>'num1 desc, num2 asc') - * - * @param array $options - * @param string $engine - * @param object $db - * @return array|null - */ - public function prepareOptions($options, $engine, $db) { - if (empty($options) || !is_array($options)) - return null; - switch ($engine) { - case 'jig': - if (array_key_exists('order', $options)) - $options['order'] = preg_replace( - ['/(?<=\h)(ASC)(?=\W|$)/i','/(?<=\h)(DESC)(?=\W|$)/i'], - ['SORT_ASC','SORT_DESC'],$options['order']); - break; - case 'mongo': - if (array_key_exists('order', $options)) { - $sorts = explode(',', $options['order']); - $sorting = array(); - foreach ($sorts as $sort) { - $sp = explode(' ', trim($sort)); - $sorting[$sp[0]] = (array_key_exists(1, $sp) && - strtoupper($sp[1]) == 'DESC') ? -1 : 1; - } - $options['order'] = $sorting; - } - if (array_key_exists('group', $options) && is_string($options['group'])) { - $keys = explode(',',$options['group']); - $options['group']=array('keys'=>array(),'initial'=>array(), - 'reduce'=>'function (obj, prev) {}','finalize'=>''); - $keys = array_combine($keys,array_fill(0,count($keys),1)); - $options['group']['keys']=$keys; - $options['group']['initial']=$keys; - } - break; - case 'sql': - $char=substr($db->quotekey(''),0,1); - if (array_key_exists('order', $options) && - FALSE===strpos($options['order'],$char)) - $options['order']=preg_replace_callback( - '/(\w+\h?\(|'. // skip function names - '\b(?!\w+)(?:\s+\w+)+|' . // skip field args - '\)\s+\w+)|'. // skip function args - '(\b\d?[a-zA-Z_](?:[\w\-.])*)/i', // match table/field keys - function($match) use($db) { - if (!isset($match[2])) - return $match[1]; - return $db->quotekey($match[2]); - }, $options['order']); - break; - } - return $options; - } -} - -class CortexCollection extends \ArrayIterator { - - protected - $relSets = array(), - $pointer = 0, - $changed = false, - $cid; - - const - E_UnknownCID = 'This Collection does not exist: %s', - E_SubsetKeysValue = '$keys must be an array or split-able string, but %s was given.'; - - public function __construct() { - $this->cid = uniqid('cortex_collection_'); - parent::__construct(); - } - - //! Prohibit cloning to ensure an existing relation cache - private function __clone() { } - - /** - * set a collection of models - * @param $models - */ - function setModels($models, $init=true) { - array_map(array($this,'add'),$models); - if ($init) - $this->changed = false; - } - - /** - * add single model to collection - * @param $model - */ - function add(Cortex $model) { - $model->addToCollection($this); - $this->append($model); - } - - public function offsetSet($i, $val) { - $this->changed=true; - parent::offsetSet($i,$val); - } - - public function hasChanged() { - return $this->changed; - } - - /** - * get a related collection - * @param $key - * @return null - */ - public function getRelSet($key) { - return (isset($this->relSets[$key])) ? $this->relSets[$key] : null; - } - - /** - * set a related collection for caching it for the lifetime of this collection - * @param $key - * @param $set - */ - public function setRelSet($key,$set) { - $this->relSets[$key] = $set; - } - - /** - * check if a related collection exists in runtime cache - * @param $key - * @return bool - */ - public function hasRelSet($key) { - return array_key_exists($key,$this->relSets); - } - - public function expose() { - return $this->getArrayCopy(); - } - - /** - * get an intersection from a cached relation-set, based on given keys - * @param string $prop - * @param array|string $keys - * @return array - */ - public function getSubset($prop, $keys) { - if (is_string($keys)) - $keys = \Base::instance()->split($keys); - if (!is_array($keys)) - trigger_error(sprintf(self::E_SubsetKeysValue,gettype($keys)),E_USER_ERROR); - if (!$this->hasRelSet($prop) || !($relSet = $this->getRelSet($prop))) - return null; - foreach ($keys as &$key) { - if ($key instanceof \MongoId || $key instanceof \MongoDB\BSON\ObjectId) - $key = (string) $key; - unset($key); - } - return array_values(array_intersect_key($relSet, array_flip($keys))); - } - - /** - * returns all values of a specified property from all models - * @param string $prop - * @param bool $raw - * @return array - */ - public function getAll($prop, $raw = false) { - $out = array(); - foreach ($this->getArrayCopy() as $model) { - if ($model instanceof Cortex && $model->exists($prop,true)) { - $val = $model->get($prop, $raw); - if (!empty($val)) - $out[] = $val; - } elseif($raw) - $out[] = $model; - } - return $out; - } - - /** - * cast all contained mappers to a nested array - * @param int|array $rel_depths depths to resolve relations - * @return array - */ - public function castAll($rel_depths=1) { - $out = array(); - foreach ($this->getArrayCopy() as $model) - $out[] = $model->cast(null,$rel_depths); - return $out; - } - - /** - * return all models keyed by a specified index key - * @param string $index - * @param bool $nested - * @return array - */ - public function getBy($index, $nested = false) { - $out = array(); - foreach ($this->getArrayCopy() as $model) - if ($model->exists($index)) { - $val = $model->get($index, true); - if (!empty($val)) - if($nested) $out[(string) $val][] = $model; - else $out[(string) $val] = $model; - } - return $out; - } - - /** - * re-assort the current collection using a sql-like syntax - * @param $cond - */ - public function orderBy($cond) { - $cols=\Base::instance()->split($cond); - $this->uasort(function($val1,$val2) use($cols) { - foreach ($cols as $col) { - $parts=explode(' ',$col,2); - $order=empty($parts[1])?'ASC':$parts[1]; - $col=$parts[0]; - list($v1,$v2)=array($val1[$col],$val2[$col]); - if ($out=strnatcmp($v1,$v2)* - ((strtoupper($order)=='ASC')*2-1)) - return $out; - } - return 0; - }); - } - - /** - * slice the collection - * @param $offset - * @param null $limit - */ - public function slice($offset, $limit=null) { - $this->rewind(); - $i=0; - $del=array(); - while ($this->valid()) { - if ($i < $offset) - $del[]=$this->key(); - elseif ($i >= $offset && $limit && $i >= ($offset+$limit)) - $del[]=$this->key(); - $i++; - $this->next(); - } - foreach ($del as $ii) - unset($this[$ii]); - } - - /** - * compare collection with a given ID stack - * @param array|CortexCollection $stack - * @param string $cpm_key - * @return array - */ - public function compare($stack,$cpm_key='_id') { - if ($stack instanceof CortexCollection) - $stack = $stack->getAll($cpm_key,true); - $keys = $this->getAll($cpm_key,true); - $out = []; - $new = array_diff($stack,$keys); - $old = array_diff($keys,$stack); - if ($new) - $out['new'] = $new; - if ($old) - $out['old'] = $old; - return $out; - } - - /** - * check if the collection contains a record with the given key-val set - * @param mixed $val - * @param string $key - * @return bool - */ - public function contains($val,$key='_id') { - $rel_ids = $this->getAll($key, true); - if ($val instanceof Cursor) - $val = $val->{$key}; - return in_array($val,$rel_ids); - } - - /** - * create a new hydrated collection from the given records - * @param $records - * @return CortexCollection - */ - static public function factory($records) { - $cc = new self(); - $cc->setModels($records); - return $cc; - } - -} \ No newline at end of file diff --git a/app/lib/db/cursor.php b/app/lib/db/cursor.php deleted file mode 100644 index 6edbc0e0..00000000 --- a/app/lib/db/cursor.php +++ /dev/null @@ -1,388 +0,0 @@ -. - -*/ - -namespace DB; - -//! Simple cursor implementation -abstract class Cursor extends \Magic implements \IteratorAggregate { - - //@{ Error messages - const - E_Field='Undefined field %s'; - //@} - - protected - //! Query results - $query=[], - //! Current position - $ptr=0, - //! Event listeners - $trigger=[]; - - /** - * Return database type - * @return string - **/ - abstract function dbtype(); - - /** - * Return field names - * @return array - **/ - abstract function fields(); - - /** - * Return fields of mapper object as an associative array - * @return array - * @param $obj object - **/ - abstract function cast($obj=NULL); - - /** - * Return records (array of mapper objects) that match criteria - * @return array - * @param $filter string|array - * @param $options array - * @param $ttl int - **/ - abstract function find($filter=NULL,array $options=NULL,$ttl=0); - - /** - * Count records that match criteria - * @return int - * @param $filter array - * @param $options array - * @param $ttl int - **/ - abstract function count($filter=NULL,array $options=NULL,$ttl=0); - - /** - * Insert new record - * @return array - **/ - abstract function insert(); - - /** - * Update current record - * @return array - **/ - abstract function update(); - - /** - * Hydrate mapper object using hive array variable - * @return NULL - * @param $var array|string - * @param $func callback - **/ - abstract function copyfrom($var,$func=NULL); - - /** - * Populate hive array variable with mapper fields - * @return NULL - * @param $key string - **/ - abstract function copyto($key); - - /** - * Get cursor's equivalent external iterator - * Causes a fatal error in PHP 5.3.5 if uncommented - * return ArrayIterator - **/ - abstract function getiterator(); - - - /** - * Return TRUE if current cursor position is not mapped to any record - * @return bool - **/ - function dry() { - return empty($this->query[$this->ptr]); - } - - /** - * Return first record (mapper object) that matches criteria - * @return static|FALSE - * @param $filter string|array - * @param $options array - * @param $ttl int - **/ - function findone($filter=NULL,array $options=NULL,$ttl=0) { - if (!$options) - $options=[]; - // Override limit - $options['limit']=1; - return ($data=$this->find($filter,$options,$ttl))?$data[0]:FALSE; - } - - /** - * Return array containing subset of records matching criteria, - * total number of records in superset, specified limit, number of - * subsets available, and actual subset position - * @return array - * @param $pos int - * @param $size int - * @param $filter string|array - * @param $options array - * @param $ttl int - * @param $bounce bool - **/ - function paginate( - $pos=0,$size=10,$filter=NULL,array $options=NULL,$ttl=0,$bounce=TRUE) { - $total=$this->count($filter,$options,$ttl); - $count=ceil($total/$size); - if ($bounce) - $pos=max(0,min($pos,$count-1)); - return [ - 'subset'=>($bounce || $pos<$count)?$this->find($filter, - array_merge( - $options?:[], - ['limit'=>$size,'offset'=>$pos*$size] - ), - $ttl - ):[], - 'total'=>$total, - 'limit'=>$size, - 'count'=>$count, - 'pos'=>$bounce?($pos<$count?$pos:0):$pos - ]; - } - - /** - * Map to first record that matches criteria - * @return array|FALSE - * @param $filter string|array - * @param $options array - * @param $ttl int - **/ - function load($filter=NULL,array $options=NULL,$ttl=0) { - $this->reset(); - return ($this->query=$this->find($filter,$options,$ttl)) && - $this->skip(0)?$this->query[$this->ptr]:FALSE; - } - - /** - * Return the count of records loaded - * @return int - **/ - function loaded() { - return count($this->query); - } - - /** - * Map to first record in cursor - * @return mixed - **/ - function first() { - return $this->skip(-$this->ptr); - } - - /** - * Map to last record in cursor - * @return mixed - **/ - function last() { - return $this->skip(($ofs=count($this->query)-$this->ptr)?$ofs-1:0); - } - - /** - * Map to nth record relative to current cursor position - * @return mixed - * @param $ofs int - **/ - function skip($ofs=1) { - $this->ptr+=$ofs; - return $this->ptr>-1 && $this->ptrquery)? - $this->query[$this->ptr]:FALSE; - } - - /** - * Map next record - * @return mixed - **/ - function next() { - return $this->skip(); - } - - /** - * Map previous record - * @return mixed - **/ - function prev() { - return $this->skip(-1); - } - - /** - * Return whether current iterator position is valid. - */ - function valid() { - return !$this->dry(); - } - - /** - * Save mapped record - * @return mixed - **/ - function save() { - return $this->query?$this->update():$this->insert(); - } - - /** - * Delete current record - * @return int|bool - **/ - function erase() { - $this->query=array_slice($this->query,0,$this->ptr,TRUE)+ - array_slice($this->query,$this->ptr,NULL,TRUE); - $this->skip(0); - } - - /** - * Define onload trigger - * @return callback - * @param $func callback - **/ - function onload($func) { - return $this->trigger['load']=$func; - } - - /** - * Define beforeinsert trigger - * @return callback - * @param $func callback - **/ - function beforeinsert($func) { - return $this->trigger['beforeinsert']=$func; - } - - /** - * Define afterinsert trigger - * @return callback - * @param $func callback - **/ - function afterinsert($func) { - return $this->trigger['afterinsert']=$func; - } - - /** - * Define oninsert trigger - * @return callback - * @param $func callback - **/ - function oninsert($func) { - return $this->afterinsert($func); - } - - /** - * Define beforeupdate trigger - * @return callback - * @param $func callback - **/ - function beforeupdate($func) { - return $this->trigger['beforeupdate']=$func; - } - - /** - * Define afterupdate trigger - * @return callback - * @param $func callback - **/ - function afterupdate($func) { - return $this->trigger['afterupdate']=$func; - } - - /** - * Define onupdate trigger - * @return callback - * @param $func callback - **/ - function onupdate($func) { - return $this->afterupdate($func); - } - - /** - * Define beforesave trigger - * @return callback - * @param $func callback - **/ - function beforesave($func) { - $this->trigger['beforeinsert']=$func; - $this->trigger['beforeupdate']=$func; - return $func; - } - - /** - * Define aftersave trigger - * @return callback - * @param $func callback - **/ - function aftersave($func) { - $this->trigger['afterinsert']=$func; - $this->trigger['afterupdate']=$func; - return $func; - } - - /** - * Define onsave trigger - * @return callback - * @param $func callback - **/ - function onsave($func) { - return $this->aftersave($func); - } - - /** - * Define beforeerase trigger - * @return callback - * @param $func callback - **/ - function beforeerase($func) { - return $this->trigger['beforeerase']=$func; - } - - /** - * Define aftererase trigger - * @return callback - * @param $func callback - **/ - function aftererase($func) { - return $this->trigger['aftererase']=$func; - } - - /** - * Define onerase trigger - * @return callback - * @param $func callback - **/ - function onerase($func) { - return $this->aftererase($func); - } - - /** - * Reset cursor - * @return NULL - **/ - function reset() { - $this->query=[]; - $this->ptr=0; - } - -} diff --git a/app/lib/db/jig.php b/app/lib/db/jig.php deleted file mode 100644 index fe3a302c..00000000 --- a/app/lib/db/jig.php +++ /dev/null @@ -1,175 +0,0 @@ -. - -*/ - -namespace DB; - -//! In-memory/flat-file DB wrapper -class Jig { - - //@{ Storage formats - const - FORMAT_JSON=0, - FORMAT_Serialized=1; - //@} - - protected - //! UUID - $uuid, - //! Storage location - $dir, - //! Current storage format - $format, - //! Jig log - $log, - //! Memory-held data - $data, - //! lazy load/save files - $lazy; - - /** - * Read data from memory/file - * @return array - * @param $file string - **/ - function &read($file) { - if (!$this->dir || !is_file($dst=$this->dir.$file)) { - if (!isset($this->data[$file])) - $this->data[$file]=[]; - return $this->data[$file]; - } - if ($this->lazy && isset($this->data[$file])) - return $this->data[$file]; - $fw=\Base::instance(); - $raw=$fw->read($dst); - switch ($this->format) { - case self::FORMAT_JSON: - $data=json_decode($raw,TRUE); - break; - case self::FORMAT_Serialized: - $data=$fw->unserialize($raw); - break; - } - $this->data[$file] = $data; - return $this->data[$file]; - } - - /** - * Write data to memory/file - * @return int - * @param $file string - * @param $data array - **/ - function write($file,array $data=NULL) { - if (!$this->dir || $this->lazy) - return count($this->data[$file]=$data); - $fw=\Base::instance(); - switch ($this->format) { - case self::FORMAT_JSON: - $out=json_encode($data,JSON_PRETTY_PRINT); - break; - case self::FORMAT_Serialized: - $out=$fw->serialize($data); - break; - } - return $fw->write($this->dir.'/'.$file,$out); - } - - /** - * Return directory - * @return string - **/ - function dir() { - return $this->dir; - } - - /** - * Return UUID - * @return string - **/ - function uuid() { - return $this->uuid; - } - - /** - * Return profiler results (or disable logging) - * @param $flag bool - * @return string - **/ - function log($flag=TRUE) { - if ($flag) - return $this->log; - $this->log=FALSE; - } - - /** - * Jot down log entry - * @return NULL - * @param $frame string - **/ - function jot($frame) { - if ($frame) - $this->log.=date('r').' '.$frame.PHP_EOL; - } - - /** - * Clean storage - * @return NULL - **/ - function drop() { - if ($this->lazy) // intentional - $this->data=[]; - if (!$this->dir) - $this->data=[]; - elseif ($glob=@glob($this->dir.'/*',GLOB_NOSORT)) - foreach ($glob as $file) - @unlink($file); - } - - //! Prohibit cloning - private function __clone() { - } - - /** - * Instantiate class - * @param $dir string - * @param $format int - **/ - function __construct($dir=NULL,$format=self::FORMAT_JSON,$lazy=FALSE) { - if ($dir && !is_dir($dir)) - mkdir($dir,\Base::MODE,TRUE); - $this->uuid=\Base::instance()->hash($this->dir=$dir); - $this->format=$format; - $this->lazy=$lazy; - } - - /** - * save file on destruction - **/ - function __destruct() { - if ($this->lazy) { - $this->lazy = FALSE; - foreach ($this->data?:[] as $file => $data) - $this->write($file,$data); - } - } - -} diff --git a/app/lib/db/jig/mapper.php b/app/lib/db/jig/mapper.php deleted file mode 100644 index ef878f13..00000000 --- a/app/lib/db/jig/mapper.php +++ /dev/null @@ -1,541 +0,0 @@ -. - -*/ - -namespace DB\Jig; - -//! Flat-file DB mapper -class Mapper extends \DB\Cursor { - - protected - //! Flat-file DB wrapper - $db, - //! Data file - $file, - //! Document identifier - $id, - //! Document contents - $document=[], - //! field map-reduce handlers - $_reduce; - - /** - * Return database type - * @return string - **/ - function dbtype() { - return 'Jig'; - } - - /** - * Return TRUE if field is defined - * @return bool - * @param $key string - **/ - function exists($key) { - return array_key_exists($key,$this->document); - } - - /** - * Assign value to field - * @return scalar|FALSE - * @param $key string - * @param $val scalar - **/ - function set($key,$val) { - return ($key=='_id')?FALSE:($this->document[$key]=$val); - } - - /** - * Retrieve value of field - * @return scalar|FALSE - * @param $key string - **/ - function &get($key) { - if ($key=='_id') - return $this->id; - if (array_key_exists($key,$this->document)) - return $this->document[$key]; - user_error(sprintf(self::E_Field,$key),E_USER_ERROR); - } - - /** - * Delete field - * @return NULL - * @param $key string - **/ - function clear($key) { - if ($key!='_id') - unset($this->document[$key]); - } - - /** - * Convert array to mapper object - * @return object - * @param $id string - * @param $row array - **/ - function factory($id,$row) { - $mapper=clone($this); - $mapper->reset(); - $mapper->id=$id; - foreach ($row as $field=>$val) - $mapper->document[$field]=$val; - $mapper->query=[clone($mapper)]; - if (isset($mapper->trigger['load'])) - \Base::instance()->call($mapper->trigger['load'],$mapper); - return $mapper; - } - - /** - * Return fields of mapper object as an associative array - * @return array - * @param $obj object - **/ - function cast($obj=NULL) { - if (!$obj) - $obj=$this; - return $obj->document+['_id'=>$this->id]; - } - - /** - * Convert tokens in string expression to variable names - * @return string - * @param $str string - **/ - function token($str) { - $str=preg_replace_callback( - '/(?stringify(substr($expr[1],1)): - (preg_match('/^\w+/', - $mix=$this->token($expr[2]))? - $fw->stringify($mix): - $mix)). - ']'; - }, - $token[1] - ); - }, - $str - ); - return trim($str); - } - - /** - * Return records that match criteria - * @return static[]|FALSE - * @param $filter array - * @param $options array - * @param $ttl int|array - * @param $log bool - **/ - function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) { - if (!$options) - $options=[]; - $options+=[ - 'order'=>NULL, - 'limit'=>0, - 'offset'=>0, - 'group'=>NULL, - ]; - $fw=\Base::instance(); - $cache=\Cache::instance(); - $db=$this->db; - $now=microtime(TRUE); - $data=[]; - $tag=''; - if (is_array($ttl)) - list($ttl,$tag)=$ttl; - if (!$fw->CACHE || !$ttl || !($cached=$cache->exists( - $hash=$fw->hash($this->db->dir(). - $fw->stringify([$filter,$options])).($tag?'.'.$tag:'').'.jig',$data)) || - $cached[0]+$ttlread($this->file); - if (is_null($data)) - return FALSE; - foreach ($data as $id=>&$doc) { - $doc['_id']=$id; - unset($doc); - } - if ($filter) { - if (!is_array($filter)) - return FALSE; - // Normalize equality operator - $expr=preg_replace('/(?<=[^<>!=])=(?!=)/','==',$filter[0]); - // Prepare query arguments - $args=isset($filter[1]) && is_array($filter[1])? - $filter[1]: - array_slice($filter,1,NULL,TRUE); - $args=is_array($args)?$args:[1=>$args]; - $keys=$vals=[]; - $tokens=array_slice( - token_get_all('token($expr)),1); - $data=array_filter($data, - function($_row) use($fw,$args,$tokens) { - $_expr=''; - $ctr=0; - $named=FALSE; - foreach ($tokens as $token) { - if (is_string($token)) - if ($token=='?') { - // Positional - $ctr++; - $key=$ctr; - } - else { - if ($token==':') - $named=TRUE; - else - $_expr.=$token; - continue; - } - elseif ($named && - token_name($token[0])=='T_STRING') { - $key=':'.$token[1]; - $named=FALSE; - } - else { - $_expr.=$token[1]; - continue; - } - $_expr.=$fw->stringify( - is_string($args[$key])? - addcslashes($args[$key],'\''): - $args[$key]); - } - // Avoid conflict with user code - unset($fw,$tokens,$args,$ctr,$token,$key,$named); - extract($_row); - // Evaluate pseudo-SQL expression - return eval('return '.$_expr.';'); - } - ); - } - if (isset($options['group'])) { - $cols=array_reverse($fw->split($options['group'])); - // sort into groups - $data=$this->sort($data,$options['group']); - foreach($data as $i=>&$row) { - if (!isset($prev)) { - $prev=$row; - $prev_i=$i; - } - $drop=false; - foreach ($cols as $col) - if ($prev_i!=$i && array_key_exists($col,$row) && - array_key_exists($col,$prev) && $row[$col]==$prev[$col]) - // reduce/modify - $drop=!isset($this->_reduce[$col]) || call_user_func_array( - $this->_reduce[$col][0],[&$prev,&$row])!==FALSE; - elseif (isset($this->_reduce[$col])) { - $null=null; - // initial - call_user_func_array($this->_reduce[$col][0],[&$row,&$null]); - } - if ($drop) - unset($data[$i]); - else { - $prev=&$row; - $prev_i=$i; - } - unset($row); - } - // finalize - if ($this->_reduce[$col][1]) - foreach($data as $i=>&$row) { - $row=call_user_func($this->_reduce[$col][1],$row); - if (!$row) - unset($data[$i]); - unset($row); - } - } - if (isset($options['order'])) - $data=$this->sort($data,$options['order']); - $data=array_slice($data, - $options['offset'],$options['limit']?:NULL,TRUE); - if ($fw->CACHE && $ttl) - // Save to cache backend - $cache->set($hash,$data,$ttl); - } - $out=[]; - foreach ($data as $id=>&$doc) { - unset($doc['_id']); - $out[]=$this->factory($id,$doc); - unset($doc); - } - if ($log && isset($args)) { - if ($filter) - foreach ($args as $key=>$val) { - $vals[]=$fw->stringify(is_array($val)?$val[0]:$val); - $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/'; - } - $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. - $this->file.' [find] '. - ($filter?preg_replace($keys,$vals,$filter[0],1):'')); - } - return $out; - } - - /** - * Sort a collection - * @param $data - * @param $cond - * @return mixed - */ - protected function sort($data,$cond) { - $cols=\Base::instance()->split($cond); - uasort( - $data, - function($val1,$val2) use($cols) { - foreach ($cols as $col) { - $parts=explode(' ',$col,2); - $order=empty($parts[1])? - SORT_ASC: - constant($parts[1]); - $col=$parts[0]; - if (!array_key_exists($col,$val1)) - $val1[$col]=NULL; - if (!array_key_exists($col,$val2)) - $val2[$col]=NULL; - list($v1,$v2)=[$val1[$col],$val2[$col]]; - if ($out=strnatcmp($v1,$v2)* - (($order==SORT_ASC)*2-1)) - return $out; - } - return 0; - } - ); - return $data; - } - - /** - * Add reduce handler for grouped fields - * @param $key string - * @param $handler callback - * @param $finalize callback - */ - function reduce($key,$handler,$finalize=null){ - $this->_reduce[$key]=[$handler,$finalize]; - } - - /** - * Count records that match criteria - * @return int - * @param $filter array - * @param $options array - * @param $ttl int|array - **/ - function count($filter=NULL,array $options=NULL,$ttl=0) { - $now=microtime(TRUE); - $out=count($this->find($filter,$options,$ttl,FALSE)); - $this->db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. - $this->file.' [count] '.($filter?json_encode($filter):'')); - return $out; - } - - /** - * Return record at specified offset using criteria of previous - * load() call and make it active - * @return array - * @param $ofs int - **/ - function skip($ofs=1) { - $this->document=($out=parent::skip($ofs))?$out->document:[]; - $this->id=$out?$out->id:NULL; - if ($this->document && isset($this->trigger['load'])) - \Base::instance()->call($this->trigger['load'],$this); - return $out; - } - - /** - * Insert new record - * @return array - **/ - function insert() { - if ($this->id) - return $this->update(); - $db=$this->db; - $now=microtime(TRUE); - while (($id=uniqid(NULL,TRUE)) && - ($data=&$db->read($this->file)) && isset($data[$id]) && - !connection_aborted()) - usleep(mt_rand(0,100)); - $this->id=$id; - $pkey=['_id'=>$this->id]; - if (isset($this->trigger['beforeinsert']) && - \Base::instance()->call($this->trigger['beforeinsert'], - [$this,$pkey])===FALSE) - return $this->document; - $data[$id]=$this->document; - $db->write($this->file,$data); - $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. - $this->file.' [insert] '.json_encode($this->document)); - if (isset($this->trigger['afterinsert'])) - \Base::instance()->call($this->trigger['afterinsert'], - [$this,$pkey]); - $this->load(['@_id=?',$this->id]); - return $this->document; - } - - /** - * Update current record - * @return array - **/ - function update() { - $db=$this->db; - $now=microtime(TRUE); - $data=&$db->read($this->file); - if (isset($this->trigger['beforeupdate']) && - \Base::instance()->call($this->trigger['beforeupdate'], - [$this,['_id'=>$this->id]])===FALSE) - return $this->document; - $data[$this->id]=$this->document; - $db->write($this->file,$data); - $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. - $this->file.' [update] '.json_encode($this->document)); - if (isset($this->trigger['afterupdate'])) - \Base::instance()->call($this->trigger['afterupdate'], - [$this,['_id'=>$this->id]]); - return $this->document; - } - - /** - * Delete current record - * @return bool - * @param $filter array - * @param $quick bool - **/ - function erase($filter=NULL,$quick=FALSE) { - $db=$this->db; - $now=microtime(TRUE); - $data=&$db->read($this->file); - $pkey=['_id'=>$this->id]; - if ($filter) { - foreach ($this->find($filter,NULL,FALSE) as $mapper) - if (!$mapper->erase(null,$quick)) - return FALSE; - return TRUE; - } - elseif (isset($this->id)) { - unset($data[$this->id]); - parent::erase(); - } - else - return FALSE; - if (!$quick && isset($this->trigger['beforeerase']) && - \Base::instance()->call($this->trigger['beforeerase'], - [$this,$pkey])===FALSE) - return FALSE; - $db->write($this->file,$data); - if ($filter) { - $args=isset($filter[1]) && is_array($filter[1])? - $filter[1]: - array_slice($filter,1,NULL,TRUE); - $args=is_array($args)?$args:[1=>$args]; - foreach ($args as $key=>$val) { - $vals[]=\Base::instance()-> - stringify(is_array($val)?$val[0]:$val); - $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/'; - } - } - $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. - $this->file.' [erase] '. - ($filter?preg_replace($keys,$vals,$filter[0],1):'')); - if (!$quick && isset($this->trigger['aftererase'])) - \Base::instance()->call($this->trigger['aftererase'], - [$this,$pkey]); - return TRUE; - } - - /** - * Reset cursor - * @return NULL - **/ - function reset() { - $this->id=NULL; - $this->document=[]; - parent::reset(); - } - - /** - * Hydrate mapper object using hive array variable - * @return NULL - * @param $var array|string - * @param $func callback - **/ - function copyfrom($var,$func=NULL) { - if (is_string($var)) - $var=\Base::instance()->$var; - if ($func) - $var=call_user_func($func,$var); - foreach ($var as $key=>$val) - $this->set($key,$val); - } - - /** - * Populate hive array variable with mapper fields - * @return NULL - * @param $key string - **/ - function copyto($key) { - $var=&\Base::instance()->ref($key); - foreach ($this->document as $key=>$field) - $var[$key]=$field; - } - - /** - * Return field names - * @return array - **/ - function fields() { - return array_keys($this->document); - } - - /** - * Retrieve external iterator for fields - * @return object - **/ - function getiterator() { - return new \ArrayIterator($this->cast()); - } - - /** - * Instantiate class - * @return void - * @param $db object - * @param $file string - **/ - function __construct(\DB\Jig $db,$file) { - $this->db=$db; - $this->file=$file; - $this->reset(); - } - -} diff --git a/app/lib/db/jig/session.php b/app/lib/db/jig/session.php deleted file mode 100644 index 03faa6cc..00000000 --- a/app/lib/db/jig/session.php +++ /dev/null @@ -1,194 +0,0 @@ -. - -*/ - -namespace DB\Jig; - -//! Jig-managed session handler -class Session extends Mapper { - - protected - //! Session ID - $sid, - //! Anti-CSRF token - $_csrf, - //! User agent - $_agent, - //! IP, - $_ip, - //! Suspect callback - $onsuspect; - - /** - * Open session - * @return TRUE - * @param $path string - * @param $name string - **/ - function open($path,$name) { - return TRUE; - } - - /** - * Close session - * @return TRUE - **/ - function close() { - $this->reset(); - $this->sid=NULL; - return TRUE; - } - - /** - * Return session data in serialized format - * @return string - * @param $id string - **/ - function read($id) { - $this->load(['@session_id=?',$this->sid=$id]); - if ($this->dry()) - return ''; - if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { - $fw=\Base::instance(); - if (!isset($this->onsuspect) || - $fw->call($this->onsuspect,[$this,$id])===FALSE) { - // NB: `session_destroy` can't be called at that stage; - // `session_start` not completed - $this->destroy($id); - $this->close(); - unset($fw->{'COOKIE.'.session_name()}); - $fw->error(403); - } - } - return $this->get('data'); - } - - /** - * Write session data - * @return TRUE - * @param $id string - * @param $data string - **/ - function write($id,$data) { - $this->set('session_id',$id); - $this->set('data',$data); - $this->set('ip',$this->_ip); - $this->set('agent',$this->_agent); - $this->set('stamp',time()); - $this->save(); - return TRUE; - } - - /** - * Destroy session - * @return TRUE - * @param $id string - **/ - function destroy($id) { - $this->erase(['@session_id=?',$id]); - return TRUE; - } - - /** - * Garbage collector - * @return TRUE - * @param $max int - **/ - function cleanup($max) { - $this->erase(['@stamp+?sid; - } - - /** - * Return anti-CSRF token - * @return string - **/ - function csrf() { - return $this->_csrf; - } - - /** - * Return IP address - * @return string - **/ - function ip() { - return $this->_ip; - } - - /** - * Return Unix timestamp - * @return string|FALSE - **/ - function stamp() { - if (!$this->sid) - session_start(); - return $this->dry()?FALSE:$this->get('stamp'); - } - - /** - * Return HTTP user agent - * @return string|FALSE - **/ - function agent() { - return $this->_agent; - } - - /** - * Instantiate class - * @param $db \DB\Jig - * @param $file string - * @param $onsuspect callback - * @param $key string - **/ - function __construct(\DB\Jig $db,$file='sessions',$onsuspect=NULL,$key=NULL) { - parent::__construct($db,$file); - $this->onsuspect=$onsuspect; - session_set_save_handler( - [$this,'open'], - [$this,'close'], - [$this,'read'], - [$this,'write'], - [$this,'destroy'], - [$this,'cleanup'] - ); - register_shutdown_function('session_commit'); - $fw=\Base::instance(); - $headers=$fw->HEADERS; - $this->_csrf=$fw->hash($fw->SEED. - extension_loaded('openssl')? - implode(unpack('L',openssl_random_pseudo_bytes(4))): - mt_rand() - ); - if ($key) - $fw->$key=$this->_csrf; - $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; - $this->_ip=$fw->IP; - } - -} diff --git a/app/lib/db/mongo.php b/app/lib/db/mongo.php deleted file mode 100644 index 86ac184c..00000000 --- a/app/lib/db/mongo.php +++ /dev/null @@ -1,145 +0,0 @@ -. - -*/ - -namespace DB; - -//! MongoDB wrapper -class Mongo { - - //@{ - const - E_Profiler='MongoDB profiler is disabled'; - //@} - - protected - //! UUID - $uuid, - //! Data source name - $dsn, - //! MongoDB object - $db, - //! Legacy flag - $legacy, - //! MongoDB log - $log; - - /** - * Return data source name - * @return string - **/ - function dsn() { - return $this->dsn; - } - - /** - * Return UUID - * @return string - **/ - function uuid() { - return $this->uuid; - } - - /** - * Return MongoDB profiler results (or disable logging) - * @param $flag bool - * @return string - **/ - function log($flag=TRUE) { - if ($flag) { - $cursor=$this->db->selectcollection('system.profile')->find(); - foreach (iterator_to_array($cursor) as $frame) - if (!preg_match('/\.system\..+$/',$frame['ns'])) - $this->log.=date('r',$this->legacy() ? - $frame['ts']->sec : (round((string)$frame['ts'])/1000)). - ' ('.sprintf('%.1f',$frame['millis']).'ms) '. - $frame['ns'].' ['.$frame['op'].'] '. - (empty($frame['query'])? - '':json_encode($frame['query'])). - (empty($frame['command'])? - '':json_encode($frame['command'])). - PHP_EOL; - } else { - $this->log=FALSE; - if ($this->legacy) - $this->db->setprofilinglevel(-1); - else - $this->db->command(['profile'=>-1]); - } - return $this->log; - } - - /** - * Intercept native call to re-enable profiler - * @return int - **/ - function drop() { - $out=$this->db->drop(); - if ($this->log!==FALSE) { - if ($this->legacy) - $this->db->setprofilinglevel(2); - else - $this->db->command(['profile'=>2]); - } - return $out; - } - - /** - * Redirect call to MongoDB object - * @return mixed - * @param $func string - * @param $args array - **/ - function __call($func,array $args) { - return call_user_func_array([$this->db,$func],$args); - } - - /** - * Return TRUE if legacy driver is loaded - * @return bool - **/ - function legacy() { - return $this->legacy; - } - - //! Prohibit cloning - private function __clone() { - } - - /** - * Instantiate class - * @param $dsn string - * @param $dbname string - * @param $options array - **/ - function __construct($dsn,$dbname,array $options=NULL) { - $this->uuid=\Base::instance()->hash($this->dsn=$dsn); - if ($this->legacy=class_exists('\MongoClient')) { - $this->db=new \MongoDB(new \MongoClient($dsn,$options?:[]),$dbname); - $this->db->setprofilinglevel(2); - } - else { - $this->db=(new \MongoDB\Client($dsn,$options?:[]))->$dbname; - $this->db->command(['profile'=>2]); - } - } - -} diff --git a/app/lib/db/mongo/mapper.php b/app/lib/db/mongo/mapper.php deleted file mode 100644 index 4ad4cdfe..00000000 --- a/app/lib/db/mongo/mapper.php +++ /dev/null @@ -1,405 +0,0 @@ -. - -*/ - -namespace DB\Mongo; - -//! MongoDB mapper -class Mapper extends \DB\Cursor { - - protected - //! MongoDB wrapper - $db, - //! Legacy flag - $legacy, - //! Mongo collection - $collection, - //! Mongo document - $document=[], - //! Mongo cursor - $cursor, - //! Defined fields - $fields; - - /** - * Return database type - * @return string - **/ - function dbtype() { - return 'Mongo'; - } - - /** - * Return TRUE if field is defined - * @return bool - * @param $key string - **/ - function exists($key) { - return array_key_exists($key,$this->document); - } - - /** - * Assign value to field - * @return scalar|FALSE - * @param $key string - * @param $val scalar - **/ - function set($key,$val) { - return $this->document[$key]=$val; - } - - /** - * Retrieve value of field - * @return scalar|FALSE - * @param $key string - **/ - function &get($key) { - if ($this->exists($key)) - return $this->document[$key]; - user_error(sprintf(self::E_Field,$key),E_USER_ERROR); - } - - /** - * Delete field - * @return NULL - * @param $key string - **/ - function clear($key) { - unset($this->document[$key]); - } - - /** - * Convert array to mapper object - * @return static - * @param $row array - **/ - function factory($row) { - $mapper=clone($this); - $mapper->reset(); - foreach ($row as $key=>$val) - $mapper->document[$key]=$val; - $mapper->query=[clone($mapper)]; - if (isset($mapper->trigger['load'])) - \Base::instance()->call($mapper->trigger['load'],$mapper); - return $mapper; - } - - /** - * Return fields of mapper object as an associative array - * @return array - * @param $obj object - **/ - function cast($obj=NULL) { - if (!$obj) - $obj=$this; - return $obj->document; - } - - /** - * Build query and execute - * @return static[] - * @param $fields string - * @param $filter array - * @param $options array - * @param $ttl int|array - **/ - function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) { - if (!$options) - $options=[]; - $options+=[ - 'group'=>NULL, - 'order'=>NULL, - 'limit'=>0, - 'offset'=>0 - ]; - $tag=''; - if (is_array($ttl)) - list($ttl,$tag)=$ttl; - $fw=\Base::instance(); - $cache=\Cache::instance(); - if (!($cached=$cache->exists($hash=$fw->hash($this->db->dsn(). - $fw->stringify([$fields,$filter,$options])).($tag?'.'.$tag:'').'.mongo', - $result)) || !$ttl || $cached[0]+$ttlcollection->group( - $options['group']['keys'], - $options['group']['initial'], - $options['group']['reduce'], - [ - 'condition'=>$filter, - 'finalize'=>$options['group']['finalize'] - ] - ); - $tmp=$this->db->selectcollection( - $fw->HOST.'.'.$fw->BASE.'.'. - uniqid(NULL,TRUE).'.tmp' - ); - $tmp->batchinsert($grp['retval'],['w'=>1]); - $filter=[]; - $collection=$tmp; - } - else { - $filter=$filter?:[]; - $collection=$this->collection; - } - if ($this->legacy) { - $this->cursor=$collection->find($filter,$fields?:[]); - if ($options['order']) - $this->cursor=$this->cursor->sort($options['order']); - if ($options['limit']) - $this->cursor=$this->cursor->limit($options['limit']); - if ($options['offset']) - $this->cursor=$this->cursor->skip($options['offset']); - $result=[]; - while ($this->cursor->hasnext()) - $result[]=$this->cursor->getnext(); - } - else { - $this->cursor=$collection->find($filter,[ - 'sort'=>$options['order'], - 'limit'=>$options['limit'], - 'skip'=>$options['offset'] - ]); - $result=$this->cursor->toarray(); - } - if ($options['group']) - $tmp->drop(); - if ($fw->CACHE && $ttl) - // Save to cache backend - $cache->set($hash,$result,$ttl); - } - $out=[]; - foreach ($result as $doc) - $out[]=$this->factory($doc); - return $out; - } - - /** - * Return records that match criteria - * @return static[] - * @param $filter array - * @param $options array - * @param $ttl int|array - **/ - function find($filter=NULL,array $options=NULL,$ttl=0) { - if (!$options) - $options=[]; - $options+=[ - 'group'=>NULL, - 'order'=>NULL, - 'limit'=>0, - 'offset'=>0 - ]; - return $this->select($this->fields,$filter,$options,$ttl); - } - - /** - * Count records that match criteria - * @return int - * @param $filter array - * @param $options array - * @param $ttl int|array - **/ - function count($filter=NULL,array $options=NULL,$ttl=0) { - $fw=\Base::instance(); - $cache=\Cache::instance(); - $tag=''; - if (is_array($ttl)) - list($ttl,$tag)=$ttl; - if (!($cached=$cache->exists($hash=$fw->hash($fw->stringify( - [$filter])).($tag?'.'.$tag:'').'.mongo',$result)) || !$ttl || - $cached[0]+$ttlcollection->count($filter?:[]); - if ($fw->CACHE && $ttl) - // Save to cache backend - $cache->set($hash,$result,$ttl); - } - return $result; - } - - /** - * Return record at specified offset using criteria of previous - * load() call and make it active - * @return array - * @param $ofs int - **/ - function skip($ofs=1) { - $this->document=($out=parent::skip($ofs))?$out->document:[]; - if ($this->document && isset($this->trigger['load'])) - \Base::instance()->call($this->trigger['load'],$this); - return $out; - } - - /** - * Insert new record - * @return array - **/ - function insert() { - if (isset($this->document['_id'])) - return $this->update(); - if (isset($this->trigger['beforeinsert']) && - \Base::instance()->call($this->trigger['beforeinsert'], - [$this,['_id'=>$this->document['_id']]])===FALSE) - return $this->document; - if ($this->legacy) { - $this->collection->insert($this->document); - $pkey=['_id'=>$this->document['_id']]; - } - else { - $result=$this->collection->insertone($this->document); - $pkey=['_id'=>$result->getinsertedid()]; - } - if (isset($this->trigger['afterinsert'])) - \Base::instance()->call($this->trigger['afterinsert'], - [$this,$pkey]); - $this->load($pkey); - return $this->document; - } - - /** - * Update current record - * @return array - **/ - function update() { - $pkey=['_id'=>$this->document['_id']]; - if (isset($this->trigger['beforeupdate']) && - \Base::instance()->call($this->trigger['beforeupdate'], - [$this,$pkey])===FALSE) - return $this->document; - $upsert=['upsert'=>TRUE]; - if ($this->legacy) - $this->collection->update($pkey,$this->document,$upsert); - else - $this->collection->replaceone($pkey,$this->document,$upsert); - if (isset($this->trigger['afterupdate'])) - \Base::instance()->call($this->trigger['afterupdate'], - [$this,$pkey]); - return $this->document; - } - - /** - * Delete current record - * @return bool - * @param $quick bool - * @param $filter array - **/ - function erase($filter=NULL,$quick=TRUE) { - if ($filter) { - if (!$quick) { - foreach ($this->find($filter) as $mapper) - if (!$mapper->erase()) - return FALSE; - return TRUE; - } - return $this->legacy? - $this->collection->remove($filter): - $this->collection->deletemany($filter); - } - $pkey=['_id'=>$this->document['_id']]; - if (isset($this->trigger['beforeerase']) && - \Base::instance()->call($this->trigger['beforeerase'], - [$this,$pkey])===FALSE) - return FALSE; - $result=$this->legacy? - $this->collection->remove(['_id'=>$this->document['_id']]): - $this->collection->deleteone(['_id'=>$this->document['_id']]); - parent::erase(); - if (isset($this->trigger['aftererase'])) - \Base::instance()->call($this->trigger['aftererase'], - [$this,$pkey]); - return $result; - } - - /** - * Reset cursor - * @return NULL - **/ - function reset() { - $this->document=[]; - parent::reset(); - } - - /** - * Hydrate mapper object using hive array variable - * @return NULL - * @param $var array|string - * @param $func callback - **/ - function copyfrom($var,$func=NULL) { - if (is_string($var)) - $var=\Base::instance()->$var; - if ($func) - $var=call_user_func($func,$var); - foreach ($var as $key=>$val) - $this->set($key,$val); - } - - /** - * Populate hive array variable with mapper fields - * @return NULL - * @param $key string - **/ - function copyto($key) { - $var=&\Base::instance()->ref($key); - foreach ($this->document as $key=>$field) - $var[$key]=$field; - } - - /** - * Return field names - * @return array - **/ - function fields() { - return array_keys($this->document); - } - - /** - * Return the cursor from last query - * @return object|NULL - **/ - function cursor() { - return $this->cursor; - } - - /** - * Retrieve external iterator for fields - * @return object - **/ - function getiterator() { - return new \ArrayIterator($this->cast()); - } - - /** - * Instantiate class - * @return void - * @param $db object - * @param $collection string - * @param $fields array - **/ - function __construct(\DB\Mongo $db,$collection,$fields=NULL) { - $this->db=$db; - $this->legacy=$db->legacy(); - $this->collection=$db->selectcollection($collection); - $this->fields=$fields; - $this->reset(); - } - -} diff --git a/app/lib/db/mongo/session.php b/app/lib/db/mongo/session.php deleted file mode 100644 index e0194771..00000000 --- a/app/lib/db/mongo/session.php +++ /dev/null @@ -1,194 +0,0 @@ -. - -*/ - -namespace DB\Mongo; - -//! MongoDB-managed session handler -class Session extends Mapper { - - protected - //! Session ID - $sid, - //! Anti-CSRF token - $_csrf, - //! User agent - $_agent, - //! IP, - $_ip, - //! Suspect callback - $onsuspect; - - /** - * Open session - * @return TRUE - * @param $path string - * @param $name string - **/ - function open($path,$name) { - return TRUE; - } - - /** - * Close session - * @return TRUE - **/ - function close() { - $this->reset(); - $this->sid=NULL; - return TRUE; - } - - /** - * Return session data in serialized format - * @return string - * @param $id string - **/ - function read($id) { - $this->load(['session_id'=>$this->sid=$id]); - if ($this->dry()) - return ''; - if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { - $fw=\Base::instance(); - if (!isset($this->onsuspect) || - $fw->call($this->onsuspect,[$this,$id])===FALSE) { - // NB: `session_destroy` can't be called at that stage; - // `session_start` not completed - $this->destroy($id); - $this->close(); - unset($fw->{'COOKIE.'.session_name()}); - $fw->error(403); - } - } - return $this->get('data'); - } - - /** - * Write session data - * @return TRUE - * @param $id string - * @param $data string - **/ - function write($id,$data) { - $this->set('session_id',$id); - $this->set('data',$data); - $this->set('ip',$this->_ip); - $this->set('agent',$this->_agent); - $this->set('stamp',time()); - $this->save(); - return TRUE; - } - - /** - * Destroy session - * @return TRUE - * @param $id string - **/ - function destroy($id) { - $this->erase(['session_id'=>$id]); - return TRUE; - } - - /** - * Garbage collector - * @return TRUE - * @param $max int - **/ - function cleanup($max) { - $this->erase(['$where'=>'this.stamp+'.$max.'<'.time()]); - return TRUE; - } - - /** - * Return session id (if session has started) - * @return string|NULL - **/ - function sid() { - return $this->sid; - } - - /** - * Return anti-CSRF token - * @return string - **/ - function csrf() { - return $this->_csrf; - } - - /** - * Return IP address - * @return string - **/ - function ip() { - return $this->_ip; - } - - /** - * Return Unix timestamp - * @return string|FALSE - **/ - function stamp() { - if (!$this->sid) - session_start(); - return $this->dry()?FALSE:$this->get('stamp'); - } - - /** - * Return HTTP user agent - * @return string - **/ - function agent() { - return $this->_agent; - } - - /** - * Instantiate class - * @param $db \DB\Mongo - * @param $table string - * @param $onsuspect callback - * @param $key string - **/ - function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL,$key=NULL) { - parent::__construct($db,$table); - $this->onsuspect=$onsuspect; - session_set_save_handler( - [$this,'open'], - [$this,'close'], - [$this,'read'], - [$this,'write'], - [$this,'destroy'], - [$this,'cleanup'] - ); - register_shutdown_function('session_commit'); - $fw=\Base::instance(); - $headers=$fw->HEADERS; - $this->_csrf=$fw->hash($fw->SEED. - extension_loaded('openssl')? - implode(unpack('L',openssl_random_pseudo_bytes(4))): - mt_rand() - ); - if ($key) - $fw->$key=$this->_csrf; - $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; - $this->_ip=$fw->IP; - } - -} diff --git a/app/lib/db/sql.php b/app/lib/db/sql.php deleted file mode 100644 index 923f3d87..00000000 --- a/app/lib/db/sql.php +++ /dev/null @@ -1,523 +0,0 @@ -. - -*/ - -namespace DB; - -//! PDO wrapper -class SQL { - - //@{ Error messages - const - E_PKey='Table %s does not have a primary key'; - //@} - - const - PARAM_FLOAT='float'; - - protected - //! UUID - $uuid, - //! Raw PDO - $pdo, - //! Data source name - $dsn, - //! Database engine - $engine, - //! Database name - $dbname, - //! Transaction flag - $trans=FALSE, - //! Number of rows affected by query - $rows=0, - //! SQL log - $log; - - /** - * Begin SQL transaction - * @return bool - **/ - function begin() { - $out=$this->pdo->begintransaction(); - $this->trans=TRUE; - return $out; - } - - /** - * Rollback SQL transaction - * @return bool - **/ - function rollback() { - $out=$this->pdo->rollback(); - $this->trans=FALSE; - return $out; - } - - /** - * Commit SQL transaction - * @return bool - **/ - function commit() { - $out=$this->pdo->commit(); - $this->trans=FALSE; - return $out; - } - - /** - * Return transaction flag - * @return bool - **/ - function trans() { - return $this->trans; - } - - /** - * Map data type of argument to a PDO constant - * @return int - * @param $val scalar - **/ - function type($val) { - switch (gettype($val)) { - case 'NULL': - return \PDO::PARAM_NULL; - case 'boolean': - return \PDO::PARAM_BOOL; - case 'integer': - return \PDO::PARAM_INT; - case 'resource': - return \PDO::PARAM_LOB; - case 'float': - return self::PARAM_FLOAT; - default: - return \PDO::PARAM_STR; - } - } - - /** - * Cast value to PHP type - * @return mixed - * @param $type string - * @param $val mixed - **/ - function value($type,$val) { - switch ($type) { - case self::PARAM_FLOAT: - if (!is_string($val)) - $val=str_replace(',','.',$val); - return $val; - case \PDO::PARAM_NULL: - return NULL; - case \PDO::PARAM_INT: - return (int)$val; - case \PDO::PARAM_BOOL: - return (bool)$val; - case \PDO::PARAM_STR: - return (string)$val; - case \PDO::PARAM_LOB: - return (binary)$val; - } - } - - /** - * Execute SQL statement(s) - * @return array|int|FALSE - * @param $cmds string|array - * @param $args string|array - * @param $ttl int|array - * @param $log bool - * @param $stamp bool - **/ - function exec($cmds,$args=NULL,$ttl=0,$log=TRUE,$stamp=FALSE) { - $tag=''; - if (is_array($ttl)) - list($ttl,$tag)=$ttl; - $auto=FALSE; - if (is_null($args)) - $args=[]; - elseif (is_scalar($args)) - $args=[1=>$args]; - if (is_array($cmds)) { - if (count($args)<($count=count($cmds))) - // Apply arguments to SQL commands - $args=array_fill(0,$count,$args); - if (!$this->trans) { - $this->begin(); - $auto=TRUE; - } - } - else { - $count=1; - $cmds=[$cmds]; - $args=[$args]; - } - if ($this->log===FALSE) - $log=FALSE; - $fw=\Base::instance(); - $cache=\Cache::instance(); - $result=FALSE; - for ($i=0;$i<$count;$i++) { - $cmd=$cmds[$i]; - $arg=$args[$i]; - // ensure 1-based arguments - if (array_key_exists(0,$arg)) { - array_unshift($arg,''); - unset($arg[0]); - } - if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd)) - continue; - $now=microtime(TRUE); - $keys=$vals=[]; - if ($fw->CACHE && $ttl && ($cached=$cache->exists( - $hash=$fw->hash($this->dsn.$cmd. - $fw->stringify($arg)).($tag?'.'.$tag:'').'.sql',$result)) && - $cached[0]+$ttl>microtime(TRUE)) { - foreach ($arg as $key=>$val) { - $vals[]=$fw->stringify(is_array($val)?$val[0]:$val); - $keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). - '/'; - } - if ($log) - $this->log.=($stamp?(date('r').' '):'').'('. - sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. - '[CACHED] '. - preg_replace($keys,$vals, - str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; - } - elseif (is_object($query=$this->pdo->prepare($cmd))) { - foreach ($arg as $key=>$val) { - if (is_array($val)) { - // User-specified data type - $query->bindvalue($key,$val[0], - $val[1]==self::PARAM_FLOAT?\PDO::PARAM_STR:$val[1]); - $vals[]=$fw->stringify($this->value($val[1],$val[0])); - } - else { - // Convert to PDO data type - $query->bindvalue($key,$val, - ($type=$this->type($val))==self::PARAM_FLOAT? - \PDO::PARAM_STR:$type); - $vals[]=$fw->stringify($this->value($type,$val)); - } - $keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). - '/'; - } - if ($log) - $this->log.=($stamp?(date('r').' '):'').'(-0ms) '. - preg_replace($keys,$vals, - str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; - $query->execute(); - if ($log) - $this->log=str_replace('(-0ms)', - '('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms)', - $this->log); - if (($error=$query->errorinfo()) && $error[0]!=\PDO::ERR_NONE) { - // Statement-level error occurred - if ($this->trans) - $this->rollback(); - user_error('PDOStatement: '.$error[2],E_USER_ERROR); - } - if (preg_match('/(?:^[\s\(]*'. - '(?:WITH|EXPLAIN|SELECT|PRAGMA|SHOW)|RETURNING)\b/is',$cmd) || - (preg_match('/^\s*(?:CALL|EXEC)\b/is',$cmd) && - $query->columnCount())) { - $result=$query->fetchall(\PDO::FETCH_ASSOC); - // Work around SQLite quote bug - if (preg_match('/sqlite2?/',$this->engine)) - foreach ($result as $pos=>$rec) { - unset($result[$pos]); - $result[$pos]=[]; - foreach ($rec as $key=>$val) - $result[$pos][trim($key,'\'"[]`')]=$val; - } - $this->rows=count($result); - if ($fw->CACHE && $ttl) - // Save to cache backend - $cache->set($hash,$result,$ttl); - } - else - $this->rows=$result=$query->rowcount(); - $query->closecursor(); - unset($query); - } - elseif (($error=$this->errorinfo()) && $error[0]!=\PDO::ERR_NONE) { - // PDO-level error occurred - if ($this->trans) - $this->rollback(); - user_error('PDO: '.$error[2],E_USER_ERROR); - } - - } - if ($this->trans && $auto) - $this->commit(); - return $result; - } - - /** - * Return number of rows affected by last query - * @return int - **/ - function count() { - return $this->rows; - } - - /** - * Return SQL profiler results (or disable logging) - * @return string - * @param $flag bool - **/ - function log($flag=TRUE) { - if ($flag) - return $this->log; - $this->log=FALSE; - } - - /** - * Return TRUE if table exists - * @return bool - * @param $table string - **/ - function exists($table) { - $mode=$this->pdo->getAttribute(\PDO::ATTR_ERRMODE); - $this->pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_SILENT); - $out=$this->pdo-> - query('SELECT 1 FROM '.$this->quotekey($table).' LIMIT 1'); - $this->pdo->setAttribute(\PDO::ATTR_ERRMODE,$mode); - return is_object($out); - } - - /** - * Retrieve schema of SQL table - * @return array|FALSE - * @param $table string - * @param $fields array|string - * @param $ttl int|array - **/ - function schema($table,$fields=NULL,$ttl=0) { - $fw=\Base::instance(); - $cache=\Cache::instance(); - if ($fw->CACHE && $ttl && - ($cached=$cache->exists( - $hash=$fw->hash($this->dsn.$table).'.schema',$result)) && - $cached[0]+$ttl>microtime(TRUE)) - return $result; - if (strpos($table,'.')) - list($schema,$table)=explode('.',$table); - // Supported engines - $cmd=[ - 'sqlite2?'=>[ - 'PRAGMA table_info(`'.$table.'`)', - 'name','type','dflt_value','notnull',0,'pk',TRUE], - 'mysql'=>[ - 'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`', - 'Field','Type','Default','Null','YES','Key','PRI'], - 'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>[ - 'SELECT '. - 'C.COLUMN_NAME AS field,'. - 'C.DATA_TYPE AS type,'. - 'C.COLUMN_DEFAULT AS defval,'. - 'C.IS_NULLABLE AS nullable,'. - 'T.CONSTRAINT_TYPE AS pkey '. - 'FROM INFORMATION_SCHEMA.COLUMNS AS C '. - 'LEFT OUTER JOIN '. - 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS K '. - 'ON '. - 'C.TABLE_NAME=K.TABLE_NAME AND '. - 'C.COLUMN_NAME=K.COLUMN_NAME AND '. - 'C.TABLE_SCHEMA=K.TABLE_SCHEMA '. - ($this->dbname? - ('AND C.TABLE_CATALOG=K.TABLE_CATALOG '):''). - 'LEFT OUTER JOIN '. - 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS T ON '. - 'K.TABLE_NAME=T.TABLE_NAME AND '. - 'K.CONSTRAINT_NAME=T.CONSTRAINT_NAME AND '. - 'K.TABLE_SCHEMA=T.TABLE_SCHEMA '. - ($this->dbname? - ('AND K.TABLE_CATALOG=T.TABLE_CATALOG '):''). - 'WHERE '. - 'C.TABLE_NAME='.$this->quote($table). - ($this->dbname? - (' AND C.TABLE_CATALOG='. - $this->quote($this->dbname)):''), - 'field','type','defval','nullable','YES','pkey','PRIMARY KEY'], - 'oci'=>[ - 'SELECT c.column_name AS field, '. - 'c.data_type AS type, '. - 'c.data_default AS defval, '. - 'c.nullable AS nullable, '. - '(SELECT t.constraint_type '. - 'FROM all_cons_columns acc '. - 'LEFT OUTER JOIN all_constraints t '. - 'ON acc.constraint_name=t.constraint_name '. - 'WHERE acc.table_name='.$this->quote($table).' '. - 'AND acc.column_name=c.column_name '. - 'AND constraint_type='.$this->quote('P').') AS pkey '. - 'FROM all_tab_cols c '. - 'WHERE c.table_name='.$this->quote($table), - 'FIELD','TYPE','DEFVAL','NULLABLE','Y','PKEY','P'] - ]; - if (is_string($fields)) - $fields=\Base::instance()->split($fields); - $conv=[ - 'int\b|integer'=>\PDO::PARAM_INT, - 'bool'=>\PDO::PARAM_BOOL, - 'blob|bytea|image|binary'=>\PDO::PARAM_LOB, - 'float|real|double|decimal|numeric'=>self::PARAM_FLOAT, - '.+'=>\PDO::PARAM_STR - ]; - foreach ($cmd as $key=>$val) - if (preg_match('/'.$key.'/',$this->engine)) { - $rows=[]; - foreach ($this->exec($val[0],NULL) as $row) - if (!$fields || in_array($row[$val[1]],$fields)) { - foreach ($conv as $regex=>$type) - if (preg_match('/'.$regex.'/i',$row[$val[2]])) - break; - $rows[$row[$val[1]]]=[ - 'type'=>$row[$val[2]], - 'pdo_type'=>$type, - 'default'=>is_string($row[$val[3]])? - preg_replace('/^\s*([\'"])(.*)\1\s*/','\2', - $row[$val[3]]):$row[$val[3]], - 'nullable'=>$row[$val[4]]==$val[5], - 'pkey'=>$row[$val[6]]==$val[7] - ]; - } - if ($fw->CACHE && $ttl) - // Save to cache backend - $cache->set($hash,$rows,$ttl); - return $rows; - } - user_error(sprintf(self::E_PKey,$table),E_USER_ERROR); - return FALSE; - } - - /** - * Quote string - * @return string - * @param $val mixed - * @param $type int - **/ - function quote($val,$type=\PDO::PARAM_STR) { - return $this->engine=='odbc'? - (is_string($val)? - \Base::instance()->stringify(str_replace('\'','\'\'',$val)): - $val): - $this->pdo->quote($val,$type); - } - - /** - * Return UUID - * @return string - **/ - function uuid() { - return $this->uuid; - } - - /** - * Return parent object - * @return \PDO - **/ - function pdo() { - return $this->pdo; - } - - /** - * Return database engine - * @return string - **/ - function driver() { - return $this->engine; - } - - /** - * Return server version - * @return string - **/ - function version() { - return $this->pdo->getattribute(\PDO::ATTR_SERVER_VERSION); - } - - /** - * Return database name - * @return string - **/ - function name() { - return $this->dbname; - } - - /** - * Return quoted identifier name - * @return string - * @param $key - * @param bool $split - **/ - function quotekey($key, $split=TRUE) { - $delims=[ - 'sqlite2?|mysql'=>'``', - 'pgsql|oci'=>'""', - 'mssql|sqlsrv|odbc|sybase|dblib'=>'[]' - ]; - $use=''; - foreach ($delims as $engine=>$delim) - if (preg_match('/'.$engine.'/',$this->engine)) { - $use=$delim; - break; - } - return $use[0].($split ? implode($use[1].'.'.$use[0],explode('.',$key)) - : $key).$use[1]; - } - - /** - * Redirect call to PDO object - * @return mixed - * @param $func string - * @param $args array - **/ - function __call($func,array $args) { - return call_user_func_array([$this->pdo,$func],$args); - } - - //! Prohibit cloning - private function __clone() { - } - - /** - * Instantiate class - * @param $dsn string - * @param $user string - * @param $pw string - * @param $options array - **/ - function __construct($dsn,$user=NULL,$pw=NULL,array $options=NULL) { - $fw=\Base::instance(); - $this->uuid=$fw->hash($this->dsn=$dsn); - if (preg_match('/^.+?(?:dbname|database)=(.+?)(?=;|$)/is',$dsn,$parts)) - $this->dbname=$parts[1]; - if (!$options) - $options=[]; - if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql') - $options+=[\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '. - strtolower(str_replace('-','',$fw->ENCODING)).';']; - $this->pdo=new \PDO($dsn,$user,$pw,$options); - $this->engine=$this->pdo->getattribute(\PDO::ATTR_DRIVER_NAME); - } - -} diff --git a/app/lib/db/sql/mapper.php b/app/lib/db/sql/mapper.php deleted file mode 100644 index ed7ef533..00000000 --- a/app/lib/db/sql/mapper.php +++ /dev/null @@ -1,700 +0,0 @@ -. - -*/ - -namespace DB\SQL; - -//! SQL data mapper -class Mapper extends \DB\Cursor { - - protected - //! PDO wrapper - $db, - //! Database engine - $engine, - //! SQL table - $source, - //! SQL table (quoted) - $table, - //! Alias for SQL table - $as, - //! Last insert ID - $_id, - //! Defined fields - $fields, - //! Adhoc fields - $adhoc=[], - //! Dynamic properties - $props=[]; - - /** - * Return database type - * @return string - **/ - function dbtype() { - return 'SQL'; - } - - /** - * Return mapped table - * @return string - **/ - function table() { - return $this->source; - } - - /** - * Return TRUE if any/specified field value has changed - * @return bool - * @param $key string - **/ - function changed($key=NULL) { - if (isset($key)) - return $this->fields[$key]['changed']; - foreach($this->fields as $key=>$field) - if ($field['changed']) - return TRUE; - return FALSE; - } - - /** - * Return TRUE if field is defined - * @return bool - * @param $key string - **/ - function exists($key) { - return array_key_exists($key,$this->fields+$this->adhoc); - } - - /** - * Assign value to field - * @return scalar - * @param $key string - * @param $val scalar - **/ - function set($key,$val) { - if (array_key_exists($key,$this->fields)) { - $val=is_null($val) && $this->fields[$key]['nullable']? - NULL:$this->db->value($this->fields[$key]['pdo_type'],$val); - if ($this->fields[$key]['initial']!==$val || - $this->fields[$key]['default']!==$val && is_null($val)) - $this->fields[$key]['changed']=TRUE; - return $this->fields[$key]['value']=$val; - } - // Adjust result on existing expressions - if (isset($this->adhoc[$key])) - $this->adhoc[$key]['value']=$val; - elseif (is_string($val)) - // Parenthesize expression in case it's a subquery - $this->adhoc[$key]=['expr'=>'('.$val.')','value'=>NULL]; - else - $this->props[$key]=$val; - return $val; - } - - /** - * Retrieve value of field - * @return scalar - * @param $key string - **/ - function &get($key) { - if ($key=='_id') - return $this->_id; - elseif (array_key_exists($key,$this->fields)) - return $this->fields[$key]['value']; - elseif (array_key_exists($key,$this->adhoc)) - return $this->adhoc[$key]['value']; - elseif (array_key_exists($key,$this->props)) - return $this->props[$key]; - user_error(sprintf(self::E_Field,$key),E_USER_ERROR); - } - - /** - * Clear value of field - * @return NULL - * @param $key string - **/ - function clear($key) { - if (array_key_exists($key,$this->adhoc)) - unset($this->adhoc[$key]); - else - unset($this->props[$key]); - } - - /** - * Invoke dynamic method - * @return mixed - * @param $func string - * @param $args array - **/ - function __call($func,$args) { - return call_user_func_array( - (array_key_exists($func,$this->props)? - $this->props[$func]: - $this->$func),$args - ); - } - - /** - * Convert array to mapper object - * @return static - * @param $row array - **/ - function factory($row) { - $mapper=clone($this); - $mapper->reset(); - foreach ($row as $key=>$val) { - if (array_key_exists($key,$this->fields)) - $var='fields'; - elseif (array_key_exists($key,$this->adhoc)) - $var='adhoc'; - else - continue; - $mapper->{$var}[$key]['value']=$val; - $mapper->{$var}[$key]['initial']=$val; - if ($var=='fields' && $mapper->{$var}[$key]['pkey']) - $mapper->{$var}[$key]['previous']=$val; - } - $mapper->query=[clone($mapper)]; - if (isset($mapper->trigger['load'])) - \Base::instance()->call($mapper->trigger['load'],$mapper); - return $mapper; - } - - /** - * Return fields of mapper object as an associative array - * @return array - * @param $obj object - **/ - function cast($obj=NULL) { - if (!$obj) - $obj=$this; - return array_map( - function($row) { - return $row['value']; - }, - $obj->fields+$obj->adhoc - ); - } - - /** - * Build query string and arguments - * @return array - * @param $fields string - * @param $filter string|array - * @param $options array - **/ - function stringify($fields,$filter=NULL,array $options=NULL) { - if (!$options) - $options=[]; - $options+=[ - 'group'=>NULL, - 'order'=>NULL, - 'limit'=>0, - 'offset'=>0, - 'comment'=>NULL - ]; - $db=$this->db; - $sql='SELECT '.$fields.' FROM '.$this->table; - if (isset($this->as)) - $sql.=' AS '.$this->db->quotekey($this->as); - $args=[]; - if (is_array($filter)) { - $args=isset($filter[1]) && is_array($filter[1])? - $filter[1]: - array_slice($filter,1,NULL,TRUE); - $args=is_array($args)?$args:[1=>$args]; - list($filter)=$filter; - } - if ($filter) - $sql.=' WHERE '.$filter; - if ($options['group']) { - $sql.=' GROUP BY '.implode(',',array_map( - function($str) use($db) { - return preg_replace_callback( - '/\b(\w+[._\-\w]*)\h*(HAVING.+|$)/i', - function($parts) use($db) { - return $db->quotekey($parts[1]). - (isset($parts[2])?(' '.$parts[2]):''); - }, - $str - ); - }, - explode(',',$options['group']))); - } - if ($options['order']) { - $char=substr($db->quotekey(''),0,1);// quoting char - $order=' ORDER BY '.(is_bool(strpos($options['order'],$char))? - implode(',',array_map(function($str) use($db) { - return preg_match('/^\h*(\w+[._\-\w]*)'. - '(?:\h+((?:ASC|DESC)[\w\h]*))?\h*$/i', - $str,$parts)? - ($db->quotekey($parts[1]). - (isset($parts[2])?(' '.$parts[2]):'')):$str; - },explode(',',$options['order']))): - $options['order']); - } - // SQL Server fixes - if (preg_match('/mssql|sqlsrv|odbc/', $this->engine) && - ($options['limit'] || $options['offset'])) { - // order by pkey when no ordering option was given - if (!$options['order']) - foreach ($this->fields as $key=>$field) - if ($field['pkey']) { - $order=' ORDER BY '.$db->quotekey($key); - break; - } - $ofs=$options['offset']?(int)$options['offset']:0; - $lmt=$options['limit']?(int)$options['limit']:0; - if (strncmp($db->version(),'11',2)>=0) { - // SQL Server >= 2012 - $sql.=$order.' OFFSET '.$ofs.' ROWS'; - if ($lmt) - $sql.=' FETCH NEXT '.$lmt.' ROWS ONLY'; - } - else { - // SQL Server 2008 - $sql=preg_replace('/SELECT/', - 'SELECT '. - ($lmt>0?'TOP '.($ofs+$lmt):'').' ROW_NUMBER() '. - 'OVER ('.$order.') AS rnum,',$sql.$order,1); - $sql='SELECT * FROM ('.$sql.') x WHERE rnum > '.($ofs); - } - } - else { - if (isset($order)) - $sql.=$order; - if ($options['limit']) - $sql.=' LIMIT '.(int)$options['limit']; - if ($options['offset']) - $sql.=' OFFSET '.(int)$options['offset']; - } - if ($options['comment']) - $sql.="\n".' /* '.$options['comment'].' */'; - return [$sql,$args]; - } - - /** - * Build query string and execute - * @return static[] - * @param $fields string - * @param $filter string|array - * @param $options array - * @param $ttl int|array - **/ - function select($fields,$filter=NULL,array $options=NULL,$ttl=0) { - list($sql,$args)=$this->stringify($fields,$filter,$options); - $result=$this->db->exec($sql,$args,$ttl); - $out=[]; - foreach ($result as &$row) { - foreach ($row as $field=>&$val) { - if (array_key_exists($field,$this->fields)) { - if (!is_null($val) || !$this->fields[$field]['nullable']) - $val=$this->db->value( - $this->fields[$field]['pdo_type'],$val); - } - unset($val); - } - $out[]=$this->factory($row); - unset($row); - } - return $out; - } - - /** - * Return records that match criteria - * @return static[] - * @param $filter string|array - * @param $options array - * @param $ttl int|array - **/ - function find($filter=NULL,array $options=NULL,$ttl=0) { - if (!$options) - $options=[]; - $options+=[ - 'group'=>NULL, - 'order'=>NULL, - 'limit'=>0, - 'offset'=>0 - ]; - $adhoc=''; - foreach ($this->adhoc as $key=>$field) - $adhoc.=','.$field['expr'].' AS '.$this->db->quotekey($key); - return $this->select( - ($options['group'] && !preg_match('/mysql|sqlite/',$this->engine)? - $options['group']: - implode(',',array_map([$this->db,'quotekey'], - array_keys($this->fields)))).$adhoc,$filter,$options,$ttl); - } - - /** - * Count records that match criteria - * @return int - * @param $filter string|array - * @param $options array - * @param $ttl int|array - **/ - function count($filter=NULL,array $options=NULL,$ttl=0) { - if (!($subquery_mode=($options && !empty($options['group'])))) - $this->adhoc['_rows']=['expr'=>'COUNT(*)','value'=>NULL]; - $adhoc=[]; - foreach ($this->adhoc as $key=>$field) - // Add all adhoc fields - // (make them available for grouping, sorting, having) - $adhoc[]=$field['expr'].' AS '.$this->db->quotekey($key); - $fields=implode(',',$adhoc); - if ($subquery_mode) { - if (empty($fields)) - // Select at least one field, ideally the grouping fields - // or sqlsrv fails - $fields=preg_replace('/HAVING.+$/i','',$options['group']); - if (preg_match('/mssql|dblib|sqlsrv/',$this->engine)) - $fields='TOP 100 PERCENT '.$fields; - } - list($sql,$args)=$this->stringify($fields,$filter,$options); - if ($subquery_mode) - $sql='SELECT COUNT(*) AS '.$this->db->quotekey('_rows').' '. - 'FROM ('.$sql.') AS '.$this->db->quotekey('_temp'); - $result=$this->db->exec($sql,$args,$ttl); - unset($this->adhoc['_rows']); - return (int)$result[0]['_rows']; - } - /** - * Return record at specified offset using same criteria as - * previous load() call and make it active - * @return static - * @param $ofs int - **/ - function skip($ofs=1) { - $out=parent::skip($ofs); - $dry=$this->dry(); - foreach ($this->fields as $key=>&$field) { - $field['value']=$dry?NULL:$out->fields[$key]['value']; - $field['initial']=$field['value']; - $field['changed']=FALSE; - if ($field['pkey']) - $field['previous']=$dry?NULL:$out->fields[$key]['value']; - unset($field); - } - foreach ($this->adhoc as $key=>&$field) { - $field['value']=$dry?NULL:$out->adhoc[$key]['value']; - unset($field); - } - if (!$dry && isset($this->trigger['load'])) - \Base::instance()->call($this->trigger['load'],$this); - return $out; - } - - /** - * Insert new record - * @return static - **/ - function insert() { - $args=[]; - $actr=0; - $nctr=0; - $fields=''; - $values=''; - $filter=''; - $pkeys=[]; - $nkeys=[]; - $ckeys=[]; - $inc=NULL; - foreach ($this->fields as $key=>$field) - if ($field['pkey']) - $pkeys[$key]=$field['previous']; - if (isset($this->trigger['beforeinsert']) && - \Base::instance()->call($this->trigger['beforeinsert'], - [$this,$pkeys])===FALSE) - return $this; - foreach ($this->fields as $key=>&$field) { - if ($field['pkey']) { - $field['previous']=$field['value']; - if (!$inc && $field['pdo_type']==\PDO::PARAM_INT && - empty($field['value']) && !$field['nullable']) - $inc=$key; - $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; - $nkeys[$nctr+1]=[$field['value'],$field['pdo_type']]; - $nctr++; - } - if ($field['changed'] && $key!=$inc) { - $fields.=($actr?',':'').$this->db->quotekey($key); - $values.=($actr?',':'').'?'; - $args[$actr+1]=[$field['value'],$field['pdo_type']]; - $actr++; - $ckeys[]=$key; - } - } - if ($fields) { - $add=$aik=''; - if ($this->engine=='pgsql' && !empty($pkeys)) { - $names=array_keys($pkeys); - $aik=end($names); - $add=' RETURNING '.$this->db->quotekey($aik); - } - $lID=$this->db->exec( - (preg_match('/mssql|dblib|sqlsrv/',$this->engine) && - array_intersect(array_keys($pkeys),$ckeys)? - 'SET IDENTITY_INSERT '.$this->table.' ON;':''). - 'INSERT INTO '.$this->table.' ('.$fields.') '. - 'VALUES ('.$values.')'.$add,$args - ); - if ($this->engine=='pgsql' && $lID && $aik) - $this->_id=$lID[0][$aik]; - elseif ($this->engine!='oci') - $this->_id=$this->db->lastinsertid(); - // Reload to obtain default and auto-increment field values - if ($reload=(($inc && $this->_id) || $filter)) - $this->load($inc? - [$inc.'=?',$this->db->value( - $this->fields[$inc]['pdo_type'],$this->_id)]: - [$filter,$nkeys]); - if (isset($this->trigger['afterinsert'])) - \Base::instance()->call($this->trigger['afterinsert'], - [$this,$pkeys]); - // reset changed flag after calling afterinsert - if (!$reload) - foreach ($this->fields as $key=>&$field) { - $field['changed']=FALSE; - $field['initial']=$field['value']; - unset($field); - } - } - return $this; - } - - /** - * Update current record - * @return static - **/ - function update() { - $args=[]; - $ctr=0; - $pairs=''; - $filter=''; - $pkeys=[]; - foreach ($this->fields as $key=>$field) - if ($field['pkey']) - $pkeys[$key]=$field['previous']; - if (isset($this->trigger['beforeupdate']) && - \Base::instance()->call($this->trigger['beforeupdate'], - [$this,$pkeys])===FALSE) - return $this; - foreach ($this->fields as $key=>$field) - if ($field['changed']) { - $pairs.=($pairs?',':'').$this->db->quotekey($key).'=?'; - $args[++$ctr]=[$field['value'],$field['pdo_type']]; - } - foreach ($this->fields as $key=>$field) - if ($field['pkey']) { - $filter.=($filter?' AND ':' WHERE '). - $this->db->quotekey($key).'=?'; - $args[++$ctr]=[$field['previous'],$field['pdo_type']]; - } - if ($pairs) { - $sql='UPDATE '.$this->table.' SET '.$pairs.$filter; - $this->db->exec($sql,$args); - } - if (isset($this->trigger['afterupdate'])) - \Base::instance()->call($this->trigger['afterupdate'], - [$this,$pkeys]); - // reset changed flag after calling afterupdate - foreach ($this->fields as $key=>&$field) { - $field['changed']=FALSE; - $field['initial']=$field['value']; - unset($field); - } - return $this; - } - - /** - * Delete current record - * @return int - * @param $quick bool - * @param $filter string|array - **/ - function erase($filter=NULL,$quick=TRUE) { - if (isset($filter)) { - if (!$quick) { - $out=0; - foreach ($this->find($filter) as $mapper) - $out+=$mapper->erase(); - return $out; - } - $args=[]; - if (is_array($filter)) { - $args=isset($filter[1]) && is_array($filter[1])? - $filter[1]: - array_slice($filter,1,NULL,TRUE); - $args=is_array($args)?$args:[1=>$args]; - list($filter)=$filter; - } - return $this->db-> - exec('DELETE FROM '.$this->table. - ($filter?' WHERE '.$filter:'').';',$args); - } - $args=[]; - $ctr=0; - $filter=''; - $pkeys=[]; - foreach ($this->fields as $key=>&$field) { - if ($field['pkey']) { - $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; - $args[$ctr+1]=[$field['previous'],$field['pdo_type']]; - $pkeys[$key]=$field['previous']; - $ctr++; - } - $field['value']=NULL; - $field['changed']=(bool)$field['default']; - if ($field['pkey']) - $field['previous']=NULL; - unset($field); - } - foreach ($this->adhoc as &$field) { - $field['value']=NULL; - unset($field); - } - parent::erase(); - if (isset($this->trigger['beforeerase']) && - \Base::instance()->call($this->trigger['beforeerase'], - [$this,$pkeys])===FALSE) - return 0; - $out=$this->db-> - exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args); - if (isset($this->trigger['aftererase'])) - \Base::instance()->call($this->trigger['aftererase'], - [$this,$pkeys]); - return $out; - } - - /** - * Reset cursor - * @return NULL - **/ - function reset() { - foreach ($this->fields as &$field) { - $field['value']=NULL; - $field['initial']=NULL; - $field['changed']=FALSE; - if ($field['pkey']) - $field['previous']=NULL; - unset($field); - } - foreach ($this->adhoc as &$field) { - $field['value']=NULL; - unset($field); - } - parent::reset(); - } - - /** - * Hydrate mapper object using hive array variable - * @return NULL - * @param $var array|string - * @param $func callback - **/ - function copyfrom($var,$func=NULL) { - if (is_string($var)) - $var=\Base::instance()->$var; - if ($func) - $var=call_user_func($func,$var); - foreach ($var as $key=>$val) - if (in_array($key,array_keys($this->fields))) - $this->set($key,$val); - } - - /** - * Populate hive array variable with mapper fields - * @return NULL - * @param $key string - **/ - function copyto($key) { - $var=&\Base::instance()->ref($key); - foreach ($this->fields+$this->adhoc as $key=>$field) - $var[$key]=$field['value']; - } - - /** - * Return schema and, if the first argument is provided, update it - * @return array - * @param $fields NULL|array - **/ - function schema($fields=null) { - if ($fields) - $this->fields = $fields; - return $this->fields; - } - - /** - * Return field names - * @return array - * @param $adhoc bool - **/ - function fields($adhoc=TRUE) { - return array_keys($this->fields+($adhoc?$this->adhoc:[])); - } - - /** - * Return TRUE if field is not nullable - * @return bool - * @param $field string - **/ - function required($field) { - return isset($this->fields[$field]) && - !$this->fields[$field]['nullable']; - } - - /** - * Retrieve external iterator for fields - * @return object - **/ - function getiterator() { - return new \ArrayIterator($this->cast()); - } - - /** - * Assign alias for table - * @param $alias string - **/ - function alias($alias) { - $this->as=$alias; - return $this; - } - - /** - * Instantiate class - * @param $db \DB\SQL - * @param $table string - * @param $fields array|string - * @param $ttl int|array - **/ - function __construct(\DB\SQL $db,$table,$fields=NULL,$ttl=60) { - $this->db=$db; - $this->engine=$db->driver(); - if ($this->engine=='oci') - $table=strtoupper($table); - $this->source=$table; - $this->table=$this->db->quotekey($table); - $this->fields=$db->schema($table,$fields,$ttl); - $this->reset(); - } - -} diff --git a/app/lib/db/sql/schema.php b/app/lib/db/sql/schema.php deleted file mode 100644 index 015fccbd..00000000 --- a/app/lib/db/sql/schema.php +++ /dev/null @@ -1,1320 +0,0 @@ - - * https://github.com/ikkez/F3-Sugar/ - * - * @package DB - * @version 2.2.3 - * @date 15.05.2018 - **/ - - -namespace DB\SQL; - -class Schema { - - use DB_Utils; - - public - $dataTypes = array( - 'BOOLEAN' => array('mysql' => 'tinyint(1)', - 'sqlite2?|pgsql' => 'BOOLEAN', - 'mssql|sybase|dblib|odbc|sqlsrv' => 'bit', - 'ibm' => 'numeric(1,0)', - ), - 'INT1' => array('mysql' => 'tinyint(4)', - 'sqlite2?' => 'integer(4)', - 'mssql|sybase|dblib|odbc|sqlsrv' => 'tinyint', - 'pgsql|ibm' => 'smallint', - ), - 'INT2' => array('mysql' => 'smallint(6)', - 'sqlite2?' => 'integer(6)', - 'pgsql|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'smallint', - ), - 'INT4' => array('sqlite2?' => 'integer(11)', - 'pgsql|imb' => 'integer', - 'mysql' => 'int(11)', - 'mssql|dblib|sybase|odbc|sqlsrv' => 'int', - ), - 'INT8' => array('sqlite2?' => 'integer(20)', - 'pgsql|mssql|sybase|dblib|odbc|sqlsrv|imb' => 'bigint', - 'mysql' => 'bigint(20)', - ), - 'FLOAT' => array('mysql|sqlite2?' => 'FLOAT', - 'pgsql' => 'double precision', - 'mssql|sybase|dblib|odbc|sqlsrv' => 'float', - 'imb' => 'decfloat' - ), - 'DOUBLE' => array('mysql|ibm' => 'decimal(18,6)', - 'sqlite2?' => 'decimal(15,6)', // max 15-digit on sqlite - 'pgsql' => 'numeric(18,6)', - 'mssql|dblib|sybase|odbc|sqlsrv' => 'decimal(18,6)', - ), - 'VARCHAR128' => array('mysql|sqlite2?|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'varchar(128)', - 'pgsql' => 'character varying(128)', - ), - 'VARCHAR256' => array('mysql|sqlite2?|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'varchar(255)', - 'pgsql' => 'character varying(255)', - ), - 'VARCHAR512' => array('mysql|sqlite2?|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'varchar(512)', - 'pgsql' => 'character varying(512)', - ), - 'TEXT' => array('mysql|sqlite2?|pgsql|mssql' => 'text', - 'sybase|dblib|odbc|sqlsrv' => 'nvarchar(max)', - 'ibm' => 'BLOB SUB_TYPE TEXT', - ), - 'LONGTEXT' => array('mysql' => 'LONGTEXT', - 'sqlite2?|pgsql|mssql' => 'text', - 'sybase|dblib|odbc|sqlsrv' => 'nvarchar(max)', - 'ibm' => 'CLOB(2000000000)', - ), - 'DATE' => array('mysql|sqlite2?|pgsql|mssql|sybase|dblib|odbc|sqlsrv|ibm' => 'date', - ), - 'DATETIME' => array('pgsql' => 'timestamp without time zone', - 'mysql|sqlite2?|mssql|sybase|dblib|odbc|sqlsrv' => 'datetime', - 'ibm' => 'timestamp', - ), - 'TIMESTAMP' => array('mysql|ibm' => 'timestamp', - 'pgsql|odbc' => 'timestamp without time zone', - 'sqlite2?|mssql|sybase|dblib|sqlsrv'=>'DATETIME', - ), - 'BLOB' => array('mysql|odbc|sqlite2?|ibm' => 'blob', - 'pgsql' => 'bytea', - 'mssql|sybase|dblib' => 'image', - 'sqlsrv' => 'varbinary(max)', - ), - ), - $defaultTypes = array( - 'CUR_STAMP' => array('mysql' => 'CURRENT_TIMESTAMP', - 'mssql|sybase|dblib|odbc|sqlsrv' => 'getdate()', - 'pgsql' => 'LOCALTIMESTAMP(0)', - 'sqlite2?' => "(datetime('now','localtime'))", - ), - ); - - public - $name; - - public static - $strict = FALSE; - - const - // DataTypes and Aliases - DT_BOOL = 'BOOLEAN', - DT_BOOLEAN = 'BOOLEAN', - DT_INT1 = 'INT1', - DT_TINYINT = 'INT1', - DT_INT2 = 'INT2', - DT_SMALLINT = 'INT2', - DT_INT4 = 'INT4', - DT_INT = 'INT4', - DT_INT8 = 'INT8', - DT_BIGINT = 'INT8', - DT_FLOAT = 'FLOAT', - DT_DOUBLE = 'DOUBLE', - DT_DECIMAL = 'DOUBLE', - DT_VARCHAR128 = 'VARCHAR128', - DT_VARCHAR256 = 'VARCHAR256', - DT_VARCHAR512 = 'VARCHAR512', - DT_TEXT = 'TEXT', - DT_LONGTEXT = 'LONGTEXT', - DT_DATE = 'DATE', - DT_DATETIME = 'DATETIME', - DT_TIMESTAMP = 'TIMESTAMP', - DT_BLOB = 'BLOB', - DT_BINARY = 'BLOB', - - // column default values - DF_CURRENT_TIMESTAMP = 'CUR_STAMP'; - - - public function __construct(\DB\SQL $db) - { - $this->db = $db; - } - - /** - * get a list of all databases - * @return array|bool - */ - public function getDatabases() - { - $cmd = array( - 'mysql' => 'SHOW DATABASES', - 'pgsql' => 'SELECT datname FROM pg_catalog.pg_database', - 'mssql|sybase|dblib|sqlsrv|odbc' => 'EXEC SP_HELPDB', - ); - $query = $this->findQuery($cmd); - if (!$query) return false; - $result = $this->db->exec($query); - if (!is_array($result)) return false; - foreach($result as &$db) - if (is_array($db)) $db = array_shift($db); - return $result; - } - - /** - * get all tables of current DB - * @return bool|array list of tables, or false - */ - public function getTables() - { - $cmd = array( - 'mysql' => array( - "show tables"), - 'sqlite2?' => array( - "SELECT name FROM sqlite_master WHERE type='table' AND name!='sqlite_sequence'"), - 'pgsql|sybase|dblib' => array( - "select table_name from information_schema.tables where table_schema = 'public'"), - 'mssql|sqlsrv|odbc' => array( - "select table_name from information_schema.tables"), - 'ibm' => array("select TABLE_NAME from sysibm.tables"), - ); - $query = $this->findQuery($cmd); - if (!$query[0]) return false; - $tables = $this->db->exec($query[0]); - if ($tables && is_array($tables) && count($tables) > 0) - foreach ($tables as &$table) - $table = array_shift($table); - return $tables; - } - - /** - * returns a table object for creation - * @param $name - * @return bool|TableCreator - */ - public function createTable($name) - { - return new TableCreator($name,$this); - } - - /** - * returns a table object for altering operations - * @param $name - * @return bool|TableModifier - */ - public function alterTable($name) - { - return new TableModifier($name,$this); - } - - /** - * rename a table - * @param string $name - * @param string $new_name - * @param bool $exec - * @return bool - */ - public function renameTable($name, $new_name, $exec = true) - { - $name = $this->db->quotekey($name); - $new_name = $this->db->quotekey($new_name); - if (preg_match('/odbc/', $this->db->driver())) { - $queries = array(); - $queries[] = "SELECT * INTO $new_name FROM $name;"; - $queries[] = $this->dropTable($name, false); - return ($exec) ? $this->db->exec($queries) : implode("\n",$queries); - } else { - $cmd = array( - 'sqlite2?|pgsql' => - "ALTER TABLE $name RENAME TO $new_name;", - 'mysql|ibm' => - "RENAME TABLE $name TO $new_name;", - 'mssql|sqlsrv|sybase|dblib|odbc' => - "sp_rename {$name}, $new_name" - ); - $query = $this->findQuery($cmd); - if (!$exec) return $query; - return (preg_match('/mssql|sybase|dblib|sqlsrv/', $this->db->driver())) - ? @$this->db->exec($query) : $this->db->exec($query); - } - } - - /** - * drop a table - * @param \DB\SQL\TableBuilder|string $name - * @param bool $exec - * @return bool - */ - public function dropTable($name, $exec = true) - { - if (is_object($name) && $name instanceof TableBuilder) - $name = $name->name; - $cmd = array( - 'mysql|ibm|sqlite2?|pgsql|sybase|dblib' => - 'DROP TABLE IF EXISTS '.$this->db->quotekey($name).';', - 'mssql|sqlsrv|odbc' => - "IF OBJECT_ID('[$name]', 'U') IS NOT NULL DROP TABLE [$name];" - ); - $query = $this->findQuery($cmd); - return ($exec) ? $this->db->exec($query) : $query; - } - - /** - * clear a table - * @param $name - * @param bool $exec - * @return array|bool|FALSE|int|string - */ - public function truncateTable($name, $exec = true) { - if (is_object($name) && $name instanceof TableBuilder) - $name = $name->name; - $cmd = array( - 'mysql|ibm|pgsql|sybase|dblib|mssql|sqlsrv|odbc' => - 'TRUNCATE TABLE '.$this->db->quotekey($name).';', - 'sqlite2?' => array( - 'DELETE FROM '.$this->db->quotekey($name).';', -// 'UPDATE SQLITE_SEQUENCE SET seq = 0 WHERE name = '.$this->db->quotekey($name).';', - ), - ); - $query = $this->findQuery($cmd); - return ($exec) ? $this->db->exec($query) : $query; - } - - /** - * check if a data type is compatible with a given column definition - * @param string $colType (i.e: BOOLEAN) - * @param string $colDef (i.e: tinyint(1)) - * @return int - */ - public function isCompatible($colType,$colDef) { - $raw_type=$this->findQuery($this->dataTypes[strtoupper($colType)]); - preg_match_all('/(?P\w+)($|\((?P(\d+|(.*)))\))/', $raw_type, $match); - return (bool) preg_match_all('/'.preg_quote($match['type'][0]).'($|\('. - preg_quote($match['length'][0]).'\))/i',$colDef); - } -} - -abstract class TableBuilder { - - use DB_Utils; - - protected $columns, $pkeys, $queries, $increments, $rebuild_cmd, $suppress; - public $name; - /** @var Schema */ - public $schema; - - const - TEXT_NoDefaultForTEXT = "Column `%s` of type TEXT can't have a default value.", - TEXT_ColumnExists = "Cannot add the column `%s`. It already exists."; - - /** - * @param string $name - * @param Schema $schema - */ - public function __construct($name, Schema $schema) - { - $this->name = $name; - $this->schema = $schema; - $this->columns = array(); - $this->queries = array(); - $this->pkeys = array('id'); - $this->increments = 'id'; - $this->db = $schema->db; - } - - /** - * generate SQL query and execute it if $exec is true - * @param bool $exec - */ - abstract public function build($exec = TRUE); - - /** - * add a new column to this table - * @param string|Column $key column name or object - * @param null|array $args optional config array - * @return \DB\SQL\Column - */ - public function addColumn($key,$args = null) - { - if ($key instanceof Column) { - $args = $key->getColumnArray(); - $key = $key->name; - } - if (array_key_exists($key,$this->columns)) - trigger_error(sprintf(self::TEXT_ColumnExists,$key),E_USER_ERROR); - $column = new Column($key, $this); - if ($args) - foreach ($args as $arg => $val) - $column->{$arg} = $val; - // skip default pkey field - if (count($this->pkeys) == 1 && in_array($key,$this->pkeys)) - return $column; - return $this->columns[$key] =& $column; - } - - /** - * create index on one or more columns - * @param string|array $index_cols Column(s) to be indexed - * @param $search_cols - * @param bool $unique Unique index - * @param int $length index length for text fields in mysql - */ - protected function _addIndex($index_cols, $search_cols, $unique, $length) - { - if (!is_array($index_cols)) - $index_cols = array($index_cols); - $quotedCols = array_map(array($this->db, 'quotekey'), $index_cols); - if (preg_match('/mysql/', $this->db->driver())) - foreach($quotedCols as $i=>&$col) - if(strtoupper($search_cols[$index_cols[$i]]['type']) == 'TEXT') - $col.='('.$length.')'; - $cols = implode(',', $quotedCols); - $name = $this->assembleIndexKey($index_cols,$this->name); - $name = $this->db->quotekey($name); - $table = $this->db->quotekey($this->name); - $index = $unique ? 'UNIQUE INDEX' : 'INDEX'; - $cmd = array( - 'pgsql|sqlite2?|ibm|mssql|sybase|dblib|odbc|sqlsrv' => - "CREATE $index $name ON $table ($cols);", - 'mysql' => //ALTER TABLE is used because of MySQL bug #48875 - "ALTER TABLE $table ADD $index $name ($cols);", - ); - $query = $this->findQuery($cmd); - $this->queries[] = $query; - } - - /** - * create index name from one or more given column names, max. 64 char lengths - * @param string|array $index_cols - * @return string - */ - protected function assembleIndexKey($index_cols,$table_name) { - if (!is_array($index_cols)) - $index_cols = array($index_cols); - $name = $table_name.'___'.implode('__', $index_cols); - if (strlen($name)>64) - $name=$table_name.'___'.\Base::instance()->hash(implode('__', $index_cols)); - if (strlen($name)>64) - $name='___'.\Base::instance()->hash($table_name.'___'.implode('__', $index_cols)); - return $name; - } - - /** - * set primary / composite key to table - * @param string|array $pkeys - * @return bool - */ - public function primary($pkeys) { - if (empty($pkeys)) - return false; - if (!is_array($pkeys)) - $pkeys = array($pkeys); - // single pkey - $this->increments = $pkeys[0]; - $this->pkeys = $pkeys; - // drop duplicate pkey definition - if (array_key_exists($this->increments,$this->columns)) - unset($this->columns[$this->increments]); - // set flag on new fields - foreach ($pkeys as $name) - if(array_key_exists($name,$this->columns)) - $this->columns[$name]->pkey = true; - // composite key - if (count($pkeys) > 1) { - $pkeys_quoted = array_map(array($this->db,'quotekey'), $pkeys); - $pk_string = implode(', ', $pkeys_quoted); - if (preg_match('/sqlite2?/', $this->db->driver())) { - // rebuild table with new primary keys - $this->rebuild_cmd['pkeys'] = $pkeys; - return; - } else { - $table = $this->db->quotekey($this->name); - $table_key = $this->db->quotekey($this->name.'_pkey'); - $cmd = array( - 'odbc' => - "CREATE INDEX $table_key ON $table ( $pk_string );", - 'mysql' => - "ALTER TABLE $table DROP PRIMARY KEY, ADD PRIMARY KEY ( $pk_string );", - 'mssql|sybase|dblib|sqlsrv' => array( - "ALTER TABLE $table DROP CONSTRAINT PK_".$this->name."_ID;", - "ALTER TABLE $table ADD CONSTRAINT $table_key PRIMARY KEY ( $pk_string );", - ), - 'pgsql' => array( - "ALTER TABLE $table DROP CONSTRAINT $table_key;", - "ALTER TABLE $table ADD CONSTRAINT $table_key PRIMARY KEY ( $pk_string );", - ), - ); - $query = $this->findQuery($cmd); - if (!is_array($query)) - $query = array($query); - foreach ($query as $q) - $this->queries[] = $q; - } - } - } - -} - -class TableCreator extends TableBuilder { - - const - TEXT_TableAlreadyExists = "Table `%s` already exists. Cannot create it."; - - protected $charset='utf8'; - - public function setCharset($str) { - $this->charset=$str; - } - - /** - * generate SQL query for creating a basic table, containing an ID serial field - * and execute it if $exec is true, otherwise just return the generated query string - * @param bool $exec - * @return bool|TableModifier|string - */ - public function build($exec = TRUE) - { - // check if already existing - if ($exec && in_array($this->name, $this->schema->getTables())) { - trigger_error(sprintf(self::TEXT_TableAlreadyExists,$this->name),E_USER_ERROR); - return false; - } - $cols = ''; - if (!empty($this->columns)) - foreach ($this->columns as $cname => $column) { - // no defaults for TEXT type - if ($column->default !== false && is_int(strpos(strtoupper($column->type),'TEXT'))) { - trigger_error(sprintf(self::TEXT_NoDefaultForTEXT, $column->name),E_USER_ERROR); - return false; - } - $cols .= ', '.$column->getColumnQuery(); - } - $table = $this->db->quotekey($this->name); - $id = $this->db->quotekey($this->increments); - $cmd = array( - 'sqlite2?|sybase|dblib' => - "CREATE TABLE $table ($id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT".$cols.");", - 'mysql' => - "CREATE TABLE $table ($id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT".$cols.") DEFAULT CHARSET=$this->charset COLLATE ".$this->charset."_unicode_ci;", - 'pgsql' => - "CREATE TABLE $table ($id SERIAL PRIMARY KEY".$cols.");", - 'mssql|odbc|sqlsrv' => - "CREATE TABLE $table ($id INT IDENTITY CONSTRAINT PK_".$this->name."_ID PRIMARY KEY".$cols.");", - 'ibm' => - "CREATE TABLE $table ($id INTEGER AS IDENTITY NOT NULL $cols, PRIMARY KEY($id));", - ); - $query = $this->findQuery($cmd); - // composite key for sqlite - if (count($this->pkeys) > 1 && preg_match('/sqlite2?/', $this->db->driver())) { - $pk_string = implode(', ', $this->pkeys); - $query = "CREATE TABLE $table ($id INTEGER NULL".$cols.", PRIMARY KEY ($pk_string) );"; - $newTable = new TableModifier($this->name, $this->schema); - // auto-incrementation in composite primary keys - $pk_queries = $newTable->_sqlite_increment_trigger($this->increments); - $this->queries = array_merge($this->queries, $pk_queries); - } - array_unshift($this->queries, $query); - // indexes - foreach ($this->columns as $cname => $column) - if ($column->index) - $this->addIndex($cname, $column->unique); - if (!$exec) - return $this->queries; - $this->db->exec($this->queries); - return isset($newTable) ? $newTable : new TableModifier($this->name,$this->schema); - } - - /** - * create index on one or more columns - * @param string|array $columns Column(s) to be indexed - * @param bool $unique Unique index - * @param int $length index length for text fields in mysql - */ - public function addIndex($columns, $unique = FALSE, $length = 20) - { - if (!is_array($columns)) - $columns = array($columns); - $cols = $this->columns; - foreach ($cols as &$col) - $col = $col->getColumnArray(); - parent::_addIndex($columns,$cols,$unique,$length); - } - -} - - -class TableModifier extends TableBuilder { - - protected - $colTypes, $rebuild_cmd; - - const - // error messages - TEXT_TableNotExisting = "Unable to alter table `%s`. It does not exist.", - TEXT_NotNullFieldNeedsDefault = 'You cannot add the not nullable column `%s` without specifying a default value', - TEXT_ENGINE_NOT_SUPPORTED = 'DB Engine `%s` is not supported for this action.'; - - /** - * generate SQL queries for altering the table and execute it if $exec is true, - * otherwise return the generated query string - * @param bool $exec - * @return array|FALSE - */ - public function build($exec = TRUE) - { - // check if table exists - if (!in_array($this->name, $this->schema->getTables())) - trigger_error(sprintf(self::TEXT_TableNotExisting, $this->name),E_USER_ERROR); - - if ($sqlite = preg_match('/sqlite2?/', $this->db->driver())) { - $sqlite_queries = array(); - } - $rebuild = false; - $additional_queries = $this->queries; - $this->queries = array(); - // add new columns - foreach ($this->columns as $cname => $column) { - /** @var Column $column */ - // not nullable fields should have a default value, when altering a table - if ($column->default === false && $column->nullable === false) { - trigger_error(sprintf(self::TEXT_NotNullFieldNeedsDefault, $column->name),E_USER_ERROR); - return false; - } - // no defaults for TEXT type - if($column->default !== false && is_int(strpos(strtoupper($column->type),'TEXT'))) { - trigger_error(sprintf(self::TEXT_NoDefaultForTEXT, $column->name),E_USER_ERROR); - return false; - } - $table = $this->db->quotekey($this->name); - $col_query = $column->getColumnQuery(); - if ($sqlite) { - // sqlite: dynamic column default only works when rebuilding the table - if($column->default === Schema::DF_CURRENT_TIMESTAMP) { - $rebuild = true; - break; - } else - $sqlite_queries[] = "ALTER TABLE $table ADD $col_query;"; - } else { - $cmd = array( - 'mysql|pgsql|mssql|sybase|dblib|odbc|sqlsrv' => - "ALTER TABLE $table ADD $col_query;", - 'ibm' => - "ALTER TABLE $table ADD COLUMN $col_query;", - ); - $this->queries[] = $this->findQuery($cmd); - } - } - if ($sqlite) - if ($rebuild || !empty($this->rebuild_cmd)) $this->_sqlite_rebuild($exec); - else $this->queries += $sqlite_queries; - $this->queries = array_merge($this->queries,$additional_queries); - // add new indexes - foreach ($this->columns as $cname => $column) - if ($column->index) - $this->addIndex($cname, $column->unique); - if (empty($this->queries)) - return false; - if (is_array($this->queries) && count($this->queries) == 1) - $this->queries = $this->queries[0]; - if (!$exec) return $this->queries; - $result = ($this->suppress) - ? @$this->db->exec($this->queries) : $this->db->exec($this->queries); - $this->queries = $this->columns = $this->rebuild_cmd = array(); - return $result; - } - - /** - * rebuild a sqlite table with additional schema changes - */ - protected function _sqlite_rebuild($exec=true) - { - $new_columns = $this->columns; - $existing_columns = $this->getCols(true); - // find after sorts - $after = array(); - foreach ($new_columns as $cname => $column) - if(!empty($column->after)) - $after[$column->after][] = $cname; - // find rename commands - $rename = (!empty($this->rebuild_cmd) && array_key_exists('rename',$this->rebuild_cmd)) - ? $this->rebuild_cmd['rename'] : array(); - // get primary-key fields - foreach ($existing_columns as $key => $col) - if ($col['pkey']) - $pkeys[array_key_exists($key,$rename) ? $rename[$key] : $key] = $col; - foreach ($new_columns as $key => $col) - if ($col->pkey) - $pkeys[$key] = $col; - // indexes - $indexes = $this->listIndex(); - // drop fields - if (!empty($this->rebuild_cmd) && array_key_exists('drop', $this->rebuild_cmd)) - foreach ($this->rebuild_cmd['drop'] as $name) - if (array_key_exists($name, $existing_columns)) { - if (array_key_exists($name, $pkeys)) { - unset($pkeys[$name]); - // drop composite key - if(count($pkeys) == 1) { - $incrementTrigger = $this->db->quotekey($this->name.'_insert'); - $this->queries[] = 'DROP TRIGGER IF EXISTS '.$incrementTrigger; - } - } - unset($existing_columns[$name]); - // drop index - foreach (array_keys($indexes) as $col) { - // new index names - if ($col == $this->name.'___'.$name) - unset($indexes[$this->name.'___'.$name]); - // check if column is part of an existing combined index - if (is_int(strpos($col, '__'))) { - if (is_int(strpos($col, '___'))) { - $col = explode('___', $col); - $ci = explode('__', $col[1]); - $col = implode('___',$col); - // drop combined index - if (in_array($name, $ci)) - unset($indexes[$col]); - } - } - } - } - // create new table - $oname = $this->name; - $this->queries[] = $this->rename($oname.'_temp', false); - $newTable = $this->schema->createTable($oname); - // add existing fields - foreach ($existing_columns as $name => $col) { - $colName = array_key_exists($name, $rename) ? $rename[$name] : $name; - // update column datatype - if (array_key_exists('update',$this->rebuild_cmd) - && in_array($name,array_keys($this->rebuild_cmd['update']))) { - $cdat = $this->rebuild_cmd['update'][$name]; - if ($cdat instanceof Column) - $col = $cdat->getColumnArray(); - else - $col['type'] = $cdat; - } - $newTable->addColumn($colName, $col)->passThrough(); - // add new fields with after flag - if (array_key_exists($name,$after)) - foreach (array_reverse($after[$name]) as $acol) { - $newTable->addColumn($new_columns[$acol]); - unset($new_columns[$acol]); - } - } - // add remaining new fields - foreach ($new_columns as $ncol) - $newTable->addColumn($ncol); - $newTable->primary(array_keys($pkeys)); - // add existing indexes - foreach (array_reverse($indexes) as $name=>$conf) { - if (is_int(strpos($name, '___'))) - list($tname,$name) = explode('___', $name); - if (is_int(strpos($name, '__'))) - $name = explode('__', $name); - if ($exec) { - $t = $this->schema->alterTable($oname); - $t->dropIndex($name); - $t->build(); - } - $newTable->addIndex($name,$conf['unique']); - } - // build new table - $newTableQueries = $newTable->build(false); - $this->queries = array_merge($this->queries,$newTableQueries); - // copy data - if (!empty($existing_columns)) { - foreach (array_keys($existing_columns) as $name) { - $fields_from[] = $this->db->quotekey($name); - $toName = array_key_exists($name, $rename) ? $rename[$name] : $name; - $fields_to[] = $this->db->quotekey($toName); - } - $this->queries[] = - 'INSERT INTO '.$this->db->quotekey($newTable->name).' ('.implode(', ', $fields_to).') '. - 'SELECT '.implode(', ', $fields_from).' FROM '.$this->db->quotekey($this->name).';'; - } - $this->queries[] = $this->drop(false); - $this->name = $oname; - } - - /** - * create an insert trigger to work-a-round auto-incrementation in composite primary keys - * @param $pkey - * @return array - */ - public function _sqlite_increment_trigger($pkey) { - $table = $this->db->quotekey($this->name); - $pkey = $this->db->quotekey($pkey); - $triggerName = $this->db->quotekey($this->name.'_insert'); - $queries[] = "DROP TRIGGER IF EXISTS $triggerName;"; - $queries[] = 'CREATE TRIGGER '.$triggerName.' AFTER INSERT ON '.$table. - ' WHEN (NEW.'.$pkey.' IS NULL) BEGIN'. - ' UPDATE '.$table.' SET '.$pkey.' = ('. - ' select coalesce( max( '.$pkey.' ), 0 ) + 1 from '.$table. - ') WHERE ROWID = NEW.ROWID;'. - ' END;'; - return $queries; - } - - /** - * get columns of a table - * @param bool $types - * @return array - */ - public function getCols($types = false) - { - $schema = $this->db->schema($this->name, null, 0); - if (!$types) - return array_keys($schema); - else - foreach ($schema as $name => &$cols) { - $default = ($cols['default'] === '') ? null : $cols['default']; - if (!is_null($default) && ((is_int(strpos($curdef=strtolower( - $this->findQuery($this->schema->defaultTypes['CUR_STAMP'])), - strtolower($default))) || is_int(strpos(strtolower($default),$curdef))) - || $default == "('now'::text)::timestamp(0) without time zone")) - { - $default = 'CUR_STAMP'; - } elseif (!is_null($default)) { - // remove single-qoutes - if (preg_match('/sqlite2?/', $this->db->driver())) - $default=preg_replace('/^\s*([\'"])(.*)\1\s*$/','\2',$default); - elseif (preg_match('/mssql|sybase|dblib|odbc|sqlsrv/', $this->db->driver())) - $default=preg_replace('/^\s*(\(\')(.*)(\'\))\s*$/','\2',$default); - // extract value from character_data in postgre - elseif (preg_match('/pgsql/', $this->db->driver())) - if (is_int(strpos($default, 'nextval'))) - $default = null; // drop autoincrement default - elseif (preg_match("/^\'*(.*)\'*::(\s*\w)+/", $default, $match)) - $default = $match[1]; - } else - $default=false; - $cols['default'] = $default; - } - return $schema; - } - - /** - * check if a data type is compatible with an existing column type - * @param string $colType (i.e: BOOLEAN) - * @param string $column (i.e: active) - * @return bool - */ - public function isCompatible($colType,$column) { - $cols = $this->getCols(true); - return $this->schema->isCompatible($colType,$cols[$column]['type']); - } - - /** - * removes a column from a table - * @param string $name - * @return bool - */ - public function dropColumn($name) - { - $colTypes = $this->getCols(true); - // check if column exists - if (!in_array($name, array_keys($colTypes))) return true; - if (preg_match('/sqlite2?/', $this->db->driver())) { - // SQlite does not support drop column directly - $this->rebuild_cmd['drop'][] = $name; - } else { - $quotedTable = $this->db->quotekey($this->name); - $quotedColumn = $this->db->quotekey($name); - $cmd = array( - 'mysql' => - "ALTER TABLE $quotedTable DROP $quotedColumn;", - 'pgsql|odbc|ibm|mssql|sybase|dblib|sqlsrv' => - "ALTER TABLE $quotedTable DROP COLUMN $quotedColumn;", - ); - if (preg_match('/mssql|sybase|dblib|sqlsrv/', $this->db->driver())) - $this->suppress=true; - $this->queries[] = $this->findQuery($cmd); - } - } - - /** - * rename a column - * @param $name - * @param $new_name - * @return void - */ - public function renameColumn($name, $new_name) - { - $existing_columns = $this->getCols(true); - // check if column is already existing - if (!in_array($name, array_keys($existing_columns))) - trigger_error('cannot rename column. it does not exist.',E_USER_ERROR); - if (in_array($new_name, array_keys($existing_columns))) - trigger_error('cannot rename column. new column already exist.',E_USER_ERROR); - - if (preg_match('/sqlite2?/', $this->db->driver())) - // SQlite does not support drop or rename column directly - $this->rebuild_cmd['rename'][$name] = $new_name; - elseif (preg_match('/odbc/', $this->db->driver())) { - // no rename column for odbc, create temp column - $this->addColumn($new_name, $existing_columns[$name])->passThrough(); - $this->queries[] = "UPDATE $this->name SET $new_name = $name"; - $this->dropColumn($name); - } else { - $existing_columns = $this->getCols(true); - $quotedTable = $this->db->quotekey($this->name); - $quotedColumn = $this->db->quotekey($name); - $quotedColumnNew = $this->db->quotekey($new_name); - $cmd = array( - 'mysql' => - "ALTER TABLE $quotedTable CHANGE $quotedColumn $quotedColumnNew ".$existing_columns[$name]['type'].";", - 'pgsql|ibm' => - "ALTER TABLE $quotedTable RENAME COLUMN $quotedColumn TO $quotedColumnNew;", - 'mssql|sybase|dblib|sqlsrv' => - "sp_rename [$this->name.$name], '$new_name', 'Column'", - ); - if (preg_match('/mssql|sybase|dblib|sqlsrv/', $this->db->driver())) - $this->suppress = true; - $this->queries[] = $this->findQuery($cmd); - } - } - - /** - * modifies column datatype - * @param string $name - * @param string|Column $datatype - * @param bool $force - */ - public function updateColumn($name, $datatype, $force = false) - { - if ($datatype instanceof Column) { - $col = $datatype; - $datatype = $col->type; - $force = $col->passThrough; - } - if(!$force) - $datatype = $this->findQuery($this->schema->dataTypes[strtoupper($datatype)]); - $table = $this->db->quotekey($this->name); - $column = $this->db->quotekey($name); - if (preg_match('/sqlite2?/', $this->db->driver())){ - $this->rebuild_cmd['update'][$name] = isset($col)?$col:$datatype; - } else { - $dat = isset($col) ? $col->getColumnQuery() : - $column.' '.$datatype; - $cmd = array( - 'mysql' => - "ALTER TABLE $table MODIFY COLUMN $dat;", - 'pgsql' => - "ALTER TABLE $table ALTER COLUMN $column TYPE $datatype;", - 'sqlsrv|mssql|sybase|dblib|ibm' => - "ALTER TABLE $table ALTER COLUMN $column $datatype;", - ); - if (isset($col)) { - $cmd['pgsql'] = array($cmd['pgsql']); - $cmd['pgsql'][] = "ALTER TABLE $table ALTER COLUMN $column SET DEFAULT ".$col->getDefault().";"; - if ($col->nullable) - $cmd['pgsql'][] = "ALTER TABLE $table ALTER COLUMN $column DROP NOT NULL;"; - else - $cmd['pgsql'][] = "ALTER TABLE $table ALTER COLUMN $column SET NOT NULL;"; - $df_key = 'DF_'.$this->name.'_'.$name; - $cmd['sqlsrv|mssql|sybase|dblib|ibm'] = array( - "ALTER TABLE $table ALTER COLUMN $column $datatype ".$col->getNullable().";", - "DECLARE @ConstraintName nvarchar(200) - SELECT @ConstraintName = Name FROM SYS.DEFAULT_CONSTRAINTS WHERE PARENT_OBJECT_ID = OBJECT_ID('$this->name') - AND PARENT_COLUMN_ID = (SELECT column_id FROM sys.columns WHERE NAME = N'$name' - AND object_id = OBJECT_ID(N'$this->name')) - IF @ConstraintName IS NOT NULL - EXEC('ALTER TABLE $this->name DROP CONSTRAINT ' + @ConstraintName) - ", - "ALTER TABLE $table ADD CONSTRAINT $df_key DEFAULT ".$col->getDefault()." FOR $column;", - ); - } - $this->queries[] = $this->findQuery($cmd); - } - } - - /** - * create index on one or more columns - * @param string|array $columns Column(s) to be indexed - * @param bool $unique Unique index - * @param int $length index length for text fields in mysql - */ - public function addIndex($columns, $unique = FALSE, $length = 20) - { - if (!is_array($columns)) - $columns = array($columns); - $existingCol = $this->columns; - foreach ($existingCol as &$col) - $col = $col->getColumnArray(); - $allCols = array_merge($this->getCols(true), $existingCol); - parent::_addIndex($columns, $allCols, $unique, $length); - } - - /** - * drop a column index - * @param string|array $name - */ - public function dropIndex($name) - { - if (is_array($name)) - $name = $this->name.'___'.implode('__', $name); - elseif(!is_int(strpos($name,'___'))) - $name = $this->name.'___'.$name; - $name = $this->db->quotekey($name); - $table = $this->db->quotekey($this->name); - $cmd = array( - 'pgsql|sqlite2?|ibm' => - "DROP INDEX $name;", - 'mssql|sybase|dblib|odbc|sqlsrv' => - "DROP INDEX $table.$name;", - 'mysql'=> - "ALTER TABLE $table DROP INDEX $name;", - ); - $query = $this->findQuery($cmd); - $this->queries[] = $query; - } - - /** - * returns table indexes as assoc array - * @return array - */ - public function listIndex() - { - $table = $this->db->quotekey($this->name); - $cmd = array( - 'sqlite2?' => - "PRAGMA index_list($table);", - 'mysql' => - "SHOW INDEX FROM $table;", - 'mssql|sybase|dblib|sqlsrv' => - "select * from sys.indexes ". - "where object_id = (select object_id from sys.objects where name = '$this->name')", - 'pgsql' => - "select i.relname as name, ix.indisunique as unique ". - "from pg_class t, pg_class i, pg_index ix ". - "where t.oid = ix.indrelid and i.oid = ix.indexrelid ". - "and t.relkind = 'r' and t.relname = '$this->name'". - "group by t.relname, i.relname, ix.indisunique;", - ); - $result = $this->db->exec($this->findQuery($cmd)); - $indexes = array(); - if (preg_match('/pgsql|sqlite2?/', $this->db->driver())) { - foreach($result as $row) - $indexes[$row['name']] = array('unique' => $row['unique']); - } elseif (preg_match('/mssql|sybase|dblib|sqlsrv/', $this->db->driver())) { - foreach ($result as $row) - $indexes[$row['name']] = array('unique' => $row['is_unique']); - } elseif (preg_match('/mysql/', $this->db->driver())) { - foreach($result as $row) - $indexes[$row['Key_name']] = array('unique' => !(bool)$row['Non_unique']); - } else - trigger_error(sprintf(self::TEXT_ENGINE_NOT_SUPPORTED, $this->db->driver()),E_USER_ERROR); - return $indexes; - } - - /** - * rename this table - * @param string $new_name - * @param bool $exec - * @return $this|bool - */ - public function rename($new_name, $exec = true) { - $query = $this->schema->renameTable($this->name, $new_name, $exec); - $this->name = $new_name; - return ($exec) ? $this : $query; - } - - /** - * drop this table - * @param bool $exec - * @return mixed - */ - public function drop($exec = true) { - return $this->schema->dropTable($this,$exec); - } - -} - -/** - * defines a table column configuration - * Class Column - * @package DB\SQL - */ -class Column { - - use DB_Utils; - - public $name, $type, $nullable, $default, $after, $index, $unique, $passThrough, $pkey; - protected $table, $schema, $type_val; - - const - TEXT_NoDataType = 'The specified datatype %s is not defined in %s driver. Add passThrough option to enforce this datatype.', - TEXT_CurrentStampDataType = 'Current timestamp as column default is only possible for TIMESTAMP datatype'; - - /** - * @param string $name - * @param TableBuilder $table - */ - public function __construct($name, TableBuilder $table) { - $this->name = $name; - $this->nullable = true; - $this->default = false; - $this->after = false; - $this->index = false; - $this->unique = false; - $this->passThrough = false; - $this->pkey = false; - - $this->table = $table; - $this->schema = $table->schema; - $this->db = $this->schema->db; - } - - /** - * @param string $datatype - * @param bool $force don't match datatype against DT array - * @return $this - */ - public function type($datatype, $force = FALSE) { - $this->type = $datatype; - $this->passThrough = $force; - return $this; - } - - public function type_tinyint() { - $this->type = Schema::DT_INT1; - return $this; - } - - public function type_smallint() { - $this->type = Schema::DT_INT2; - return $this; - } - - public function type_int() { - $this->type = Schema::DT_INT4; - return $this; - } - - public function type_bigint() { - $this->type = Schema::DT_INT8; - return $this; - } - - public function type_float() { - $this->type = Schema::DT_FLOAT; - return $this; - } - - public function type_decimal() { - $this->type = Schema::DT_DOUBLE; - return $this; - } - - public function type_text() { - $this->type = Schema::DT_TEXT; - return $this; - } - - public function type_longtext() { - $this->type = Schema::DT_LONGTEXT; - return $this; - } - - public function type_varchar($length = 255) { - $this->type = "varchar($length)"; - $this->passThrough = true; - return $this; - } - - public function type_date() { - $this->type = Schema::DT_DATE; - return $this; - } - - public function type_datetime() { - $this->type = Schema::DT_DATETIME; - return $this; - } - - public function type_timestamp($asDefault=false) { - $this->type = Schema::DT_TIMESTAMP; - if ($asDefault) - $this->default = Schema::DF_CURRENT_TIMESTAMP; - return $this; - } - - public function type_blob() { - $this->type = Schema::DT_BLOB; - return $this; - } - - public function type_bool() { - $this->type = Schema::DT_BOOLEAN; - return $this; - } - - public function passThrough($state = TRUE) { - $this->passThrough = $state; - return $this; - } - - public function nullable($nullable) { - $this->nullable = $nullable; - return $this; - } - - public function defaults($default) { - $this->default = $default; - return $this; - } - - public function after($name) { - $this->after = $name; - return $this; - } - - public function index($unique = FALSE) { - $this->index = true; - $this->unique = $unique; - return $this; - } - - /** - * feed column from array or hive key - * @param string|array $args - */ - public function copyfrom($args) { - if (($args || \Base::instance()->exists($args,$args)) - && is_array($args)) - foreach ($args as $arg => $val) - $this->{$arg} = $val; - } - - /** - * returns an array of this column configuration - * @return array - */ - public function getColumnArray() { - $fields = array('name','type','passThrough','default','nullable', - 'index','unique','after','pkey'); - $fields = array_flip($fields); - foreach($fields as $key => &$val) - $val = $this->{$key}; - unset($val); - return $fields; - } - - /** - * return resolved column datatype - * @return bool|string - */ - public function getTypeVal() { - if (!$this->type) - trigger_error(sprintf('Cannot build a column query for `%s`: no column type set',$this->name),E_USER_ERROR); - if ($this->passThrough) - $this->type_val = $this->type; - else { - $this->type_val = $this->findQuery($this->schema->dataTypes[strtoupper($this->type)]); - if (!$this->type_val) { - if (Schema::$strict) { - trigger_error(sprintf(self::TEXT_NoDataType, strtoupper($this->type), - $this->db->driver()),E_USER_ERROR); - return FALSE; - } else { - // auto pass-through if not found - $this->type_val = $this->type; - } - } - } - return $this->type_val; - } - - /** - * generate SQL column definition query - * @return bool|string - */ - public function getColumnQuery() { - // prepare column types - $type_val = $this->getTypeVal(); - // build query - $query = $this->db->quotekey($this->name).' '.$type_val.' '. - $this->getNullable(); - // unify default for booleans - if (preg_match('/bool/i', $type_val) && $this->default!==null) - $this->default = (int) $this->default; - // default value - if ($this->default !== false) { - $def_cmds = array( - 'sqlite2?|mysql|pgsql' => 'DEFAULT', - 'mssql|sybase|dblib|odbc|sqlsrv' => 'constraint DF_'.$this->table->name.'_'.$this->name.' DEFAULT', - 'ibm' => 'WITH DEFAULT', - ); - $def_cmd = $this->findQuery($def_cmds).' '.$this->getDefault(); - $query .= ' '.$def_cmd; - } - if (!empty($this->after) && $this->table instanceof TableModifier) { - // `after` feature only works for mysql - if (preg_match('/mysql/', $this->db->driver())) { - $after_cmd = 'AFTER '.$this->db->quotekey($this->after); - $query .= ' '.$after_cmd; - } - } - return $query; - } - - /** - * return query part for nullable - * @return string - */ - public function getNullable() { - return $this->nullable ? 'NULL' : 'NOT NULL'; - } - - /** - * return query part for default value - * @return string - */ - public function getDefault() { - // timestamp default - if ($this->default === Schema::DF_CURRENT_TIMESTAMP) { - // check for right datatpye - $stamp_type = $this->findQuery($this->schema->dataTypes['TIMESTAMP']); - if ($this->type != 'TIMESTAMP' && - ($this->passThrough && strtoupper($this->type) != strtoupper($stamp_type)) - ) - trigger_error(self::TEXT_CurrentStampDataType,E_USER_ERROR); - return $this->findQuery($this->schema->defaultTypes[strtoupper($this->default)]); - } else { - // static defaults - $type_val = $this->getTypeVal(); - $pdo_type = preg_match('/int|bool/i', $type_val, $parts) ? - constant('\PDO::PARAM_'.strtoupper($parts[0])) : \PDO::PARAM_STR; - return ($this->default === NULL ? 'NULL' : - $this->db->quote(htmlspecialchars($this->default, ENT_QUOTES, - \Base::instance()->get('ENCODING')), $pdo_type)); - } - } -} - - -trait DB_Utils { - - /** @var \DB\SQL */ - public $db; - - /** - * parse command array and return backend specific query - * @param $cmd - * @param $cmd array - * @return bool|string - */ - public function findQuery($cmd) { - foreach ($cmd as $backend => $val) - if (preg_match('/'.$backend.'/', $this->db->driver())) - return $val; - trigger_error(sprintf('DB Engine `%s` is not supported for this action.', $this->db->driver()),E_USER_ERROR); - } -} \ No newline at end of file diff --git a/app/lib/db/sql/session.php b/app/lib/db/sql/session.php deleted file mode 100644 index efabd600..00000000 --- a/app/lib/db/sql/session.php +++ /dev/null @@ -1,221 +0,0 @@ -. - -*/ - -namespace DB\SQL; - -//! SQL-managed session handler -class Session extends Mapper { - - protected - //! Session ID - $sid, - //! Anti-CSRF token - $_csrf, - //! User agent - $_agent, - //! IP, - $_ip, - //! Suspect callback - $onsuspect; - - /** - * Open session - * @return TRUE - * @param $path string - * @param $name string - **/ - function open($path,$name) { - return TRUE; - } - - /** - * Close session - * @return TRUE - **/ - function close() { - $this->reset(); - $this->sid=NULL; - return TRUE; - } - - /** - * Return session data in serialized format - * @return string - * @param $id string - **/ - function read($id) { - $this->load(['session_id=?',$this->sid=$id]); - if ($this->dry()) - return ''; - if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { - $fw=\Base::instance(); - if (!isset($this->onsuspect) || - $fw->call($this->onsuspect,[$this,$id])===FALSE) { - //NB: `session_destroy` can't be called at that stage (`session_start` not completed) - $this->destroy($id); - $this->close(); - unset($fw->{'COOKIE.'.session_name()}); - $fw->error(403); - } - } - return $this->get('data'); - } - - /** - * Write session data - * @return TRUE - * @param $id string - * @param $data string - **/ - function write($id,$data) { - $this->set('session_id',$id); - $this->set('data',$data); - $this->set('ip',$this->_ip); - $this->set('agent',$this->_agent); - $this->set('stamp',time()); - $this->save(); - return TRUE; - } - - /** - * Destroy session - * @return TRUE - * @param $id string - **/ - function destroy($id) { - $this->erase(['session_id=?',$id]); - return TRUE; - } - - /** - * Garbage collector - * @return TRUE - * @param $max int - **/ - function cleanup($max) { - $this->erase(['stamp+?sid; - } - - /** - * Return anti-CSRF token - * @return string - **/ - function csrf() { - return $this->_csrf; - } - - /** - * Return IP address - * @return string - **/ - function ip() { - return $this->_ip; - } - - /** - * Return Unix timestamp - * @return string|FALSE - **/ - function stamp() { - if (!$this->sid) - session_start(); - return $this->dry()?FALSE:$this->get('stamp'); - } - - /** - * Return HTTP user agent - * @return string - **/ - function agent() { - return $this->_agent; - } - - /** - * Instantiate class - * @param $db \DB\SQL - * @param $table string - * @param $force bool - * @param $onsuspect callback - * @param $key string - **/ - function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL,$key=NULL) { - if ($force) { - $eol="\n"; - $tab="\t"; - $sqlsrv=preg_match('/mssql|sqlsrv|sybase/',$db->driver()); - $db->exec( - ($sqlsrv? - ('IF NOT EXISTS (SELECT * FROM sysobjects WHERE '. - 'name='.$db->quote($table).' AND xtype=\'U\') '. - 'CREATE TABLE dbo.'): - ('CREATE TABLE IF NOT EXISTS '. - ((($name=$db->name())&&$db->driver()!='pgsql')? - ($db->quotekey($name,FALSE).'.'):''))). - $db->quotekey($table,FALSE).' ('.$eol. - ($sqlsrv?$tab.$db->quotekey('id').' INT IDENTITY,'.$eol:''). - $tab.$db->quotekey('session_id').' VARCHAR(255),'.$eol. - $tab.$db->quotekey('data').' TEXT,'.$eol. - $tab.$db->quotekey('ip').' VARCHAR(45),'.$eol. - $tab.$db->quotekey('agent').' VARCHAR(300),'.$eol. - $tab.$db->quotekey('stamp').' INTEGER,'.$eol. - $tab.'PRIMARY KEY ('.$db->quotekey($sqlsrv?'id':'session_id').')'.$eol. - ($sqlsrv?',CONSTRAINT [UK_session_id] UNIQUE(session_id)':''). - ');' - ); - } - parent::__construct($db,$table); - $this->onsuspect=$onsuspect; - session_set_save_handler( - [$this,'open'], - [$this,'close'], - [$this,'read'], - [$this,'write'], - [$this,'destroy'], - [$this,'cleanup'] - ); - register_shutdown_function('session_commit'); - $fw=\Base::instance(); - $headers=$fw->HEADERS; - $this->_csrf=$fw->hash($fw->SEED. - extension_loaded('openssl')? - implode(unpack('L',openssl_random_pseudo_bytes(4))): - mt_rand() - ); - if ($key) - $fw->$key=$this->_csrf; - $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; - if (strlen($this->_agent) > 300) { - $this->_agent = substr($this->_agent, 0, 300); - } - $this->_ip=$fw->IP; - } - -} diff --git a/app/lib/f3.php b/app/lib/f3.php deleted file mode 100644 index 3b96a63c..00000000 --- a/app/lib/f3.php +++ /dev/null @@ -1,42 +0,0 @@ -. - -*/ - -//! Legacy mode enabler -class F3 { - - static - //! Framework instance - $fw; - - /** - * Forward function calls to framework - * @return mixed - * @param $func callback - * @param $args array - **/ - static function __callstatic($func,array $args) { - if (!self::$fw) - self::$fw=Base::instance(); - return call_user_func_array([self::$fw,$func],$args); - } - -} diff --git a/app/lib/image.php b/app/lib/image.php deleted file mode 100644 index 3efc0cca..00000000 --- a/app/lib/image.php +++ /dev/null @@ -1,616 +0,0 @@ -. - -*/ - -//! Image manipulation tools -class Image { - - //@{ Messages - const - E_Color='Invalid color specified: %s', - E_File='File not found', - E_Font='CAPTCHA font not found', - E_TTF='No TrueType support in GD module', - E_Length='Invalid CAPTCHA length: %s'; - //@} - - //@{ Positional cues - const - POS_Left=1, - POS_Center=2, - POS_Right=4, - POS_Top=8, - POS_Middle=16, - POS_Bottom=32; - //@} - - protected - //! Source filename - $file, - //! Image resource - $data, - //! Enable/disable history - $flag=FALSE, - //! Filter count - $count=0; - - /** - * Convert RGB hex triad to array - * @return array|FALSE - * @param $color int|string - **/ - function rgb($color) { - if (is_string($color)) - $color=hexdec($color); - $hex=str_pad($hex=dechex($color),$color<4096?3:6,'0',STR_PAD_LEFT); - if (($len=strlen($hex))>6) - user_error(sprintf(self::E_Color,'0x'.$hex),E_USER_ERROR); - $color=str_split($hex,$len/3); - foreach ($color as &$hue) { - $hue=hexdec(str_repeat($hue,6/$len)); - unset($hue); - } - return $color; - } - - /** - * Invert image - * @return object - **/ - function invert() { - imagefilter($this->data,IMG_FILTER_NEGATE); - return $this->save(); - } - - /** - * Adjust brightness (range:-255 to 255) - * @return object - * @param $level int - **/ - function brightness($level) { - imagefilter($this->data,IMG_FILTER_BRIGHTNESS,$level); - return $this->save(); - } - - /** - * Adjust contrast (range:-100 to 100) - * @return object - * @param $level int - **/ - function contrast($level) { - imagefilter($this->data,IMG_FILTER_CONTRAST,$level); - return $this->save(); - } - - /** - * Convert to grayscale - * @return object - **/ - function grayscale() { - imagefilter($this->data,IMG_FILTER_GRAYSCALE); - return $this->save(); - } - - /** - * Adjust smoothness - * @return object - * @param $level int - **/ - function smooth($level) { - imagefilter($this->data,IMG_FILTER_SMOOTH,$level); - return $this->save(); - } - - /** - * Emboss the image - * @return object - **/ - function emboss() { - imagefilter($this->data,IMG_FILTER_EMBOSS); - return $this->save(); - } - - /** - * Apply sepia effect - * @return object - **/ - function sepia() { - imagefilter($this->data,IMG_FILTER_GRAYSCALE); - imagefilter($this->data,IMG_FILTER_COLORIZE,90,60,45); - return $this->save(); - } - - /** - * Pixelate the image - * @return object - * @param $size int - **/ - function pixelate($size) { - imagefilter($this->data,IMG_FILTER_PIXELATE,$size,TRUE); - return $this->save(); - } - - /** - * Blur the image using Gaussian filter - * @return object - * @param $selective bool - **/ - function blur($selective=FALSE) { - imagefilter($this->data, - $selective?IMG_FILTER_SELECTIVE_BLUR:IMG_FILTER_GAUSSIAN_BLUR); - return $this->save(); - } - - /** - * Apply sketch effect - * @return object - **/ - function sketch() { - imagefilter($this->data,IMG_FILTER_MEAN_REMOVAL); - return $this->save(); - } - - /** - * Flip on horizontal axis - * @return object - **/ - function hflip() { - $tmp=imagecreatetruecolor( - $width=$this->width(),$height=$this->height()); - imagesavealpha($tmp,TRUE); - imagefill($tmp,0,0,IMG_COLOR_TRANSPARENT); - imagecopyresampled($tmp,$this->data, - 0,0,$width-1,0,$width,$height,-$width,$height); - imagedestroy($this->data); - $this->data=$tmp; - return $this->save(); - } - - /** - * Flip on vertical axis - * @return object - **/ - function vflip() { - $tmp=imagecreatetruecolor( - $width=$this->width(),$height=$this->height()); - imagesavealpha($tmp,TRUE); - imagefill($tmp,0,0,IMG_COLOR_TRANSPARENT); - imagecopyresampled($tmp,$this->data, - 0,0,0,$height-1,$width,$height,$width,-$height); - imagedestroy($this->data); - $this->data=$tmp; - return $this->save(); - } - - /** - * Crop the image - * @return object - * @param $x1 int - * @param $y1 int - * @param $x2 int - * @param $y2 int - **/ - function crop($x1,$y1,$x2,$y2) { - $tmp=imagecreatetruecolor($width=$x2-$x1+1,$height=$y2-$y1+1); - imagesavealpha($tmp,TRUE); - imagefill($tmp,0,0,IMG_COLOR_TRANSPARENT); - imagecopyresampled($tmp,$this->data, - 0,0,$x1,$y1,$width,$height,$width,$height); - imagedestroy($this->data); - $this->data=$tmp; - return $this->save(); - } - - /** - * Resize image (Maintain aspect ratio); Crop relative to center - * if flag is enabled; Enlargement allowed if flag is enabled - * @return object - * @param $width int - * @param $height int - * @param $crop bool - * @param $enlarge bool - **/ - function resize($width=NULL,$height=NULL,$crop=TRUE,$enlarge=TRUE) { - if (is_null($width) && is_null($height)) - return $this; - $origw=$this->width(); - $origh=$this->height(); - if (is_null($width)) - $width=round(($height/$origh)*$origw); - if (is_null($height)) - $height=round(($width/$origw)*$origh); - // Adjust dimensions; retain aspect ratio - $ratio=$origw/$origh; - if (!$crop) { - if ($width/$ratio<=$height) - $height=round($width/$ratio); - else - $width=round($height*$ratio); - } - if (!$enlarge) { - $width=min($origw,$width); - $height=min($origh,$height); - } - // Create blank image - $tmp=imagecreatetruecolor($width,$height); - imagesavealpha($tmp,TRUE); - imagefill($tmp,0,0,IMG_COLOR_TRANSPARENT); - // Resize - if ($crop) { - if ($width/$ratio<=$height) { - $cropw=round($origh*$width/$height); - imagecopyresampled($tmp,$this->data, - 0,0,($origw-$cropw)/2,0,$width,$height,$cropw,$origh); - } - else { - $croph=round($origw*$height/$width); - imagecopyresampled($tmp,$this->data, - 0,0,0,($origh-$croph)/2,$width,$height,$origw,$croph); - } - } - else - imagecopyresampled($tmp,$this->data, - 0,0,0,0,$width,$height,$origw,$origh); - imagedestroy($this->data); - $this->data=$tmp; - return $this->save(); - } - - /** - * Rotate image - * @return object - * @param $angle int - **/ - function rotate($angle) { - $this->data=imagerotate($this->data,$angle, - imagecolorallocatealpha($this->data,0,0,0,127)); - imagesavealpha($this->data,TRUE); - return $this->save(); - } - - /** - * Apply an image overlay - * @return object - * @param $img object - * @param $align int|array - * @param $alpha int - **/ - function overlay(Image $img,$align=NULL,$alpha=100) { - if (is_null($align)) - $align=self::POS_Right|self::POS_Bottom; - if (is_array($align)) { - list($posx,$posy)=$align; - $align = 0; - } - $ovr=imagecreatefromstring($img->dump()); - imagesavealpha($ovr,TRUE); - $imgw=$this->width(); - $imgh=$this->height(); - $ovrw=imagesx($ovr); - $ovrh=imagesy($ovr); - if ($align & self::POS_Left) - $posx=0; - if ($align & self::POS_Center) - $posx=($imgw-$ovrw)/2; - if ($align & self::POS_Right) - $posx=$imgw-$ovrw; - if ($align & self::POS_Top) - $posy=0; - if ($align & self::POS_Middle) - $posy=($imgh-$ovrh)/2; - if ($align & self::POS_Bottom) - $posy=$imgh-$ovrh; - if (empty($posx)) - $posx=0; - if (empty($posy)) - $posy=0; - if ($alpha==100) - imagecopy($this->data,$ovr,$posx,$posy,0,0,$ovrw,$ovrh); - else { - $cut=imagecreatetruecolor($ovrw,$ovrh); - imagecopy($cut,$this->data,0,0,$posx,$posy,$ovrw,$ovrh); - imagecopy($cut,$ovr,0,0,0,0,$ovrw,$ovrh); - imagecopymerge($this->data, - $cut,$posx,$posy,0,0,$ovrw,$ovrh,$alpha); - } - return $this->save(); - } - - /** - * Generate identicon - * @return object - * @param $str string - * @param $size int - * @param $blocks int - **/ - function identicon($str,$size=64,$blocks=4) { - $sprites=[ - [.5,1,1,0,1,1], - [.5,0,1,0,.5,1,0,1], - [.5,0,1,0,1,1,.5,1,1,.5], - [0,.5,.5,0,1,.5,.5,1,.5,.5], - [0,.5,1,0,1,1,0,1,1,.5], - [1,0,1,1,.5,1,1,.5,.5,.5], - [0,0,1,0,1,.5,0,0,.5,1,0,1], - [0,0,.5,0,1,.5,.5,1,0,1,.5,.5], - [.5,0,.5,.5,1,.5,1,1,.5,1,.5,.5,0,.5], - [0,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1], - [0,.5,.5,1,1,.5,.5,0,1,0,1,1,0,1], - [.5,0,1,0,1,1,.5,1,1,.75,.5,.5,1,.25], - [0,.5,.5,0,.5,.5,1,0,1,.5,.5,1,.5,.5,0,1], - [0,0,1,0,1,1,0,1,1,.5,.5,.25,.5,.75,0,.5,.5,.25], - [0,.5,.5,.5,.5,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1], - [0,0,1,0,.5,.5,.5,0,0,.5,1,.5,.5,1,.5,.5,0,1] - ]; - $hash=sha1($str); - $this->data=imagecreatetruecolor($size,$size); - list($r,$g,$b)=$this->rgb(hexdec(substr($hash,-3))); - $fg=imagecolorallocate($this->data,$r,$g,$b); - imagefill($this->data,0,0,IMG_COLOR_TRANSPARENT); - $ctr=count($sprites); - $dim=$blocks*floor($size/$blocks)*2/$blocks; - for ($j=0,$y=ceil($blocks/2);$j<$y;$j++) - for ($i=$j,$x=$blocks-1-$j;$i<$x;$i++) { - $sprite=imagecreatetruecolor($dim,$dim); - imagefill($sprite,0,0,IMG_COLOR_TRANSPARENT); - $block=$sprites[hexdec($hash[($j*$blocks+$i)*2])%$ctr]; - for ($k=0,$pts=count($block);$k<$pts;$k++) - $block[$k]*=$dim; - imagefilledpolygon($sprite,$block,$pts/2,$fg); - for ($k=0;$k<4;$k++) { - imagecopyresampled($this->data,$sprite, - $i*$dim/2,$j*$dim/2,0,0,$dim/2,$dim/2,$dim,$dim); - $this->data=imagerotate($this->data,90, - imagecolorallocatealpha($this->data,0,0,0,127)); - } - imagedestroy($sprite); - } - imagesavealpha($this->data,TRUE); - return $this->save(); - } - - /** - * Generate CAPTCHA image - * @return object|FALSE - * @param $font string - * @param $size int - * @param $len int - * @param $key string - * @param $path string - * @param $fg int - * @param $bg int - **/ - function captcha($font,$size=24,$len=5, - $key=NULL,$path='',$fg=0xFFFFFF,$bg=0x000000) { - if ((!$ssl=extension_loaded('openssl')) && ($len<4 || $len>13)) { - user_error(sprintf(self::E_Length,$len),E_USER_ERROR); - return FALSE; - } - if (!function_exists('imagettftext')) { - user_error(self::E_TTF,E_USER_ERROR); - return FALSE; - } - $fw=Base::instance(); - foreach ($fw->split($path?:$fw->UI.';./') as $dir) - if (is_file($path=$dir.$font)) { - $seed=strtoupper(substr( - $ssl?bin2hex(openssl_random_pseudo_bytes($len)):uniqid(), - -$len)); - $block=$size*3; - $tmp=[]; - for ($i=0,$width=0,$height=0;$i<$len;$i++) { - // Process at 2x magnification - $box=imagettfbbox($size*2,0,$path,$seed[$i]); - $w=$box[2]-$box[0]; - $h=$box[1]-$box[5]; - $char=imagecreatetruecolor($block,$block); - imagefill($char,0,0,$bg); - imagettftext($char,$size*2,0, - ($block-$w)/2,$block-($block-$h)/2, - $fg,$path,$seed[$i]); - $char=imagerotate($char,mt_rand(-30,30), - imagecolorallocatealpha($char,0,0,0,127)); - // Reduce to normal size - $tmp[$i]=imagecreatetruecolor( - ($w=imagesx($char))/2,($h=imagesy($char))/2); - imagefill($tmp[$i],0,0,IMG_COLOR_TRANSPARENT); - imagecopyresampled($tmp[$i], - $char,0,0,0,0,$w/2,$h/2,$w,$h); - imagedestroy($char); - $width+=$i+1<$len?$block/2:$w/2; - $height=max($height,$h/2); - } - $this->data=imagecreatetruecolor($width,$height); - imagefill($this->data,0,0,IMG_COLOR_TRANSPARENT); - for ($i=0;$i<$len;$i++) { - imagecopy($this->data,$tmp[$i], - $i*$block/2,($height-imagesy($tmp[$i]))/2,0,0, - imagesx($tmp[$i]),imagesy($tmp[$i])); - imagedestroy($tmp[$i]); - } - imagesavealpha($this->data,TRUE); - if ($key) - $fw->$key=$seed; - return $this->save(); - } - user_error(self::E_Font,E_USER_ERROR); - return FALSE; - } - - /** - * Return image width - * @return int - **/ - function width() { - return imagesx($this->data); - } - - /** - * Return image height - * @return int - **/ - function height() { - return imagesy($this->data); - } - - /** - * Send image to HTTP client - * @return NULL - **/ - function render() { - $args=func_get_args(); - $format=$args?array_shift($args):'png'; - if (PHP_SAPI!='cli') { - header('Content-Type: image/'.$format); - header('X-Powered-By: '.Base::instance()->PACKAGE); - } - call_user_func_array( - 'image'.$format, - array_merge([$this->data,NULL],$args) - ); - } - - /** - * Return image as a string - * @return string - **/ - function dump() { - $args=func_get_args(); - $format=$args?array_shift($args):'png'; - ob_start(); - call_user_func_array( - 'image'.$format, - array_merge([$this->data,NULL],$args) - ); - return ob_get_clean(); - } - - /** - * Return image resource - * @return resource - **/ - function data() { - return $this->data; - } - - /** - * Save current state - * @return object - **/ - function save() { - $fw=Base::instance(); - if ($this->flag) { - if (!is_dir($dir=$fw->TEMP)) - mkdir($dir,Base::MODE,TRUE); - $this->count++; - $fw->write($dir.'/'.$fw->SEED.'.'. - $fw->hash($this->file).'-'.$this->count.'.png', - $this->dump()); - } - return $this; - } - - /** - * Revert to specified state - * @return object - * @param $state int - **/ - function restore($state=1) { - $fw=Base::instance(); - if ($this->flag && is_file($file=($path=$fw->TEMP. - $fw->SEED.'.'.$fw->hash($this->file).'-').$state.'.png')) { - if (is_resource($this->data)) - imagedestroy($this->data); - $this->data=imagecreatefromstring($fw->read($file)); - imagesavealpha($this->data,TRUE); - foreach (glob($path.'*.png',GLOB_NOSORT) as $match) - if (preg_match('/-(\d+)\.png/',$match,$parts) && - $parts[1]>$state) - @unlink($match); - $this->count=$state; - } - return $this; - } - - /** - * Undo most recently applied filter - * @return object - **/ - function undo() { - if ($this->flag) { - if ($this->count) - $this->count--; - return $this->restore($this->count); - } - return $this; - } - - /** - * Load string - * @return object|FALSE - * @param $str string - **/ - function load($str) { - if (!$this->data=@imagecreatefromstring($str)) - return FALSE; - imagesavealpha($this->data,TRUE); - $this->save(); - return $this; - } - - /** - * Instantiate image - * @param $file string - * @param $flag bool - * @param $path string - **/ - function __construct($file=NULL,$flag=FALSE,$path=NULL) { - $this->flag=$flag; - if ($file) { - $fw=Base::instance(); - // Create image from file - $this->file=$file; - if (!isset($path)) - $path=$fw->UI.';./'; - foreach ($fw->split($path,FALSE) as $dir) - if (is_file($dir.$file)) - return $this->load($fw->read($dir.$file)); - user_error(self::E_File,E_USER_ERROR); - } - } - - /** - * Wrap-up - * @return NULL - **/ - function __destruct() { - if (is_resource($this->data)) { - imagedestroy($this->data); - $fw=Base::instance(); - $path=$fw->TEMP.$fw->SEED.'.'.$fw->hash($this->file); - if ($glob=@glob($path.'*.png',GLOB_NOSORT)) - foreach ($glob as $match) - if (preg_match('/-(\d+)\.png/',$match)) - @unlink($match); - } - } - -} diff --git a/app/lib/log.php b/app/lib/log.php deleted file mode 100644 index a80ea695..00000000 --- a/app/lib/log.php +++ /dev/null @@ -1,68 +0,0 @@ -. - -*/ - -//! Custom logger -class Log { - - protected - //! File name - $file; - - /** - * Write specified text to log file - * @return string - * @param $text string - * @param $format string - **/ - function write($text,$format='r') { - $fw=Base::instance(); - foreach (preg_split('/\r?\n|\r/',trim($text)) as $line) - $fw->write( - $this->file, - date($format). - (isset($_SERVER['REMOTE_ADDR'])? - (' ['.$_SERVER['REMOTE_ADDR'].']'):'').' '. - trim($line).PHP_EOL, - TRUE - ); - } - - /** - * Erase log - * @return NULL - **/ - function erase() { - @unlink($this->file); - } - - /** - * Instantiate class - * @param $file string - **/ - function __construct($file) { - $fw=Base::instance(); - if (!is_dir($dir=$fw->LOGS)) - mkdir($dir,Base::MODE,TRUE); - $this->file=$dir.$file; - } - -} diff --git a/app/lib/magic.php b/app/lib/magic.php deleted file mode 100644 index 2502bf19..00000000 --- a/app/lib/magic.php +++ /dev/null @@ -1,139 +0,0 @@ -. - -*/ - -//! PHP magic wrapper -abstract class Magic implements ArrayAccess { - - /** - * Return TRUE if key is not empty - * @return bool - * @param $key string - **/ - abstract function exists($key); - - /** - * Bind value to key - * @return mixed - * @param $key string - * @param $val mixed - **/ - abstract function set($key,$val); - - /** - * Retrieve contents of key - * @return mixed - * @param $key string - **/ - abstract function &get($key); - - /** - * Unset key - * @return NULL - * @param $key string - **/ - abstract function clear($key); - - /** - * Convenience method for checking property value - * @return mixed - * @param $key string - **/ - function offsetexists($key) { - return Base::instance()->visible($this,$key)? - isset($this->$key):$this->exists($key); - } - - /** - * Convenience method for assigning property value - * @return mixed - * @param $key string - * @param $val scalar - **/ - function offsetset($key,$val) { - return Base::instance()->visible($this,$key)? - ($this->$key=$val):$this->set($key,$val); - } - - /** - * Convenience method for retrieving property value - * @return mixed - * @param $key string - **/ - function &offsetget($key) { - if (Base::instance()->visible($this,$key)) - $val=&$this->$key; - else - $val=&$this->get($key); - return $val; - } - - /** - * Convenience method for removing property value - * @return NULL - * @param $key string - **/ - function offsetunset($key) { - if (Base::instance()->visible($this,$key)) - unset($this->$key); - else - $this->clear($key); - } - - /** - * Alias for offsetexists() - * @return mixed - * @param $key string - **/ - function __isset($key) { - return $this->offsetexists($key); - } - - /** - * Alias for offsetset() - * @return mixed - * @param $key string - * @param $val scalar - **/ - function __set($key,$val) { - return $this->offsetset($key,$val); - } - - /** - * Alias for offsetget() - * @return mixed - * @param $key string - **/ - function &__get($key) { - $val=&$this->offsetget($key); - return $val; - } - - /** - * Alias for offsetunset() - * @return NULL - * @param $key string - **/ - function __unset($key) { - $this->offsetunset($key); - } - -} diff --git a/app/lib/markdown.php b/app/lib/markdown.php deleted file mode 100644 index 9abcd183..00000000 --- a/app/lib/markdown.php +++ /dev/null @@ -1,572 +0,0 @@ -. - -*/ - -//! Markdown-to-HTML converter -class Markdown extends Prefab { - - protected - //! Parsing rules - $blocks, - //! Special characters - $special; - - /** - * Process blockquote - * @return string - * @param $str string - **/ - protected function _blockquote($str) { - $str=preg_replace('/(?<=^|\n)\h?>\h?(.*?(?:\n+|$))/','\1',$str); - return strlen($str)? - ('
'.$this->build($str).'
'."\n\n"):''; - } - - /** - * Process whitespace-prefixed code block - * @return string - * @param $str string - **/ - protected function _pre($str) { - $str=preg_replace('/(?<=^|\n)(?: {4}|\t)(.+?(?:\n+|$))/','\1', - $this->esc($str)); - return strlen($str)? - ('
'.
-				$this->esc($this->snip($str)).
-			'
'."\n\n"): - ''; - } - - /** - * Process fenced code block - * @return string - * @param $hint string - * @param $str string - **/ - protected function _fence($hint,$str) { - $str=$this->snip($str); - $fw=Base::instance(); - if ($fw->HIGHLIGHT) { - switch (strtolower($hint)) { - case 'php': - $str=$fw->highlight($str); - break; - case 'apache': - preg_match_all('/(?<=^|\n)(\h*)'. - '(?:(<\/?)(\w+)((?:\h+[^>]+)*)(>)|'. - '(?:(\w+)(\h.+?)))(\h*(?:\n+|$))/', - $str,$matches,PREG_SET_ORDER); - $out=''; - foreach ($matches as $match) - $out.=$match[1]. - ($match[3]? - (''. - $this->esc($match[2]).$match[3]. - ''. - ($match[4]? - (''. - $this->esc($match[4]). - ''): - ''). - ''. - $this->esc($match[5]). - ''): - (''. - $match[6]. - ''. - ''. - $this->esc($match[7]). - '')). - $match[8]; - $str=''.$out.''; - break; - case 'html': - preg_match_all( - '/(?:(?:<(\/?)(\w+)'. - '((?:\h+(?:\w+\h*=\h*)?".+?"|[^>]+)*|'. - '\h+.+?)(\h*\/?)>)|(.+?))/s', - $str,$matches,PREG_SET_ORDER - ); - $out=''; - foreach ($matches as $match) { - if ($match[2]) { - $out.='<'. - $match[1].$match[2].''; - if ($match[3]) { - preg_match_all( - '/(?:\h+(?:(?:(\w+)\h*=\h*)?'. - '(".+?")|(.+)))/', - $match[3],$parts,PREG_SET_ORDER - ); - foreach ($parts as $part) - $out.=' '. - (empty($part[3])? - ((empty($part[1])? - '': - (''. - $part[1].'=')). - ''. - $part[2].''): - (''. - $part[3].'')); - } - $out.=''. - $match[4].'>'; - } - else - $out.=$this->esc($match[5]); - } - $str=''.$out.''; - break; - case 'ini': - preg_match_all( - '/(?<=^|\n)(?:'. - '(;[^\n]*)|(?:<\?php.+?\?>?)|'. - '(?:\[(.+?)\])|'. - '(.+?)\h*=\h*'. - '((?:\\\\\h*\r?\n|.+?)*)'. - ')((?:\r?\n)+|$)/', - $str,$matches,PREG_SET_ORDER - ); - $out=''; - foreach ($matches as $match) { - if ($match[1]) - $out.=''.$match[1]. - ''; - elseif ($match[2]) - $out.='['.$match[2].']'. - ''; - elseif ($match[3]) - $out.=''.$match[3]. - '='. - ($match[4]? - (''. - $match[4].''):''); - else - $out.=$match[0]; - if (isset($match[5])) - $out.=$match[5]; - } - $str=''.$out.''; - break; - default: - $str=''.$this->esc($str).''; - break; - } - } - else - $str=''.$this->esc($str).''; - return '
'.$str.'
'."\n\n"; - } - - /** - * Process horizontal rule - * @return string - **/ - protected function _hr() { - return '
'."\n\n"; - } - - /** - * Process atx-style heading - * @return string - * @param $type string - * @param $str string - **/ - protected function _atx($type,$str) { - $level=strlen($type); - return ''. - $this->scan($str).''."\n\n"; - } - - /** - * Process setext-style heading - * @return string - * @param $str string - * @param $type string - **/ - protected function _setext($str,$type) { - $level=strpos('=-',$type)+1; - return ''. - $this->scan($str).''."\n\n"; - } - - /** - * Process ordered/unordered list - * @return string - * @param $str string - **/ - protected function _li($str) { - // Initialize list parser - $len=strlen($str); - $ptr=0; - $dst=''; - $first=TRUE; - $tight=TRUE; - $type='ul'; - // Main loop - while ($ptr<$len) { - if (preg_match('/^\h*[*-](?:\h?[*-]){2,}(?:\n+|$)/', - substr($str,$ptr),$match)) { - $ptr+=strlen($match[0]); - // Embedded horizontal rule - return (strlen($dst)? - ('<'.$type.'>'."\n".$dst.''."\n\n"):''). - '
'."\n\n".$this->build(substr($str,$ptr)); - } - elseif (preg_match('/(?<=^|\n)([*+-]|\d+\.)\h'. - '(.+?(?:\n+|$))((?:(?: {4}|\t)+.+?(?:\n+|$))*)/s', - substr($str,$ptr),$match)) { - $match[3]=preg_replace('/(?<=^|\n)(?: {4}|\t)/','',$match[3]); - $found=FALSE; - foreach (array_slice($this->blocks,0,-1) as $regex) - if (preg_match($regex,$match[3])) { - $found=TRUE; - break; - } - // List - if ($first) { - // First pass - if (is_numeric($match[1])) - $type='ol'; - if (preg_match('/\n{2,}$/',$match[2]. - ($found?'':$match[3]))) - // Loose structure; Use paragraphs - $tight=FALSE; - $first=FALSE; - } - // Strip leading whitespaces - $ptr+=strlen($match[0]); - $tmp=$this->snip($match[2].$match[3]); - if ($tight) { - if ($found) - $tmp=$match[2].$this->build($this->snip($match[3])); - } - else - $tmp=$this->build($tmp); - $dst.='
  • '.$this->scan(trim($tmp)).'
  • '."\n"; - } - } - return strlen($dst)? - ('<'.$type.'>'."\n".$dst.''."\n\n"):''; - } - - /** - * Ignore raw HTML - * @return string - * @param $str string - **/ - protected function _raw($str) { - return $str; - } - - /** - * Process paragraph - * @return string - * @param $str string - **/ - protected function _p($str) { - $str=trim($str); - if (strlen($str)) { - if (preg_match('/^(.+?\n)([>#].+)$/s',$str,$parts)) - return $this->_p($parts[1]).$this->build($parts[2]); - $str=preg_replace_callback( - '/([^<>\[]+)?(<[\?%].+?[\?%]>|<.+?>|\[.+?\]\s*\(.+?\))|'. - '(.+)/s', - function($expr) { - $tmp=''; - if (isset($expr[4])) - $tmp.=$this->esc($expr[4]); - else { - if (isset($expr[1])) - $tmp.=$this->esc($expr[1]); - $tmp.=$expr[2]; - if (isset($expr[3])) - $tmp.=$this->esc($expr[3]); - } - return $tmp; - }, - $str - ); - $str=preg_replace('/\s{2}\r?\n/','
    ',$str); - return '

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

    '."\n\n"; - } - return ''; - } - - /** - * Process strong/em/strikethrough spans - * @return string - * @param $str string - **/ - protected function _text($str) { - $tmp=''; - while ($str!=$tmp) - $str=preg_replace_callback( - '/(?<=\s|^)(?'.$expr[2].'
    '; - case 2: - return ''.$expr[2].''; - case 3: - return ''.$expr[2].''; - } - }, - preg_replace( - '/(?\1', - $tmp=$str - ) - ); - return $str; - } - - /** - * Process image span - * @return string - * @param $str string - **/ - protected function _img($str) { - return preg_replace_callback( - '/!(?:\[(.+?)\])?\h*\(?(?:\h*"(.*?)"\h*)?\)/', - function($expr) { - return ''.$this->esc($expr[1]).''; - }, - $str - ); - } - - /** - * Process anchor span - * @return string - * @param $str string - **/ - protected function _a($str) { - return preg_replace_callback( - '/(??(?:\h*"(.*?)"\h*)?\)/', - function($expr) { - return ''.$this->scan($expr[1]).''; - }, - $str - ); - } - - /** - * Auto-convert links - * @return string - * @param $str string - **/ - protected function _auto($str) { - return preg_replace_callback( - '/`.*?<(.+?)>.*?`|<(.+?)>/', - function($expr) { - if (empty($expr[1]) && parse_url($expr[2],PHP_URL_SCHEME)) { - $expr[2]=$this->esc($expr[2]); - return ''.$expr[2].''; - } - return $expr[0]; - }, - $str - ); - } - - /** - * Process code span - * @return string - * @param $str string - **/ - protected function _code($str) { - return preg_replace_callback( - '/`` (.+?) ``|(?'. - $this->esc(empty($expr[1])?$expr[2]:$expr[1]).''; - }, - $str - ); - } - - /** - * Convert characters to HTML entities - * @return string - * @param $str string - **/ - function esc($str) { - if (!$this->special) - $this->special=[ - '...'=>'…', - '(tm)'=>'™', - '(r)'=>'®', - '(c)'=>'©' - ]; - foreach ($this->special as $key=>$val) - $str=preg_replace('/'.preg_quote($key,'/').'/i',$val,$str); - return htmlspecialchars($str,ENT_COMPAT, - Base::instance()->ENCODING,FALSE); - } - - /** - * Reduce multiple line feeds - * @return string - * @param $str string - **/ - protected function snip($str) { - return preg_replace('/(?:(?<=\n)\n+)|\n+$/',"\n",$str); - } - - /** - * Scan line for convertible spans - * @return string - * @param $str string - **/ - function scan($str) { - $inline=['img','a','text','auto','code']; - foreach ($inline as $func) - $str=$this->{'_'.$func}($str); - return $str; - } - - /** - * Assemble blocks - * @return string - * @param $str string - **/ - protected function build($str) { - if (!$this->blocks) { - // Regexes for capturing entire blocks - $this->blocks=[ - 'blockquote'=>'/^(?:\h?>\h?.*?(?:\n+|$))+/', - 'pre'=>'/^(?:(?: {4}|\t).+?(?:\n+|$))+/', - 'fence'=>'/^`{3}\h*(\w+)?.*?[^\n]*\n+(.+?)`{3}[^\n]*'. - '(?:\n+|$)/s', - 'hr'=>'/^\h*[*_-](?:\h?[\*_-]){2,}\h*(?:\n+|$)/', - 'atx'=>'/^\h*(#{1,6})\h?(.+?)\h*(?:#.*)?(?:\n+|$)/', - 'setext'=>'/^\h*(.+?)\h*\n([=-])+\h*(?:\n+|$)/', - 'li'=>'/^(?:(?:[*+-]|\d+\.)\h.+?(?:\n+|$)'. - '(?:(?: {4}|\t)+.+?(?:\n+|$))*)+/s', - 'raw'=>'/^((?:|'. - '<(address|article|aside|audio|blockquote|canvas|dd|'. - 'div|dl|fieldset|figcaption|figure|footer|form|h\d|'. - 'header|hgroup|hr|noscript|object|ol|output|p|pre|'. - 'section|table|tfoot|ul|video).*?'. - '(?:\/>|>(?:(?>[^><]+)|(?R))*<\/\2>))'. - '\h*(?:\n{2,}|\n*$)|<[\?%].+?[\?%]>\h*(?:\n?$|\n*))/s', - 'p'=>'/^(.+?(?:\n{2,}|\n*$))/s' - ]; - } - // Treat lines with nothing but whitespaces as empty lines - $str=preg_replace('/\n\h+(?=\n)/',"\n",$str); - // Initialize block parser - $len=strlen($str); - $ptr=0; - $dst=''; - // Main loop - while ($ptr<$len) { - if (preg_match('/^ {0,3}\[([^\[\]]+)\]:\s*?\s*'. - '(?:"([^\n]*)")?(?:\n+|$)/s',substr($str,$ptr),$match)) { - // Reference-style link; Backtrack - $ptr+=strlen($match[0]); - $tmp=''; - // Catch line breaks in title attribute - $ref=preg_replace('/\h/','\s',preg_quote($match[1],'/')); - while ($dst!=$tmp) { - $dst=preg_replace_callback( - '/(?esc($match[2]).'"'. - (empty($match[3])? - '': - (' title="'. - $this->esc($match[3]).'"')).'>'. - // Link - $this->scan( - empty($expr[3])? - (empty($expr[1])? - $expr[4]: - $expr[1]): - $expr[3] - ).''): - // Image - (''.
-										$this->esc($expr[3]).''); - }, - $tmp=$dst - ); - } - } - else - foreach ($this->blocks as $func=>$regex) - if (preg_match($regex,substr($str,$ptr),$match)) { - $ptr+=strlen($match[0]); - $dst.=call_user_func_array( - [$this,'_'.$func], - count($match)>1?array_slice($match,1):$match - ); - break; - } - } - return $dst; - } - - /** - * Render HTML equivalent of markdown - * @return string - * @param $txt string - **/ - function convert($txt) { - $txt=preg_replace_callback( - '/(.+?<\/code>|'. - '<[^>\n]+>|\([^\n\)]+\)|"[^"\n]+")|'. - '\\\\(.)/s', - function($expr) { - // Process escaped characters - return empty($expr[1])?$expr[2]:$expr[1]; - }, - $this->build(preg_replace('/\r\n|\r/',"\n",$txt)) - ); - return $this->snip($txt); - } - -} diff --git a/app/lib/matrix.php b/app/lib/matrix.php deleted file mode 100644 index d643f492..00000000 --- a/app/lib/matrix.php +++ /dev/null @@ -1,113 +0,0 @@ -. - -*/ - -//! Generic array utilities -class Matrix extends Prefab { - - /** - * Retrieve values from a specified column of a multi-dimensional - * array variable - * @return array - * @param $var array - * @param $col mixed - **/ - function pick(array $var,$col) { - return array_map( - function($row) use($col) { - return $row[$col]; - }, - $var - ); - } - - /** - * Rotate a two-dimensional array variable - * @return NULL - * @param $var array - **/ - function transpose(array &$var) { - $out=[]; - foreach ($var as $keyx=>$cols) - foreach ($cols as $keyy=>$valy) - $out[$keyy][$keyx]=$valy; - $var=$out; - } - - /** - * Sort a multi-dimensional array variable on a specified column - * @return bool - * @param $var array - * @param $col mixed - * @param $order int - **/ - function sort(array &$var,$col,$order=SORT_ASC) { - uasort( - $var, - function($val1,$val2) use($col,$order) { - list($v1,$v2)=[$val1[$col],$val2[$col]]; - $out=is_numeric($v1) && is_numeric($v2)? - Base::instance()->sign($v1-$v2):strcmp($v1,$v2); - if ($order==SORT_DESC) - $out=-$out; - return $out; - } - ); - $var=array_values($var); - } - - /** - * Change the key of a two-dimensional array element - * @return NULL - * @param $var array - * @param $old string - * @param $new string - **/ - function changekey(array &$var,$old,$new) { - $keys=array_keys($var); - $vals=array_values($var); - $keys[array_search($old,$keys)]=$new; - $var=array_combine($keys,$vals); - } - - /** - * Return month calendar of specified date, with optional setting for - * first day of week (0 for Sunday) - * @return array - * @param $date string|int - * @param $first int - **/ - function calendar($date='now',$first=0) { - $out=FALSE; - if (extension_loaded('calendar')) { - if (is_string($date)) - $date=strtotime($date); - $parts=getdate($date); - $days=cal_days_in_month(CAL_GREGORIAN,$parts['mon'],$parts['year']); - $ref=date('w',strtotime(date('Y-m',$parts[0]).'-01'))+(7-$first)%7; - $out=[]; - for ($i=0;$i<$days;$i++) - $out[floor(($ref+$i)/7)][($ref+$i)%7]=$i+1; - } - return $out; - } - -} diff --git a/app/lib/session.php b/app/lib/session.php deleted file mode 100644 index 5ab88948..00000000 --- a/app/lib/session.php +++ /dev/null @@ -1,196 +0,0 @@ -. - -*/ - -//! Cache-based session handler -class Session { - - protected - //! Session ID - $sid, - //! Anti-CSRF token - $_csrf, - //! User agent - $_agent, - //! IP, - $_ip, - //! Suspect callback - $onsuspect, - //! Cache instance - $_cache; - - /** - * Open session - * @return TRUE - * @param $path string - * @param $name string - **/ - function open($path,$name) { - return TRUE; - } - - /** - * Close session - * @return TRUE - **/ - function close() { - $this->sid=NULL; - return TRUE; - } - - /** - * Return session data in serialized format - * @return string - * @param $id string - **/ - function read($id) { - $this->sid=$id; - if (!$data=$this->_cache->get($id.'.@')) - return ''; - if ($data['ip']!=$this->_ip || $data['agent']!=$this->_agent) { - $fw=Base::instance(); - if (!isset($this->onsuspect) || - $fw->call($this->onsuspect,[$this,$id])===FALSE) { - //NB: `session_destroy` can't be called at that stage (`session_start` not completed) - $this->destroy($id); - $this->close(); - unset($fw->{'COOKIE.'.session_name()}); - $fw->error(403); - } - } - return $data['data']; - } - - /** - * Write session data - * @return TRUE - * @param $id string - * @param $data string - **/ - function write($id,$data) { - $fw=Base::instance(); - $jar=$fw->JAR; - $this->_cache->set($id.'.@', - [ - 'data'=>$data, - 'ip'=>$this->_ip, - 'agent'=>$this->_agent, - 'stamp'=>time() - ], - $jar['expire'] - ); - return TRUE; - } - - /** - * Destroy session - * @return TRUE - * @param $id string - **/ - function destroy($id) { - $this->_cache->clear($id.'.@'); - return TRUE; - } - - /** - * Garbage collector - * @return TRUE - * @param $max int - **/ - function cleanup($max) { - $this->_cache->reset('.@',$max); - return TRUE; - } - - /** - * Return session id (if session has started) - * @return string|NULL - **/ - function sid() { - return $this->sid; - } - - /** - * Return anti-CSRF token - * @return string - **/ - function csrf() { - return $this->_csrf; - } - - /** - * Return IP address - * @return string - **/ - function ip() { - return $this->_ip; - } - - /** - * Return Unix timestamp - * @return string|FALSE - **/ - function stamp() { - if (!$this->sid) - session_start(); - return $this->_cache->exists($this->sid.'.@',$data)? - $data['stamp']:FALSE; - } - - /** - * Return HTTP user agent - * @return string - **/ - function agent() { - return $this->_agent; - } - - /** - * Instantiate class - * @param $onsuspect callback - * @param $key string - **/ - function __construct($onsuspect=NULL,$key=NULL,$cache=null) { - $this->onsuspect=$onsuspect; - $this->_cache=$cache?:Cache::instance(); - session_set_save_handler( - [$this,'open'], - [$this,'close'], - [$this,'read'], - [$this,'write'], - [$this,'destroy'], - [$this,'cleanup'] - ); - register_shutdown_function('session_commit'); - $fw=\Base::instance(); - $headers=$fw->HEADERS; - $this->_csrf=$fw->hash($fw->SEED. - extension_loaded('openssl')? - implode(unpack('L',openssl_random_pseudo_bytes(4))): - mt_rand() - ); - if ($key) - $fw->$key=$this->_csrf; - $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; - $this->_ip=$fw->IP; - } - -} diff --git a/app/lib/sheet.php b/app/lib/sheet.php deleted file mode 100644 index f9f9ec48..00000000 --- a/app/lib/sheet.php +++ /dev/null @@ -1,216 +0,0 @@ -read($filepath,true); - - if(!preg_match_all('/((?:.*?)'.$delimiter.'(?:'.$enclosure.'.*?'. - $enclosure.'|['.$delimiter.'(?:\d|\.|\/)*\d])*\n)/s',$data,$matches)) - user_error('no rows found'); - - $out = array_map(function($val) use($delimiter,$enclosure) { - return str_getcsv($val,$delimiter,$enclosure); - },$matches[0]); - return $out; - } - - /** - * use specified headers or first row as label for each row item key - * @param $rows - * @param null $headers - * @return array - */ - public function applyHeader($rows,$headers=null) { - if (!$headers) - $headers=array_shift($rows); - return array_map(function($row) use($headers) { - return array_combine(array_values($headers),array_values($row)); - },$rows); - } - - /** - * build and return xls file data - * @param $rows - * @param $headers - * @return string - */ - public function dumpXLS($rows,$headers) { - $numColumns = count($headers); - $numRows = count($rows); - foreach($headers as $key=>$val) - if (is_numeric($key)) { - $headers[$val]=ucfirst($val); - unset($headers[$key]); - } - $xls = $this->xlsBOF(); - for ($i = 0; $i <= $numRows; $i++) { - for ($c = 0; $c <= $numColumns; $c++) { - $ckey = key($headers); - $val=''; - if ($i==0) - $val = current($headers); - elseif (isset($rows[$i-1][$ckey])) - $val = trim($rows[$i-1][$ckey]); - if (is_array($val)) - $val = json_encode($val); - $xls.= (is_int($val) - || (ctype_digit($val) && ($val[0]!='0' && strlen($val)>1))) - ? $this->xlsWriteNumber($i,$c,$val) - : $this->xlsWriteString($i,$c,utf8_decode($val)); - next($headers); - } - reset($headers); - } - $xls .= $this->xlsEOF(); - return $xls; - } - - /** - * render xls file and send to HTTP client - * @param $rows - * @param $headers - * @param $filename - */ - function renderXLS($rows,$headers,$filename) { - $data = $this->dumpXLS($rows,$headers); - header("Expires: 0"); - header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); - header('Content-Type: application/xls'); - header("Content-Disposition: attachment;filename=".$filename); - header("Content-Transfer-Encoding: binary"); - echo $data; - exit(); - } - - /** - * start file - * @return string - */ - protected function xlsBOF() { - return pack("ssssss", 0x809, 0x8, 0x0, 0x10, 0x0, 0x0); - } - - /** - * end file - * @return string - */ - protected function xlsEOF() { - return pack("ss", 0x0A, 0x00); - } - - /** - * put number - * @param $row - * @param $col - * @param $val - * @return string - */ - protected function xlsWriteNumber($row, $col, $val) { - $out = pack("sssss", 0x203, 14, $row, $col, 0x0); - $out.= pack("d", $val); - return $out; - } - - /** - * put string - * @param $row - * @param $col - * @param $val - * @return string - */ - protected function xlsWriteString($row, $col, $val ) { - $l = strlen($val); - $out = pack("ssssss", 0x204, 8+$l, $row, $col, 0x0, $l); - $out.= $val; - return $out; - } - - /** - * build and return CSV data sheet - * @param $rows - * @param $headers - * @param string $delimiter - * @param string $enclosure - * @param bool $encloseAll - * @return string - */ - public function dumpCSV($rows,$headers,$delimiter=';',$enclosure='"',$encloseAll=true) { - $numColumns = count($headers); - $numRows = count($rows); - foreach($headers as $key=>$val) - if (is_numeric($key)) { - $headers[$val]=ucfirst($val); - unset($headers[$key]); - } - $out = array(); - for ($i = 0; $i <= $numRows; $i++) { - $line = array(); - for ($c = 0; $c <= $numColumns; $c++) { - $ckey = key($headers); - $field=''; - if ($i==0) - $field = current($headers); - elseif (isset($rows[$i-1][$ckey])) - $field = trim($rows[$i-1][$ckey]); - if (is_array($field)) - $field = json_encode($field); - if (empty($field) && $field !== 0) - $line[] = ''; - elseif ($encloseAll || preg_match('/(?:'.preg_quote($delimiter, '/').'|'. - preg_quote($enclosure, '/').'|\s)/', $field)) - $line[] = $enclosure.str_replace($enclosure, $enclosure.$enclosure, $field).$enclosure; - else - $line[] = $field; - next($headers); - } - $out[] = implode($delimiter, $line); - reset($headers); - } - return implode("\n",$out); - } - - /** - * send CSV file to client - * @param $rows - * @param $headers - * @param $filename - */ - function renderCSV($rows,$headers,$filename) { - $data = $this->dumpCSV($rows,$headers); - header("Expires: 0"); - header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); - header('Content-Type: text/csv;charset=UTF-16LE'); - header("Content-Disposition: attachment;filename=".$filename); - header("Content-Transfer-Encoding: binary"); - echo "\xFF"."\xFE".mb_convert_encoding($data, 'UTF-16LE', 'UTF-8'); - exit(); - } - -} \ No newline at end of file diff --git a/app/lib/smtp.php b/app/lib/smtp.php deleted file mode 100644 index caaebd56..00000000 --- a/app/lib/smtp.php +++ /dev/null @@ -1,358 +0,0 @@ -. - -*/ - -//! SMTP plug-in -class SMTP extends Magic { - - //@{ Locale-specific error/exception messages - const - E_Header='%s: header is required', - E_Blank='Message must not be blank', - E_Attach='Attachment %s not found'; - //@} - - protected - //! Message properties - $headers, - //! E-mail attachments - $attachments, - //! SMTP host - $host, - //! SMTP port - $port, - //! TLS/SSL - $scheme, - //! User ID - $user, - //! Password - $pw, - //! TLS/SSL stream context - $context, - //! TCP/IP socket - $socket, - //! Server-client conversation - $log; - - /** - * Fix header - * @return string - * @param $key string - **/ - protected function fixheader($key) { - return str_replace(' ','-', - ucwords(preg_replace('/[_-]/',' ',strtolower($key)))); - } - - /** - * Return TRUE if header exists - * @return bool - * @param $key - **/ - function exists($key) { - $key=$this->fixheader($key); - return isset($this->headers[$key]); - } - - /** - * Bind value to e-mail header - * @return string - * @param $key string - * @param $val string - **/ - function set($key,$val) { - $key=$this->fixheader($key); - return $this->headers[$key]=$val; - } - - /** - * Return value of e-mail header - * @return string|NULL - * @param $key string - **/ - function &get($key) { - $key=$this->fixheader($key); - if (isset($this->headers[$key])) - $val=&$this->headers[$key]; - else - $val=NULL; - return $val; - } - - /** - * Remove header - * @return NULL - * @param $key string - **/ - function clear($key) { - $key=$this->fixheader($key); - unset($this->headers[$key]); - } - - /** - * Return client-server conversation history - * @return string - **/ - function log() { - return str_replace("\n",PHP_EOL,$this->log); - } - - /** - * Send SMTP command and record server response - * @return string - * @param $cmd string - * @param $log bool|string - * @param $mock bool - **/ - protected function dialog($cmd=NULL,$log=TRUE,$mock=FALSE) { - $reply=''; - if ($mock) { - $host=str_replace('ssl://','',$this->host); - switch ($cmd) { - case NULL: - $reply='220 '.$host.' ESMTP ready'."\n"; - break; - case 'DATA': - $reply='354 Go ahead'."\n"; - break; - case 'QUIT': - $reply='221 '.$host.' closing connection'."\n"; - break; - default: - $reply='250 OK'."\n"; - break; - } - } - else { - $socket=&$this->socket; - if ($cmd) - fputs($socket,$cmd."\r\n"); - while (!feof($socket) && ($info=stream_get_meta_data($socket)) && - !$info['timed_out'] && $str=fgets($socket,4096)) { - $reply.=$str; - if (preg_match('/(?:^|\n)\d{3} .+?\r\n/s',$reply)) - break; - } - } - if ($log) { - if ($cmd) - $this->log.=$cmd."\n"; - $this->log.=str_replace("\r",'',$reply); - } - return $reply; - } - - /** - * Add e-mail attachment - * @return NULL - * @param $file string - * @param $alias string - * @param $cid string - **/ - function attach($file,$alias=NULL,$cid=NULL) { - if (!is_file($file)) - user_error(sprintf(self::E_Attach,$file),E_USER_ERROR); - if ($alias) - $file=[$alias,$file]; - $this->attachments[]=['filename'=>$file,'cid'=>$cid]; - } - - /** - * Transmit message - * @return bool - * @param $message string - * @param $log bool|string - * @param $mock bool - **/ - function send($message,$log=TRUE,$mock=FALSE) { - if ($this->scheme=='ssl' && !extension_loaded('openssl')) - return FALSE; - // Message should not be blank - if (!$message) - user_error(self::E_Blank,E_USER_ERROR); - $fw=Base::instance(); - // Retrieve headers - $headers=$this->headers; - // Connect to the server - if (!$mock) { - $socket=&$this->socket; - $socket=@stream_socket_client($this->host.':'.$this->port, - $errno,$errstr,ini_get('default_socket_timeout'), - STREAM_CLIENT_CONNECT,$this->context); - if (!$socket) { - $fw->error(500,$errstr); - return FALSE; - } - stream_set_blocking($socket,TRUE); - } - // Get server's initial response - $this->dialog(NULL,$log,$mock); - // Announce presence - $reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock); - if (strtolower($this->scheme)=='tls') { - $this->dialog('STARTTLS',$log,$mock); - if (!$mock) { - $method=STREAM_CRYPTO_METHOD_TLS_CLIENT; - if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { - $method|=STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; - $method|=STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; - } - stream_socket_enable_crypto($socket,TRUE,$method); - } - $reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock); - } - $message=wordwrap($message,998); - if (preg_match('/8BITMIME/',$reply)) - $headers['Content-Transfer-Encoding']='8bit'; - else { - $headers['Content-Transfer-Encoding']='quoted-printable'; - $message=preg_replace('/^\.(.+)/m', - '..$1',quoted_printable_encode($message)); - } - if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) { - // Authenticate - $this->dialog('AUTH LOGIN',$log,$mock); - $this->dialog(base64_encode($this->user),$log,$mock); - $reply=$this->dialog(base64_encode($this->pw),$log,$mock); - if (!preg_match('/^235\s.*/',$reply)) { - $this->dialog('QUIT',$log,$mock); - if (!$mock && $socket) - fclose($socket); - return FALSE; - } - } - if (empty($headers['Message-Id'])) - $headers['Message-Id']='<'.uniqid('',TRUE).'@'.$this->host.'>'; - if (empty($headers['Date'])) - $headers['Date']=date('r'); - // Required headers - $reqd=['From','To','Subject']; - foreach ($reqd as $id) - if (empty($headers[$id])) - user_error(sprintf(self::E_Header,$id),E_USER_ERROR); - $eol="\r\n"; - // Stringify headers - foreach ($headers as $key=>&$val) { - if (in_array($key,['From','To','Cc','Bcc'])) { - $email=''; - preg_match_all('/(?:".+?" )?(?:<.+?>|[^ ,]+)/', - $val,$matches,PREG_SET_ORDER); - foreach ($matches as $raw) - $email.=($email?', ':''). - (preg_match('/<.+?>/',$raw[0])? - $raw[0]: - ('<'.$raw[0].'>')); - $val=$email; - } - unset($val); - } - // Start message dialog - $this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),$log,$mock); - foreach ($fw->split($headers['To']. - (isset($headers['Cc'])?(';'.$headers['Cc']):''). - (isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst) { - $this->dialog('RCPT TO: '.strstr($dst,'<'),$log,$mock); - } - $this->dialog('DATA',$log,$mock); - if ($this->attachments) { - // Replace Content-Type - $type=$headers['Content-Type']; - unset($headers['Content-Type']); - $enc=$headers['Content-Transfer-Encoding']; - unset($headers['Content-Transfer-Encoding']); - $hash=uniqid(NULL,TRUE); - // Send mail headers - $out='Content-Type: multipart/mixed; boundary="'.$hash.'"'.$eol; - foreach ($headers as $key=>$val) - if ($key!='Bcc') - $out.=$key.': '.$val.$eol; - $out.=$eol; - $out.='This is a multi-part message in MIME format'.$eol; - $out.=$eol; - $out.='--'.$hash.$eol; - $out.='Content-Type: '.$type.$eol; - $out.='Content-Transfer-Encoding: '.$enc.$eol; - $out.=$eol; - $out.=$message.$eol; - foreach ($this->attachments as $attachment) { - if (is_array($attachment['filename'])) - list($alias,$file)=$attachment['filename']; - else - $alias=basename($file=$attachment['filename']); - $out.='--'.$hash.$eol; - $out.='Content-Type: application/octet-stream'.$eol; - $out.='Content-Transfer-Encoding: base64'.$eol; - if ($attachment['cid']) - $out.='Content-Id: '.$attachment['cid'].$eol; - $out.='Content-Disposition: attachment; '. - 'filename="'.$alias.'"'.$eol; - $out.=$eol; - $out.=chunk_split(base64_encode( - file_get_contents($file))).$eol; - } - $out.=$eol; - $out.='--'.$hash.'--'.$eol; - $out.='.'; - $this->dialog($out,preg_match('/verbose/i',$log),$mock); - } - else { - // Send mail headers - $out=''; - foreach ($headers as $key=>$val) - if ($key!='Bcc') - $out.=$key.': '.$val.$eol; - $out.=$eol; - $out.=$message.$eol; - $out.='.'; - // Send message - $this->dialog($out,preg_match('/verbose/i',$log),$mock); - } - $this->dialog('QUIT',$log,$mock); - if (!$mock && $socket) - fclose($socket); - return TRUE; - } - - /** - * Instantiate class - * @param $host string - * @param $port int - * @param $scheme string - * @param $user string - * @param $pw string - * @param $ctx resource - **/ - function __construct( - $host='localhost',$port=25,$scheme=NULL,$user=NULL,$pw=NULL,$ctx=NULL) { - $this->headers=[ - 'MIME-Version'=>'1.0', - 'Content-Type'=>'text/plain; '. - 'charset='.Base::instance()->ENCODING - ]; - $this->host=strtolower((($this->scheme=strtolower($scheme))=='ssl'? - 'ssl':'tcp').'://'.$host); - $this->port=$port; - $this->user=$user; - $this->pw=$pw; - $this->context=stream_context_create($ctx); - } - -} diff --git a/app/lib/template.php b/app/lib/template.php deleted file mode 100644 index 0e130344..00000000 --- a/app/lib/template.php +++ /dev/null @@ -1,353 +0,0 @@ -. - -*/ - -//! XML-style template engine -class Template extends Preview { - - //@{ Error messages - const - E_Method='Call to undefined method %s()'; - //@} - - protected - //! Template tags - $tags, - //! Custom tag handlers - $custom=[]; - - /** - * Template -set- tag handler - * @return string - * @param $node array - **/ - protected function _set(array $node) { - $out=''; - foreach ($node['@attrib'] as $key=>$val) - $out.='$'.$key.'='. - (preg_match('/\{\{(.+?)\}\}/',$val)? - $this->token($val): - Base::instance()->stringify($val)).'; '; - return ''; - } - - /** - * Template -include- tag handler - * @return string - * @param $node array - **/ - protected function _include(array $node) { - $attrib=$node['@attrib']; - $hive=isset($attrib['with']) && - ($attrib['with']=$this->token($attrib['with'])) && - preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/', - $attrib['with'],$pairs,PREG_SET_ORDER)? - ('['.implode(',', - array_map(function($pair) { - return '\''.$pair[1].'\'=>'. - (preg_match('/^\'.*\'$/',$pair[2]) || - preg_match('/\$/',$pair[2])? - $pair[2]:Base::instance()->stringify( - Base::instance()->cast($pair[2]))); - },$pairs)).']+get_defined_vars()'): - 'get_defined_vars()'; - $ttl=isset($attrib['ttl'])?(int)$attrib['ttl']:0; - return - 'token($attrib['if']).') '):''). - ('echo $this->render('. - (preg_match('/^\{\{(.+?)\}\}$/',$attrib['href'])? - $this->token($attrib['href']): - Base::instance()->stringify($attrib['href'])).','. - 'NULL,'.$hive.','.$ttl.'); ?>'); - } - - /** - * Template -exclude- tag handler - * @return string - **/ - protected function _exclude() { - return ''; - } - - /** - * Template -ignore- tag handler - * @return string - * @param $node array - **/ - protected function _ignore(array $node) { - return $node[0]; - } - - /** - * Template -loop- tag handler - * @return string - * @param $node array - **/ - protected function _loop(array $node) { - $attrib=$node['@attrib']; - unset($node['@attrib']); - return - 'token($attrib['from']).';'. - $this->token($attrib['to']).';'. - $this->token($attrib['step']).'): ?>'. - $this->build($node). - ''; - } - - /** - * Template -repeat- tag handler - * @return string - * @param $node array - **/ - protected function _repeat(array $node) { - $attrib=$node['@attrib']; - unset($node['@attrib']); - return - 'token($attrib['counter'])).'=0; '):''). - 'foreach (('. - $this->token($attrib['group']).'?:[]) as '. - (isset($attrib['key'])? - ($this->token($attrib['key']).'=>'):''). - $this->token($attrib['value']).'):'. - (isset($ctr)?(' '.$ctr.'++;'):'').' ?>'. - $this->build($node). - ''; - } - - /** - * Template -check- tag handler - * @return string - * @param $node array - **/ - protected function _check(array $node) { - $attrib=$node['@attrib']; - unset($node['@attrib']); - // Grab and blocks - foreach ($node as $pos=>$block) - if (isset($block['true'])) - $true=[$pos,$block]; - elseif (isset($block['false'])) - $false=[$pos,$block]; - if (isset($true,$false) && $true[0]>$false[0]) - // Reverse and blocks - list($node[$true[0]],$node[$false[0]])=[$false[1],$true[1]]; - return - 'token($attrib['if']).'): ?>'. - $this->build($node). - ''; - } - - /** - * Template -true- tag handler - * @return string - * @param $node array - **/ - protected function _true(array $node) { - return $this->build($node); - } - - /** - * Template -false- tag handler - * @return string - * @param $node array - **/ - protected function _false(array $node) { - return ''.$this->build($node); - } - - /** - * Template -switch- tag handler - * @return string - * @param $node array - **/ - protected function _switch(array $node) { - $attrib=$node['@attrib']; - unset($node['@attrib']); - foreach ($node as $pos=>$block) - if (is_string($block) && !preg_replace('/\s+/','',$block)) - unset($node[$pos]); - return - 'token($attrib['expr']).'): ?>'. - $this->build($node). - ''; - } - - /** - * Template -case- tag handler - * @return string - * @param $node array - **/ - protected function _case(array $node) { - $attrib=$node['@attrib']; - unset($node['@attrib']); - return - 'token($attrib['value']): - Base::instance()->stringify($attrib['value'])).': ?>'. - $this->build($node). - 'token($attrib['break']).') ':''). - 'break; ?>'; - } - - /** - * Template -default- tag handler - * @return string - * @param $node array - **/ - protected function _default(array $node) { - return - ''. - $this->build($node). - ''; - } - - /** - * Assemble markup - * @return string - * @param $node array|string - **/ - function build($node) { - if (is_string($node)) - return parent::build($node); - $out=''; - foreach ($node as $key=>$val) - $out.=is_int($key)?$this->build($val):$this->{'_'.$key}($val); - return $out; - } - - /** - * Extend template with custom tag - * @return NULL - * @param $tag string - * @param $func callback - **/ - function extend($tag,$func) { - $this->tags.='|'.$tag; - $this->custom['_'.$tag]=$func; - } - - /** - * Call custom tag handler - * @return string|FALSE - * @param $func string - * @param $args array - **/ - function __call($func,array $args) { - if ($func[0]=='_') - return call_user_func_array($this->custom[$func],$args); - if (method_exists($this,$func)) - return call_user_func_array([$this,$func],$args); - user_error(sprintf(self::E_Method,$func),E_USER_ERROR); - } - - /** - * Parse string for template directives and tokens - * @return array - * @param $text string - **/ - function parse($text) { - $text=parent::parse($text); - // Build tree structure - for ($ptr=0,$w=5,$len=strlen($text),$tree=[],$tmp='';$ptr<$len;) - if (preg_match('/^(.{0,'.$w.'}?)<(\/?)(?:F3:)?'. - '('.$this->tags.')\b((?:\s+[\w.:@!-]+'. - '(?:\h*=\h*(?:"(?:.*?)"|\'(?:.*?)\'))?|'. - '\h*\{\{.+?\}\})*)\h*(\/?)>/is', - substr($text,$ptr),$match)) { - if (strlen($tmp) || $match[1]) - $tree[]=$tmp.$match[1]; - // Element node - if ($match[2]) { - // Find matching start tag - $stack=[]; - for($i=count($tree)-1;$i>=0;$i--) { - $item=$tree[$i]; - if (is_array($item) && - array_key_exists($match[3],$item) && - !isset($item[$match[3]][0])) { - // Start tag found - $tree[$i][$match[3]]+=array_reverse($stack); - $tree=array_slice($tree,0,$i+1); - break; - } - else $stack[]=$item; - } - } - else { - // Start tag - $node=&$tree[][$match[3]]; - $node=[]; - if ($match[4]) { - // Process attributes - preg_match_all( - '/(?:(\{\{.+?\}\})|([^\s\/"\'=]+))'. - '\h*(?:=\h*(?:"(.*?)"|\'(.*?)\'))?/s', - $match[4],$attr,PREG_SET_ORDER); - foreach ($attr as $kv) - if (!empty($kv[1]) && !isset($kv[3]) && !isset($kv[4])) - $node['@attrib'][]=$kv[1]; - else - $node['@attrib'][$kv[1]?:$kv[2]]= - (isset($kv[3]) && $kv[3]!==''? - $kv[3]: - (isset($kv[4]) && $kv[4]!==''? - $kv[4]:NULL)); - } - } - $tmp=''; - $ptr+=strlen($match[0]); - $w=5; - } - else { - // Text node - $tmp.=substr($text,$ptr,$w); - $ptr+=$w; - if ($w<50) - $w++; - } - if (strlen($tmp)) - // Append trailing text - $tree[]=$tmp; - // Break references - unset($node); - return $tree; - } - - /** - * Class constructor - * return object - **/ - function __construct() { - $ref=new ReflectionClass(get_called_class()); - $this->tags=''; - foreach ($ref->getmethods() as $method) - if (preg_match('/^_(?=[[:alpha:]])/',$method->name)) - $this->tags.=(strlen($this->tags)?'|':''). - substr($method->name,1); - parent::__construct(); - } - -} diff --git a/app/lib/test.php b/app/lib/test.php deleted file mode 100644 index 721faf76..00000000 --- a/app/lib/test.php +++ /dev/null @@ -1,96 +0,0 @@ -. - -*/ - -//! Unit test kit -class Test { - - //@{ Reporting level - const - FLAG_False=0, - FLAG_True=1, - FLAG_Both=2; - //@} - - protected - //! Test results - $data=[], - //! Success indicator - $passed=TRUE; - - /** - * Return test results - * @return array - **/ - function results() { - return $this->data; - } - - /** - * Return FALSE if at least one test case fails - * @return bool - **/ - function passed() { - return $this->passed; - } - - /** - * Evaluate condition and save test result - * @return object - * @param $cond bool - * @param $text string - **/ - function expect($cond,$text=NULL) { - $out=(bool)$cond; - if ($this->level==$out || $this->level==self::FLAG_Both) { - $data=['status'=>$out,'text'=>$text,'source'=>NULL]; - foreach (debug_backtrace() as $frame) - if (isset($frame['file'])) { - $data['source']=Base::instance()-> - fixslashes($frame['file']).':'.$frame['line']; - break; - } - $this->data[]=$data; - } - if (!$out && $this->passed) - $this->passed=FALSE; - return $this; - } - - /** - * Append message to test results - * @return NULL - * @param $text string - **/ - function message($text) { - $this->expect(TRUE,$text); - } - - /** - * Class constructor - * @return NULL - * @param $level int - **/ - function __construct($level=self::FLAG_Both) { - $this->level=$level; - } - -} diff --git a/app/lib/utf.php b/app/lib/utf.php deleted file mode 100644 index a010c66f..00000000 --- a/app/lib/utf.php +++ /dev/null @@ -1,199 +0,0 @@ -. - -*/ - -//! Unicode string manager -class UTF extends Prefab { - - /** - * Get string length - * @return int - * @param $str string - **/ - function strlen($str) { - preg_match_all('/./us',$str,$parts); - return count($parts[0]); - } - - /** - * Reverse a string - * @return string - * @param $str string - **/ - function strrev($str) { - preg_match_all('/./us',$str,$parts); - return implode('',array_reverse($parts[0])); - } - - /** - * Find position of first occurrence of a string (case-insensitive) - * @return int|FALSE - * @param $stack string - * @param $needle string - * @param $ofs int - **/ - function stripos($stack,$needle,$ofs=0) { - return $this->strpos($stack,$needle,$ofs,TRUE); - } - - /** - * Find position of first occurrence of a string - * @return int|FALSE - * @param $stack string - * @param $needle string - * @param $ofs int - * @param $case bool - **/ - function strpos($stack,$needle,$ofs=0,$case=FALSE) { - return preg_match('/^(.{'.$ofs.'}.*?)'. - preg_quote($needle,'/').'/us'.($case?'i':''),$stack,$match)? - $this->strlen($match[1]):FALSE; - } - - /** - * Returns part of haystack string from the first occurrence of - * needle to the end of haystack (case-insensitive) - * @return string|FALSE - * @param $stack string - * @param $needle string - * @param $before bool - **/ - function stristr($stack,$needle,$before=FALSE) { - return $this->strstr($stack,$needle,$before,TRUE); - } - - /** - * Returns part of haystack string from the first occurrence of - * needle to the end of haystack - * @return string|FALSE - * @param $stack string - * @param $needle string - * @param $before bool - * @param $case bool - **/ - function strstr($stack,$needle,$before=FALSE,$case=FALSE) { - if (!$needle) - return FALSE; - preg_match('/^(.*?)'.preg_quote($needle,'/').'/us'.($case?'i':''), - $stack,$match); - return isset($match[1])? - ($before? - $match[1]: - $this->substr($stack,$this->strlen($match[1]))): - FALSE; - } - - /** - * Return part of a string - * @return string|FALSE - * @param $str string - * @param $start int - * @param $len int - **/ - function substr($str,$start,$len=0) { - if ($start<0) - $start=$this->strlen($str)+$start; - if (!$len) - $len=$this->strlen($str)-$start; - return preg_match('/^.{'.$start.'}(.{0,'.$len.'})/us',$str,$match)? - $match[1]:FALSE; - } - - /** - * Count the number of substring occurrences - * @return int - * @param $stack string - * @param $needle string - **/ - function substr_count($stack,$needle) { - preg_match_all('/'.preg_quote($needle,'/').'/us',$stack, - $matches,PREG_SET_ORDER); - return count($matches); - } - - /** - * Strip whitespaces from the beginning of a string - * @return string - * @param $str string - **/ - function ltrim($str) { - return preg_replace('/^[\pZ\pC]+/u','',$str); - } - - /** - * Strip whitespaces from the end of a string - * @return string - * @param $str string - **/ - function rtrim($str) { - return preg_replace('/[\pZ\pC]+$/u','',$str); - } - - /** - * Strip whitespaces from the beginning and end of a string - * @return string - * @param $str string - **/ - function trim($str) { - return preg_replace('/^[\pZ\pC]+|[\pZ\pC]+$/u','',$str); - } - - /** - * Return UTF-8 byte order mark - * @return string - **/ - function bom() { - return chr(0xef).chr(0xbb).chr(0xbf); - } - - /** - * Convert code points to Unicode symbols - * @return string - * @param $str string - **/ - function translate($str) { - return html_entity_decode( - preg_replace('/\\\\u([[:xdigit:]]+)/i','&#x\1;',$str)); - } - - /** - * Translate emoji tokens to Unicode font-supported symbols - * @return string - * @param $str string - **/ - function emojify($str) { - $map=[ - ':('=>'\u2639', // frown - ':)'=>'\u263a', // smile - '<3'=>'\u2665', // heart - ':D'=>'\u1f603', // grin - 'XD'=>'\u1f606', // laugh - ';)'=>'\u1f609', // wink - ':P'=>'\u1f60b', // tongue - ':,'=>'\u1f60f', // think - ':/'=>'\u1f623', // skeptic - '8O'=>'\u1f632', // oops - ]+Base::instance()->EMOJI; - return $this->translate(str_replace(array_keys($map), - array_values($map),$str)); - } - -} diff --git a/app/lib/web.php b/app/lib/web.php deleted file mode 100644 index 9c1b546e..00000000 --- a/app/lib/web.php +++ /dev/null @@ -1,932 +0,0 @@ -. - -*/ - -//! Wrapper for various HTTP utilities -class Web extends Prefab { - - //@{ Error messages - const - E_Request='No suitable HTTP request engine found'; - //@} - - protected - //! HTTP request engine - $wrapper; - - /** - * Detect MIME type using file extension - * @return string - * @param $file string - **/ - function mime($file) { - if (preg_match('/\w+$/',$file,$ext)) { - $map=[ - 'au'=>'audio/basic', - 'avi'=>'video/avi', - 'bmp'=>'image/bmp', - 'bz2'=>'application/x-bzip2', - 'css'=>'text/css', - 'dtd'=>'application/xml-dtd', - 'doc'=>'application/msword', - 'gif'=>'image/gif', - 'gz'=>'application/x-gzip', - 'hqx'=>'application/mac-binhex40', - 'html?'=>'text/html', - 'jar'=>'application/java-archive', - 'jpe?g|jfif?'=>'image/jpeg', - 'js'=>'application/x-javascript', - 'midi'=>'audio/x-midi', - 'mp3'=>'audio/mpeg', - 'mpe?g'=>'video/mpeg', - 'ogg'=>'audio/vorbis', - 'pdf'=>'application/pdf', - 'png'=>'image/png', - 'ppt'=>'application/vnd.ms-powerpoint', - 'ps'=>'application/postscript', - 'qt'=>'video/quicktime', - 'ram?'=>'audio/x-pn-realaudio', - 'rdf'=>'application/rdf', - 'rtf'=>'application/rtf', - 'sgml?'=>'text/sgml', - 'sit'=>'application/x-stuffit', - 'svg'=>'image/svg+xml', - 'swf'=>'application/x-shockwave-flash', - 'tgz'=>'application/x-tar', - 'tiff'=>'image/tiff', - 'txt'=>'text/plain', - 'wav'=>'audio/wav', - 'xls'=>'application/vnd.ms-excel', - 'xml'=>'application/xml', - 'zip'=>'application/x-zip-compressed' - ]; - foreach ($map as $key=>$val) - if (preg_match('/'.$key.'/',strtolower($ext[0]))) - return $val; - } - return 'application/octet-stream'; - } - - /** - * Return the MIME types stated in the HTTP Accept header as an array; - * If a list of MIME types is specified, return the best match; or - * FALSE if none found - * @return array|string|FALSE - * @param $list string|array - **/ - function acceptable($list=NULL) { - $accept=[]; - foreach (explode(',',str_replace(' ','',@$_SERVER['HTTP_ACCEPT'])) - as $mime) - if (preg_match('/(.+?)(?:;q=([\d\.]+)|$)/',$mime,$parts)) - $accept[$parts[1]]=isset($parts[2])?$parts[2]:1; - if (!$accept) - $accept['*/*']=1; - else { - krsort($accept); - arsort($accept); - } - if ($list) { - if (is_string($list)) - $list=explode(',',$list); - foreach ($accept as $mime=>$q) - if ($q && $out=preg_grep('/'. - str_replace('\*','.*',preg_quote($mime,'/')).'/',$list)) - return current($out); - return FALSE; - } - return $accept; - } - - /** - * Transmit file to HTTP client; Return file size if successful, - * FALSE otherwise - * @return int|FALSE - * @param $file string - * @param $mime string - * @param $kbps int - * @param $force bool - * @param $name string - * @param $flush bool - **/ - function send($file,$mime=NULL,$kbps=0,$force=TRUE,$name=NULL,$flush=TRUE) { - if (!is_file($file)) - return FALSE; - $size=filesize($file); - if (PHP_SAPI!='cli') { - header('Content-Type: '.($mime?:$this->mime($file))); - if ($force) - header('Content-Disposition: attachment; '. - 'filename="'.($name!==NULL?$name:basename($file)).'"'); - header('Accept-Ranges: bytes'); - header('Content-Length: '.$size); - header('X-Powered-By: '.Base::instance()->PACKAGE); - } - if (!$kbps && $flush) { - while (ob_get_level()) - ob_end_clean(); - readfile($file); - } - else { - $ctr=0; - $handle=fopen($file,'rb'); - $start=microtime(TRUE); - while (!feof($handle) && - ($info=stream_get_meta_data($handle)) && - !$info['timed_out'] && !connection_aborted()) { - if ($kbps) { - // Throttle output - $ctr++; - if ($ctr/$kbps>$elapsed=microtime(TRUE)-$start) - usleep(1e6*($ctr/$kbps-$elapsed)); - } - // Send 1KiB and reset timer - echo fread($handle,1024); - if ($flush) { - ob_flush(); - flush(); - } - } - fclose($handle); - } - return $size; - } - - /** - * Receive file(s) from HTTP client - * @return array|bool - * @param $func callback - * @param $overwrite bool - * @param $slug callback|bool - **/ - function receive($func=NULL,$overwrite=FALSE,$slug=TRUE) { - $fw=Base::instance(); - $dir=$fw->UPLOADS; - if (!is_dir($dir)) - mkdir($dir,Base::MODE,TRUE); - if ($fw->VERB=='PUT') { - $tmp=$fw->TEMP.$fw->SEED.'.'.$fw->hash(uniqid()); - if (!$fw->RAW) - $fw->write($tmp,$fw->BODY); - else { - $src=@fopen('php://input','r'); - $dst=@fopen($tmp,'w'); - if (!$src || !$dst) - return FALSE; - while (!feof($src) && - ($info=stream_get_meta_data($src)) && - !$info['timed_out'] && $str=fgets($src,4096)) - fputs($dst,$str,strlen($str)); - fclose($dst); - fclose($src); - } - $base=basename($fw->URI); - $file=[ - 'name'=>$dir. - ($slug && preg_match('/(.+?)(\.\w+)?$/',$base,$parts)? - (is_callable($slug)? - $slug($base): - ($this->slug($parts[1]). - (isset($parts[2])?$parts[2]:''))): - $base), - 'tmp_name'=>$tmp, - 'type'=>$this->mime($base), - 'size'=>filesize($tmp) - ]; - return (!file_exists($file['name']) || $overwrite) && - (!$func || $fw->call($func,[$file])!==FALSE) && - rename($tmp,$file['name']); - } - $fetch=function($arr) use(&$fetch) { - if (!is_array($arr)) - return [$arr]; - $data=[]; - foreach($arr as $k=>$sub) - $data=array_merge($data,$fetch($sub)); - return $data; - }; - $out=[]; - foreach ($_FILES as $name=>$item) { - $files=[]; - foreach ($item as $k=>$mix) - foreach ($fetch($mix) as $i=>$val) - $files[$i][$k]=$val; - foreach ($files as $file) { - if (empty($file['name'])) - continue; - $base=basename($file['name']); - $file['name']=$dir. - ($slug && preg_match('/(.+?)(\.\w+)?$/',$base,$parts)? - (is_callable($slug)? - $slug($base,$name): - ($this->slug($parts[1]). - (isset($parts[2])?$parts[2]:''))): - $base); - $out[$file['name']]=!$file['error'] && - (!file_exists($file['name']) || $overwrite) && - (!$func || $fw->call($func,[$file,$name])!==FALSE) && - move_uploaded_file($file['tmp_name'],$file['name']); - } - } - return $out; - } - - /** - * Return upload progress in bytes, FALSE on failure - * @return int|FALSE - * @param $id string - **/ - function progress($id) { - // ID returned by session.upload_progress.name - return ini_get('session.upload_progress.enabled') && - isset($_SESSION[$id]['bytes_processed'])? - $_SESSION[$id]['bytes_processed']:FALSE; - } - - /** - * HTTP request via cURL - * @return array - * @param $url string - * @param $options array - **/ - protected function _curl($url,$options) { - $curl=curl_init($url); - if (!$open_basedir=ini_get('open_basedir')) - curl_setopt($curl,CURLOPT_FOLLOWLOCATION, - $options['follow_location']); - curl_setopt($curl,CURLOPT_MAXREDIRS, - $options['max_redirects']); - curl_setopt($curl,CURLOPT_PROTOCOLS,CURLPROTO_HTTP|CURLPROTO_HTTPS); - curl_setopt($curl,CURLOPT_REDIR_PROTOCOLS,CURLPROTO_HTTP|CURLPROTO_HTTPS); - curl_setopt($curl,CURLOPT_CUSTOMREQUEST,$options['method']); - if (isset($options['header'])) - curl_setopt($curl,CURLOPT_HTTPHEADER,$options['header']); - if (isset($options['content'])) - curl_setopt($curl,CURLOPT_POSTFIELDS,$options['content']); - if (isset($options['proxy'])) - curl_setopt($curl,CURLOPT_PROXY,$options['proxy']); - curl_setopt($curl,CURLOPT_ENCODING,'gzip,deflate'); - $timeout=isset($options['timeout'])? - $options['timeout']: - ini_get('default_socket_timeout'); - curl_setopt($curl,CURLOPT_CONNECTTIMEOUT,$timeout); - curl_setopt($curl,CURLOPT_TIMEOUT,$timeout); - $headers=[]; - curl_setopt($curl,CURLOPT_HEADERFUNCTION, - // Callback for response headers - function($curl,$line) use(&$headers) { - if ($trim=trim($line)) - $headers[]=$trim; - return strlen($line); - } - ); - curl_setopt($curl,CURLOPT_SSL_VERIFYHOST,2); - curl_setopt($curl,CURLOPT_SSL_VERIFYPEER,FALSE); - ob_start(); - curl_exec($curl); - $err=curl_error($curl); - curl_close($curl); - $body=ob_get_clean(); - if (!$err && - $options['follow_location'] && $open_basedir && - preg_grep('/HTTP\/1\.\d 3\d{2}/',$headers) && - preg_match('/^Location: (.+)$/m',implode(PHP_EOL,$headers),$loc)) { - $options['max_redirects']--; - if($loc[1][0] == '/') { - $parts=parse_url($url); - $loc[1]=$parts['scheme'].'://'.$parts['host']. - ((isset($parts['port']) && !in_array($parts['port'],[80,443])) - ?':'.$parts['port']:'').$loc[1]; - } - return $this->request($loc[1],$options); - } - return [ - 'body'=>$body, - 'headers'=>$headers, - 'engine'=>'cURL', - 'cached'=>FALSE, - 'error'=>$err - ]; - } - - /** - * HTTP request via PHP stream wrapper - * @return array - * @param $url string - * @param $options array - **/ - protected function _stream($url,$options) { - $eol="\r\n"; - if (isset($options['proxy'])) { - $options['proxy']=preg_replace('/https?/i','tcp',$options['proxy']); - $options['request_fulluri']=true; - if (preg_match('/socks4?/i',$options['proxy'])) - return $this->_socket($url,$options); - } - $options['header']=implode($eol,$options['header']); - $body=@file_get_contents($url,FALSE, - stream_context_create(['http'=>$options])); - $headers=isset($http_response_header)? - $http_response_header:[]; - $err=''; - if (is_string($body)) { - $match=NULL; - foreach ($headers as $header) - if (preg_match('/Content-Encoding: (.+)/i',$header,$match)) - break; - if ($match) - switch ($match[1]) { - case 'gzip': - $body=gzdecode($body); - break; - case 'deflate': - $body=gzuncompress($body); - break; - } - } - else { - $tmp=error_get_last(); - $err=$tmp['message']; - } - return [ - 'body'=>$body, - 'headers'=>$headers, - 'engine'=>'stream', - 'cached'=>FALSE, - 'error'=>$err - ]; - } - - /** - * HTTP request via low-level TCP/IP socket - * @return array - * @param $url string - * @param $options array - **/ - protected function _socket($url,$options) { - $eol="\r\n"; - $headers=[]; - $body=''; - $parts=parse_url($url); - $hostname=$parts['host']; - $proxy=false; - if ($parts['scheme']=='https') - $parts['host']='ssl://'.$parts['host']; - if (empty($parts['port'])) - $parts['port']=$parts['scheme']=='https'?443:80; - if (empty($parts['path'])) - $parts['path']='/'; - if (empty($parts['query'])) - $parts['query']=''; - if (isset($options['proxy'])) { - $req=$url; - $pp=parse_url($options['proxy']); - $proxy=$pp['scheme']; - if ($pp['scheme']=='https') - $pp['host']='ssl://'.$pp['host']; - if (empty($pp['port'])) - $pp['port']=$pp['scheme']=='https'?443:80; - $socket=@fsockopen($pp['host'],$pp['port'],$code,$err); - } else { - $req=$parts['path'].($parts['query']?('?'.$parts['query']):''); - $socket=@fsockopen($parts['host'],$parts['port'],$code,$err); - } - if ($socket) { - stream_set_blocking($socket,TRUE); - stream_set_timeout($socket,isset($options['timeout'])? - $options['timeout']:ini_get('default_socket_timeout')); - if ($proxy=='socks4') { - // SOCKS4; http://en.wikipedia.org/wiki/SOCKS#Protocol - $packet="\x04\x01".pack("n", $parts['port']). - pack("H*",dechex(ip2long(gethostbyname($hostname))))."\0"; - fputs($socket, $packet, strlen($packet)); - $response=fread($socket, 9); - if (strlen($response)==8 && (ord($response[0])==0 || ord($response[0])==4) - && ord($response[1])==90) { - $options['header'][]='Host: '.$hostname; - } else - $err='Socket Status '.ord($response[1]); - } - fputs($socket,$options['method'].' '.$req.' HTTP/1.0'.$eol); - fputs($socket,implode($eol,$options['header']).$eol.$eol); - if (isset($options['content'])) - fputs($socket,$options['content'].$eol); - // Get response - $content=''; - while (!feof($socket) && - ($info=stream_get_meta_data($socket)) && - !$info['timed_out'] && !connection_aborted() && - $str=fgets($socket,4096)) - $content.=$str; - fclose($socket); - $html=explode($eol.$eol,$content,2); - $body=isset($html[1])?$html[1]:''; - $headers=array_merge($headers,$current=explode($eol,$html[0])); - $match=NULL; - foreach ($current as $header) - if (preg_match('/Content-Encoding: (.+)/i',$header,$match)) - break; - if ($match) - switch ($match[1]) { - case 'gzip': - $body=gzdecode($body); - break; - case 'deflate': - $body=gzuncompress($body); - break; - } - if ($options['follow_location'] && - preg_grep('/HTTP\/1\.\d 3\d{2}/',$headers) && - preg_match('/Location: (.+?)'.preg_quote($eol).'/', - $html[0],$loc)) { - $options['max_redirects']--; - return $this->request($loc[1],$options); - } - } - return [ - 'body'=>$body, - 'headers'=>$headers, - 'engine'=>'socket', - 'cached'=>FALSE, - 'error'=>$err - ]; - } - - /** - * Specify the HTTP request engine to use; If not available, - * fall back to an applicable substitute - * @return string - * @param $arg string - **/ - function engine($arg='curl') { - $arg=strtolower($arg); - $flags=[ - 'curl'=>extension_loaded('curl'), - 'stream'=>ini_get('allow_url_fopen'), - 'socket'=>function_exists('fsockopen') - ]; - if ($flags[$arg]) - return $this->wrapper=$arg; - foreach ($flags as $key=>$val) - if ($val) - return $this->wrapper=$key; - user_error(self::E_Request,E_USER_ERROR); - } - - /** - * Replace old headers with new elements - * @return NULL - * @param $old array - * @param $new string|array - **/ - function subst(array &$old,$new) { - if (is_string($new)) - $new=[$new]; - foreach ($new as $hdr) { - $old=preg_grep('/'.preg_quote(strstr($hdr,':',TRUE),'/').':.+/', - $old,PREG_GREP_INVERT); - array_push($old,$hdr); - } - } - - /** - * Submit HTTP request; Use HTTP context options (described in - * http://www.php.net/manual/en/context.http.php) if specified; - * Cache the page as instructed by remote server - * @return array|FALSE - * @param $url string - * @param $options array - **/ - function request($url,array $options=NULL) { - $fw=Base::instance(); - $parts=parse_url($url); - if (empty($parts['scheme'])) { - // Local URL - $url=$fw->SCHEME.'://'.$fw->HOST. - (in_array($fw->PORT,[80,443])?'':(':'.$fw->PORT)). - ($url[0]!='/'?($fw->BASE.'/'):'').$url; - $parts=parse_url($url); - } - elseif (!preg_match('/https?/',$parts['scheme'])) - return FALSE; - if (!is_array($options)) - $options=[]; - if (empty($options['header'])) - $options['header']=[]; - elseif (is_string($options['header'])) - $options['header']=[$options['header']]; - if (!$this->wrapper) - $this->engine(); - if ($this->wrapper!='stream') { - // PHP streams can't cope with redirects when Host header is set - $this->subst($options['header'],'Host: '.$parts['host']); - } - $this->subst($options['header'], - [ - 'Accept-Encoding: gzip,deflate', - 'User-Agent: '.(isset($options['user_agent'])? - $options['user_agent']: - 'Mozilla/5.0 (compatible; '.php_uname('s').')'), - 'Connection: close' - ] - ); - if (isset($options['content']) && is_string($options['content'])) { - if ($options['method']=='POST' && - !preg_grep('/^Content-Type:/i',$options['header'])) - $this->subst($options['header'], - 'Content-Type: application/x-www-form-urlencoded'); - $this->subst($options['header'], - 'Content-Length: '.strlen($options['content'])); - } - if (isset($parts['user'],$parts['pass'])) - $this->subst($options['header'], - 'Authorization: Basic '. - base64_encode($parts['user'].':'.$parts['pass']) - ); - $options+=[ - 'method'=>'GET', - 'header'=>$options['header'], - 'follow_location'=>TRUE, - 'max_redirects'=>20, - 'ignore_errors'=>FALSE - ]; - $eol="\r\n"; - if ($fw->CACHE && - preg_match('/GET|HEAD/',$options['method'])) { - $cache=Cache::instance(); - if ($cache->exists( - $hash=$fw->hash($options['method'].' '.$url).'.url',$data)) { - if (preg_match('/Last-Modified: (.+?)'.preg_quote($eol).'/', - implode($eol,$data['headers']),$mod)) - $this->subst($options['header'], - 'If-Modified-Since: '.$mod[1]); - } - } - $result=$this->{'_'.$this->wrapper}($url,$options); - if ($result && isset($cache)) { - if (preg_match('/HTTP\/1\.\d 304/', - implode($eol,$result['headers']))) { - $result=$cache->get($hash); - $result['cached']=TRUE; - } - elseif (preg_match('/Cache-Control:(?:.*)max-age=(\d+)(?:,?.*'. - preg_quote($eol).')/i',implode($eol,$result['headers']),$exp)) - $cache->set($hash,$result,$exp[1]); - } - $req=[$options['method'].' '.$url]; - foreach ($options['header'] as $header) - array_push($req,$header); - return array_merge(['request'=>$req],$result); - } - - /** - * Strip Javascript/CSS files of extraneous whitespaces and comments; - * Return combined output as a minified string - * @return string - * @param $files string|array - * @param $mime string - * @param $header bool - * @param $path string - **/ - function minify($files,$mime=NULL,$header=TRUE,$path=NULL) { - $fw=Base::instance(); - if (is_string($files)) - $files=$fw->split($files); - if (!$mime) - $mime=$this->mime($files[0]); - preg_match('/\w+$/',$files[0],$ext); - $cache=Cache::instance(); - $dst=''; - if (!isset($path)) - $path=$fw->UI.';./'; - foreach ($fw->split($path,FALSE) as $dir) - foreach ($files as $file) - if (is_file($save=$fw->fixslashes($dir.$file)) && - is_bool(strpos($save,'../')) && - preg_match('/\.(css|js)$/i',$file)) { - if ($fw->CACHE && - ($cached=$cache->exists( - $hash=$fw->hash($save).'.'.$ext[0],$data)) && - $cached[0]>filemtime($save)) - $dst.=$data; - else { - $data=''; - $src=$fw->read($save); - for ($ptr=0,$len=strlen($src);$ptr<$len;) { - if (preg_match('/^@import\h+url'. - '\(\h*([\'"])((?!(?:https?:)?\/\/).+?)\1\h*\)[^;]*;/', - substr($src,$ptr),$parts)) { - $path=dirname($file); - $data.=$this->minify( - ($path?($path.'/'):'').$parts[2], - $mime,$header - ); - $ptr+=strlen($parts[0]); - continue; - } - if ($src[$ptr]=='/') { - if ($src[$ptr+1]=='*') { - // Multiline comment - $str=strstr( - substr($src,$ptr+2),'*/',TRUE); - $ptr+=strlen($str)+4; - } - elseif ($src[$ptr+1]=='/') { - // Single-line comment - $str=strstr( - substr($src,$ptr+2),"\n",TRUE); - $ptr+=(empty($str))? - strlen(substr($src,$ptr)):strlen($str)+2; - } - else { - // Presume it's a regex pattern - $regex=TRUE; - // Backtrack and validate - for ($ofs=$ptr;$ofs;$ofs--) { - // Pattern should be preceded by - // open parenthesis, colon, - // object property or operator - if (preg_match( - '/(return|[(:=!+\-*&|])$/', - substr($src,0,$ofs))) { - $data.='/'; - $ptr++; - while ($ptr<$len) { - $data.=$src[$ptr]; - $ptr++; - if ($src[$ptr-1]=='\\') { - $data.=$src[$ptr]; - $ptr++; - } - elseif ($src[$ptr-1]=='/') - break; - } - break; - } - elseif (!ctype_space($src[$ofs-1])) { - // Not a regex pattern - $regex=FALSE; - break; - } - } - if (!$regex) { - // Division operator - $data.=$src[$ptr]; - $ptr++; - } - } - continue; - } - if (in_array($src[$ptr],['\'','"'])) { - $match=$src[$ptr]; - $data.=$match; - $ptr++; - // String literal - while ($ptr<$len) { - $data.=$src[$ptr]; - $ptr++; - if ($src[$ptr-1]=='\\') { - $data.=$src[$ptr]; - $ptr++; - } - elseif ($src[$ptr-1]==$match) - break; - } - continue; - } - if (ctype_space($src[$ptr])) { - if ($ptr+1CACHE) - $cache->set($hash,$data); - $dst.=$data; - } - } - if (PHP_SAPI!='cli' && $header) - header('Content-Type: '.$mime.'; charset='.$fw->ENCODING); - return $dst; - } - - /** - * Retrieve RSS feed and return as an array - * @return array|FALSE - * @param $url string - * @param $max int - * @param $tags string - **/ - function rss($url,$max=10,$tags=NULL) { - if (!$data=$this->request($url)) - return FALSE; - // Suppress errors caused by invalid XML structures - libxml_use_internal_errors(TRUE); - $xml=simplexml_load_string($data['body'], - NULL,LIBXML_NOBLANKS|LIBXML_NOERROR); - if (!is_object($xml)) - return FALSE; - $out=[]; - if (isset($xml->channel)) { - $out['source']=(string)$xml->channel->title; - $max=min($max,count($xml->channel->item)); - for ($i=0;$i<$max;$i++) { - $item=$xml->channel->item[$i]; - $list=[''=>NULL]+$item->getnamespaces(TRUE); - $fields=[]; - foreach ($list as $ns=>$uri) - foreach ($item->children($uri) as $key=>$val) - $fields[$ns.($ns?':':'').$key]=(string)$val; - $out['feed'][]=$fields; - } - } - else - return FALSE; - Base::instance()->scrub($out,$tags); - return $out; - } - - /** - * Retrieve information from whois server - * @return string|FALSE - * @param $addr string - * @param $server string - **/ - function whois($addr,$server='whois.internic.net') { - $socket=@fsockopen($server,43,$errno,$errstr); - if (!$socket) - // Can't establish connection - return FALSE; - // Set connection timeout parameters - stream_set_blocking($socket,FALSE); - stream_set_timeout($socket,ini_get('default_socket_timeout')); - // Send request - fputs($socket,$addr."\r\n"); - $info=stream_get_meta_data($socket); - // Get response - $response=''; - while (!feof($socket) && !$info['timed_out']) { - $response.=fgets($socket,4096); // MDFK97 - $info=stream_get_meta_data($socket); - } - fclose($socket); - return $info['timed_out']?FALSE:trim($response); - } - - /** - * Return preset diacritics translation table - * @return array - **/ - function diacritics() { - return [ - 'Ǎ'=>'A','А'=>'A','Ā'=>'A','Ă'=>'A','Ą'=>'A','Å'=>'A', - 'Ǻ'=>'A','Ä'=>'Ae','Á'=>'A','À'=>'A','Ã'=>'A','Â'=>'A', - 'Æ'=>'AE','Ǽ'=>'AE','Б'=>'B','Ç'=>'C','Ć'=>'C','Ĉ'=>'C', - 'Č'=>'C','Ċ'=>'C','Ц'=>'C','Ч'=>'Ch','Ð'=>'Dj','Đ'=>'Dj', - 'Ď'=>'Dj','Д'=>'Dj','É'=>'E','Ę'=>'E','Ё'=>'E','Ė'=>'E', - 'Ê'=>'E','Ě'=>'E','Ē'=>'E','È'=>'E','Е'=>'E','Э'=>'E', - 'Ë'=>'E','Ĕ'=>'E','Ф'=>'F','Г'=>'G','Ģ'=>'G','Ġ'=>'G', - 'Ĝ'=>'G','Ğ'=>'G','Х'=>'H','Ĥ'=>'H','Ħ'=>'H','Ï'=>'I', - 'Ĭ'=>'I','İ'=>'I','Į'=>'I','Ī'=>'I','Í'=>'I','Ì'=>'I', - 'И'=>'I','Ǐ'=>'I','Ĩ'=>'I','Î'=>'I','IJ'=>'IJ','Ĵ'=>'J', - 'Й'=>'J','Я'=>'Ja','Ю'=>'Ju','К'=>'K','Ķ'=>'K','Ĺ'=>'L', - 'Л'=>'L','Ł'=>'L','Ŀ'=>'L','Ļ'=>'L','Ľ'=>'L','М'=>'M', - 'Н'=>'N','Ń'=>'N','Ñ'=>'N','Ņ'=>'N','Ň'=>'N','Ō'=>'O', - 'О'=>'O','Ǿ'=>'O','Ǒ'=>'O','Ơ'=>'O','Ŏ'=>'O','Ő'=>'O', - 'Ø'=>'O','Ö'=>'Oe','Õ'=>'O','Ó'=>'O','Ò'=>'O','Ô'=>'O', - 'Œ'=>'OE','П'=>'P','Ŗ'=>'R','Р'=>'R','Ř'=>'R','Ŕ'=>'R', - 'Ŝ'=>'S','Ş'=>'S','Š'=>'S','Ș'=>'S','Ś'=>'S','С'=>'S', - 'Ш'=>'Sh','Щ'=>'Shch','Ť'=>'T','Ŧ'=>'T','Ţ'=>'T','Ț'=>'T', - 'Т'=>'T','Ů'=>'U','Ű'=>'U','Ŭ'=>'U','Ũ'=>'U','Ų'=>'U', - 'Ū'=>'U','Ǜ'=>'U','Ǚ'=>'U','Ù'=>'U','Ú'=>'U','Ü'=>'Ue', - 'Ǘ'=>'U','Ǖ'=>'U','У'=>'U','Ư'=>'U','Ǔ'=>'U','Û'=>'U', - 'В'=>'V','Ŵ'=>'W','Ы'=>'Y','Ŷ'=>'Y','Ý'=>'Y','Ÿ'=>'Y', - 'Ź'=>'Z','З'=>'Z','Ż'=>'Z','Ž'=>'Z','Ж'=>'Zh','á'=>'a', - 'ă'=>'a','â'=>'a','à'=>'a','ā'=>'a','ǻ'=>'a','å'=>'a', - 'ä'=>'ae','ą'=>'a','ǎ'=>'a','ã'=>'a','а'=>'a','ª'=>'a', - 'æ'=>'ae','ǽ'=>'ae','б'=>'b','č'=>'c','ç'=>'c','ц'=>'c', - 'ċ'=>'c','ĉ'=>'c','ć'=>'c','ч'=>'ch','ð'=>'dj','ď'=>'dj', - 'д'=>'dj','đ'=>'dj','э'=>'e','é'=>'e','ё'=>'e','ë'=>'e', - 'ê'=>'e','е'=>'e','ĕ'=>'e','è'=>'e','ę'=>'e','ě'=>'e', - 'ė'=>'e','ē'=>'e','ƒ'=>'f','ф'=>'f','ġ'=>'g','ĝ'=>'g', - 'ğ'=>'g','г'=>'g','ģ'=>'g','х'=>'h','ĥ'=>'h','ħ'=>'h', - 'ǐ'=>'i','ĭ'=>'i','и'=>'i','ī'=>'i','ĩ'=>'i','į'=>'i', - 'ı'=>'i','ì'=>'i','î'=>'i','í'=>'i','ï'=>'i','ij'=>'ij', - 'ĵ'=>'j','й'=>'j','я'=>'ja','ю'=>'ju','ķ'=>'k','к'=>'k', - 'ľ'=>'l','ł'=>'l','ŀ'=>'l','ĺ'=>'l','ļ'=>'l','л'=>'l', - 'м'=>'m','ņ'=>'n','ñ'=>'n','ń'=>'n','н'=>'n','ň'=>'n', - 'ʼn'=>'n','ó'=>'o','ò'=>'o','ǒ'=>'o','ő'=>'o','о'=>'o', - 'ō'=>'o','º'=>'o','ơ'=>'o','ŏ'=>'o','ô'=>'o','ö'=>'oe', - 'õ'=>'o','ø'=>'o','ǿ'=>'o','œ'=>'oe','п'=>'p','р'=>'r', - 'ř'=>'r','ŕ'=>'r','ŗ'=>'r','ſ'=>'s','ŝ'=>'s','ș'=>'s', - 'š'=>'s','ś'=>'s','с'=>'s','ş'=>'s','ш'=>'sh','щ'=>'shch', - 'ß'=>'ss','ţ'=>'t','т'=>'t','ŧ'=>'t','ť'=>'t','ț'=>'t', - 'у'=>'u','ǘ'=>'u','ŭ'=>'u','û'=>'u','ú'=>'u','ų'=>'u', - 'ù'=>'u','ű'=>'u','ů'=>'u','ư'=>'u','ū'=>'u','ǚ'=>'u', - 'ǜ'=>'u','ǔ'=>'u','ǖ'=>'u','ũ'=>'u','ü'=>'ue','в'=>'v', - 'ŵ'=>'w','ы'=>'y','ÿ'=>'y','ý'=>'y','ŷ'=>'y','ź'=>'z', - 'ž'=>'z','з'=>'z','ż'=>'z','ж'=>'zh','ь'=>'','ъ'=>'', - '\''=>'', - ]; - } - - /** - * Return a URL/filesystem-friendly version of string - * @return string - * @param $text string - **/ - function slug($text) { - return trim(strtolower(preg_replace('/([^\pL\pN])+/u','-', - trim(strtr($text,Base::instance()->DIACRITICS+$this->diacritics())))),'-'); - } - - /** - * Return chunk of text from standard Lorem Ipsum passage - * @return string - * @param $count int - * @param $max int - * @param $std bool - **/ - function filler($count=1,$max=20,$std=TRUE) { - $out=''; - if ($std) - $out='Lorem ipsum dolor sit amet, consectetur adipisicing elit, '. - 'sed do eiusmod tempor incididunt ut labore et dolore magna '. - 'aliqua.'; - $rnd=explode(' ', - 'a ab ad accusamus adipisci alias aliquam amet animi aperiam '. - 'architecto asperiores aspernatur assumenda at atque aut beatae '. - 'blanditiis cillum commodi consequatur corporis corrupti culpa '. - 'cum cupiditate debitis delectus deleniti deserunt dicta '. - 'dignissimos distinctio dolor ducimus duis ea eaque earum eius '. - 'eligendi enim eos error esse est eum eveniet ex excepteur '. - 'exercitationem expedita explicabo facere facilis fugiat harum '. - 'hic id illum impedit in incidunt ipsa iste itaque iure iusto '. - 'laborum laudantium libero magnam maiores maxime minim minus '. - 'modi molestiae mollitia nam natus necessitatibus nemo neque '. - 'nesciunt nihil nisi nobis non nostrum nulla numquam occaecati '. - 'odio officia omnis optio pariatur perferendis perspiciatis '. - 'placeat porro possimus praesentium proident quae quia quibus '. - 'quo ratione recusandae reiciendis rem repellat reprehenderit '. - 'repudiandae rerum saepe sapiente sequi similique sint soluta '. - 'suscipit tempora tenetur totam ut ullam unde vel veniam vero '. - 'vitae voluptas'); - for ($i=0,$add=$count-(int)$std;$i<$add;$i++) { - shuffle($rnd); - $words=array_slice($rnd,0,mt_rand(3,$max)); - $out.=(!$std&&$i==0?'':' ').ucfirst(implode(' ',$words)).'.'; - } - return $out; - } - -} - -if (!function_exists('gzdecode')) { - - /** - * Decode gzip-compressed string - * @param $str string - **/ - function gzdecode($str) { - $fw=Base::instance(); - if (!is_dir($tmp=$fw->TEMP)) - mkdir($tmp,Base::MODE,TRUE); - file_put_contents($file=$tmp.'/'.$fw->SEED.'.'. - $fw->hash(uniqid(NULL,TRUE)).'.gz',$str,LOCK_EX); - ob_start(); - readgzfile($file); - $out=ob_get_clean(); - @unlink($file); - return $out; - } - -} diff --git a/app/lib/web/geo.php b/app/lib/web/geo.php deleted file mode 100644 index 80031cf3..00000000 --- a/app/lib/web/geo.php +++ /dev/null @@ -1,111 +0,0 @@ -. - -*/ - -namespace Web; - -//! Geo plug-in -class Geo extends \Prefab { - - /** - * Return information about specified Unix time zone - * @return array - * @param $zone string - **/ - function tzinfo($zone) { - $ref=new \DateTimeZone($zone); - $loc=$ref->getLocation(); - $trn=$ref->getTransitions($now=time(),$now); - $out=[ - 'offset'=>$ref-> - getOffset(new \DateTime('now',new \DateTimeZone('UTC')))/3600, - 'country'=>$loc['country_code'], - 'latitude'=>$loc['latitude'], - 'longitude'=>$loc['longitude'], - 'dst'=>$trn[0]['isdst'] - ]; - unset($ref); - return $out; - } - - /** - * Return geolocation data based on specified/auto-detected IP address - * @return array|FALSE - * @param $ip string - **/ - function location($ip=NULL) { - $fw=\Base::instance(); - $web=\Web::instance(); - if (!$ip) - $ip=$fw->IP; - $public=filter_var($ip,FILTER_VALIDATE_IP, - FILTER_FLAG_IPV4|FILTER_FLAG_IPV6| - FILTER_FLAG_NO_RES_RANGE|FILTER_FLAG_NO_PRIV_RANGE); - if (function_exists('geoip_db_avail') && - geoip_db_avail(GEOIP_CITY_EDITION_REV1) && - $out=@geoip_record_by_name($ip)) { - $out['request']=$ip; - $out['region_code']=$out['region']; - $out['region_name']=''; - if (!empty($out['country_code']) && !empty($out['region'])) - $out['region_name']=geoip_region_name_by_code( - $out['country_code'],$out['region'] - ); - unset($out['country_code3'],$out['region'],$out['postal_code']); - return $out; - } - if (($req=$web->request('http://www.geoplugin.net/json.gp'. - ($public?('?ip='.$ip):''))) && - $data=json_decode($req['body'],TRUE)) { - $out=[]; - foreach ($data as $key=>$val) - if (!strpos($key,'currency') && $key!=='geoplugin_status' - && $key!=='geoplugin_region') - $out[$fw->snakecase(substr($key, 10))]=$val; - return $out; - } - return FALSE; - } - - /** - * Return weather data based on specified latitude/longitude - * @return array|FALSE - * @param $latitude float - * @param $longitude float - * @param $key string - **/ - function weather($latitude,$longitude,$key) { - $fw=\Base::instance(); - $web=\Web::instance(); - $query=[ - 'lat'=>$latitude, - 'lon'=>$longitude, - 'APPID'=>$key, - 'units'=>'metric' - ]; - return ($req=$web->request( - 'http://api.openweathermap.org/data/2.5/weather?'. - http_build_query($query)))? - json_decode($req['body'],TRUE): - FALSE; - } - -} diff --git a/app/lib/web/google/recaptcha.php b/app/lib/web/google/recaptcha.php deleted file mode 100644 index 9c32979c..00000000 --- a/app/lib/web/google/recaptcha.php +++ /dev/null @@ -1,58 +0,0 @@ -. - -*/ - -namespace Web\Google; - -//! Google ReCAPTCHA v2 plug-in -class Recaptcha { - - const - //! API URL - URL_Recaptcha='https://www.google.com/recaptcha/api/siteverify'; - - /** - * Verify reCAPTCHA response - * @param string $secret - * @param string $response - * @return bool - **/ - static function verify($secret,$response=NULL) { - $fw=\Base::instance(); - if (!isset($response)) - $response=$fw->{'POST.g-recaptcha-response'}; - if (!$response) - return FALSE; - $web=\Web::instance(); - $out=$web->request(self::URL_Recaptcha,[ - 'method'=>'POST', - 'content'=>http_build_query([ - 'secret'=>$secret, - 'response'=>$response, - 'remoteip'=>$fw->IP - ]), - ]); - return isset($out['body']) && - ($json=json_decode($out['body'],TRUE)) && - isset($json['success']) && $json['success']; - } - -} diff --git a/app/lib/web/google/staticmap.php b/app/lib/web/google/staticmap.php deleted file mode 100644 index 8310fe4c..00000000 --- a/app/lib/web/google/staticmap.php +++ /dev/null @@ -1,65 +0,0 @@ -. - -*/ - -namespace Web\Google; - -//! Google Static Maps API v2 plug-in -class StaticMap { - - const - //! API URL - URL_Static='http://maps.googleapis.com/maps/api/staticmap'; - - protected - //! Query arguments - $query=array(); - - /** - * Specify API key-value pair via magic call - * @return object - * @param $func string - * @param $args array - **/ - function __call($func,array $args) { - $this->query[]=array($func,$args[0]); - return $this; - } - - /** - * Generate map - * @return string - **/ - function dump() { - $fw=\Base::instance(); - $web=\Web::instance(); - $out=''; - return ($req=$web->request( - self::URL_Static.'?'.array_reduce( - $this->query, - function($out,$item) { - return ($out.=($out?'&':''). - urlencode($item[0]).'='.urlencode($item[1])); - } - ))) && $req['body']?$req['body']:FALSE; - } - -} diff --git a/app/lib/web/oauth2.php b/app/lib/web/oauth2.php deleted file mode 100644 index fd9acf9f..00000000 --- a/app/lib/web/oauth2.php +++ /dev/null @@ -1,152 +0,0 @@ -. - -*/ - -namespace Web; - -//! Lightweight OAuth2 client -class OAuth2 extends \Magic { - - protected - //! Scopes and claims - $args=[]; - - /** - * Return OAuth2 authentication URI - * @return string - * @param $endpoint string - * @param $query bool - **/ - function uri($endpoint,$query=TRUE) { - return $endpoint.($query?('?'.http_build_query($this->args)):''); - } - - /** - * Send request to API/token endpoint - * @return string|FALSE - * @param $uri string - * @param $method string - * @param $token array - **/ - function request($uri,$method,$token=NULL) { - $web=\Web::instance(); - $options=[ - 'method'=>$method, - 'content'=>http_build_query($this->args), - 'header'=>['Accept: application/json'] - ]; - if ($token) - array_push($options['header'],'Authorization: Bearer '.$token); - elseif ($method=='POST' && isset($this->args['client_id'])) - array_push($options['header'],'Authorization: Basic '. - base64_encode( - $this->args['client_id'].':'. - $this->args['client_secret'] - ) - ); - $response=$web->request($uri,$options); - if ($response['error']) - user_error($response['error'],E_USER_ERROR); - if (isset($response['body'])) { - if (preg_grep('/^Content-Type:.*application\/json/i', - $response['headers'])) { - $token=json_decode($response['body'],TRUE); - if (isset($token['error_description'])) - user_error($token['error_description'],E_USER_ERROR); - if (isset($token['error'])) - user_error($token['error'],E_USER_ERROR); - return $token; - } - else - return $response['body']; - } - return FALSE; - } - - /** - * Parse JSON Web token - * @return array - * @param $token string - **/ - function jwt($token) { - return json_decode( - base64_decode( - str_replace(['-','_'],['+','/'],explode('.',$token)[1]) - ), - TRUE - ); - } - - /** - * URL-safe base64 encoding - * @return array - * @param $data string - **/ - function b64url($data) { - return trim(strtr(base64_encode($data),'+/','-_'),'='); - } - - /** - * Return TRUE if scope/claim exists - * @return bool - * @param $key string - **/ - function exists($key) { - return isset($this->args[$key]); - } - - /** - * Bind value to scope/claim - * @return string - * @param $key string - * @param $val string - **/ - function set($key,$val) { - return $this->args[$key]=$val; - } - - /** - * Return value of scope/claim - * @return mixed - * @param $key string - **/ - function &get($key) { - if (isset($this->args[$key])) - $val=&$this->args[$key]; - else - $val=NULL; - return $val; - } - - /** - * Remove scope/claim - * @return NULL - * @param $key string - **/ - function clear($key=NULL) { - if ($key) - unset($this->args[$key]); - else - $this->args=[]; - } - -} - diff --git a/app/lib/web/openid.php b/app/lib/web/openid.php deleted file mode 100644 index 435d89ca..00000000 --- a/app/lib/web/openid.php +++ /dev/null @@ -1,248 +0,0 @@ -. - -*/ - -namespace Web; - -//! OpenID consumer -class OpenID extends \Magic { - - protected - //! OpenID provider endpoint URL - $url, - //! HTTP request parameters - $args=[]; - - /** - * Determine OpenID provider - * @return string|FALSE - * @param $proxy string - **/ - protected function discover($proxy) { - // Normalize - if (!preg_match('/https?:\/\//i',$this->args['endpoint'])) - $this->args['endpoint']='http://'.$this->args['endpoint']; - $url=parse_url($this->args['endpoint']); - // Remove fragment; reconnect parts - $this->args['endpoint']=$url['scheme'].'://'. - (isset($url['user'])? - ($url['user']. - (isset($url['pass'])?(':'.$url['pass']):'').'@'):''). - strtolower($url['host']).(isset($url['path'])?$url['path']:'/'). - (isset($url['query'])?('?'.$url['query']):''); - // HTML-based discovery of OpenID provider - $req=\Web::instance()-> - request($this->args['endpoint'],['proxy'=>$proxy]); - if (!$req) - return FALSE; - $type=array_values(preg_grep('/Content-Type:/',$req['headers'])); - if ($type && - preg_match('/application\/xrds\+xml|text\/xml/',$type[0]) && - ($sxml=simplexml_load_string($req['body'])) && - ($xrds=json_decode(json_encode($sxml),TRUE)) && - isset($xrds['XRD'])) { - // XRDS document - $svc=$xrds['XRD']['Service']; - if (isset($svc[0])) - $svc=$svc[0]; - $svc_type=is_array($svc['Type'])?$svc['Type']:array($svc['Type']); - if (preg_grep('/http:\/\/specs\.openid\.net\/auth\/2.0\/'. - '(?:server|signon)/',$svc_type)) { - $this->args['provider']=$svc['URI']; - if (isset($svc['LocalID'])) - $this->args['localidentity']=$svc['LocalID']; - elseif (isset($svc['CanonicalID'])) - $this->args['localidentity']=$svc['CanonicalID']; - } - $this->args['server']=$svc['URI']; - if (isset($svc['Delegate'])) - $this->args['delegate']=$svc['Delegate']; - } - else { - $len=strlen($req['body']); - $ptr=0; - // Parse document - while ($ptr<$len) - if (preg_match( - '/^/is', - substr($req['body'],$ptr),$parts)) { - if ($parts[1] && - // Process attributes - preg_match_all('/\b(rel|href)\h*=\h*'. - '(?:"(.+?)"|\'(.+?)\')/s',$parts[1],$attr, - PREG_SET_ORDER)) { - $node=[]; - foreach ($attr as $kv) - $node[$kv[1]]=isset($kv[2])?$kv[2]:$kv[3]; - if (isset($node['rel']) && - preg_match('/openid2?\.(\w+)/', - $node['rel'],$var) && - isset($node['href'])) - $this->args[$var[1]]=$node['href']; - - } - $ptr+=strlen($parts[0]); - } - else - $ptr++; - } - // Get OpenID provider's endpoint URL - if (isset($this->args['provider'])) { - // OpenID 2.0 - $this->args['ns']='http://specs.openid.net/auth/2.0'; - if (isset($this->args['localidentity'])) - $this->args['identity']=$this->args['localidentity']; - if (isset($this->args['trust_root'])) - $this->args['realm']=$this->args['trust_root']; - } - elseif (isset($this->args['server'])) { - // OpenID 1.1 - $this->args['ns']='http://openid.net/signon/1.1'; - if (isset($this->args['delegate'])) - $this->args['identity']=$this->args['delegate']; - } - if (isset($this->args['provider'])) { - // OpenID 2.0 - if (empty($this->args['claimed_id'])) - $this->args['claimed_id']=$this->args['identity']; - return $this->args['provider']; - } - elseif (isset($this->args['server'])) - // OpenID 1.1 - return $this->args['server']; - else - return FALSE; - } - - /** - * Initiate OpenID authentication sequence; Return FALSE on failure - * or redirect to OpenID provider URL - * @return bool - * @param $proxy string - * @param $attr array - * @param $reqd string|array - **/ - function auth($proxy=NULL,$attr=[],array $reqd=NULL) { - $fw=\Base::instance(); - $root=$fw->SCHEME.'://'.$fw->HOST; - if (empty($this->args['trust_root'])) - $this->args['trust_root']=$root.$fw->BASE.'/'; - if (empty($this->args['return_to'])) - $this->args['return_to']=$root.$_SERVER['REQUEST_URI']; - $this->args['mode']='checkid_setup'; - if ($this->url=$this->discover($proxy)) { - if ($attr) { - $this->args['ns.ax']='http://openid.net/srv/ax/1.0'; - $this->args['ax.mode']='fetch_request'; - foreach ($attr as $key=>$val) - $this->args['ax.type.'.$key]=$val; - $this->args['ax.required']=is_string($reqd)? - $reqd:implode(',',$reqd); - } - $var=[]; - foreach ($this->args as $key=>$val) - $var['openid.'.$key]=$val; - $fw->reroute($this->url.'?'.http_build_query($var)); - } - return FALSE; - } - - /** - * Return TRUE if OpenID verification was successful - * @return bool - * @param $proxy string - **/ - function verified($proxy=NULL) { - preg_match_all('/(?<=^|&)openid\.([^=]+)=([^&]+)/', - $_SERVER['QUERY_STRING'],$matches,PREG_SET_ORDER); - foreach ($matches as $match) - $this->args[$match[1]]=urldecode($match[2]); - if (isset($this->args['mode']) && - $this->args['mode']!='error' && - $this->url=$this->discover($proxy)) { - $this->args['mode']='check_authentication'; - $var=[]; - foreach ($this->args as $key=>$val) - $var['openid.'.$key]=$val; - $req=\Web::instance()->request( - $this->url, - [ - 'method'=>'POST', - 'content'=>http_build_query($var), - 'proxy'=>$proxy - ] - ); - return (bool)preg_match('/is_valid:true/i',$req['body']); - } - return FALSE; - } - - /** - * Return OpenID response fields - * @return array - **/ - function response() { - return $this->args; - } - - /** - * Return TRUE if OpenID request parameter exists - * @return bool - * @param $key string - **/ - function exists($key) { - return isset($this->args[$key]); - } - - /** - * Bind value to OpenID request parameter - * @return string - * @param $key string - * @param $val string - **/ - function set($key,$val) { - return $this->args[$key]=$val; - } - - /** - * Return value of OpenID request parameter - * @return mixed - * @param $key string - **/ - function &get($key) { - if (isset($this->args[$key])) - $val=&$this->args[$key]; - else - $val=NULL; - return $val; - } - - /** - * Remove OpenID request parameter - * @return NULL - * @param $key - **/ - function clear($key) { - unset($this->args[$key]); - } - -} diff --git a/app/lib/web/pingback.php b/app/lib/web/pingback.php deleted file mode 100644 index a68430ca..00000000 --- a/app/lib/web/pingback.php +++ /dev/null @@ -1,176 +0,0 @@ -. - -*/ - -namespace Web; - -//! Pingback 1.0 protocol (client and server) implementation -class Pingback extends \Prefab { - - protected - //! Transaction history - $log; - - /** - * Return TRUE if URL points to a pingback-enabled resource - * @return bool - * @param $url - **/ - protected function enabled($url) { - $web=\Web::instance(); - $req=$web->request($url); - $found=FALSE; - if ($req['body']) { - // Look for pingback header - foreach ($req['headers'] as $header) - if (preg_match('/^X-Pingback:\h*(.+)/',$header,$href)) { - $found=$href[1]; - break; - } - if (!$found && - // Scan page for pingback link tag - preg_match('//i',$req['body'],$parts) && - preg_match('/rel\h*=\h*"pingback"/i',$parts[1]) && - preg_match('/href\h*=\h*"\h*(.+?)\h*"/i',$parts[1],$href)) - $found=$href[1]; - } - return $found; - } - - /** - * Load local page contents, parse HTML anchor tags, find permalinks, - * and send XML-RPC calls to corresponding pingback servers - * @return NULL - * @param $source string - **/ - function inspect($source) { - $fw=\Base::instance(); - $web=\Web::instance(); - $parts=parse_url($source); - if (empty($parts['scheme']) || empty($parts['host']) || - $parts['host']==$fw->HOST) { - $req=$web->request($source); - $doc=new \DOMDocument('1.0',$fw->ENCODING); - $doc->stricterrorchecking=FALSE; - $doc->recover=TRUE; - if (@$doc->loadhtml($req['body'])) { - // Parse anchor tags - $links=$doc->getelementsbytagname('a'); - foreach ($links as $link) { - $permalink=$link->getattribute('href'); - // Find pingback-enabled resources - if ($permalink && $found=$this->enabled($permalink)) { - $req=$web->request($found, - [ - 'method'=>'POST', - 'header'=>'Content-Type: application/xml', - 'content'=>xmlrpc_encode_request( - 'pingback.ping', - [$source,$permalink], - ['encoding'=>$fw->ENCODING] - ) - ] - ); - if ($req['body']) - $this->log.=date('r').' '. - $permalink.' [permalink:'.$found.']'.PHP_EOL. - $req['body'].PHP_EOL; - } - } - } - unset($doc); - } - } - - /** - * Receive ping, check if local page is pingback-enabled, verify - * source contents, and return XML-RPC response - * @return string - * @param $func callback - * @param $path string - **/ - function listen($func,$path=NULL) { - $fw=\Base::instance(); - if (PHP_SAPI!='cli') { - header('X-Powered-By: '.$fw->PACKAGE); - header('Content-Type: application/xml; '. - 'charset='.$charset=$fw->ENCODING); - } - if (!$path) - $path=$fw->BASE; - $web=\Web::instance(); - $args=xmlrpc_decode_request($fw->BODY,$method,$charset); - $options=['encoding'=>$charset]; - if ($method=='pingback.ping' && isset($args[0],$args[1])) { - list($source,$permalink)=$args; - $doc=new \DOMDocument('1.0',$fw->ENCODING); - // Check local page if pingback-enabled - $parts=parse_url($permalink); - if ((empty($parts['scheme']) || - $parts['host']==$fw->HOST) && - preg_match('/^'.preg_quote($path,'/').'/'. - ($fw->CASELESS?'i':''),$parts['path']) && - $this->enabled($permalink)) { - // Check source - $parts=parse_url($source); - if ((empty($parts['scheme']) || - $parts['host']==$fw->HOST) && - ($req=$web->request($source)) && - $doc->loadhtml($req['body'])) { - $links=$doc->getelementsbytagname('a'); - foreach ($links as $link) { - if ($link->getattribute('href')==$permalink) { - call_user_func_array($func,[$source,$req['body']]); - // Success - die(xmlrpc_encode_request(NULL,$source,$options)); - } - } - // No link to local page - die(xmlrpc_encode_request(NULL,0x11,$options)); - } - // Source failure - die(xmlrpc_encode_request(NULL,0x10,$options)); - } - // Doesn't exist (or not pingback-enabled) - die(xmlrpc_encode_request(NULL,0x21,$options)); - } - // Access denied - die(xmlrpc_encode_request(NULL,0x31,$options)); - } - - /** - * Return transaction history - * @return string - **/ - function log() { - return $this->log; - } - - /** - * Instantiate class - * @return object - **/ - function __construct() { - // Suppress errors caused by invalid HTML structures - libxml_use_internal_errors(TRUE); - } - -} diff --git a/app/main/controller/controller.php b/app/main/controller/controller.php index 89d78cb1..161597da 100644 --- a/app/main/controller/controller.php +++ b/app/main/controller/controller.php @@ -132,7 +132,7 @@ class Controller { $f3->get('SESSION_CACHE') === 'mysql' && ($db = $f3->DB->getDB('PF')) instanceof SQL ){ - if(!headers_sent() && session_status()!=PHP_SESSION_ACTIVE){ + if(!headers_sent() && session_status() != PHP_SESSION_ACTIVE){ /** * callback() for suspect sessions * @param $session diff --git a/app/main/lib/Cron.php b/app/main/lib/Cron.php index ae5d69e1..29f02938 100644 --- a/app/main/lib/Cron.php +++ b/app/main/lib/Cron.php @@ -104,13 +104,19 @@ class Cron extends \Cron { * @return mixed|void */ public function registerJob(string $name, array $jobConf){ - if($job = $this->getJob($name)){ - if($job->dry()){ - $job->name = $name; + // method is called from /setup page -> DB might not be created at this point! + // -> check if DB exists here. Otherwise Cortex()->__construct() + \Base::instance()->DB->setSilent(true); + if(\Base::instance()->DB->getDB(Pathfinder\AbstractPathfinderModel::DB_ALIAS)){ + if($job = $this->getJob($name)){ + if($job->dry()){ + $job->name = $name; + } + $job->setData($jobConf); + return $job->save(); } - $job->setData($jobConf); - return $job->save(); } + \Base::instance()->DB->setSilent(false); } /** diff --git a/app/main/lib/Monolog.php b/app/main/lib/Monolog.php index e1d37a08..ab44103e 100644 --- a/app/main/lib/Monolog.php +++ b/app/main/lib/Monolog.php @@ -12,6 +12,7 @@ namespace lib; use controller\LogController; use lib\logging; use Monolog\Registry; +use Monolog\Processor\ProcessorInterface; use Monolog\Formatter\FormatterInterface; use Monolog\Handler\HandlerInterface; use Monolog\Handler\BufferHandler; @@ -20,10 +21,24 @@ use Monolog\Logger; class Monolog extends \Prefab { + /** + * error message for unknown formatter + */ const ERROR_FORMATTER = 'Unknown log formatter for key "%s"'; + + /** + * error message for unknown handler + */ const ERROR_HANDLER = 'Unknown log handler for key "%s"'; + + /** + * error message for unknown processor + */ const ERROR_PROCESSOR = 'Unknown log processor for key "%s"'; + /** + * available formatters + */ const FORMATTER = [ 'line' => 'Monolog\Formatter\LineFormatter', 'json' => 'Monolog\Formatter\JsonFormatter', @@ -31,6 +46,9 @@ class Monolog extends \Prefab { 'mail' => 'lib\logging\formatter\MailFormatter' ]; + /** + * available handlers + */ const HANDLER = [ 'stream' => 'Monolog\Handler\StreamHandler', 'mail' => 'Monolog\Handler\SwiftMailerHandler', @@ -41,6 +59,9 @@ class Monolog extends \Prefab { 'discordRally' => 'lib\logging\handler\DiscordRallyWebhookHandler' ]; + /** + * available processors + */ const PROCESSOR = [ 'psr' => 'Monolog\Processor\PsrLogMessageProcessor' ]; @@ -54,12 +75,7 @@ class Monolog extends \Prefab { ]; public function __construct(){ - // set timezone for all Logger instances - if(class_exists(Logger::class)){ - if( is_callable($getTimezone = \Base::instance()->get('getTimeZone')) ){ - Logger::setTimezone($getTimezone()); - }; - }else{ + if(!class_exists(Logger::class)){ LogController::getLogger('ERROR')->write(sprintf(Config::ERROR_CLASS_NOT_EXISTS_COMPOSER, Logger::class)); } } @@ -107,6 +123,10 @@ class Monolog extends \Prefab { }else{ $logger = new Logger($log->getChannelName()); + if(is_callable($getTimezone = \Base::instance()->get('getTimeZone'))){ + $logger->setTimezone($getTimezone()); + } + // disable microsecond timestamps (seconds should be fine) $logger->useMicrosecondTimestamps(true); @@ -143,7 +163,8 @@ class Monolog extends \Prefab { $logger->pushProcessor($processorCallback); }else{ // get Monolog Processor class - $processor = $this->getProcessor($processorKey); + $processorParams = $log->getProcessorParams($processorKey); + $processor = $this->getProcessor($processorKey, $processorParams); $logger->pushProcessor($processor); } } @@ -187,7 +208,7 @@ class Monolog extends \Prefab { * @return HandlerInterface * @throws \Exception */ - private function getHandler(string $handlerKey, array $handlerParams = []): HandlerInterface{ + private function getHandler(string $handlerKey, array $handlerParams = []) : HandlerInterface{ if(array_key_exists($handlerKey, self::HANDLER)){ $handlerClass = self::HANDLER[$handlerKey]; $handler = new $handlerClass(...$handlerParams); @@ -201,13 +222,14 @@ class Monolog extends \Prefab { /** * get Monolog Processor instance by key * @param string $processorKey - * @return callable + * @param array $processorParams + * @return ProcessorInterface * @throws \Exception */ - private function getProcessor(string $processorKey): callable { + private function getProcessor(string $processorKey, array $processorParams = []) : ProcessorInterface { if(array_key_exists($processorKey, self::PROCESSOR)){ $ProcessorClass = self::PROCESSOR[$processorKey]; - $processor = new $ProcessorClass(); + $processor = new $ProcessorClass(...$processorParams); }else{ throw new \Exception(sprintf(self::ERROR_PROCESSOR, $processorKey)); } diff --git a/app/main/lib/logging/AbstractLog.php b/app/main/lib/logging/AbstractLog.php index 6ad4c607..3313d8ea 100644 --- a/app/main/lib/logging/AbstractLog.php +++ b/app/main/lib/logging/AbstractLog.php @@ -14,11 +14,36 @@ use Monolog\Logger; abstract class AbstractLog implements LogInterface { + /** + * error message invalid log level + */ const ERROR_LEVEL = 'Invalid log level "%s"'; + + /** + * error message invalid log tag + */ const ERROR_TAG = 'Invalid log tag "%s"'; + + /** + * error message unknown Handler key + */ const ERROR_HANDLER_KEY = 'Handler key "%s" not found in handlerConfig (%s)'; + + /** + * error message undefined Handler params + */ const ERROR_HANDLER_PARAMS = 'No handler parameters found for handler key "%s"'; + /** + * error message unknown Processor key + */ + const ERROR_PROCESSOR_KEY = 'Processor key "%s" not found in processorConfig (%s)'; + + /** + * error message undefined Processor params + */ + const ERROR_PROCESSOR_PARAMS = 'No processor parameters found for processor key "%s"'; + /** * PSR-3 log levels */ @@ -55,6 +80,15 @@ abstract class AbstractLog implements LogInterface { */ protected $handlerParamsConfig = []; + /** + * some processor need individual configuration parameters + * -> see $processorConfig end getProcessorParams() + * @var array + */ + protected $processorParamsConfig = [ + 'psr' => ['Y-m-d\A\TH:i:s.uP', false] + ]; + /** * multiple Log() objects can be marked as "grouped" * -> Logs with Slack Handler should be grouped by map (send multiple log data in once @@ -110,6 +144,10 @@ abstract class AbstractLog implements LogInterface { private $buffer = true; + /** + * AbstractLog constructor. + * @param string $action + */ public function __construct(string $action){ $this->setF3(); $this->action = $action; @@ -176,7 +214,7 @@ abstract class AbstractLog implements LogInterface { * @param array $data * @return LogInterface */ - public function setData(array $data) : LogInterface{ + public function setData(array $data) : LogInterface { $this->data = $data; return $this; } @@ -185,7 +223,7 @@ abstract class AbstractLog implements LogInterface { * @param array $data * @return LogInterface */ - public function setTempData(array $data) : LogInterface{ + public function setTempData(array $data) : LogInterface { $this->tmpData = $data; return $this; } @@ -227,7 +265,7 @@ abstract class AbstractLog implements LogInterface { /** * @return array */ - public function getHandlerConfig() : array{ + public function getHandlerConfig() : array { return $this->handlerConfig; } @@ -238,8 +276,6 @@ abstract class AbstractLog implements LogInterface { * @throws \Exception */ public function getHandlerParams(string $handlerKey) : array { - $params = []; - if($this->hasHandlerKey($handlerKey)){ switch($handlerKey){ case 'stream': $params = $this->getHandlerParamsStream(); @@ -255,10 +291,10 @@ abstract class AbstractLog implements LogInterface { $params = $this->getHandlerParamsSlack($handlerKey); break; default: - throw new \Exception( sprintf(self::ERROR_HANDLER_PARAMS, $handlerKey)); + throw new \Exception(sprintf(self::ERROR_HANDLER_PARAMS, $handlerKey)); } }else{ - throw new \Exception( sprintf(self::ERROR_HANDLER_KEY, $handlerKey, implode(', ', array_flip($this->handlerConfig)))); + throw new \Exception(sprintf(self::ERROR_HANDLER_KEY, $handlerKey, implode(', ', array_flip($this->handlerConfig)))); } return $params; @@ -278,58 +314,79 @@ abstract class AbstractLog implements LogInterface { return $this->processorConfig; } + /** + * get __construct() parameters for a given $processorKey + * @param string $processorKey + * @return array + * @throws \Exception + */ + public function getProcessorParams(string $processorKey) : array { + if($this->hasProcessorKey($processorKey)){ + switch($processorKey){ + case 'psr': $params = $this->getProcessorParamsPsr(); + break; + default: + throw new \Exception(sprintf(self::ERROR_PROCESSOR_PARAMS, $processorKey)); + } + }else{ + throw new \Exception(sprintf(self::ERROR_PROCESSOR_KEY, $processorKey, implode(', ', array_flip($this->processorConfig)))); + } + + return $params; + } + /** * @return string */ - public function getMessage() : string{ + public function getMessage() : string { return $this->message; } /** * @return string */ - public function getAction() : string{ + public function getAction() : string { return $this->action; } /** * @return string */ - public function getChannelType() : string{ + public function getChannelType() : string { return $this->channelType; } /** * @return string */ - public function getChannelName() : string{ + public function getChannelName() : string { return $this->getChannelType(); } /** * @return string */ - public function getLevel() : string{ + public function getLevel() : string { return $this->level; } /** * @return string */ - public function getTag() : string{ + public function getTag() : string { return $this->tag; } /** * @return array */ - public function getData() : array{ + public function getData() : array { return $this->data; } /** * @return array */ - public function getContext() : array{ + public function getContext() : array { $context = [ 'data' => $this->getData(), 'tag' => $this->getTag() @@ -351,7 +408,7 @@ abstract class AbstractLog implements LogInterface { /** * @return array */ - public function getHandlerGroups() : array{ + public function getHandlerGroups() : array { return $this->handlerGroups; } @@ -372,7 +429,7 @@ abstract class AbstractLog implements LogInterface { * @param string $handlerKey * @return bool */ - public function hasHandlerKey(string $handlerKey) : bool{ + public function hasHandlerKey(string $handlerKey) : bool { return array_key_exists($handlerKey, $this->handlerConfig); } @@ -380,21 +437,29 @@ abstract class AbstractLog implements LogInterface { * @param string $handlerKey * @return bool */ - public function hasHandlerGroupKey(string $handlerKey) : bool{ + public function hasHandlerGroupKey(string $handlerKey) : bool { return in_array($handlerKey, $this->getHandlerGroups()); } + /** + * @param string $processorKey + * @return bool + */ + public function hasProcessorKey(string $processorKey) : bool { + return array_key_exists($processorKey, $this->processorConfig); + } + /** * @return bool */ - public function hasBuffer() : bool{ + public function hasBuffer() : bool { return $this->buffer; } /** * @return bool */ - public function isGrouped() : bool{ + public function isGrouped() : bool { return !empty($this->getHandlerGroups()); } @@ -415,12 +480,12 @@ abstract class AbstractLog implements LogInterface { unset($this->handlerParamsConfig[$handlerKey]); } - // Handler parameters for Monolog\Handler\AbstractHandler --------------------------------------------------------- + // Handler parameters for Monolog\Handler\* instances ------------------------------------------------------------- /** * @return array */ - protected function getHandlerParamsStream() : array{ + protected function getHandlerParamsStream() : array { $params = []; if( !empty($conf = $this->handlerParamsConfig['stream']) ){ $params[] = $conf->stream; @@ -541,6 +606,16 @@ abstract class AbstractLog implements LogInterface { return $params; } + // Processor parameters for Monolog\Processor\* instances --------------------------------------------------------- + + /** + * get __construct() params for PsrLogMessageProcessor() call + * @return array + */ + protected function getProcessorParamsPsr() : array { + return !empty($conf = $this->processorParamsConfig['psr']) ? $conf : []; + } + /** * send this Log to global log buffer storage */ diff --git a/app/main/lib/logging/LogInterface.php b/app/main/lib/logging/LogInterface.php index 00d50c0b..ef26c261 100644 --- a/app/main/lib/logging/LogInterface.php +++ b/app/main/lib/logging/LogInterface.php @@ -17,47 +17,51 @@ interface LogInterface { public function setTag(string $tag); - public function setData(array $data): LogInterface; + public function setData(array $data) : LogInterface; - public function setTempData(array $data): LogInterface; + public function setTempData(array $data) : LogInterface; - public function addHandler(string $handlerKey, string $formatterKey = null, \stdClass $handlerParams = null): LogInterface; + public function addHandler(string $handlerKey, string $formatterKey = null, \stdClass $handlerParams = null) : LogInterface; - public function addHandlerGroup(string $handlerKey): LogInterface; + public function addHandlerGroup(string $handlerKey) : LogInterface; - public function getHandlerConfig(): array; + public function getHandlerConfig() : array; - public function getHandlerParamsConfig(): array; + public function getHandlerParamsConfig() : array; - public function getProcessorConfig(): array; + public function getProcessorConfig() : array; - public function getHandlerParams(string $handlerKey): array; + public function getProcessorParams(string $processorKey) : array; - public function getMessage(): string; + public function getHandlerParams(string $handlerKey) : array; - public function getAction(): string; + public function getMessage() : string; - public function getChannelType(): string; + public function getAction() : string; - public function getChannelName(): string; + public function getChannelType() : string; - public function getLevel(): string; + public function getChannelName() : string; - public function getData(): array; + public function getLevel() : string; - public function getContext(): array; + public function getData() : array; - public function getHandlerGroups(): array; + public function getContext() : array; - public function getGroupHash(): string; + public function getHandlerGroups() : array; - public function hasHandlerKey(string $handlerKey): bool; + public function getGroupHash() : string; - public function hasHandlerGroupKey(string $handlerKey): bool; + public function hasHandlerKey(string $handlerKey) : bool; - public function hasBuffer(): bool; + public function hasHandlerGroupKey(string $handlerKey) : bool; - public function isGrouped(): bool; + public function hasProcessorKey(string $processorKey) : bool; + + public function hasBuffer() : bool; + + public function isGrouped() : bool; public function removeHandlerGroups(); diff --git a/app/main/lib/logging/handler/AbstractWebhookHandler.php b/app/main/lib/logging/handler/AbstractWebhookHandler.php index 2bfaca53..7dd2cb09 100644 --- a/app/main/lib/logging/handler/AbstractWebhookHandler.php +++ b/app/main/lib/logging/handler/AbstractWebhookHandler.php @@ -127,7 +127,7 @@ abstract class AbstractWebhookHandler extends Handler\AbstractProcessingHandler * * @param array $record */ - protected function write(array $record){ + protected function write(array $record) : void { $record = $this->excludeFields($record); $postData = $this->getSlackData($record); diff --git a/app/main/lib/logging/handler/SocketHandler.php b/app/main/lib/logging/handler/SocketHandler.php index 24d2b9fb..4687e0c4 100644 --- a/app/main/lib/logging/handler/SocketHandler.php +++ b/app/main/lib/logging/handler/SocketHandler.php @@ -31,7 +31,7 @@ class SocketHandler extends \Monolog\Handler\SocketHandler { * @param array $record * @return bool */ - public function handle(array $record){ + public function handle(array $record) : bool { if (!$this->isHandling($record)) { return false; } diff --git a/app/main/model/pathfinder/alliancemodel.php b/app/main/model/pathfinder/alliancemodel.php index 680dcacd..10ad8d06 100644 --- a/app/main/model/pathfinder/alliancemodel.php +++ b/app/main/model/pathfinder/alliancemodel.php @@ -63,7 +63,7 @@ class AllianceModel extends AbstractPathfinderModel { $allianceData->shared = $this->shared; return $allianceData; - }/** @noinspection PhpHierarchyChecksInspection */ + } /** * Event "Hook" function diff --git a/app/main/model/pathfinder/connectionmodel.php b/app/main/model/pathfinder/connectionmodel.php index 9eb3139e..4b2e5ab2 100644 --- a/app/main/model/pathfinder/connectionmodel.php +++ b/app/main/model/pathfinder/connectionmodel.php @@ -156,7 +156,7 @@ class ConnectionModel extends AbstractMapTrackingModel { /** * setter for connection type * @param $type - * @return int|number + * @return array */ public function set_type($type){ // remove unwanted types -> they should not be send from client diff --git a/app/requirements.ini b/app/requirements.ini index 2c293d1c..b183ea12 100644 --- a/app/requirements.ini +++ b/app/requirements.ini @@ -9,7 +9,7 @@ APACHE.VERSION = 2.5 NGINX.VERSION = 1.9 [REQUIREMENTS.PHP] -VERSION = 7.1 +VERSION = 7.2 ; 64-bit version of PHP (4 = 32-bit, 8 = 64-bit) PHP_INT_SIZE = 8 diff --git a/composer-dev.json b/composer-dev.json index 8a3b377f..1536b4c4 100644 --- a/composer-dev.json +++ b/composer-dev.json @@ -1,6 +1,8 @@ { "name": "exodus4d/pathfinder", "description": "Mapping tool for EVE ONLINE", + "type": "project", + "homepage": "https://github.com/exodus4d/pathfinder", "minimum-stability": "stable", "license": "MIT", "authors": [ @@ -9,6 +11,10 @@ "email": "pathfinder@exodus4d.de" } ], + "config": { + "optimize-autoloader": true, + "lock": false + }, "autoload": { "psr-4": { "Exodus4D\\Pathfinder\\": "app/main" @@ -18,25 +24,29 @@ { "type": "vcs", "url": "../pathfinder_esi" - }], + } + ], "require": { - "php-64bit": ">=7.1", + "php-64bit": ">=7.2", "ext-pdo": "*", "ext-openssl": "*", "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", "ext-ctype": "*", - "monolog/monolog": "1.*", - "swiftmailer/swiftmailer": "^6.0", - "league/html-to-markdown": "4.8.*", + "bcosca/fatfree-core": "3.7.*", + "ikkez/f3-cortex": "dev-master#0d7754a5897a639e563add6b8d6db53fc0fae677", + "xfra35/f3-cron": "1.2.*", + "monolog/monolog": "2.*", + "swiftmailer/swiftmailer": "6.2.*", + "league/html-to-markdown": "4.9.*", "cache/redis-adapter": "1.0.*", "cache/filesystem-adapter": "1.0.*", "cache/array-adapter": "1.0.*", "cache/void-adapter": "1.0.*", "cache/namespaced-cache": "1.0.*", - "react/socket": "1.2.*", - "react/promise-stream": "1.1.*", + "react/socket": "1.3.*", + "react/promise-stream": "1.2.*", "clue/ndjson-react": "1.0.*", "exodus4d/pathfinder_esi": "dev-develop as 0.0.x-dev" }, diff --git a/composer.json b/composer.json index 40fa6e9b..b7395b40 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,8 @@ { "name": "exodus4d/pathfinder", "description": "Mapping tool for EVE ONLINE", + "type": "project", + "homepage": "https://github.com/exodus4d/pathfinder", "minimum-stability": "stable", "license": "MIT", "authors": [ @@ -9,6 +11,10 @@ "email": "pathfinder@exodus4d.de" } ], + "config": { + "optimize-autoloader": true, + "lock": true + }, "autoload": { "psr-4": { "Exodus4D\\Pathfinder\\": "app/main" @@ -18,7 +24,8 @@ { "type": "vcs", "url": "https://github.com/exodus4d/pathfinder_esi" - }], + } + ], "require": { "php-64bit": ">=7.1", "ext-pdo": "*", @@ -27,18 +34,21 @@ "ext-json": "*", "ext-mbstring": "*", "ext-ctype": "*", - "monolog/monolog": "1.*", - "swiftmailer/swiftmailer": "^6.0", - "league/html-to-markdown": "4.8.*", + "bcosca/fatfree-core": "3.7.*", + "ikkez/f3-cortex": "1.6.*", + "xfra35/f3-cron": "1.2.*", + "monolog/monolog": "2.*", + "swiftmailer/swiftmailer": "6.2.x", + "league/html-to-markdown": "4.9.*", "cache/redis-adapter": "1.0.*", "cache/filesystem-adapter": "1.0.*", "cache/array-adapter": "1.0.*", "cache/void-adapter": "1.0.*", "cache/namespaced-cache": "1.0.*", - "react/socket": "1.2.*", - "react/promise-stream": "1.1.*", + "react/socket": "1.3.*", + "react/promise-stream": "1.2.*", "clue/ndjson-react": "1.0.*", - "exodus4d/pathfinder_esi": "v1.3.2" + "exodus4d/pathfinder_esi": "1.3.3" }, "suggest": { "ext-redis": "Redis can be used as cache backend." diff --git a/index.php b/index.php index 22e115b8..98b54884 100644 --- a/index.php +++ b/index.php @@ -6,7 +6,7 @@ if(file_exists($composerAutoloader)){ require_once($composerAutoloader); } -$f3 = require_once('app/lib/base.php'); +$f3 = \Base::instance(); // load main config $f3->config('app/config.ini', true); diff --git a/js/app/ui/dialog/map_info.js b/js/app/ui/dialog/map_info.js index 04359396..6ac3bb80 100644 --- a/js/app/ui/dialog/map_info.js +++ b/js/app/ui/dialog/map_info.js @@ -1069,20 +1069,35 @@ define([ title: '', width: 100, className: ['text-right'].join(' '), - data: 'datetime.date', + data: 'datetime', render: { _: function(data, type, row, meta){ - // strip microseconds - let logDateString = data.substring(0, 19); - let logDate = new Date(logDateString.replace(/-/g, '/')); - data = Util.convertDateToString(logDate, true); - - // check whether log is new (today) -> - if(logDate.setHours(0,0,0,0) === serverHours){ - // replace dd/mm/YYYY - data = 'today' + data.substring(10); + let value = ''; + let logDateString; + if(typeof data === 'string' && data.length){ + // NEW: > v1.5.5 e.g: '2019-12-09T22:07:01.382455+00:00' + logDateString = data; + }else if(data && data.date){ + // OLD: <= v1.5.5 object data.date: '2019-12-09 14:50:46.608484' + logDateString = data.date; } - return data; + + if(logDateString){ + logDateString = logDateString + .substring(0, 19) + .replace(/-/g, '/') + .replace(/T/g, ' '); + let logDate = new Date(logDateString); + value = Util.convertDateToString(logDate, true); + + // check whether log is new (today) -> + if(logDate.setHours(0,0,0,0) === serverHours){ + // replace dd/mm/YYYY + value = 'today' + value.substring(10); + } + } + + return value; } } },{ diff --git a/public/js/v1.5.6/app/ui/dialog/map_info.js b/public/js/v1.5.6/app/ui/dialog/map_info.js index 04359396..6ac3bb80 100644 --- a/public/js/v1.5.6/app/ui/dialog/map_info.js +++ b/public/js/v1.5.6/app/ui/dialog/map_info.js @@ -1069,20 +1069,35 @@ define([ title: '', width: 100, className: ['text-right'].join(' '), - data: 'datetime.date', + data: 'datetime', render: { _: function(data, type, row, meta){ - // strip microseconds - let logDateString = data.substring(0, 19); - let logDate = new Date(logDateString.replace(/-/g, '/')); - data = Util.convertDateToString(logDate, true); - - // check whether log is new (today) -> - if(logDate.setHours(0,0,0,0) === serverHours){ - // replace dd/mm/YYYY - data = 'today' + data.substring(10); + let value = ''; + let logDateString; + if(typeof data === 'string' && data.length){ + // NEW: > v1.5.5 e.g: '2019-12-09T22:07:01.382455+00:00' + logDateString = data; + }else if(data && data.date){ + // OLD: <= v1.5.5 object data.date: '2019-12-09 14:50:46.608484' + logDateString = data.date; } - return data; + + if(logDateString){ + logDateString = logDateString + .substring(0, 19) + .replace(/-/g, '/') + .replace(/T/g, ' '); + let logDate = new Date(logDateString); + value = Util.convertDateToString(logDate, true); + + // check whether log is new (today) -> + if(logDate.setHours(0,0,0,0) === serverHours){ + // replace dd/mm/YYYY + value = 'today' + value.substring(10); + } + } + + return value; } } },{ diff --git a/public/templates/view/setup.html b/public/templates/view/setup.html index 5479ddc0..85ee08bc 100644 --- a/public/templates/view/setup.html +++ b/public/templates/view/setup.html @@ -1159,7 +1159,7 @@ {{ mb_strimwidth(pathinfo(@logData.fileName, PATHINFO_FILENAME), 0, 15, '…') }} {{ @logData.message }} - +