diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..bdb0cabc8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore index ab7a3e9cb..0f9021904 100644 --- a/.gitignore +++ b/.gitignore @@ -47,5 +47,12 @@ Temporary Items # project files # ======================== .idea -.sass-cache .usage +*.gz +composer-dev.lock +package-lock.json +/conf/ +/node_modules/ +/public/js/vX.X.X/ +/vendor/ +/history/ diff --git a/.htaccess b/.htaccess index cface6a91..4efdc2625 100644 --- a/.htaccess +++ b/.htaccess @@ -1,8 +1,12 @@ -# Enable rewrite engine and route requests to framework +# HTTPS over SSL version +# Information: https://github.com/exodus4d/pathfinder/wiki/Apache + +# Enable rewrite engine and route requests to framework =========================================== RewriteEngine On -# HTTP to HTTPS ---------------------------------------------------------------- +# HTTP to HTTPS =================================================================================== RewriteCond %{HTTPS} off +RewriteCond %{HTTP_HOST} !^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ RewriteCond %{HTTP_HOST} !=localhost # First rewrite to HTTPS: @@ -10,21 +14,25 @@ RewriteCond %{HTTP_HOST} !=localhost # the subsequent rule will catch it. RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] -# Now, rewrite any request to the wrong domain to use www. +# Rewrite NONE www. to force www. ================================================================= RewriteCond %{HTTP_HOST} !^www\. +# skip "localhost" (dev environment)... RewriteCond %{HTTP_HOST} !=localhost +# skip IP calls (dev environment) +RewriteCond %{HTTP_HOST} !^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ +# rewrite everything else to "https://" and "www." RewriteRule .* https://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - # Some servers require you to specify the `RewriteBase` directive # In such cases, it should be the path (relative to the document root) -# containing this .htaccess file -# -#RewriteBase /app/ +# containing this .htaccess file: +# RewriteBase /app/ +# Protect system files ============================================================================ RewriteCond %{ENV:REDIRECT_STATUS} ^$ RewriteRule ^(lib|tmp)\/|\.(ini|php)$ - [R=404] +# Rewrite "everything" to index.php (dispatcher) ================================================== RewriteCond %{REQUEST_FILENAME} !-l RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d @@ -32,18 +40,19 @@ RewriteRule .* index.php [L,QSA] RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] -# PHP global Vars +# PHP global Vars (can be set in php.ini as well,...) ============================================= php_value max_input_vars 5000 php_value suhosin.get.max_vars 5000 php_value suhosin.post.max_vars 5000 php_value suhosin.request.max_vars 5000 - -# PHP error logs +# Activate PHP error log ========================================================================== php_flag log_errors on -# php_value error_log "/www/htdocs/w0128162/www.pathfinder.exodus4d.de/logs/php_errors.log" +# php_value error_log "/www/htdocs/www.pathfinder-w.space/logs/php_errors.log" -# caching +# Cache Header ==================================================================================== +# You should not change anything in here! +# New versioned files come with a unique path (e.g. ../js/v1.0.0/..) to force client cache busting. # fonts @@ -69,7 +78,11 @@ php_flag log_errors on ## js/source maps - ExpiresActive off + ExpiresActive on + ExpiresDefault "access plus 1 year" + Header append Cache-Control "public" + FileETag None + Header unset ETag # html templates @@ -77,6 +90,4 @@ php_flag log_errors on ExpiresActive on ExpiresDefault "access plus 1 week" - - - + \ No newline at end of file diff --git a/.htaccess_HTTP b/.htaccess_HTTP new file mode 100644 index 000000000..0063dc180 --- /dev/null +++ b/.htaccess_HTTP @@ -0,0 +1,82 @@ +# HTTP version +# Information: https://github.com/exodus4d/pathfinder/wiki/Apache + +# Enable rewrite engine and route requests to framework =========================================== +RewriteEngine On + +# Rewrite NONE www. to force www. ================================================================= +RewriteCond %{HTTP_HOST} !^www\. +# skip "localhost" (dev environment)... +RewriteCond %{HTTP_HOST} !=localhost +# skip IP calls (dev environment) e.g. 127.0.0.1 +RewriteCond %{HTTP_HOST} !^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ +# rewrite everything else to "http://" and "www." +RewriteRule .* http://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + +# Some servers require you to specify the `RewriteBase` directive +# In such cases, it should be the path (relative to the document root) +# containing this .htaccess file: +# RewriteBase /app/ + +# Protect system files ============================================================================ +RewriteCond %{ENV:REDIRECT_STATUS} ^$ +RewriteRule ^(lib|tmp)\/|\.(ini|php)$ - [R=404] + +# Rewrite "everything" to index.php (dispatcher) ================================================== +RewriteCond %{REQUEST_FILENAME} !-l +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule .* index.php [L,QSA] +RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] + +# PHP global Vars (can be set in php.ini as well,...) ============================================= +php_value max_input_vars 5000 +php_value suhosin.get.max_vars 5000 +php_value suhosin.post.max_vars 5000 +php_value suhosin.request.max_vars 5000 + +# Activate PHP error log ========================================================================== +php_flag log_errors on +php_value error_log "/www/htdocs/w0128162/www.pathfinder-dev.exodus4d.de/logs/php_errors.log" + +# Cache Header ==================================================================================== +# You should not change anything in here! +# New versioned files come with a unique path (e.g. ../js/v1.0.0/..) to force client cache busting. + + # fonts + + ExpiresActive on + ExpiresDefault "access plus 1 month" + Header append Cache-Control "public" + + + # images/vector graphics + + ExpiresActive on + ExpiresDefault "access plus 1 year" + Header append Cache-Control "public" + FileETag None + Header unset ETag + + + # css + + ExpiresActive on + ExpiresDefault "access plus 1 month" + + + ## js/source maps + + ExpiresActive on + ExpiresDefault "access plus 1 year" + Header append Cache-Control "public" + FileETag None + Header unset ETag + + + # html templates + + ExpiresActive on + ExpiresDefault "access plus 1 week" + + \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 000000000..c61df68ca --- /dev/null +++ b/.jshintrc @@ -0,0 +1,75 @@ +{ + /* + * ENVIRONMENTS + * ================= + */ + + // Define globals exposed by modern browsers. + "browser": true, + + // Define globals exposed by jQuery. + "jquery": true, + + // Define globals exposed by Node.js. + "node": true, + + // Allow ES8. + "esversion": 9, + + /* + * ENFORCING OPTIONS + * ================= + */ + + // Force all variable names to use either camelCase style or UPPER_CASE + // with underscores. + "camelcase": false, + + // Prohibit use of == and != in favor of === and !==. + "eqeqeq": true, + + // Enforce tab width of 2 spaces. + "indent": 2, + + // Require variables/functions to be defined before being used + "latedef": false, + + // Enforce line length to 100 characters + "maxlen": 220, + + // Require capitalized names for constructor functions. + "newcap": true, + + // Enforce use of single quotation marks for strings. + "quotmark": "single", + + // Enforce placing 'use strict' at the top function scope + "strict": true, + + // Prohibit use of explicitly undeclared variables. + "undef": true, + + // Warn when variables are defined but never used. + "unused": false, + + // Prohibit use of empty blocks + "noempty": true, + + /* + * RELAXING OPTIONS + * ================= + */ + + // Suppress warnings about == null comparisons. + "eqnull": true, + + "predef": [ + "requirejs", + "define", + "jsPlumb", + "Magnetizer", + "Morris", + "TweenLite", + "Circ" + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..ac2448a97 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Mark Friedrich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 1f84df191..bc7d3705e 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,84 @@ -## *PATHFINDER* -Mapping tool for *EVE ONLINE* +# ![Pathfinder logo](favicon/favicon-32x32.png "Logo") *PATHFINDER* +#### Mapping tool for [*EVE ONLINE*](https://www.eveonline.com) -url: https://www.pathfinder.exodus4d.de +- Project URL [https://www.pathfinder-w.space](https://www.pathfinder-w.space) +- Screenshots [imgur.com](http://imgur.com/a/k2aVa) +- Videos [youtube.com](https://www.youtube.com/channel/UC7HU7XEoMbqRwqxDTbMjSPg) +- Licence [MIT](http://opensource.org/licenses/MIT) -### Project requirements -------------------------------------------------- -#### APACHE Webserver - - PHP 5.3.4 or higher - - PCRE 8.02 or higher (usually shipped with PHP package, but needs to be additionally updated on CentOS or Red Hat systems) - - mod_rewrite and mod_headers enabled - - GD libary (for Image plugin) - - cURL, sockets or stream extension (for Web plugin) - - Gzip compression +#### Development +- Test server: [https://www.dev.pathfinder-w.space](https://www.dev.pathfinder-w.space) + - Running current `develop` branch + - _SISI_ _ESI_ (make sure to use your test-server client) + - Available for public testing (e.g. new feature,… ) + - Database will be cleared from time to time +- Installation guide: + - [wiki](https://github.com/exodus4d/pathfinder/wiki) +- Developer [Slack](https://slack.com) chat: + - https://pathfinder-eve-online.slack.com + - Join channel [pathfinder-eve-online.slack.com](https://join.slack.com/t/pathfinder-eve-online/shared_invite/enQtMzMyOTkyMjczMTA3LWI2NGE1OTY5ODBmNDZlMDY3MDIzYjk5ZTljM2JjZjIwNDRkNzMyMTEwMDUzOGQwM2E3ZjE1NGEwNThlMzYzY2Y) + - Can´t join? pathfinder@exodus4d.de + +**Feel free to check the code for bugs and security issues. +Issues should be reported in the [Issue](https://github.com/exodus4d/pathfinder/issues) section.** + +*** + +### Project structure +
+ ─╮
+  ├─ app/              [0755] → PHP root
+  │  ├─ Controller/           → controller classes for app/ajax endpoints (see routes.ini)
+  │  ├─ Cron/                 → controller classes cronjob endpoints (see cron.ini)
+  │  ├─ Data/                 → classes for data handling
+  │  ├─ Db/                   → classes for DB handling
+  │  ├─ Exception/            → custom exceptions
+  │  ├─ Lib/                  → libs
+  │  ├─ Model/                → ORM
+  │  ├─ config.ini            → config - F3 core config: SystemVariables
+  │  ├─ cron.ini              → config - cronjobs
+  │  ├─ environment.ini       → config - system environment
+  │  ├─ pathfinder.ini        → config - pathfinder
+  │  ├─ plugin.ini            → config - custom plugins
+  │  ├─ requirements.ini      → config - system requirements
+  │  └─ routes.ini            → config - routes
+  ├─ export/           [0755] → static data
+  │  ├─ csv/                  → *.csv used by /setup page
+  │  └─ sql/                  → DB dump for import (eve_universe.sql.zip)
+  ├─ favicon/          [0755] → favicons
+  ├─ history/          [0777] → log files (map history logs) [optional]
+  ├─ js/               [0755] → JS source files (not used for production)
+  │  ├─ app/                  → "PATHFINDER" core files
+  │  ├─ lib/                  → 3rd party libs
+  │  └─ app.js                → require.js config
+  ├─ logs/             [0777] → log files
+  │  └─ …
+  ├─ public/           [0755] → static resources
+  │  ├─ css/                  → CSS dist/build folder (minified)
+  │  ├─ fonts/                → icon-/fonts
+  │  ├─ img/                  → images
+  │  ├─ js/                   → JS dist/build folder and source maps (minified, uglified)
+  │  └─ templates/            → templates
+  ├─ sass/                    → SCSS sources (not used for production)
+  ├─ tmp/              [0777] → cache folder (PHP templates)
+  │  └─ cache/         [0777] → cache folder (PHP cache)
+  ├─ .htaccess         [0755] → reroute/caching rules ("Apache" only!)
+  └─ index.php         [0755]
+
+  ━━━━━━━━━━━━━━━━━━━━━━━━━━
+  CI/CD config files:
   
-  Nginx and Lighttpd configurations are also possible.
-  http://fatfreeframework.com/system-requirements
-#### Database
-  - mysql: MySQL 5.x
-  - sqlite: SQLite 3 and SQLite 2
-  - pgsql: PostgreSQL
-  - sqlsrv: Microsoft SQL Server / SQL Azure
-  - mssql, dblib, sybase: FreeTDS / Microsoft SQL Server / Sybase
-  - odbc: ODBC v3
-  - oci: Oracle
+  ├─ .jshintrc                → "JSHint" config (not used for production)
+  ├─ composer.json            → "Composer" package definition
+  ├─ gulpfile.js              → "Gulp" task config (not used for production)
+  ├─ package.json             → "Node.js" dependency config (not used for production)
+  └─ README.md                → This file :) (not used for production)
+
+ +*** + +### Contributing - Here is a list of links to DSN connection details for all currently supported engines in the SQL layer: - http://fatfreeframework.com/sql -#### Development Environment - - t.b.a. +[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/0)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/0)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/1)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/1)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/2)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/2)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/3)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/3)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/4)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/4)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/5)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/5)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/6)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/6)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/7)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/7) -### Folder structure (production) ---------------------------------------- -``` - |-- (0755) app --> backend [*.php] - |-- app --> "Fat Free Framework" extensions - |-- lib --> "Fat Free Framework" - |-- main --> "PATHFINDER" root - |-- config.ini --> config "f3" framework - |-- cron.ini --> config cronjobs - |-- pathfinder.ini --> config pathfinder - |-- routes.ini --> config routes - |-- (0755) build_js --> JS build folder and source maps (minified, uglified) - |-- app --> "PATHFINDER" core files - |-- lib --> 3rd partie extension/library - |-- build.txt --> generated build summary - |-- (0755) js --> JS source files (raw) - |-- app --> "PASTHFINDER" core files (not used for production ) - |-- lib --> 3rd partie extension/library (not used for production ) - |-- app.js --> require.js config (!required for production!) - |-- (0777) logs --> log files - |-- ... - | -- node_modules --> node.js modules (not used for production ) - |-- ... - |-- (0755) public --> frontend source - |-- css --> CSS build folder (minified) - |-- fonts --> Web/Icon fonts - |-- img --> images - |-- templates --> templates - |-- sass --> SCSS source (not used for production ) - |-- ... - |-- (0777) tmp --> cache folder - |-- ... - |-- (0755) .htaccess --> reroute/caching rules - |-- (0755) index.php -``` diff --git a/app/Controller/AccessController.php b/app/Controller/AccessController.php new file mode 100644 index 000000000..63453f37b --- /dev/null +++ b/app/Controller/AccessController.php @@ -0,0 +1,123 @@ +isLoggedIn($f3) !== 'OK'){ + // no character found or login timer expired + $this->logoutCharacter($f3); + // skip route handler and afterroute() + $return = false; + } + } + + return $return; + } + + /** + * check login status and look or a valid character + * @param \Base $f3 + * @return string + * @throws \Exception + */ + protected function isLoggedIn(\Base $f3) : string { + $loginStatus = 'UNKNOWN'; + // disable ttl cache time here. Further getCharacter() calls should use a short ttl + if($character = $this->getCharacter(0)){ + if($character->checkLoginTimer()){ + if(( $authStatus = $character->isAuthorized()) === 'OK'){ + $loginStatus = 'OK'; + }else{ + $loginStatus = $authStatus; + } + }else{ + $loginStatus = 'MAX LOGIN TIME EXCEEDED'; + } + }else{ + $loginStatus = 'NO SESSION FOUND'; + } + + // log character access status in debug mode + if( + $loginStatus !== 'OK' && + $f3->get('DEBUG') === 3 + ){ + self::getLogger('CHARACTER_ACCESS')->write( + sprintf(Pathfinder\CharacterModel::LOG_ACCESS, + $character->_id , + $loginStatus, + $character->name + ) + ); + } + + return $loginStatus; + } + + /** + * broadcast MapModel to clients + * @param Pathfinder\MapModel $map + * @param bool $noCache + */ + protected function broadcastMap(Pathfinder\MapModel $map, bool $noCache = false) : void { + $this->broadcastMapData($this->getFormattedMapData($map, $noCache)); + } + + + /** + * broadcast map data to clients + * -> send over TCP Socket + * @param array|null $mapData + */ + protected function broadcastMapData(?array $mapData) : void { + if(!empty($mapData)){ + $this->getF3()->webSocket()->write('mapUpdate', $mapData); + } + } + + /** + * get formatted Map Data + * @param Pathfinder\MapModel $map + * @param bool $noCache + * @return array|null + */ + protected function getFormattedMapData(Pathfinder\MapModel $map, bool $noCache = false) : ?array { + $data = null; + try{ + $mapData = $map->getData($noCache); + $data = [ + 'config' => $mapData->mapData, + 'data' => [ + 'systems' => $mapData->systems, + 'connections' => $mapData->connections, + ] + ]; + }catch(\Exception $e){ + + } + + return $data; + } + +} \ No newline at end of file diff --git a/app/Controller/Admin.php b/app/Controller/Admin.php new file mode 100644 index 000000000..b419daa55 --- /dev/null +++ b/app/Controller/Admin.php @@ -0,0 +1,446 @@ + '5m', + 60 => '1h', + 1440 => '24h' + ]; + + /** + * event handler for all "views" + * some global template variables are set in here + * @param \Base $f3 + * @param $params + * @return bool + * @throws \Exception + */ + function beforeroute(\Base $f3, $params): bool { + $return = parent::beforeroute($f3, $params); + + $f3->set('tplPage', 'login'); + + if($character = $this->getAdminCharacter($f3)){ + $f3->set('tplLogged', true); + $f3->set('character', $character); + $this->dispatch($f3, $params, $character); + } + + $f3->set('tplAuthType', $f3->get('BASE') . $f3->alias( 'sso', ['action' => 'requestAdminAuthorization'])); + + // page title + $f3->set('tplPageTitle', 'Admin | ' . Config::getPathfinderData('name')); + + // main page content + $f3->set('tplPageContent', Config::getPathfinderData('view.admin')); + + // body element class + $f3->set('tplBodyClass', 'pf-landing'); + + return $return; + } + + /** + * event handler after routing + * @param \Base $f3 + */ + public function afterroute(\Base $f3) { + // js view (file) + $f3->set('tplJsView', 'admin'); + + // render view + echo \Template::instance()->render( Config::getPathfinderData('view.index') ); + + // clear all SSO related temp data + if( $f3->exists(Sso::SESSION_KEY_SSO) ){ + $f3->clear('SESSION.SSO.ERROR'); + } + } + + /** + * returns valid admin $characterModel for current user + * @param \Base $f3 + * @return CharacterModel|null + * @throws \Exception + */ + protected function getAdminCharacter(\Base $f3){ + $adminCharacter = null; + if( !$f3->exists(Sso::SESSION_KEY_SSO_ERROR) ){ + if( $character = $this->getCharacter(0) ){ + if(in_array($character->roleId->name, ['SUPER', 'CORPORATION'], true)){ + // current character is admin + $adminCharacter = $character; + }elseif( !$character->hasAdminScopes() ){ + $f3->set(Sso::SESSION_KEY_SSO_ERROR, + sprintf( + self::ERROR_SSO_CHARACTER_SCOPES, + $character->name + )); + }else{ + $f3->set(Sso::SESSION_KEY_SSO_ERROR, + sprintf( + self::ERROR_SSO_CHARACTER_ROLES, + $character->name, + implode(', ', CorporationModel::ADMIN_ROLES) + )); + } + }else{ + $f3->set(Sso::SESSION_KEY_SSO_ERROR, self::ERROR_SSO_CHARACTER_EXISTS); + } + } + + return $adminCharacter; + } + + /** + * dispatch page events by URL $params + * @param \Base $f3 + * @param $params + * @param null $character + * @throws \Exception + */ + public function dispatch(\Base $f3, $params, $character = null){ + if($character instanceof CharacterModel){ + // user logged in + $parts = array_values(array_filter(array_map('strtolower', explode('/', $params['*'])))); + $f3->set('tplPage', $parts[0]); + + switch($parts[0]){ + case 'settings': + switch($parts[1]){ + case 'save': + $objectId = (int)$parts[2]; + $values = (array)$f3->get('GET'); + $this->saveSettings($character, $objectId, $values); + + $f3->reroute('@admin(@*=/' . $parts[0] . ')'); + break; + } + $f3->set('tplDefaultRole', RoleModel::getDefaultRole()); + $f3->set('tplRoles', RoleModel::getAll()); + $this->initSettings($f3, $character); + break; + case 'members': + switch($parts[1]){ + case 'kick': + $objectId = (int)$parts[2]; + $value = (int)$parts[3]; + $this->kickCharacter($character, $objectId, $value); + + $f3->reroute('@admin(@*=/' . $parts[0] . ')'); + break; + case 'ban': + $objectId = (int)$parts[2]; + $value = (int)$parts[3]; + $this->banCharacter($character, $objectId, $value); + break; + } + $f3->set('tplKickOptions', self::KICK_OPTIONS); + $this->initMembers($f3, $character); + break; + case 'maps': + switch($parts[1]){ + case 'active': + $objectId = (int)$parts[2]; + $value = (int)$parts[3]; + $this->activateMap($character, $objectId, $value); + + $f3->reroute('@admin(@*=/' . $parts[0] . ')'); + break; + case 'delete': + $objectId = (int)$parts[2]; + $this->deleteMap($character, $objectId); + $f3->reroute('@admin(@*=/' . $parts[0] . ')'); + break; + } + $this->initMaps($f3, $character); + break; + case 'login': + default: + $f3->set('tplPage', 'login'); + break; + } + } + } + + /** + * save or delete settings (e.g. corporation rights) + * @param CharacterModel $character + * @param int $corporationId + * @param array $settings + * @throws \Exception + */ + protected function saveSettings(CharacterModel $character, int $corporationId, array $settings){ + $defaultRole = RoleModel::getDefaultRole(); + + if($corporationId && $defaultRole){ + $corporations = $this->getAccessibleCorporations($character); + foreach($corporations as $corporation){ + if($corporation->_id === $corporationId){ + // character has access to that corporation -> create/update/delete rights... + if($corporationRightsData = (array)$settings['rights']){ + // get existing corp rights + foreach($corporation->getRights($corporation::RIGHTS, ['addInactive' => true]) as $corporationRight){ + $corporationRightData = $corporationRightsData[$corporationRight->rightId->_id]; + if( + $corporationRightData && + $corporationRightData['roleId'] != $defaultRole->_id // default roles should not be saved + ){ + $corporationRight->setData($corporationRightData); + $corporationRight->setActive(true); + $corporationRight->save(); + }else{ + // right not send by user -> delete existing right + $corporationRight->erase(); + } + } + } + break; + } + } + } + } + + /** + * kick or revoke a character + * @param CharacterModel $character + * @param int $kickCharacterId + * @param int $minutes + */ + protected function kickCharacter(CharacterModel $character, $kickCharacterId, $minutes){ + $kickOptions = self::KICK_OPTIONS; + $minKickTime = key($kickOptions) ; + end($kickOptions); + $maxKickTime = key($kickOptions); + $minutes = in_array($minutes, range($minKickTime, $maxKickTime)) ? $minutes : 0; + + $kickCharacters = $this->filterValidCharacters($character, $kickCharacterId); + foreach($kickCharacters as $kickCharacter){ + $kickCharacter->kick($minutes); + $kickCharacter->save(); + + self::getLogger()->write( + sprintf( + self::LOG_TEXT_KICK_BAN, + $minutes ? 'KICK' : 'KICK REVOKE', + $kickCharacter->name, + $kickCharacter->getCorporation()->name, + $character->name + ) + ); + } + } + + /** + * @param CharacterModel $character + * @param int $banCharacterId + * @param int $value + */ + protected function banCharacter(CharacterModel $character, $banCharacterId, $value){ + $banCharacters = $this->filterValidCharacters($character, $banCharacterId); + foreach($banCharacters as $banCharacter){ + $banCharacter->ban($value); + $banCharacter->save(); + + self::getLogger()->write( + sprintf( + self::LOG_TEXT_KICK_BAN, + $value ? 'BAN' : 'BAN REVOKE', + $banCharacter->name, + $banCharacter->getCorporation()->name, + $character->name + ) + ); + } + } + + /** + * checks whether a $character has admin access rights for $characterId + * -> must be in same corporation + * @param CharacterModel $character + * @param int $characterId + * @return array|\DB\CortexCollection + */ + protected function filterValidCharacters(CharacterModel $character, $characterId){ + $characters = []; + // check if kickCharacters belong to same Corp as admin character + // -> remove admin char from valid characters... + if( !empty($characterIds = array_diff([$characterId], [$character->_id])) ){ + if($character->roleId->name === 'SUPER'){ + if($filterCharacters = CharacterModel::getAll($characterIds)){ + $characters = $filterCharacters; + } + }else{ + $characters = $character->getCorporation()->getCharacters($characterIds); + } + } + return $characters; + } + + /** + * @param CharacterModel $character + * @param int $mapId + * @param int $value + */ + protected function activateMap(CharacterModel $character, int $mapId, int $value){ + $maps = $this->filterValidMaps($character, $mapId); + foreach($maps as $map){ + $map->setActive((bool)$value); + $map->save($character); + } + } + + /** + * @param CharacterModel $character + * @param int $mapId + */ + protected function deleteMap(CharacterModel $character, int $mapId){ + $maps = $this->filterValidMaps($character, $mapId); + foreach($maps as $map){ + $map->erase(); + } + } + + /** + * checks whether a $character has admin access rights for $mapId + * @param CharacterModel $character + * @param int $mapId + * @return \DB\CortexCollection[]|MapModel[] + */ + protected function filterValidMaps(CharacterModel $character, int $mapId) { + $maps = []; + if($character->roleId->name === 'SUPER'){ + if($filterMaps = MapModel::getAll([$mapId], ['addInactive' => true])){ + $maps = $filterMaps; + } + }else{ + $maps = $character->getCorporation()->getMaps($mapId, ['addInactive' => true, 'ignoreMapCount' => true]); + } + + return $maps; + } + + /** + * get log file for "admin" logs + * @param string $type + * @return \Log + */ + static function getLogger($type = 'ADMIN') : \Log { + return parent::getLogger('ADMIN'); + } + + /** + * init /settings page data + * @param \Base $f3 + * @param CharacterModel $character + */ + protected function initSettings(\Base $f3, CharacterModel $character){ + $data = (object) []; + $corporations = $this->getAccessibleCorporations($character); + + foreach($corporations as $corporation){ + $data->corporations[$corporation->name] = $corporation; + } + + $f3->set('tplSettings', $data); + } + + /** + * init /member page data + * @param \Base $f3 + * @param CharacterModel $character + */ + protected function initMembers(\Base $f3, CharacterModel $character){ + $data = (object) []; + if($characterCorporation = $character->getCorporation()){ + $corporations = $this->getAccessibleCorporations($character); + + foreach($corporations as $corporation){ + if($characters = $corporation->getCharacters()){ + $data->corpMembers[$corporation->name] = $characters; + } + } + + // sort corporation from current user first + if( !empty($data->corpMembers[$characterCorporation->name]) ){ + $data->corpMembers = array($characterCorporation->name => $data->corpMembers[$characterCorporation->name]) + $data->corpMembers; + } + } + + $f3->set('tplMembers', $data); + } + + /** + * init /maps page data + * @param \Base $f3 + * @param CharacterModel $character + */ + protected function initMaps(\Base $f3, CharacterModel $character){ + $data = (object) []; + if($characterCorporation = $character->getCorporation()){ + $corporations = $this->getAccessibleCorporations($character); + + foreach($corporations as $corporation){ + if($maps = $corporation->getMaps(null, ['addInactive' => true, 'ignoreMapCount' => true])){ + $data->corpMaps[$corporation->name] = $maps; + } + } + } + + $f3->set('tplMaps', $data); + + if( !isset($data->corpMaps) ){ + $f3->set('tplNotification', $this->getNotificationObject('No maps found', + 'Only corporation maps could get loaded' , + 'info' + )); + } + } + + /** + * get all corporations a characters has admin access for + * @param CharacterModel $character + * @return CorporationModel[] + */ + protected function getAccessibleCorporations(CharacterModel $character) { + $corporations = []; + if($characterCorporation = $character->getCorporation()){ + switch($character->roleId->name){ + case 'SUPER': + if($accessCorporations = CorporationModel::getAll(['addNPC' => true])){ + $corporations = $accessCorporations; + } + break; + case 'CORPORATION': + $corporations[] = $characterCorporation; + break; + } + } + + return $corporations; + } + +} \ No newline at end of file diff --git a/app/Controller/Api/Access.php b/app/Controller/Api/Access.php new file mode 100644 index 000000000..d1ca15f0c --- /dev/null +++ b/app/Controller/Api/Access.php @@ -0,0 +1,67 @@ +find( [ + "LOWER(name) LIKE :token AND " . + "active = 1 AND " . + "shared = 1 ", + ':token' => '%' . $searchToken . '%' + ]); + + if($accessList){ + foreach($accessList as $accessObject){ + $accessData[] = $accessObject->getData(); + } + } + } + } + + echo json_encode($accessData); + } + +} \ No newline at end of file diff --git a/app/Controller/Api/GitHub.php b/app/Controller/Api/GitHub.php new file mode 100644 index 000000000..9f3c8c227 --- /dev/null +++ b/app/Controller/Api/GitHub.php @@ -0,0 +1,85 @@ +releasesData = []; + $return->version = (object) []; + $return->version->current = Config::getPathfinderData('version'); + $return->version->last = ''; + $return->version->delta = null; + $return->version->dev = false; + + $releases = $f3->gitHubClient()->send('getProjectReleases', 'exodus4d/pathfinder', $releaseCount); + + foreach($releases as $key => &$release){ + // check version ------------------------------------------------------------------------------------------ + if($key === 0){ + $return->version->last = $release['name']; + if(version_compare( $return->version->current, $return->version->last, '>')){ + $return->version->dev = true; + } + } + + if( + !$return->version->dev && + version_compare($release['name'], $return->version->current, '>=') + ){ + $return->version->delta = ($key === count($releases) - 1) ? '>= ' . $key : $key; + } + + // format body ------------------------------------------------------------------------------------ + $body = $release['body']; + + // remove "update information" from release text + // -> keep everything until first "***" -> horizontal line + if( ($pos = strpos($body, '***')) !== false){ + $body = substr($body, 0, $pos); + } + + // convert list style + $body = str_replace(' - ', '* ', $body); + + // convert Markdown to HTML -> use either gitHub API (in oder to create abs, issue links) + // -> or F3´s markdown as fallback + $html = $f3->gitHubClient()->send('markdownToHtml', 'exodus4d/pathfinder', $body); + + if(!empty($html)){ + $body = $html; + }else{ + $body = \Markdown::instance()->convert(trim($body)); + } + + $release['body'] = $body; + } + + $return->releasesData = $releases; + + echo json_encode($return); + } +} \ No newline at end of file diff --git a/app/Controller/Api/Map.php b/app/Controller/Api/Map.php new file mode 100644 index 000000000..14611fcfd --- /dev/null +++ b/app/Controller/Api/Map.php @@ -0,0 +1,1085 @@ +exists(self::CACHE_KEY_INIT, $return)){ + $return = (object) []; + $return->error = []; + + // static program data ------------------------------------------------------------------------------------ + $return->timer = Config::getPathfinderData('timer'); + + // get all available map types ---------------------------------------------------------------------------- + $mapType = Pathfinder\AbstractPathfinderModel::getNew('MapTypeModel'); + $rows = $mapType->find('active = 1'); + + // default map type config + $mapsDefaultConfig = Config::getMapsDefaultConfig(); + $mapTypeData = []; + foreach((array)$rows as $rowData){ + $data = [ + 'id' => $rowData->id, + 'label' => $rowData->label, + 'class' => $rowData->class, + 'classTab' => $rowData->classTab, + 'defaultConfig' => $mapsDefaultConfig[$rowData->name] + ]; + $mapTypeData[$rowData->name] = $data; + } + $return->mapTypes = $mapTypeData; + + $validInitData = $validInitData ? !empty($mapTypeData) : $validInitData; + + // get all available map scopes --------------------------------------------------------------------------- + $mapScope = Pathfinder\AbstractPathfinderModel::getNew('MapScopeModel'); + $rows = $mapScope->find('active = 1'); + $mapScopeData = []; + foreach((array)$rows as $rowData){ + $data = [ + 'id' => $rowData->id, + 'label' => $rowData->label + ]; + $mapScopeData[$rowData->name] = $data; + } + $return->mapScopes = $mapScopeData; + + $validInitData = $validInitData ? !empty($mapScopeData) : $validInitData; + + // get all available system status ------------------------------------------------------------------------ + $systemStatus = Pathfinder\AbstractPathfinderModel::getNew('SystemStatusModel'); + $rows = $systemStatus->find('active = 1'); + $systemScopeData = []; + foreach((array)$rows as $rowData){ + $data = [ + 'id' => $rowData->id, + 'label' => $rowData->label, + 'class' => $rowData->class + ]; + $systemScopeData[$rowData->name] = $data; + } + $return->systemStatus = $systemScopeData; + + $validInitData = $validInitData ? !empty($systemScopeData) : $validInitData; + + // get all available system types ------------------------------------------------------------------------- + $systemType = Pathfinder\AbstractPathfinderModel::getNew('SystemTypeModel'); + $rows = $systemType->find('active = 1'); + $systemTypeData = []; + foreach((array)$rows as $rowData){ + $data = [ + 'id' => $rowData->id, + 'name' => $rowData->name + ]; + $systemTypeData[$rowData->name] = $data; + } + $return->systemType = $systemTypeData; + + $validInitData = $validInitData ? !empty($systemTypeData) : $validInitData; + + // get available connection scopes ------------------------------------------------------------------------ + $connectionScope = Pathfinder\AbstractPathfinderModel::getNew('ConnectionScopeModel'); + $rows = $connectionScope->find('active = 1'); + $connectionScopeData = []; + foreach((array)$rows as $rowData){ + $data = [ + 'id' => $rowData->id, + 'label' => $rowData->label, + 'connectorDefinition' => $rowData->connectorDefinition + ]; + $connectionScopeData[$rowData->name] = $data; + } + $return->connectionScopes = $connectionScopeData; + + $validInitData = $validInitData ? !empty($connectionScopeData) : $validInitData; + + // get available character status ------------------------------------------------------------------------- + $characterStatus = Pathfinder\AbstractPathfinderModel::getNew('CharacterStatusModel'); + $rows = $characterStatus->find('active = 1'); + $characterStatusData = []; + foreach((array)$rows as $rowData){ + $data = [ + 'id' => $rowData->id, + 'name' => $rowData->name, + 'class' => $rowData->class + ]; + $characterStatusData[$rowData->name] = $data; + } + $return->characterStatus = $characterStatusData; + + $validInitData = $validInitData ? !empty($characterStatusData) : $validInitData; + + // route search config ------------------------------------------------------------------------------------ + $return->routeSearch = [ + 'defaultCount' => Config::getPathfinderData('route.search_default_count'), + 'maxDefaultCount' => Config::getPathfinderData('route.max_default_count'), + 'limit' => Config::getPathfinderData('route.limit') + ]; + + // get program routes ------------------------------------------------------------------------------------- + $return->routes = [ + 'ssoLogin' => $this->getF3()->alias('sso', ['action' => 'requestAuthorization']) + ]; + + // get third party APIs ----------------------------------------------------------------------------------- + $return->url = [ + 'ccpImageServer' => Config::getPathfinderData('api.ccp_image_server'), + 'zKillboard' => Config::getPathfinderData('api.z_killboard'), + 'eveeye' => Config::getPathfinderData('api.eveeye'), + 'dotlan' => Config::getPathfinderData('api.dotlan'), + 'anoik' => Config::getPathfinderData('api.anoik'), + 'eveScout' => Config::getPathfinderData('api.eve_scout') + ]; + + // get Plugin config -------------------------------------------------------------------------------------- + $return->plugin = [ + 'modules' => Config::getPluginConfig('modules') + ]; + + // Character default config ------------------------------------------------------------------------------- + $return->character = [ + 'autoLocationSelect' => (bool)Config::getPathfinderData('character.auto_location_select') + ]; + + // Slack integration status ------------------------------------------------------------------------------- + $return->slack = [ + 'status' => (bool)Config::getPathfinderData('slack.status') + ]; + + // Slack integration status ------------------------------------------------------------------------------- + $return->discord = [ + 'status' => (bool)Config::getPathfinderData('discord.status') + ]; + + // structure status --------------------------------------------------------------------------------------- + $structureStatus = Pathfinder\StructureStatusModel::getAll(); + $structureStatusData = []; + foreach($structureStatus as $status){ + $structureStatusData[$status->_id] = $status->getData(); + } + $return->structureStatus = $structureStatusData; + + $validInitData = $validInitData ? !empty($structureStatusData) : $validInitData; + + // get available wormhole types --------------------------------------------------------------------------- + /** + * @var $groupUniverseModel Universe\GroupModel + */ + $groupUniverseModel = Universe\AbstractUniverseModel::getNew('GroupModel'); + $groupUniverseModel->getById(Config::ESI_GROUP_WORMHOLE_ID); + $wormholesData = []; + /** + * @var $typeModel Universe\TypeModel + */ + foreach($types = $groupUniverseModel->getTypes(false) as $typeModel){ + if( + ($wormholeData = $typeModel->getWormholeData()) && + mb_strlen((string)$wormholeData->name) === 4 + ){ + $wormholesData[$wormholeData->name] = $wormholeData; + } + } + ksort($wormholesData); + $return->wormholes = $wormholesData; + + $validInitData = $validInitData ? !empty($wormholesData) : $validInitData; + + // universe category data --------------------------------------------------------------------------------- + /** + * @var $categoryUniverseModel Universe\CategoryModel + */ + $categoryUniverseModel = Universe\AbstractUniverseModel::getNew('CategoryModel'); + $return->universeCategories = [ + Config::ESI_CATEGORY_SHIP_ID => + ($categoryUniverseModel->getById(Config::ESI_CATEGORY_SHIP_ID) && $categoryUniverseModel->valid()) ? $categoryUniverseModel->getData(['mass']) : null, + Config::ESI_CATEGORY_STRUCTURE_ID => + ($categoryUniverseModel->getById(Config::ESI_CATEGORY_STRUCTURE_ID) && $categoryUniverseModel->valid()) ? $categoryUniverseModel->getData() : null, + ]; + + $validInitData = $validInitData ? !count(array_filter($return->universeCategories, function($v){ + return empty(array_filter((array)$v->groups)); + })) : $validInitData; + + // response should not be cached if invalid -> e.g. missing static data + if($validInitData){ + $f3->set(self::CACHE_KEY_INIT, $return, $ttl); + } + } + + // get SSO error messages that should be shown immediately ---------------------------------------------------- + // -> e.g. errors while character switch from previous HTTP requests + if($f3->exists(Controller\Ccp\Sso::SESSION_KEY_SSO_ERROR, $text)){ + $ssoError = (object) []; + $ssoError->type = 'error'; + $ssoError->title = 'Login failed'; + $ssoError->text = $text; + $return->error[] = $ssoError; + $f3->clear(Controller\Ccp\Sso::SESSION_KEY_SSO_ERROR); + }elseif($validInitData){ + // no errors and valid data -> send Cache header + $f3->expire(Config::ttlLeft($exists, $ttl)); + } + + echo json_encode($return); + } + + /** + * import new map data + * @param \Base $f3 + * @throws \Exception + */ + public function import(\Base $f3){ + $importData = (array)$f3->get('POST'); + + $return = (object) []; + $return->error = []; + $return->warning = []; + + if( + isset($importData['typeId']) && + count($importData['mapData']) > 0 + ){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + + /** + * @var $mapType Pathfinder\MapTypeModel + */ + $mapType = Pathfinder\AbstractPathfinderModel::getNew('MapTypeModel'); + $mapType->getById((int)$importData['typeId']); + + if( !$mapType->dry() ){ + $defaultConfig = Config::getMapsDefaultConfig($mapType->name); + + foreach($importData['mapData'] as $mapData){ + if( + isset($mapData['config']) && + isset($mapData['data']) + ){ + $mapDataConfig = (array)$mapData['config']; + $mapDataData = (array)$mapData['data']; + + /** + * @var $mapScope Pathfinder\MapScopeModel + */ + $mapScope = Pathfinder\AbstractPathfinderModel::getNew('MapScopeModel'); + $mapScope->getById((int)$mapDataConfig['scope']['id']); + + if( !$mapScope->dry() ){ + if( + isset($mapDataData['systems']) && + isset($mapDataData['connections']) + ){ + $mapDataSystems = (array)$mapDataData['systems']; + $mapDataConnections = (array)$mapDataData['connections']; + $systemCount = count($mapDataSystems); + if($systemCount <= $defaultConfig['max_systems']){ + + $map->copyfrom($mapDataConfig, ['name', 'icon', 'position', 'locked', 'rallyUpdated', 'rallyPoke']); + $map->typeId = $mapType; + $map->scopeId = $mapScope; + $map->save($activeCharacter); + + // new system IDs will be generated + // therefore we need to temp store a mapping between IDs + $tempSystemIdMapping = []; + + foreach($mapDataSystems as $systemData){ + if( + ($oldId = (int)$systemData['id']) && + ($systemId = (int)$systemData['systemId']) + ){ + $system = $map->getNewSystem($systemId); + $system->copyfrom($systemData, ['alias', 'status', 'locked', 'rallyUpdated', 'rallyPoke', 'position']); + $system = $map->saveSystem($system, $activeCharacter, $system->posX, $system->posY); + + $tempSystemIdMapping[$oldId] = $system->_id; + } + } + + /** + * @var $connection Pathfinder\ConnectionModel + */ + $connection = Pathfinder\AbstractPathfinderModel::getNew('ConnectionModel'); + $connection->setActivityLogging(false); + + foreach($mapDataConnections as $connectionData){ + // check if source and target IDs match with new system ID + if( + ($sourceSystemId = $tempSystemIdMapping[(int)$connectionData['source']]) && + ($targetSystemId = $tempSystemIdMapping[(int)$connectionData['target']]) + ){ + $connection->source = $sourceSystemId; + $connection->target = $targetSystemId; + $connection->copyfrom($connectionData, ['scope', 'type']); + $map->saveConnection($connection, $activeCharacter); + + $connection->reset(); + } + } + + // map access info should not automatically imported + if($map->isPrivate()){ + $map->setAccess($activeCharacter); + }elseif($map->isCorporation()){ + if($corporation = $activeCharacter->getCorporation()){ + $map->setAccess($corporation); + } + }elseif($map->isAlliance()){ + if($alliance = $activeCharacter->getAlliance()){ + $map->setAccess($alliance); + } + } + + // broadcast map Access -> and send map Data + $this->broadcastMapAccess($map); + }else{ + $maxSystemsError = (object) []; + $maxSystemsError->type = 'error'; + $maxSystemsError->text = 'Map has to many systems (' . $systemCount . ').' + .' Max system count is ' . $defaultConfig['max_systems'] . ' for ' . $mapType->name . ' maps.'; + $return->error[] = $maxSystemsError; + } + }else{ + // systems || connections missing + $missingConfigError = (object) []; + $missingConfigError->type = 'error'; + $missingConfigError->text = 'Map data not valid (systems || connections) missing'; + $return->error[] = $missingConfigError; + } + }else{ + $unknownMapScope= (object) []; + $unknownMapScope->type = 'error'; + $unknownMapScope->text = 'Map scope unknown!'; + $return->error[] = $unknownMapScope; + } + }else{ + // map config || systems/connections missing + $missingConfigError = (object) []; + $missingConfigError->type = 'error'; + $missingConfigError->text = 'Map data not valid (config || data) missing'; + $return->error[] = $missingConfigError; + } + + $map->reset(); + } + }else{ + $unknownMapType = (object) []; + $unknownMapType->type = 'error'; + $unknownMapType->text = 'Map type unknown!'; + $return->error[] = $unknownMapType; + } + }else{ + // map data missing + $missingDataError = (object) []; + $missingDataError->type = 'error'; + $missingDataError->text = 'Map data missing'; + $return->error[] = $missingDataError; + } + + echo json_encode($return); + } + + /** + * broadcast characters with map access rights to WebSocket server + * -> if characters with map access found -> broadcast mapData to them + * @param Pathfinder\MapModel $map + * @throws \Exception + */ + protected function broadcastMapAccess(Pathfinder\MapModel $map){ + $mapAccess = [ + 'id' => $map->_id, + 'name' => $map->name, + 'characterIds' => array_map(function($data){ + return $data->id; + }, $map->getCharactersData()) + ]; + + $this->getF3()->webSocket()->write('mapAccess', $mapAccess); + + // map has (probably) active connections that should receive map Data + $this->broadcastMap($map, true); + } + + /** + * get map access tokens for current character + * -> send access tokens via TCP Socket for WebSocket auth + * @param \Base $f3 + * @throws \Exception + */ + public function getAccessData(\Base $f3){ + $return = (object) []; + + $activeCharacter = $this->getCharacter(); + $characterData = $activeCharacter->getData(true); + $maps = $activeCharacter->getMaps(); + + // some character data is not required (in WebSocket) -> unset() and keep return small + if(isset($characterData->corporation->rights)){ + unset($characterData->corporation->rights); + } + + // access token + $token = bin2hex(random_bytes(16)); + + $return->data = [ + 'id' => $activeCharacter->_id, + 'token' => $token, // character access + 'characterData' => $characterData, + 'mapData' => [] + ]; + + if($maps){ + foreach($maps as $map){ + $return->data['mapData'][] = [ + 'id' => $map->_id, + 'token' => $token, // map access + 'name' => $map->name + ]; + } + } + + // send Access Data to WebSocket Server and get response (status) + // if 'OK' -> Socket exists + $status = ''; + $f3->webSocket() + ->write('mapConnectionAccess', $return->data) + ->then( + function($payload) use (&$status) { + $status = (string)$payload['load']; + }); + + $return->status = $status; + + echo json_encode($return); + } + + /** + * update maps with $mapsData where $character has access to + * @param Pathfinder\CharacterModel $character + * @param array $mapsData + * @return \stdClass + */ + protected function updateMapsData(Pathfinder\CharacterModel $character, array $mapsData) : \stdClass { + $return = (object) []; + $return->error = []; + $return->mapData = []; + + $mapIdsChanged = []; + $maps = $character->getMaps(); + + if(!empty($mapsData) && !empty($maps)){ + // loop all $mapsData that should be saved + // -> currently there will only be ONE map data change submitted -> single loop + foreach($mapsData as $data){ + $systems = []; + $connections = []; + + // check whether system data and/or connection data is send + // empty arrays are not included in ajax requests + if(isset($data['data']['systems'])){ + $systems = (array)$data['data']['systems']; + } + + if(isset($data['data']['connections'])){ + $connections = (array)$data['data']['connections']; + } + + // check if system data or connection data is send + if( + count($systems) > 0 || + count($connections) > 0 + ){ + // map changes expected =========================================================================== + + // loop current user maps and check for changes + foreach($maps as $map){ + // update system data ------------------------------------------------------------------------- + foreach($systems as $i => $systemData){ + // check if current system belongs to the current map + if($system = $map->getSystemById((int)$systemData['id'])){ + $system->copyfrom($systemData, ['alias', 'status', 'position', 'locked', 'rallyUpdated', 'rallyPoke']); + if($system->save($character)){ + if(!in_array($map->_id, $mapIdsChanged)){ + $mapIdsChanged[] = $map->_id; + } + // one system belongs to ONE map -> speed up for multiple maps + unset($systemData[$i]); + }else{ + $return->error = array_merge($return->error, $system->getErrors()); + } + } + } + + // update connection data --------------------------------------------------------------------- + foreach($connections as $i => $connectionData){ + // check if the current connection belongs to the current map + if($connection = $map->getConnectionById((int)$connectionData['id'])){ + $connection->copyfrom($connectionData, ['scope', 'type', 'endpoints']); + if($connection->save($character)){ + if(!in_array($map->_id, $mapIdsChanged)){ + $mapIdsChanged[] = $map->_id; + } + // one connection belongs to ONE map -> speed up for multiple maps + unset($connectionData[$i]); + }else{ + $return->error = array_merge($return->error, $connection->getErrors()); + } + } + } + } + } + } + } + + foreach($maps as $map){ + // format map Data for return/broadcast + if($mapData = $this->getFormattedMapData($map)){ + if(in_array($map->_id, $mapIdsChanged)){ + $this->broadcastMapData($mapData); + } + + $return->mapData[] = $mapData; + } + } + + return $return; + } + + /** + * update map data + * -> function is called continuously (trigger) by any active client + * @param \Base $f3 + * @throws \Exception + */ + public function updateData(\Base $f3){ + $postData = (array)$f3->get('POST'); + $mapsData = (array)$postData['mapData']; + $userDataRequired = (bool)$postData['getUserData']; + + $activeCharacter = $this->getCharacter(); + + $return = $this->updateMapsData($activeCharacter, $mapsData); + + // if userData is requested -> add it as well + // -> Only first trigger call should request this data! + if($userDataRequired) { + $return->userData = $activeCharacter->getUser()->getData(); + } + + echo json_encode($return); + } + + /** + * onUnload map sync + * @see https://developer.mozilla.org/docs/Web/API/Navigator/sendBeacon + * @param \Base $f3 + * @throws \Exception + */ + public function updateUnloadData(\Base $f3){ + $postData = (array)$f3->get('POST'); + + if(!empty($mapsData = (string)$postData['mapData'])){ + $mapsData = (array)json_decode($mapsData, true); + if(($jsonError = json_last_error()) === JSON_ERROR_NONE){ + $activeCharacter = $this->getCharacter(); + + $this->updateMapsData($activeCharacter, $mapsData); + } + } + } + + /** + * update map data api + * -> function is called continuously by any active client + * @param \Base $f3 + * @throws \Exception + */ + public function updateUserData(\Base $f3){ + $postData = (array)$f3->get('POST'); + $mapIds = (array)$postData['mapIds']; + $getMapUserData = (bool)$postData['getMapUserData']; + $mapTracking = (bool)$postData['mapTracking']; + $systemData = (array)$postData['systemData']; + $newSystemPositions = (array)$postData['newSystemPositions']; + $activeCharacter = $this->getCharacter(); + + $return = (object)[]; + + // update current location + // -> suppress temporary timeout errors + $activeCharacter = $activeCharacter->updateLog(); + + if( !empty($mapIds) ){ + // IMPORTANT for now -> just update a single map (save performance) + $mapId = (int)reset($mapIds); + // get map and check map access + if( !is_null($map = $activeCharacter->getMap($mapId)) ){ + // check character log (current system) and manipulate map (e.g. add new system) + if($mapTracking){ + $map = $this->updateMapByCharacter($map, $activeCharacter, $newSystemPositions); + } + + // mapUserData ---------------------------------------------------------------------------------------- + if($getMapUserData){ + $cacheKey = $this->getUserDataCacheKey($mapId); + if( !$f3->exists($cacheKey, $mapUserData) ){ + $mapUserData = $map->getUserData(); + + // cache time (seconds) should be equal or less than request trigger time + // prevent request flooding + $responseTTL = (int)Config::getPathfinderData('timer.update_server_user_data.delay') / 1000; + $f3->set($cacheKey, $mapUserData, $responseTTL); + } + $return->mapUserData[] = $mapUserData; + } + + // systemData ----------------------------------------------------------------------------------------- + if( + $mapId === (int)$systemData['mapId'] && + !is_null($system = $map->getSystemById((int)$systemData['id'])) + ){ + // data for currently selected system + $return->system = $system->getData(); + $return->system->signatures = $system->getSignaturesData(); + $return->system->sigHistory = $system->getSignaturesHistory(); + $return->system->structures = $system->getStructuresData(); + } + } + } + + // get current user data -> this should not be cached because each user has different personal data + // even if they have multiple characters using the same map! + $return->userData = $activeCharacter->getUser()->getData(); + + // add error (if exists) + $return->error = []; + + echo json_encode($return); + } + + /** + * update map connections/systems based on $character´s location logs + * @param Pathfinder\MapModel $map + * @param Pathfinder\CharacterModel $character + * @param array $newSystemPositions + * @return Pathfinder\MapModel + * @throws \Exception + */ + protected function updateMapByCharacter(Pathfinder\MapModel $map, Pathfinder\CharacterModel $character, array $newSystemPositions = []) : Pathfinder\MapModel { + // map changed. update cache (system/connection) changed + $mapDataChanged = false; + + if( + ( $mapScope = $map->getScope() ) && + ( $mapScope->name != 'none' ) && // tracking is disabled for map + ( $targetLog = $character->getLog() ) + ){ + // character is currently in a system + $targetSystemId = (int)$targetLog->systemId; + + // get 'character log' from source system. If not log found -> assume $sourceLog == $targetLog + $sourceLog = $character->getLogPrevSystem($map->_id, $targetSystemId) ? : $targetLog; + $sourceSystemId = (int)$sourceLog->systemId; + + if($sourceSystemId){ + $defaultPositions = (array)$newSystemPositions['defaults']; + $currentPosition = (array)$newSystemPositions['location']; + + $sourceSystem = null; + $targetSystem = null; + + $sourceExists = false; + $targetExists = false; + + $sameSystem = false; + + // system coordinates for system tha might be added next + $systemOffsetX = 130; + $systemOffsetY = 0; + $systemPosX = ((int)$defaultPositions[0]['x']) ? : 0; + $systemPosY = ((int)$defaultPositions[0]['y']) ? : 30; + + // check if previous (solo) system is already on the map ---------------------------------------------- + $sourceSystem = $map->getSystemByCCPId($sourceSystemId, [AbstractModel::getFilter('active', true)]); + + // if systems don´t already exists on map -> get "blank" system + // -> required for system type check (e.g. wormhole, k-space) + if($sourceSystem){ + // system exists + $sourceExists = true; + }else{ + // system not exists -> get"blank" system + $sourceSystem = $map->getNewSystem($sourceSystemId); + } + + // check if source and target systems are equal ------------------------------------------------------- + if($sourceSystemId === $targetSystemId){ + $sameSystem = true; + $targetExists = $sourceExists; + $targetSystem = $sourceSystem; + }elseif($targetSystemId){ + // check if target system is already on this map + $targetSystem = $map->getSystemByCCPId($targetSystemId, [AbstractModel::getFilter('active', true)]); + + if($targetSystem){ + $targetExists = true; + + if($targetSystemId === (int)$currentPosition['systemId']){ + $systemPosX = (int)$currentPosition['position']['x']; + $systemPosY = (int)$currentPosition['position']['y']; + } + }else{ + $targetSystem = $map->getNewSystem($targetSystemId); + } + } + + // make sure we have system objects to work with + // -> in case SDE does not have system they are null -> we can´t do anything + if( + $sourceSystem && + $targetSystem + ){ + $addSourceSystem = false; + $addTargetSystem = false; + $addConnection = false; + $route = []; + + switch($mapScope->name){ + case 'all': + if($sameSystem){ + $addSourceSystem = true; + }else{ + $addSourceSystem = true; + $addTargetSystem = true; + $addConnection = true; + } + break; + case 'k-space': + if($sameSystem){ + if($sourceSystem->isKspace()){ + $addSourceSystem = true; + } + }elseif( + $sourceSystem->isKspace() || + $targetSystem->isKspace() + ){ + $addSourceSystem = true; + $addTargetSystem = true; + $addConnection = true; + } + break; + case 'wh': + default: + if($sameSystem){ + if($sourceSystem->isWormhole()){ + $addSourceSystem = true; + } + }elseif( + $sourceSystem->isWormhole() || + $targetSystem->isWormhole() + ){ + $addSourceSystem = true; + $addTargetSystem = true; + $addConnection = true; + }elseif( + !$sourceSystem->isWormhole() && + !$targetSystem->isWormhole() + ){ + // check distance between systems (in jumps) + // -> if > 1 it is !very likely! a wormhole + $route = (new Controller\Api\Rest\Route())->searchRoute($sourceSystem->systemId, $targetSystem->systemId, 1); + + if(!$route['routePossible']){ + $addSourceSystem = true; + $addTargetSystem = true; + $addConnection = true; + } + } + break; + } + + // check for "abyss" systems ===================================================================== + if(!$map->trackAbyssalJumps){ + if( + $sourceSystem->isAbyss() || + $targetSystem->isAbyss() + ){ + $addConnection = false; + + if($sourceSystem->isAbyss()){ + $addSourceSystem = false; + } + + if($targetSystem->isAbyss()){ + $addTargetSystem = false; + } + } + } + + // save source system ============================================================================= + if( + $addSourceSystem && + $sourceSystem && + !$sourceExists + ){ + $sourceSystem = $map->saveSystem($sourceSystem, $character, $systemPosX, $systemPosY); + // get updated maps object + if($sourceSystem){ + $map = $sourceSystem->mapId; + $sourceExists = true; + $mapDataChanged = true; + + if(!empty($defaultPositions[1])){ + $systemPosX = (int)$defaultPositions[1]['x']; + $systemPosY = (int)$defaultPositions[1]['y']; + }else{ + // increase system position (prevent overlapping) + $systemPosX = $sourceSystem->posX + $systemOffsetX; + $systemPosY = $sourceSystem->posY + $systemOffsetY; + } + } + } + + // save target system ============================================================================= + if( + $addTargetSystem && + $targetSystem && + !$targetExists + ){ + $targetSystem = $map->saveSystem($targetSystem, $character, $systemPosX, $systemPosY); + // get updated maps object + if($targetSystem){ + $map = $targetSystem->mapId; + $mapDataChanged = true; + $targetExists = true; + } + } + + if( + !$sameSystem && + $sourceExists && + $targetExists && + $sourceSystem && + $targetSystem + ){ + $connection = $map->searchConnection($sourceSystem, $targetSystem); + + // save connection ============================================================================ + if( + $addConnection && + !$connection + ){ + // .. do not add connection if character got "podded" ------------------------------------- + if( + $targetLog->shipTypeId == 670 && + $character->cloneLocationId + ){ + // .. current character location must be clone location + if( + ( + 'station' == $character->cloneLocationType && + $character->cloneLocationId == $targetLog->stationId + ) || ( + 'structure' == $character->cloneLocationType && + $character->cloneLocationId == $targetLog->structureId + ) + ){ + // .. now we need to check jump distance between systems + // -> if > 1 it is !very likely! podded jump + if(empty($route)){ + $route = (new Controller\Api\Rest\Route())->searchRoute($sourceSystem->systemId, $targetSystem->systemId, 1); + } + + if(!$route['routePossible']){ + $addConnection = false; + } + } + } + + if($addConnection){ + $connection = $map->getNewConnection($sourceSystem, $targetSystem); + $connection = $map->saveConnection($connection, $character); + // get updated maps object + if($connection){ + $map = $connection->mapId; + $mapDataChanged = true; + } + } + } + + // log jump mass ============================================================================== + if( + $connection && + $connection->isWormhole() + ){ + $connection->logMass($targetLog); + } + } + } + } + } + + if($mapDataChanged){ + $this->broadcastMap($map); + } + + return $map; + } + + /** + * get connectionData + * @param \Base $f3 + * @throws \Exception + */ + public function getConnectionData(\Base $f3){ + $postData = (array)$f3->get('POST'); + + $addData = (array)$postData['addData']; + $filterData = (array)$postData['filterData']; + $connectionData = []; + + if($mapId = (int)$postData['mapId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + $map->getById($mapId); + + if($map->hasAccess($activeCharacter)){ + // get specific connections by id + $connectionIds = null; + if(is_array($postData['connectionIds'])){ + $connectionIds = array_map('intval', $postData['connectionIds']); + } + + $connections = $map->getConnections($connectionIds, 'wh'); + foreach($connections as $connection){ + $check = true; + $data = $connection->getData(in_array('signatures', $addData), in_array('logs', $addData)); + // filter result + if(in_array('signatures', $filterData) && !$data->signatures){ + $check = false; + } + + if(in_array('logs', $filterData) && !$data->logs){ + $check = false; + } + + if($check){ + $connectionData[] = $data; + } + } + } + } + + echo json_encode($connectionData); + } + + /** + * get map log data + * @param \Base $f3 + * @throws \Exception + */ + public function getLogData(\Base $f3){ + $postData = (array)$f3->get('POST'); + $return = (object) []; + $return->data = []; + + // validate query parameters + $return->query = [ + 'mapId' => (int) $postData['mapId'], + 'offset' => FileHandler::validateOffset( (int)$postData['offset'] ), + 'limit' => FileHandler::validateLimit( (int)$postData['limit'] ) + ]; + + if($mapId = (int)$postData['mapId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + $map->getById($mapId); + + if($map->hasAccess($activeCharacter)){ + $cacheKey = $this->getHistoryDataCacheKey($mapId); + if($return->query['offset'] === 0){ + // check cache + $return->data = $f3->get($cacheKey); + } + + if(empty($return->data)){ + $return->data = $map->getLogData($return->query['offset'], $return->query['limit']); + if( + $return->query['offset'] === 0 && + !empty($return->data)) + { + $f3->set($cacheKey, $return->data, (int)Config::getPathfinderData('history.cache')); + } + } + } + } + + echo json_encode($return); + } + +} diff --git a/app/Controller/Api/Rest/AbstractRestController.php b/app/Controller/Api/Rest/AbstractRestController.php new file mode 100644 index 000000000..5bf26d22a --- /dev/null +++ b/app/Controller/Api/Rest/AbstractRestController.php @@ -0,0 +1,49 @@ + $_POST does not include request data -> request BODY might contain JSON + * @param \Base $f3 + * @return array + */ + protected function getRequestData(\Base $f3) : array { + $data = []; + if($f3->get('VERB') == 'GET'){ + // get data from URL parameters + $data = (array)$f3->get('GET'); + }elseif( !empty($body = $f3->get('BODY')) ){ + // get data from HTTP body + $bodyDecode = json_decode($body, true); + if(($jsonError = json_last_error()) === JSON_ERROR_NONE){ + $data = $bodyDecode; + }else{ + $f3->set('HALT', true); + $f3->error(400, 'Request data: ' . json_last_error_msg()); + } + } + + return $data; + } + + /** + * render API response to client + * @param $output + */ + protected function out($output){ + echo json_encode($output); + } + +} \ No newline at end of file diff --git a/app/Controller/Api/Rest/Connection.php b/app/Controller/Api/Rest/Connection.php new file mode 100644 index 000000000..3b430aafd --- /dev/null +++ b/app/Controller/Api/Rest/Connection.php @@ -0,0 +1,157 @@ +getRequestData($f3); + $connectionIds = array_map('intval', explode(',', (string)$params['id'])); + $addData = (array)$requestData['addData']; + $filterData = (array)$requestData['filterData']; + $connectionData = []; + + if($mapId = (int)$requestData['mapId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + $map->getById($mapId); + + if($map->hasAccess($activeCharacter)){ + $connections = $map->getConnections($connectionIds, 'wh'); + foreach($connections as $connection){ + $check = true; + $data = $connection->getData(in_array('signatures', $addData), in_array('logs', $addData)); + // filter result + if(in_array('signatures', $filterData) && !$data->signatures){ + $check = false; + } + + if(in_array('logs', $filterData) && !$data->logs){ + $check = false; + } + + if($check){ + $connectionData[] = $data; + } + } + } + } + + $this->out($connectionData); + } + + /** + * save a new connection or updates an existing (drag/drop) between two systems + * if a connection is changed (drag&drop) to another system. -> this function is called for update + * @param \Base $f3 + * @throws \Exception + */ + public function put(\Base $f3){ + $requestData = $this->getRequestData($f3); + $connectionData = []; + + if($mapId = (int)$requestData['mapId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + $map->getById($mapId); + if($map->hasAccess($activeCharacter)){ + $source = $map->getSystemById((int)$requestData['source']); + $target = $map->getSystemById((int)$requestData['target']); + + if( + !is_null($source) && + !is_null($target) + ){ + /** + * @var $connection Pathfinder\ConnectionModel + */ + $connection = Pathfinder\AbstractPathfinderModel::getNew('ConnectionModel'); + $connection->getById((int)$requestData['id']); + + $connection->mapId = $map; + $connection->source = $source; + $connection->target = $target; + + // if scope + type data send -> use them ... + if($requestData['scope'] && !empty($requestData['type'])){ + $connection->copyfrom($requestData, ['scope', 'type']); + } + + // ... set/change default scope + type + if(!$requestData['disableAutoScope']){ + $connection->setAutoScopeAndType(); + } + + if($connection->save($activeCharacter)){ + $connectionData = $connection->getData(); + + // broadcast map changes + $this->broadcastMap($connection->mapId); + } + } + } + } + + $this->out($connectionData); + } + + /** + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function delete(\Base $f3, $params){ + $requestData = $this->getRequestData($f3); + $connectionIds = array_map('intval', explode(',', (string)$params['id'])); + $deletedConnectionIds = []; + + if($mapId = (int)$requestData['mapId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + $map->getById($mapId); + if($map->hasAccess($activeCharacter)){ + foreach($connectionIds as $connectionId){ + if($connection = $map->getConnectionById($connectionId)){ + if($connection->delete($activeCharacter)){ + $deletedConnectionIds[] = $connectionId; + } + $connection->reset(); + } + } + + // broadcast map changes + if(count($deletedConnectionIds)){ + $this->broadcastMap($map); + } + } + } + + $this->out($deletedConnectionIds); + } +} \ No newline at end of file diff --git a/app/Controller/Api/Rest/Log.php b/app/Controller/Api/Rest/Log.php new file mode 100644 index 000000000..9696ce00b --- /dev/null +++ b/app/Controller/Api/Rest/Log.php @@ -0,0 +1,112 @@ +getRequestData($f3); + $connectionData = []; + + if($connectionId = (int)$requestData['connectionId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $connection Pathfinder\ConnectionModel + */ + $connection = Pathfinder\AbstractPathfinderModel::getNew('ConnectionModel'); + $connection->getById($connectionId); + + if($connection->hasAccess($activeCharacter)){ + $log = $connection->getNewLog(); + $log->setData($requestData); + $log->record = false; // log not recorded by ESI + $log->save(); + + $connectionData[] = $log->getConnection()->getData(true, true); + } + } + + $this->out($connectionData); + } + + /** + * delete (deactivate) log data + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function delete(\Base $f3, $params){ + $logId = (int)$params['id']; + $connectionData = []; + + if($log = $this->update($logId, ['active' => false])){ + $connectionData[] = $log->getConnection()->getData(true, true); + } + + $this->out($connectionData); + } + + /** + * update log data + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function patch(\Base $f3, $params){ + $logId = (int)$params['id']; + $requestData = $this->getRequestData($f3); + $connectionData = []; + + if($log = $this->update($logId, $requestData)){ + $connectionData[] = $log->getConnection()->getData(true, true); + } + + $this->out($connectionData); + } + + // ---------------------------------------------------------------------------------------------------------------- + + /** + * update existing connectionLog with new data + * @param int $logId + * @param array $logData + * @return bool|Pathfinder\ConnectionLogModel + * @throws \Exception + */ + private function update(int $logId, array $logData){ + $log = false; + if($logId){ + $activeCharacter = $this->getCharacter(); + /** + * @var $log Pathfinder\ConnectionLogModel + */ + $log = Pathfinder\AbstractPathfinderModel::getNew('ConnectionLogModel'); + $log->getById($logId, 0, false); + + if($log->hasAccess($activeCharacter)){ + $log->setData($logData); + + if(isset($logData['active'])){ + $log->setActive((bool)$logData['active']); + } + $log->save(); + } + } + return $log; + } +} \ No newline at end of file diff --git a/app/Controller/Api/Rest/Map.php b/app/Controller/Api/Rest/Map.php new file mode 100644 index 000000000..05c64cc75 --- /dev/null +++ b/app/Controller/Api/Rest/Map.php @@ -0,0 +1,239 @@ +getRequestData($f3); + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + $mapData = $this->update($map, $requestData)->getData(); + + $this->out($mapData); + } + + /** + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function patch(\Base $f3, $params){ + $requestData = $this->getRequestData($f3); + $mapData = []; + + if($mapId = (int)$params['id']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + $map->getById($mapId); + if($map->hasAccess($activeCharacter)){ + $mapData = $this->update($map, $requestData)->getData(true); + } + } + + $this->out($mapData); + } + + /** + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function delete(\Base $f3, $params){ + $deletedMapIds = []; + + if($mapId = (int)$params['id']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + $map->getById($mapId); + + if($map->hasAccess($activeCharacter)){ + // check if character has delete right for map type + $hasRight = true; + if($map->isCorporation()){ + if($corpRight = $activeCharacter->getCorporation()->getRights(['map_delete'])){ + if($corpRight[0]->get('roleId', true) !== $activeCharacter->get('roleId', true)){ + $hasRight = false; + } + } + } + + if($hasRight){ + $map->setActive(false); + $map->save($activeCharacter); + $deletedMapIds[] = $mapId; + // broadcast map delete + $this->broadcastMapDeleted($mapId); + }else{ + $f3->set('HALT', true); + $f3->error(401, sprintf(self::ERROR_MAP_DELETE, $activeCharacter->name)); + } + } + } + + $this->out($deletedMapIds); + } + + /** + * @param Pathfinder\MapModel $map + * @param array $mapData + * @return Pathfinder\MapModel + * @throws \Exception + */ + private function update(Pathfinder\MapModel $map, array $mapData) : Pathfinder\MapModel { + $activeCharacter = $this->getCharacter(); + + $map->setData($mapData); + $typeChange = $map->changed('typeId'); + $map->save($activeCharacter); + + // save global map access. Depends on map "type" -------------------------------------------------------------- + /** + * @param Pathfinder\AbstractPathfinderModel $primaryModel + * @param array|null $modelIds + * @param int $maxShared + * @return int + */ + $setMapAccess = function(Pathfinder\AbstractPathfinderModel &$primaryModel, ?array $modelIds = [], int $maxShared = 3) use (&$map) : int { + $added = 0; + $deleted = 0; + if(is_array($modelIds)){ + // remove primaryModel id (-> re-add later) + $modelIds = array_diff(array_map('intval', $modelIds), [$primaryModel->_id]); + + // avoid abuse -> respect share limits (-1 is because the primaryModel has also access) + $modelIds = array_slice($modelIds, 0, max($maxShared - 1, 0)); + + // add the primaryModel id back (again) + $modelIds[] = $primaryModel->_id; + + // clear map access for entities that do not match the map "mapType" + $deleted += $map->clearAccessByType(); + + $compare = $map->compareAccess($modelIds); + + foreach((array)$compare['old'] as $modelId) { + $deleted += $map->removeFromAccess($modelId); + } + + $modelClass = (new \ReflectionClass($primaryModel))->getShortName(); + $tempModel = Pathfinder\AbstractPathfinderModel::getNew($modelClass); + foreach((array)$compare['new'] as $modelId) { + $tempModel->getById($modelId); + if( + $tempModel->valid() && + ( + $modelId == $primaryModel->_id || // primary model has always access (regardless of "shared" value) + $tempModel->shared == 1 // check if map shared is enabled + ) + ){ + $added += (int)$map->setAccess($tempModel); + } + + $tempModel->reset(); + } + } + return $added + $deleted; + }; + + $accessChangeCount = 0; + $mapDefaultConf = Config::getMapsDefaultConfig(); + if($map->isPrivate()){ + $accessChangeCount = $setMapAccess( + $activeCharacter, + $typeChange ? [$activeCharacter->_id] : $mapData['mapCharacters'], + (int)$mapDefaultConf['private']['max_shared'] + ); + }elseif($map->isCorporation()){ + if($corporation = $activeCharacter->getCorporation()){ + $accessChangeCount = $setMapAccess( + $corporation, + $typeChange ? [$corporation->_id] : $mapData['mapCorporations'], + (int)$mapDefaultConf['corporation']['max_shared'] + ); + } + }elseif($map->isAlliance()){ + if($alliance = $activeCharacter->getAlliance()){ + $accessChangeCount = $setMapAccess( + $alliance, + $typeChange ? [$alliance->_id] : $mapData['mapAlliances'], + (int)$mapDefaultConf['alliance']['max_shared'] + ); + } + } + + if($accessChangeCount){ + $map->touch('updated'); + $map->save($activeCharacter); + } + + // reload the same map model (refresh) + // this makes sure all data is up2date + $map->getById($map->_id, 0); + + // broadcast map Access -> and send map Data + $this->broadcastMapAccess($map); + + return $map; + } + + /** + * broadcast characters with map access rights to WebSocket server + * -> if characters with map access found -> broadcast mapData to them + * @param Pathfinder\MapModel $map + * @throws \Exception + */ + protected function broadcastMapAccess(Pathfinder\MapModel $map){ + $mapAccess = [ + 'id' => $map->_id, + 'name' => $map->name, + 'characterIds' => array_map(function($data){ + return $data->id; + }, $map->getCharactersData()) + ]; + + $this->getF3()->webSocket()->write('mapAccess', $mapAccess); + + // map has (probably) active connections that should receive map Data + $this->broadcastMap($map, true); + } + + /** + * broadcast map delete information to clients + * @param int $mapId + */ + private function broadcastMapDeleted(int $mapId){ + $this->getF3()->webSocket()->write('mapDeleted', $mapId); + } +} \ No newline at end of file diff --git a/app/Controller/Api/Rest/Route.php b/app/Controller/Api/Rest/Route.php new file mode 100644 index 000000000..af7998a4e --- /dev/null +++ b/app/Controller/Api/Rest/Route.php @@ -0,0 +1,857 @@ + each connection has a A->B and B->A entry. So we have 50 "real connections" + */ + const MAX_CONNECTION_COUNT = 100; + + /** + * cache time for static jump data (e.g. K-Space stargates) + * @var int + */ + private $staticJumpDataCacheTime = 86400; + + /** + * cache time for dynamic jump data (e.g. W-Space systems, Jumpbridges. ...) + * @var int + */ + private $dynamicJumpDataCacheTime = 10; + + /** + * cache time for Thera connections from eve-scout.com + * @var int + */ + private $theraJumpDataCacheTime = 60; + + /** + * array system information grouped by systemId + * @var array + */ + private $nameArray = []; + + /** + * array neighbour systems grouped by systemName + * @var array + */ + private $jumpArray = []; + + /** + * array with systemName => systemId matching + * @var array + */ + private $idArray = []; + + /** + * template for routeData payload + * @var array + */ + private $defaultRouteData = [ + 'routePossible' => false, + 'routeJumps' => 0, + 'maxDepth' => self::ROUTE_SEARCH_DEPTH_DEFAULT, + 'depthSearched' => 0, + 'searchType' => '', + 'route' => [], + 'error' => '' + ]; + + /** + * reset all jump data + */ + protected function resetJumpData(){ + $this->nameArray = []; + $this->jumpArray = []; + $this->idArray = []; + } + + /** + * set static system jump data for this instance + * the data is fixed and should not change + * -> jump data includes JUST "static" connections (Stargates) + * -> this data is equal for EACH route search (does not depend on map data) + */ + private function setStaticJumpData(){ + if($universeDB = $this->getDB('UNIVERSE')){ + $query = "SELECT * FROM system_neighbour"; + $rows = $universeDB->exec($query, null, $this->staticJumpDataCacheTime); + + if(count($rows) > 0){ + array_walk($rows, function(&$row){ + $row['jumpNodes'] = array_map('intval', explode(':', $row['jumpNodes'])); + }); + $this->updateJumpData($rows); + } + } + } + + /** + * set/add dynamic system jump data for specific "mapId"´s + * -> this data is dynamic and could change on any map change + * -> (e.g. new system added, connection added/updated, ...) + * @param array $mapIds + * @param array $filterData + * @throws \Exception + */ + private function setDynamicJumpData($mapIds = [], $filterData = []){ + // make sure, mapIds are integers (protect against SQL injections) + $mapIds = array_unique( array_map('intval', $mapIds), SORT_NUMERIC); + + if( !empty($mapIds) ){ + // map filter --------------------------------------------------------------------------------------------- + $whereMapIdsQuery = (count($mapIds) == 1) ? " = " . reset($mapIds) : " IN (" . implode(', ', $mapIds) . ")"; + + // connection filter -------------------------------------------------------------------------------------- + $whereQuery = ""; + $includeScopes = []; + $includeTypes = []; + $excludeTypes = []; + $includeEOL = true; + + $excludeEndpointTypes = []; + + if( $filterData['stargates'] === true){ + // include "stargates" for search + $includeScopes[] = 'stargate'; + $includeTypes[] = 'stargate'; + + } + + if( $filterData['jumpbridges'] === true ){ + // add jumpbridge connections for search + $includeScopes[] = 'jumpbridge'; + $includeTypes[] = 'jumpbridge'; + } + + if( $filterData['wormholes'] === true ){ + // add wormhole connections for search + $includeScopes[] = 'wh'; + $includeTypes[] = 'wh_fresh'; + + + if( $filterData['wormholesReduced'] === true ){ + $includeTypes[] = 'wh_reduced'; + } + + if( $filterData['wormholesCritical'] === true ){ + $includeTypes[] = 'wh_critical'; + } + + if( $filterData['wormholesEOL'] === false ){ + $includeEOL = false; + } + + if(!empty($filterData['excludeTypes'])){ + $excludeTypes = $filterData['excludeTypes']; + } + } + + if( $filterData['endpointsBubble'] !== true ){ + $excludeEndpointTypes[] = 'bubble'; + } + + // search connections ------------------------------------------------------------------------------------- + + if( !empty($includeScopes) ){ + $whereQuery .= " `connection`.`scope` IN ('" . implode("', '", $includeScopes) . "') AND "; + + if( !empty($excludeTypes) ){ + $whereQuery .= " `connection`.`type` NOT REGEXP '" . implode("|", $excludeTypes) . "' AND "; + } + + if( !empty($includeTypes) ){ + $whereQuery .= " `connection`.`type` REGEXP '" . implode("|", $includeTypes) . "' AND "; + } + + if(!$includeEOL){ + $whereQuery .= " `connection`.`eolUpdated` IS NULL AND "; + } + + if( !empty($excludeEndpointTypes) ){ + $whereQuery .= " CONCAT_WS(' ', `connection`.`sourceEndpointType`, `connection`.`targetEndpointType`) "; + $whereQuery .= " NOT REGEXP '" . implode("|", $excludeEndpointTypes) . "' AND "; + } + + $query = "SELECT + `system_src`.`systemId` systemSourceId, + `system_tar`.`systemId` systemTargetId + FROM + `connection` INNER JOIN + `map` ON + `map`.`id` = `connection`.`mapId` AND + `map`.`active` = 1 INNER JOIN + `system` `system_src` ON + `system_src`.`id` = `connection`.`source` AND + `system_src`.`active` = 1 INNER JOIN + `system` `system_tar` ON + `system_tar`.`id` = `connection`.`target` AND + `system_tar`.`active` = 1 + WHERE + " . $whereQuery . " + `connection`.`active` = 1 AND + `connection`.`mapId` " . $whereMapIdsQuery . " + "; + + $rows = $this->getDB()->exec($query, null, $this->dynamicJumpDataCacheTime); + + if(count($rows) > 0){ + $jumpData = []; + $universe = new Universe(); + + /** + * enrich dynamic jump data with static system data (from universe DB) + * @param array $row + * @param string $systemSourceKey + * @param string $systemTargetKey + */ + $enrichJumpData = function(array &$row, string $systemSourceKey, string $systemTargetKey) use (&$jumpData, &$universe) { + if( + !array_key_exists($row[$systemSourceKey], $jumpData) && + !is_null($staticData = $universe->getSystemData($row[$systemSourceKey])) + ){ + $jumpData[$row[$systemSourceKey]] = [ + 'systemId' => (int)$row[$systemSourceKey], + 'systemName' => $staticData->name, + 'constellationId' => $staticData->constellation->id, + 'regionId' => $staticData->constellation->region->id, + 'trueSec' => $staticData->trueSec, + ]; + } + + if( !in_array($row[$systemTargetKey], (array)$jumpData[$row[$systemSourceKey]]['jumpNodes']) ){ + $jumpData[$row[$systemSourceKey]]['jumpNodes'][] = (int)$row[$systemTargetKey]; + } + }; + + for($i = 0; $i < count($rows); $i++){ + $enrichJumpData($rows[$i], 'systemSourceId', 'systemTargetId'); + $enrichJumpData($rows[$i], 'systemTargetId', 'systemSourceId'); + } + + $this->updateJumpData($jumpData); + } + } + } + } + + /** + * set current Thera connections jump data for this instance + * -> Connected wormholes pulled from eve-scout.com + */ + private function setTheraJumpData(){ + if(!$this->getF3()->exists(self::CACHE_KEY_THERA_JUMP_DATA, $jumpData)){ + $jumpData = []; + $connectionsData = $this->getF3()->eveScoutClient()->send('getTheraConnections'); + + if(!empty($connectionsData) && !isset($connectionsData['error'])){ + /** + * map Thera jump data to Pathfinder format + * @param array $row + * @param string $systemSourceKey + * @param string $systemTargetKey + */ + $enrichJumpData = function(array &$row, string $systemSourceKey, string $systemTargetKey) use (&$jumpData) { + // check if response data is valid + if( + is_object($systemSource = $row[$systemSourceKey]) && !empty((array)$systemSource) && + is_object($systemTarget = $row[$systemTargetKey]) && !empty((array)$systemTarget) + ){ + if(!array_key_exists($systemSource->id, $jumpData)){ + $jumpData[$systemSource->id] = [ + 'systemId' => (int)$systemSource->id, + 'systemName' => $systemSource->name, + 'constellationId' => (int)$systemSource->constellationID, + 'regionId' => (int)$systemSource->regionId, + 'trueSec' => $systemSource->security, + ]; + } + + if( !in_array((int)$systemTarget->id, (array)$jumpData[$systemSource->id]['jumpNodes']) ){ + $jumpData[$systemSource->id]['jumpNodes'][] = (int)$systemTarget->id; + } + } + }; + + foreach((array)$connectionsData['connections'] as $connectionData){ + $enrichJumpData($connectionData, 'source', 'target'); + $enrichJumpData($connectionData, 'target', 'source'); + } + + if(!empty($jumpData)){ + $this->getF3()->set(self::CACHE_KEY_THERA_JUMP_DATA, $jumpData, $this->theraJumpDataCacheTime); + } + } + } + + $this->updateJumpData($jumpData); + } + + /** + * update jump data for this instance + * -> data is either coming from CCPs [SDE] OR from map specific data + * @param array $rows + */ + private function updateJumpData(&$rows = []){ + foreach($rows as &$row){ + $regionId = (int)$row['regionId']; + $constId = (int)$row['constellationId']; + $systemName = (string)($row['systemName']); + $systemId = (int)$row['systemId']; + $secStatus = (float)$row['trueSec']; + + // fill "nameArray" data ---------------------------------------------------------------------------------- + if( !isset($this->nameArray[$systemId]) ){ + $this->nameArray[$systemId][0] = $systemName; + $this->nameArray[$systemId][1] = $regionId; + $this->nameArray[$systemId][2] = $constId; + $this->nameArray[$systemId][3] = $secStatus; + } + + // fill "idArray" data ------------------------------------------------------------------------------------ + if( !isset($this->idArray[$systemId]) ){ + $this->idArray[$systemId] = $systemName; + } + + // fill "jumpArray" data ---------------------------------------------------------------------------------- + if( !is_array($this->jumpArray[$systemId]) ){ + $this->jumpArray[$systemId] = []; + } + $this->jumpArray[$systemId] = array_merge((array)$row['jumpNodes'], $this->jumpArray[$systemId]); + + // add systemName to end (if not already there) + if(end($this->jumpArray[$systemId]) != $systemName){ + array_push($this->jumpArray[$systemId], $systemName); + } + } + } + + /** + * filter systems (remove some systems) e.g. WH,LS,0.0 for "secure search" + * @param array $filterData + * @param array $keepSystems + */ + private function filterJumpData($filterData = [], $keepSystems = []){ + if($filterData['flag'] == 'secure'){ + // remove all systems (TrueSec < 0.5) from search arrays + $this->jumpArray = array_filter($this->jumpArray, function($systemId) use ($keepSystems) { + $systemNameData = $this->nameArray[$systemId]; + $systemSec = $systemNameData[3]; + + if( + $systemSec < 0.45 && + !in_array($systemId, $keepSystems) && + !preg_match('/^j\d+$/i', $this->idArray[$systemId]) // WHs are supposed to be "secure" + ){ + // remove system from nameArray and idArray + unset($this->nameArray[$systemId]); + unset($this->idArray[$systemId]); + return false; + }else{ + return true; + } + }, ARRAY_FILTER_USE_KEY ); + } + } + + /** + * get system data by systemId and dataName + * @param $systemId + * @param $option + * @return null + */ + private function getSystemInfoBySystemId($systemId, $option){ + $info = null; + switch($option){ + case 'systemName': + $info = $this->nameArray[$systemId][0]; + break; + case 'regionId': + $info = $this->nameArray[$systemId][1]; + break; + case 'constellationId': + $info = $this->nameArray[$systemId][2]; + break; + case 'trueSec': + $info = $this->nameArray[$systemId][3]; + break; + } + + return $info; + } + + /** + * recursive search function within a undirected graph + * @param $G + * @param $A + * @param $B + * @param int $M + * @return array + */ + private function graph_find_path(&$G, $A, $B, $M = 50000){ + $maxDepth = $M; + + // $P will hold the result path at the end. + // Remains empty if no path was found. + $P = []; + + // For each Node ID create a "visit information", + // initially set as 0 (meaning not yet visited) + // as soon as we visit a node we will tag it with the "source" + // so we can track the path when we reach the search target + + $V = []; + + // We are going to keep a list of nodes that are "within reach", + // initially this list will only contain the start node, + // then gradually expand (almost like a flood fill) + $R = [trim($A)]; + + $A = trim($A); + $B = trim($B); + + while(count($R) > 0 && $M > 0){ + $M--; + + $X = trim(array_shift($R)); + + if(array_key_exists($X, $G)){ + foreach($G[$X] as $Y){ + $Y = trim($Y); + // See if we got a solution + if($Y == $B){ + // We did? Construct a result path then + array_push($P, $B); + array_push($P, $X); + while($V[$X] != $A){ + array_push($P, trim($V[$X])); + $X = $V[$X]; + } + array_push($P, $A); + //return array_reverse($P); + return [ + 'path'=> array_reverse($P), + 'depth' => ($maxDepth - $M) + ]; + } + // First time we visit this node? + if(!array_key_exists($Y, $V)){ + // Store the path so we can track it back, + $V[$Y] = $X; + // and add it to the "within reach" list + array_push($R, $Y); + } + } + } + } + + return [ + 'path'=> $P, + 'depth' => ($maxDepth - $M) + ]; + } + + /** + * get formatted jump node data + * @param int $systemId + * @return array + */ + protected function getJumpNodeData(int $systemId) : array { + return [ + 'system' => $this->getSystemInfoBySystemId($systemId, 'systemName'), + 'security' => $this->getSystemInfoBySystemId($systemId, 'trueSec') + ]; + } + + /** + * search root between two systemIds + * -> function searches over ESI API, as fallback a custom search algorithm is used (no ESI) + * @param int $systemFromId + * @param int $systemToId + * @param int $searchDepth + * @param array $mapIds + * @param array $filterData + * @return array + * @throws \Exception + */ + public function searchRoute(int $systemFromId, int $systemToId, $searchDepth = 0, array $mapIds = [], array $filterData = []) : array { + // search root by ESI API + $routeData = $this->searchRouteESI($systemFromId, $systemToId, $searchDepth, $mapIds, $filterData); + + // Endpoint return http:404 in case no route find (e.g. from inside a wh) + // we thread that error "no route found" as a valid response! -> no fallback to custom search + if(!empty($routeData['error']) && strtolower($routeData['error']) !== 'no route found'){ + // ESI route search has errors -> fallback to custom search implementation + $routeData = $this->searchRouteCustom($systemFromId, $systemToId, $searchDepth, $mapIds, $filterData); + } + + return $routeData; + } + + /** + * uses a custom search algorithm to fine a route + * @param int $systemFromId + * @param int $systemToId + * @param int $searchDepth + * @param array $mapIds + * @param array $filterData + * @return array + * @throws \Exception + */ + private function searchRouteCustom(int $systemFromId, int $systemToId, $searchDepth = 0, array $mapIds = [], array $filterData = []) : array { + // reset all previous set jump data + $this->resetJumpData(); + + $searchDepth = $searchDepth ? $searchDepth : Config::getPathfinderData('route.search_depth'); + + $routeData = $this->defaultRouteData; + $routeData['maxDepth'] = $searchDepth; + $routeData['searchType'] = 'custom'; + + if($systemFromId && $systemToId){ + // prepare search data ------------------------------------------------------------------------------------ + // add static data (e.g. K-Space stargates,..) + $this->setStaticJumpData(); + + // add map specific data + $this->setDynamicJumpData($mapIds, $filterData); + + // add current Thera connections data + if($filterData['wormholesThera']){ + $this->setTheraJumpData(); + } + + // filter jump data (e.g. remove some systems (0.0, LS) + // --> don´t filter some systems (e.g. systemFrom, systemTo) even if they are are WH,LS,0.0 + $this->filterJumpData($filterData, [$systemFromId, $systemToId]); + + // search route ------------------------------------------------------------------------------------------- + + // jump counter + $jumpNum = 0; + $depthSearched = 0; + + if(isset($this->jumpArray[$systemFromId])){ + // check if the system we are looking for is a direct neighbour + foreach($this->jumpArray[$systemFromId] as $n){ + if($n == $systemToId){ + $jumpNum = 2; + $routeData['route'][] = $this->getJumpNodeData($n); + break; + } + } + + // system is not a direct neighbour -> search recursive its neighbours + if($jumpNum == 0){ + $searchResult = $this->graph_find_path( $this->jumpArray, $systemFromId, $systemToId, $searchDepth ); + $depthSearched = $searchResult['depth']; + foreach($searchResult['path'] as $systemId){ + if($jumpNum > 0){ + $routeData['route'][] = $this->getJumpNodeData($systemId); + } + $jumpNum++; + } + } + + if($jumpNum > 0){ + // route found + $routeData['routePossible'] = true; + // insert "from" system on top + array_unshift($routeData['route'], $this->getJumpNodeData($systemFromId)); + }else{ + // route not found + $routeData['routePossible'] = false; + } + } + + // route jumps + $routeData['routeJumps'] = $jumpNum - 1; + $routeData['depthSearched'] = $depthSearched; + } + + return $routeData; + } + + /** + * uses ESI route search endpoint to fine a route + * @param int $systemFromId + * @param int $systemToId + * @param int $searchDepth + * @param array $mapIds + * @param array $filterData + * @return array + * @throws \Exception + */ + private function searchRouteESI(int $systemFromId, int $systemToId, int $searchDepth = 0, array $mapIds = [], array $filterData = []) : array { + // reset all previous set jump data + $this->resetJumpData(); + + $searchDepth = $searchDepth ? $searchDepth : Config::getPathfinderData('route.search_depth'); + + $routeData = $this->defaultRouteData; + $routeData['maxDepth'] = $searchDepth; + $routeData['searchType'] = 'esi'; + + if($systemFromId && $systemToId){ + // ESI route search can only handle 50 $connections (100 entries) + // we want to add NON stargate connections ONLY for ESI route search + // because ESI will use them anyways! + $filterData['stargates'] = false; + + // prepare search data ------------------------------------------------------------------------------------ + + // add map specific data + $this->setDynamicJumpData($mapIds, $filterData); + + // add current Thera connections data + if($filterData['wormholesThera']){ + $this->setTheraJumpData(); + } + + // filter jump data (e.g. remove some systems (0.0, LS) + // --> don´t filter some systems (e.g. systemFrom, systemTo) even if they are are WH,LS,0.0 + $this->filterJumpData($filterData, [$systemFromId, $systemToId]); + + $connections = []; + foreach($this->jumpArray as $systemSourceId => $jumpData){ + $count = count($jumpData); + if($count > 1){ + // ... should always > 1 + // loop all connections for current source system + foreach($jumpData as $systemTargetId){ + // skip last entry + if(--$count <= 0){ + break; + } + + // systemIds exist and wer not removed before in filterJumpData() + if($systemSourceId && $systemTargetId){ + $jumpNode = [$systemSourceId, $systemTargetId]; + // jumpNode must be unique for ESI, + // ... there can be multiple connections between same systems in Pathfinder + if(!in_array($jumpNode, $connections)){ + $connections[] = [$systemSourceId, $systemTargetId]; + // check if connections limit is reached + if(count($connections) >= self::MAX_CONNECTION_COUNT){ + // ESI API limit for custom "connections" + break 2; + } + } + } + } + } + } + + // search route ------------------------------------------------------------------------------------------- + $options = [ + 'flag' => $filterData['flag'], + 'connections' => $connections + ]; + + $result = $this->getF3()->ccpClient()->send('getRoute', $systemFromId, $systemToId, $options); + + // format result ------------------------------------------------------------------------------------------ + + // jump counter + $jumpNum = 0; + $depthSearched = 0; + if( !empty($result['error']) ){ + $routeData['error'] = $result['error']; + }elseif( !empty($result['route']) ){ + $jumpNum = count($result['route']) - 1; + + // check max search depth + if($jumpNum <= $routeData['maxDepth']){ + $depthSearched = $jumpNum; + + $routeData['routePossible'] = true; + + // Now (after search) we have to "add" static jump data information + $this->setStaticJumpData(); + + foreach($result['route'] as $systemId){ + $routeData['route'][] = $this->getJumpNodeData($systemId); + } + }else{ + $depthSearched = $routeData['maxDepth']; + } + } + + // route jumps + $routeData['routeJumps'] = $jumpNum; + $routeData['depthSearched'] = $depthSearched; + } + + return $routeData; + } + + /** + * get key for route cache + * @param $mapIds + * @param $systemFrom + * @param $systemTo + * @param array $filterData + * @return string + */ + private function getRouteCacheKey($mapIds, $systemFrom, $systemTo, $filterData = []){ + + $keyParts = [ + implode('_', $mapIds), + self::formatHiveKey($systemFrom), + self::formatHiveKey($systemTo) + ]; + + $keyParts += $filterData; + return 'route_' . hash('md5', implode('_', $keyParts)); + } + + /** + * search multiple route between two systems + * @param \Base $f3 + * @throws \Exception + */ + public function post(\Base $f3){ + $requestData = $this->getRequestData($f3); + + $activeCharacter = $this->getCharacter(); + + $return = (object) []; + $return->error = []; + $return->routesData = []; + + if( !empty($requestData['routeData']) ){ + $routesData = (array)$requestData['routeData']; + + // map data where access was already checked -> cached data + $validMaps = []; + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + + // limit max search routes to max limit + array_splice($routesData, Config::getPathfinderData('route.limit')); + + foreach($routesData as $key => $routeData){ + // mapIds are optional. If mapIds is empty or not set + // route search is limited to CCPs static data + $mapData = (array)$routeData['mapIds']; + $mapData = array_flip( array_map('intval', $mapData) ); + + // check map access (filter requested mapIDs and format) ---------------------------------------------- + array_walk($mapData, function(&$item, &$key, $data){ + /** + * @var Pathfinder\MapModel $data[0] + */ + if( isset($data[1][$key]) ){ + // character has map access -> do not check again + $item = $data[1][$key]; + }else{ + // check map access for current character + $data[0]->getById($key); + + if( $data[0]->hasAccess($data[2]) ){ + $item = ['id' => $key, 'name' => $data[0]->name]; + }else{ + $item = false; + } + $data[0]->reset(); + } + + }, [$map, $validMaps, $activeCharacter]); + + // filter maps with NO access right + $mapData = array_filter($mapData); + $mapIds = array_column($mapData, 'id'); + + // add map data to cache array + $validMaps += $mapData; + + // search route with filter options + $filterData = [ + 'stargates' => (bool) $routeData['stargates'], + 'jumpbridges' => (bool) $routeData['jumpbridges'], + 'wormholes' => (bool) $routeData['wormholes'], + 'wormholesReduced' => (bool) $routeData['wormholesReduced'], + 'wormholesCritical' => (bool) $routeData['wormholesCritical'], + 'wormholesEOL' => (bool) $routeData['wormholesEOL'], + 'wormholesThera' => (bool) $routeData['wormholesThera'], + 'wormholesSizeMin' => (string) $routeData['wormholesSizeMin'], + 'excludeTypes' => (array) $routeData['excludeTypes'], + 'endpointsBubble' => (bool) $routeData['endpointsBubble'], + 'flag' => $routeData['flag'] + ]; + + $returnRoutData = [ + 'systemFromData' => $routeData['systemFromData'], + 'systemToData' => $routeData['systemToData'], + 'skipSearch' => (bool) $routeData['skipSearch'], + 'maps' => $mapData, + 'mapIds' => $mapIds + ]; + + // add filter options for each route as well + $returnRoutData += $filterData; + + if( + !$returnRoutData['skipSearch'] && + count($mapIds) > 0 + ){ + $systemFrom = $routeData['systemFromData']['name']; + $systemFromId = (int)$routeData['systemFromData']['systemId']; + $systemTo = $routeData['systemToData']['name']; + $systemToId = (int)$routeData['systemToData']['systemId']; + + $cacheKey = $this->getRouteCacheKey( + $mapIds, + $systemFrom, + $systemTo, + $filterData + ); + + if($f3->exists($cacheKey, $cachedData)){ + // get data from cache + $returnRoutData = $cachedData; + }else{ + $foundRoutData = $this->searchRoute($systemFromId, $systemToId, 0, $mapIds, $filterData); + + $returnRoutData = array_merge($returnRoutData, $foundRoutData); + + // cache if route was found + if( + isset($returnRoutData['routePossible']) && + $returnRoutData['routePossible'] === true + ){ + $f3->set($cacheKey, $returnRoutData, $this->dynamicJumpDataCacheTime); + } + } + } + + $return->routesData[] = $returnRoutData; + } + } + + $this->out($return); + } +} \ No newline at end of file diff --git a/app/Controller/Api/Rest/Signature.php b/app/Controller/Api/Rest/Signature.php new file mode 100644 index 000000000..e928d9c00 --- /dev/null +++ b/app/Controller/Api/Rest/Signature.php @@ -0,0 +1,237 @@ +getRequestData($f3); + $signaturesData = []; + + if($systemId = (int)$requestData['systemId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $system Pathfinder\SystemModel + */ + $system = Pathfinder\AbstractPathfinderModel::getNew('SystemModel'); + $system->getById($systemId, 0); + if($system->hasAccess($activeCharacter)){ + // if there is any changed/deleted/updated signature + // -> we need to update signature history data for the system + $updateSignaturesHistory = false; + + foreach((array)$requestData['signatures'] as $data){ + // we assume "systemId" or each signature is same for each signature + unset($data['systemId']); + + $signature = $system->getSignatureByName((string)$data['name']); + + if(is_null($signature)){ + $signature = $system->getNewSignature(); + }else{ + // description should not overwrite existing description + if(!empty($signature->description)){ + unset( $data['description'] ); + } + + // prevent some data from overwrite manually changes + // wormhole typeID can not figured out/saved by the sig reader dialog + // -> type could not be identified -> do not overwrite them (e.g. sig update) + if( + $data['groupId'] == 5 || + $data['typeId'] == 0 + ){ + unset($data['typeId']); + } + + // "sig reader" should not overwrite signature group information + if( + $data['groupId'] == 0 && + $signature->groupId > 0 + ){ + unset($data['groupId']); + unset($data['typeId']); + } + } + + $signature->setData($data); + $signature->save($activeCharacter); + $signaturesData[] = $signature->getData(); + $updateSignaturesHistory = true; + + $signature->reset(); + } + + // delete "old" signatures ---------------------------------------------------------------------------- + if((bool)$requestData['deleteOld']){ + // if linked ConnectionModels should be deleted as well + $deleteConnectionId = (bool)$requestData['deleteConnection']; + + $updatedSignatureIds = array_column($signaturesData, 'id'); + $signatures = $system->getSignatures(); + foreach($signatures as $signature){ + if(!in_array($signature->_id, $updatedSignatureIds)){ + // set if potential linked ConnectionModel should be deleted as well + $signature->virtual('connectionIdDeleteCascade', $deleteConnectionId); + if($signature->delete()){ + $updateSignaturesHistory = true; + } + // clear temp virtual field as well + $signature->clearVirtual('connectionIdDeleteCascade'); + } + } + } + + if($updateSignaturesHistory){ + // signature count changed -> clear fieldsCache[] + $system->reset(false); + $system->updateSignaturesHistory($activeCharacter, 'sync'); + } + } + } + + $this->out($signaturesData); + } + + /** + * put (insert) signature + * @param \Base $f3 + * @throws \Exception + */ + public function put(\Base $f3){ + $requestData = $this->getRequestData($f3); + $signaturesData = []; + + if($systemId = (int)$requestData['systemId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $system Pathfinder\SystemModel + */ + $system = Pathfinder\AbstractPathfinderModel::getNew('SystemModel'); + $system->getById($systemId); + if($system->hasAccess($activeCharacter)){ + $signature = $system->getNewSignature(); + $signature->setData($requestData); + $signature->save($activeCharacter); + $signaturesData[] = $signature->getData(); + + $signature->systemId->updateSignaturesHistory($activeCharacter, 'add'); + } + } + + $this->out($signaturesData); + } + + /** + * update existing signature + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function patch(\Base $f3, $params){ + $requestData = $this->getRequestData($f3); + $signaturesData = []; + + if($signatureId = (int)$params['id']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $signature Pathfinder\SystemSignatureModel + */ + $signature = Pathfinder\AbstractPathfinderModel::getNew('SystemSignatureModel'); + $signature->getById($signatureId); + if($signature->hasAccess($activeCharacter)){ + // if groupId changed + if(array_key_exists('groupId', $requestData)){ + // -> typeId set to 0 + $requestData['typeId'] = 0; + // -> connectionId set to 0 + $requestData['connectionId'] = 0; + } + + if($signature->hasChanged($requestData)){ + $signature->setData($requestData); + $signature->save($activeCharacter); + $signaturesData[] = $signature->getData(); + + $signature->systemId->updateSignaturesHistory($activeCharacter, 'edit'); + } + } + } + + $this->out($signaturesData); + } + + /** + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function delete(\Base $f3, $params){ + $requestData = $this->getRequestData($f3); + $signatureIds = array_map('intval', explode(',', (string)$params['id'])); + $deletedSignatureIds = []; + + if($systemId = (int)$requestData['systemId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $system Pathfinder\SystemModel + */ + $system = Pathfinder\AbstractPathfinderModel::getNew('SystemModel'); + $system->getById($systemId); + + if($system->hasAccess($activeCharacter)){ + // if linked ConnectionModels should be deleted as well + $deleteConnectionId = (bool)$requestData['deleteConnection']; + + // if there is any changed/deleted/updated signature + // -> we need to update signature history data for the system + $updateSignaturesHistory = false; + + /** + * @var $signature Pathfinder\SystemSignatureModel + */ + $signature = $system->rel('signatures'); + foreach($signatureIds as $signatureId){ + $signature->getById($signatureId); + // make sure signature belongs to main system (user has access) + if($signature->get('systemId', true) == $systemId){ + // set if potential linked ConnectionModel should be deleted as well + $signature->virtual('connectionIdDeleteCascade', $deleteConnectionId); + if($signature->delete()){ + $deletedSignatureIds[] = $signatureId; + $updateSignaturesHistory = true; + } + $signature->reset(); + // clear temp virtual field as well + $signature->clearVirtual('connectionIdDeleteCascade'); + } + } + + if($updateSignaturesHistory){ + $system->updateSignaturesHistory($activeCharacter, 'delete'); + } + } + } + + $this->out($deletedSignatureIds); + } + +} \ No newline at end of file diff --git a/app/Controller/Api/Rest/SignatureHistory.php b/app/Controller/Api/Rest/SignatureHistory.php new file mode 100644 index 000000000..ffaf2de5f --- /dev/null +++ b/app/Controller/Api/Rest/SignatureHistory.php @@ -0,0 +1,128 @@ +getCharacter(); + + /** + * @var $system Pathfinder\SystemModel + */ + $system = Pathfinder\AbstractPathfinderModel::getNew('SystemModel'); + $system->getById($systemId); + + if($system->hasAccess($activeCharacter)){ + $historyDataAll = $system->getSignaturesHistory(); + foreach($historyDataAll as $historyEntry){ + $label = [ + $historyEntry['character']->name, + $historyEntry['action'], + count($historyEntry['signatures']), + Config::formatTimeInterval((int)(microtime(true) - $historyEntry['stamp'])) + ]; + + $historyData[] = [ + 'value' => md5((string)$historyEntry['stamp']), + 'text' => implode('%%', $label) + ]; + } + } + } + + $this->out($historyData); + } + + /** + * put (load) historic signature data + * @param \Base $f3 + * @throws \Exception + */ + public function put(\Base $f3){ + $requestData = $this->getRequestData($f3); + $signaturesData = []; + + if( + ($systemId = (int)$requestData['systemId']) && + ($stamp = (string)$requestData['stamp']) + ){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $system Pathfinder\SystemModel + */ + $system = Pathfinder\AbstractPathfinderModel::getNew('SystemModel'); + $system->getById($systemId, 0); + if($system->hasAccess($activeCharacter)){ + if($historyEntry = $system->getSignatureHistoryEntry($stamp)){ + $updateSignaturesHistory = false; + + // history entry found for $stamp -> format signatures data + // -> same format as if they would come from client for save + foreach($historyEntry['signatures'] as $signatureData){ + $data = [ + 'id' => (int)$signatureData->id, + 'groupId' => (int)$signatureData->groupId, + 'typeId' => (int)$signatureData->typeId, + 'connectionId' => (int)$signatureData->connectionId, + 'name' => (string)$signatureData->name, + 'description' => (string)$signatureData->description, + ]; + + $signature = $system->getSignatureById($data['id']); + + if(is_null($signature)){ + $signature = $system->getNewSignature(); + } + + $signature->setData($data); + $signature->save($activeCharacter); + $signaturesData[] = $signature->getData(); + $updateSignaturesHistory = true; + + $signature->reset(); + } + + // delete "old" signatures ------------------------------------------------------------------------ + $updatedSignatureIds = array_column($signaturesData, 'id'); + $signatures = $system->getSignatures(); + foreach($signatures as $signature){ + if(!in_array($signature->_id, $updatedSignatureIds)){ + if($signature->delete()){ + $updateSignaturesHistory = true; + } + } + } + + if($updateSignaturesHistory){ + // signature count changed -> clear fieldsCache[] + $system->reset(false); + $system->updateSignaturesHistory($activeCharacter, 'undo'); + } + } + } + } + + $this->out($signaturesData); + } +} \ No newline at end of file diff --git a/app/Controller/Api/Rest/Structure.php b/app/Controller/Api/Rest/Structure.php new file mode 100644 index 000000000..19ea8ecb8 --- /dev/null +++ b/app/Controller/Api/Rest/Structure.php @@ -0,0 +1,125 @@ +getRequestData($f3); + $structuresData = $requestData ? $this->update($requestData) : []; + $this->out($structuresData); + } + + /** + * @param \Base $f3 + * @throws \Exception + */ + public function put(\Base $f3){ + $requestData = $this->getRequestData($f3); + $structuresData = $requestData ? $this->update([$requestData]) : []; + $this->out($structuresData); + } + + /** + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function patch(\Base $f3, $params){ + $requestData = $this->getRequestData($f3); + $structuresData = (($structureId = (int)$params['id']) && ($structureId == (int)$requestData['id'])) ? $this->update([$requestData]) : []; + $this->out($structuresData); + } + + /** + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function delete(\Base $f3, $params){ + $deletedStructureIds = []; + + if($structureId = (int)$params['id']){ + $activeCharacter = $this->getCharacter(); + /** + * @var $structure Pathfinder\StructureModel + */ + $structure = Pathfinder\AbstractPathfinderModel::getNew('StructureModel'); + $structure->getById($structureId); + if($structure->hasAccess($activeCharacter) && $structure->erase()){ + $deletedStructureIds[] = $structureId; + } + } + $this->out($deletedStructureIds); + } + + /** + * @param array $structuresData + * @return array + * @throws \Exception + */ + private function update(array $structuresData) : array { + $data = []; + + $activeCharacter = $this->getCharacter(); + if($activeCharacter->hasCorporation()){ + // structures always belong to a corporation + /** + * @var $structure Pathfinder\StructureModel + */ + $structure = Pathfinder\AbstractPathfinderModel::getNew('StructureModel'); + foreach($structuresData as $structureData){ + // reset on loop start because of potential "continue" + $structure->reset(); + + if(!empty($structureData['id']) && $structureId = (int)$structureData['id']){ + // update specific structure + $structure->getById($structureId); + if(!$structure->hasAccess($activeCharacter)){ + continue; + } + }elseif(!isset($structureData['id'])){ + // from clipboard -> search by structure by name + $structure->getByName($activeCharacter->getCorporation(), (string)$structureData['name'], (int)$structureData['systemId']); + } + + $isNew = $structure->dry(); + + $structure->setData($structureData); + $structure->save(); + + if($isNew){ + $activeCharacter->getCorporation()->saveStructure($structure); + } + + // group all updated structures by corporation -> just for return + $corporationsStructureData = $structure->getDataByCorporations(); + foreach($corporationsStructureData as $corporationId => $corporationStructureData){ + if(isset($data[$corporationId])){ + $data[$corporationId]['structures'] = array_merge( + $data[$corporationId]['structures'], + $corporationStructureData['structures'] + ); + }else{ + $data[$corporationId] = $corporationStructureData; + } + } + } + } + + return $data; + } +} \ No newline at end of file diff --git a/app/Controller/Api/Rest/System.php b/app/Controller/Api/Rest/System.php new file mode 100644 index 000000000..efceae2d4 --- /dev/null +++ b/app/Controller/Api/Rest/System.php @@ -0,0 +1,228 @@ +getRequestData($f3); + $systemData = null; + + if( + ($systemId = (int)$params['id']) && + ($mapId = (int)$requestData['mapId']) + ){ + $activeCharacter = $this->getCharacter(); + $isCcpId = (bool)$requestData['isCcpId']; + + if( + !is_null($map = $activeCharacter->getMap($mapId)) && + !is_null($system = $isCcpId ? $map->getSystemByCCPId($systemId) : $map->getSystemById($systemId)) + ){ + $systemData = $system->getData(); + $systemData->signatures = $system->getSignaturesData(); + $systemData->sigHistory = $system->getSignaturesHistory(); + $systemData->structures = $system->getStructuresData(); + $systemData->stations = $system->getStationsData(); + } + } + + $this->out($systemData); + } + + /** + * put (insert) system + * @param \Base $f3 + * @throws \Exception + */ + public function put(\Base $f3){ + $requestData = $this->getRequestData($f3); + $systemData = []; + + if($mapId = (int)$requestData['mapId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + $map->getById($mapId); + if($map->hasAccess($activeCharacter)){ + $system = $map->getNewSystem($requestData['systemId']); + $systemData = $this->update($system, $requestData)->getData(); + } + } + + $this->out($systemData); + } + + /** + * update existing system + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function patch(\Base $f3, $params){ + $requestData = $this->getRequestData($f3); + $systemData = []; + + if($systemId = (int)$params['id']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $system Pathfinder\SystemModel + */ + $system = Pathfinder\AbstractPathfinderModel::getNew('SystemModel'); + $system->getById($systemId); + + if($system->hasAccess($activeCharacter)){ + $systemData = $this->update($system, $requestData)->getData(); + } + } + + $this->out($systemData); + } + + /** + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function delete(\Base $f3, $params){ + $requestData = $this->getRequestData($f3); + $systemIds = array_map('intval', explode(',', (string)$params['id'])); + $deletedSystemIds = []; + + if($mapId = (int)$requestData['mapId']){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $map Pathfinder\MapModel + */ + $map = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + $map->getById($mapId); + + if($map->hasAccess($activeCharacter)){ + $newSystemModel = Pathfinder\AbstractPathfinderModel::getNew('SystemModel'); + foreach($systemIds as $systemId){ + if($system = $map->getSystemById($systemId)){ + // check whether system should be deleted OR set "inactive" + if($this->checkDeleteMode($map, $system)){ + // delete log + // -> first set updatedCharacterId -> required for activity log + $system->updatedCharacterId = $activeCharacter; + $system->update(); + + // ... now get fresh object and delete.. + $newSystemModel->getById($system->_id, 0); + $newSystemModel->erase(); + $newSystemModel->reset(); + }else{ + // keep data -> set "inactive" + $system->setActive(false); + $system->save($activeCharacter); + } + + $system->reset(); + + $deletedSystemIds[] = $systemId; + } + } + // broadcast map changes + if(count($deletedSystemIds)){ + $this->broadcastMap($map); + } + } + } + + $this->out($deletedSystemIds); + } + + // ---------------------------------------------------------------------------------------------------------------- + + /** + * update system with new data + * @param Pathfinder\SystemModel $system + * @param array $systemData + * @return Pathfinder\SystemModel + * @throws \Exception + */ + private function update(Pathfinder\SystemModel $system, array $systemData) : Pathfinder\SystemModel { + $activeCharacter = $this->getCharacter(); + + // statusId === 0 is 'auto' status -> keep current status + // -> relevant systems that already have a status (inactive systems) + if( (int)$systemData['statusId'] <= 0 ){ + unset($systemData['statusId']); + } + + if( !$system->dry() ){ + // activate system (e.g. was inactive)) + $system->setActive(true); + } + + $system->setData($systemData); + $system->save($activeCharacter); + + // get data from "fresh" model (e.g. some relational data has changed: "statusId") + /** + * @var $newSystem Pathfinder\SystemModel + */ + $newSystem = Pathfinder\AbstractPathfinderModel::getNew('SystemModel'); + $newSystem->getById($system->_id, 0); + $newSystem->clearCacheData(); + + // broadcast map changes + $this->broadcastMap($newSystem->mapId); + + return $newSystem; + } + + /** + * checks whether a system should be "deleted" or set "inactive" (keep persistent data) + * @param Pathfinder\MapModel $map + * @param Pathfinder\SystemModel $system + * @return bool + */ + private function checkDeleteMode(Pathfinder\MapModel $map, Pathfinder\SystemModel $system) : bool { + $delete = true; + + if(!empty($system->description)){ + // never delete systems with custom description set! + $delete = false; + }elseif( + $map->persistentAliases && + !empty($system->alias) && + ($system->alias != $system->name) + ){ + // map setting "persistentAliases" is active (default) AND + // alias is set and != name + $delete = false; + }elseif( + $map->persistentSignatures && + !empty($system->getSignatures()) + ){ + // map setting "persistentSignatures" is active (default) AND + // signatures exist + $delete = false; + } + + return $delete; + } + +} \ No newline at end of file diff --git a/app/Controller/Api/Rest/SystemGraph.php b/app/Controller/Api/Rest/SystemGraph.php new file mode 100644 index 000000000..b84a79055 --- /dev/null +++ b/app/Controller/Api/Rest/SystemGraph.php @@ -0,0 +1,112 @@ +getRequestData($f3); + $systemIds = (array)$requestData['systemIds']; + $graphsData = []; + + // valid response (data found) should be cached by server + client + $cacheResponse = false; + + // number of log entries in each table per system (24 = 24h) + $logEntryCount = Pathfinder\AbstractSystemApiBasicModel::DATA_COLUMN_COUNT; + + $ttl = 60 * 10; + + // table names with system data + $logTables = [ + 'jumps' => 'SystemJumpModel', + 'shipKills' => 'SystemShipKillModel', + 'podKills' => 'SystemPodKillModel', + 'factionKills' => 'SystemFactionKillModel' + ]; + + $exists = false; + + foreach($systemIds as $systemId){ + $cacheKey = $this->getSystemGraphCacheKey($systemId); + if(!$exists = $f3->exists($cacheKey, $graphData)){ + $graphData = []; + $cacheSystem = false; + + foreach($logTables as $label => $className){ + $systemLogModel = Pathfinder\AbstractSystemApiBasicModel::getNew($className); + $systemLogExists = false; + + // 10min cache (could be up to 1h cache time) + $systemLogModel->getByForeignKey('systemId', $systemId); + if($systemLogModel->valid()){ + $systemLogExists = true; + $cacheSystem = true; + $cacheResponse = true; + } + + $systemLogData = $systemLogModel->getData(); + + // podKills share graph with shipKills -> skip + if($label != 'podKills'){ + $graphData[$label]['logExists'] = $systemLogExists; + $graphData[$label]['updated'] = $systemLogData->updated; + } + + $logValueCount = range(0, $logEntryCount - 1); + foreach($logValueCount as $i){ + if($label == 'podKills'){ + $graphData['shipKills']['data'][$i]['z'] = $systemLogData->values[$i]; + }else{ + $graphData[$label]['data'][] = [ + 'x' => ($logEntryCount - $i - 1) . 'h', + 'y' => $systemLogData->values[$i] + ]; + } + } + } + + if($cacheSystem){ + $f3->set($cacheKey, $graphData, $ttl); + } + }else{ + // server cache data exists -> client should cache as well + $cacheResponse = true; + } + $graphsData[$systemId] = $graphData; + } + + if($cacheResponse){ + // send client cache header + $f3->expire(Config::ttlLeft($exists, $ttl)); + } + + $this->out($graphsData); + } + + // ---------------------------------------------------------------------------------------------------------------- + + /** + * get system graph cache key + * @param int $systemId + * @return string + */ + protected function getSystemGraphCacheKey(int $systemId): string { + return sprintf(self::CACHE_KEY_GRAPH, 'SYSTEM_' . $systemId); + } +} \ No newline at end of file diff --git a/app/Controller/Api/Rest/SystemThera.php b/app/Controller/Api/Rest/SystemThera.php new file mode 100644 index 000000000..d428ca8bd --- /dev/null +++ b/app/Controller/Api/Rest/SystemThera.php @@ -0,0 +1,131 @@ +exists(self::CACHE_KEY_THERA_CONNECTIONS, $connectionsData)){ + $connectionsData = $this->getEveScoutTheraConnections(); + $f3->set(self::CACHE_KEY_THERA_CONNECTIONS, $connectionsData, $ttl); + } + + $f3->expire(Config::ttlLeft($exists, $ttl)); + + $this->out($connectionsData); + } + + /** + * get Thera connections data from EveScout API + * -> map response to Pathfinder format + * @return array + */ + protected function getEveScoutTheraConnections() : array { + $connectionsData = []; + + /** + * map system data from eveScout response to Pathfinder´s 'system' format + * @param string $key + * @param array $eveScoutConnection + * @param array $connectionData + */ + $enrichWithSystemData = function(string $key, array $eveScoutConnection, array &$connectionData) : void { + $eveScoutSystem = (array)$eveScoutConnection[$key]; + $systemData = [ + 'id' => (int)$eveScoutSystem['id'], + 'name' => (string)$eveScoutSystem['name'], + 'trueSec' => round((float)$eveScoutSystem['security'], 4) + ]; + if(!empty($eveScoutSystem['constellationID'])){ + $systemData['constellation'] = ['id' => (int)$eveScoutSystem['constellationID']]; + } + if(!empty($region = (array)$eveScoutSystem['region']) && !empty($region['id'])){ + $systemData['region'] = ['id' => (int)$region['id'], 'name' => (string)$region['name']]; + } + $connectionData[$key] = $systemData; + }; + + /** + * @param string $key + * @param array $eveScoutConnection + * @param array $connectionData + */ + $enrichWithSignatureData = function(string $key, array $eveScoutConnection, array &$connectionData) : void { + $eveScoutSignature = (array)$eveScoutConnection[$key]; + $signatureData = [ + 'name' => $eveScoutSignature['name'] ? : null + ]; + if(!empty($sigType = (array)$eveScoutSignature['type']) && !empty($sigType['name'])){ + $signatureData['type'] = ['name' => strtoupper((string)$sigType['name'])]; + } + $connectionData[$key] = $signatureData; + }; + + /** + * map wormhole data from eveScout to Pathfinder´s connection format + * @param array $wormholeData + * @param array $connectionsData + */ + $enrichWithWormholeData = function(array $wormholeData, array &$connectionsData) : void { + $type = []; + if($wormholeData['mass'] === 'reduced'){ + $type[] = 'wh_reduced'; + }else if($wormholeData['mass'] === 'critical'){ + $type[] = 'wh_critical'; + }else{ + $type[] = 'wh_fresh'; + } + + if($wormholeData['eol'] === 'critical'){ + $type[] = 'wh_eol'; + } + $connectionsData['type'] = $type; + $connectionsData['estimatedEol'] = $wormholeData['estimatedEol']; + }; + + $eveScoutResponse = $this->getF3()->eveScoutClient()->send('getTheraConnections'); + if(!empty($eveScoutResponse) && !isset($eveScoutResponse['error'])){ + foreach((array)$eveScoutResponse['connections'] as $eveScoutConnection){ + if( + $eveScoutConnection['type'] === 'wormhole' && + isset($eveScoutConnection['source']) && isset($eveScoutConnection['target']) + ){ + try{ + $data = [ + 'id' => (int)$eveScoutConnection['id'], + 'scope' => 'wh', + 'created' => [ + 'created' => (new \DateTime($eveScoutConnection['created']))->getTimestamp(), + 'character' => (array)$eveScoutConnection['character'] + ], + 'updated' => (new \DateTime($eveScoutConnection['updated']))->getTimestamp() + ]; + $enrichWithWormholeData((array)$eveScoutConnection['wormhole'], $data); + $enrichWithSystemData('source', $eveScoutConnection, $data); + $enrichWithSystemData('target', $eveScoutConnection, $data); + $enrichWithSignatureData('sourceSignature', $eveScoutConnection, $data); + $enrichWithSignatureData('targetSignature', $eveScoutConnection, $data); + $connectionsData[] = $data; + }catch(\Exception $e){ + // new \DateTime Exception -> skip this data + } + } + } + } + + return $connectionsData; + } +} \ No newline at end of file diff --git a/app/Controller/Api/Setup.php b/app/Controller/Api/Setup.php new file mode 100644 index 000000000..31f85c784 --- /dev/null +++ b/app/Controller/Api/Setup.php @@ -0,0 +1,425 @@ +´s for all cronjobs + * @param \Base $f3 + */ + public function cronTable(\Base $f3){ + $return = (object) []; + $return->error = []; + $return->jobsData = Cron::instance()->getJobsConfig(); + $return->html = $this->getCronHtml($return->jobsData); + echo json_encode($return); + } + + /** + * toggle "isPaused" for a cronjob by its name + * @param \Base $f3 + */ + public function cronPause(\Base $f3){ + $postData = (array)$f3->get('POST'); + $return = (object) []; + $return->error = []; + + if($jobName = (string)$postData['job']){ + $cron = Cron::instance(); + if($job = $cron->getJob($jobName)){ + if($job->valid()){ + $job->isPaused = !$job->isPaused; + $job->save(); + + $return->jobsData = $cron->getJobsConfig([$jobName]); + $return->html = $this->getCronHtml($return->jobsData); + } + } + } + + echo json_encode($return); + } + + /** + * execute a cronjob by its name + * -> runs sync + * -> max execution time might be lower than CLI calls! + * @param \Base $f3 + */ + public function cronExecute(\Base $f3){ + $postData = (array)$f3->get('POST'); + $return = (object) []; + $return->error = []; + + if($jobName = (string)$postData['job']){ + $cron = Cron::instance(); + if($job = $cron->getJob($jobName)){ + if($job->valid()){ + $cron->execute($jobName, false); + + $return->jobsData = $cron->getJobsConfig([$jobName]); + $return->html = $this->getCronHtml($return->jobsData); + } + } + } + + echo json_encode($return); + } + + /** + * get HTML for cronJobs + * @param array $jobsData + * @return string + */ + protected function getCronHtml(array $jobsData) : string { + $tplData = [ + 'cronConfig' => [ + 'jobs' => $jobsData, + 'settings' => $this->getF3()->constants(Cron::instance(), 'DEFAULT_') + ], + 'tplCounter' => $this->counter(), + 'tplConvertBytes' => function(){ + return call_user_func_array([Number::instance(), 'bytesToString'], func_get_args()); + } + ]; + return \Template::instance()->render('templates/ui/cron_table_row.html', null, $tplData, 0); + } + + /** + * build search index from existing data (e.g. Systems) + * OR import data from ESI (e.g. Structures) + * -> optional build/import smaller chunks of data + * @param \Base $f3 + * @throws \Exception + */ + public function buildIndex(\Base $f3){ + $postData = (array)$f3->get('POST'); + $type = (string)$postData['type']; + $countAll = (int)$postData['countAll']; + $count = (int)$postData['count']; + $offset = (int)$postData['offset']; + + $return = (object) []; + $return->error = []; + $return->type = $type; + $return->count = $count; + $return->offset = $offset; + $return->countAll = $countAll; + $return->countBuild = 0; + $return->countBuildAll = 0; + $return->progress = 0; + + /** + * sum array values + * @param int $carry + * @param int $value + * @return int + */ + $sum = function(int $carry, int $value) : int { + $carry += $value; + return $carry; + }; + + /** + * calc percent + * @param int $countAll + * @param int $count + * @return int + */ + $percent = function(int $countAll, int $count){ + return $countAll ? floor((100/$countAll) * $count) : 0; + }; + + $controller = new Controller\Ccp\Universe(); + switch($type){ + case 'Systems': + $length = 100; + $buildInfo = $controller->buildSystemsIndex($offset, $length); + + $return->offset = $buildInfo['offset']; + $return->countAll = $buildInfo['countAll']; + $return->countBuild = $buildInfo['countBuild']; + $return->countBuildAll = $return->offset; + break; + case 'Wormholes': + $groupId = Config::ESI_GROUP_WORMHOLE_ID; + $length = 10; + $buildInfo = $controller->setupGroup($groupId, $offset, $length, true); + + $return->offset = $buildInfo['offset']; + $return->countAll = $buildInfo['countAll']; + $return->countBuild = $buildInfo['count']; + $return->countBuildAll = $return->offset; + break; + case 'Structures': + $categoryId = Config::ESI_CATEGORY_STRUCTURE_ID; + $length = 1; + $buildInfo = $controller->setupCategory($categoryId, $offset, $length); + $categoryUniverseModel = Model\Universe\AbstractUniverseModel::getNew('CategoryModel'); + $categoryUniverseModel->getById($categoryId, 0); + + $return->offset = $buildInfo['offset']; + $return->countBuild = $buildInfo['count']; + $return->countBuildAll = $return->offset; + $return->subCount = [ + 'countBuildAll' => $categoryUniverseModel->getTypesCount(false) + ]; + break; + case 'Ships': + $categoryId = Config::ESI_CATEGORY_SHIP_ID; + $length = 2; + $buildInfo = $controller->setupCategory($categoryId, $offset, $length); + $categoryUniverseModel = Model\Universe\AbstractUniverseModel::getNew('CategoryModel'); + $categoryUniverseModel->getById($categoryId, 0); + + $return->offset = $buildInfo['offset']; + $return->countBuild = $buildInfo['count']; + $return->countBuildAll = $return->offset; + $return->subCount = [ + 'countBuildAll' => $categoryUniverseModel->getTypesCount(false) + ]; + break; + case 'SystemStatic': + $length = 300; + $buildInfo = $this->setupSystemStaticTable($offset, $length); + + $return->offset = $buildInfo['offset']; + $return->countAll = $buildInfo['countAll']; + $return->countBuild = $buildInfo['count']; + $return->countBuildAll = $return->offset; + break; + case 'SystemNeighbour': + $length = 1500; + $buildInfo = $this->setupSystemJumpTable($offset, $length); + + $return->offset = $buildInfo['offset']; + $return->countAll = $buildInfo['countAll']; + $return->countBuild = $buildInfo['count']; + $return->countBuildAll = $return->offset; + break; + } + + $return->progress = $percent($return->countAll, $return->countBuildAll); + + if($return->countBuildAll < $return->countAll){ + $return->count++; + } + + echo json_encode($return); + } + + /** + * clear search index + * @param \Base $f3 + * @throws \Exception + */ + public function clearIndex(\Base $f3){ + $postData = (array)$f3->get('POST'); + $type = (string)$postData['type']; + + $return = (object) []; + $return->error = []; + $return->type = $type; + $return->count = 0; + $return->countAll = 0; + $return->countBuild = 0; + $return->countBuildAll = 0; + $return->progress = 0; + + $controller = new Controller\Ccp\Universe(); + switch($type) { + case 'Systems': + $controller->clearSystemsIndex(); + $systemUniverseModel = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); + $return->countAll = $systemUniverseModel->getRowCount(); + break; + case 'SystemNeighbour': + $systemNeighbourModel = Model\Universe\AbstractUniverseModel::getNew('SystemNeighbourModel'); + $systemNeighbourModel->truncate(); + $return->countAll = (int)$f3->get('REQUIREMENTS.DATA.NEIGHBOURS'); + break; + } + + echo json_encode($return); + } + + /** + * import static 'system_static` table data from *.csv + * @param int $offset + * @param int $length + * @return array + * @throws \Exception + */ + protected function setupSystemStaticTable(int $offset = 0, int $length = 0) : array { + $info = ['countAll' => 0, 'countChunk' => 0, 'count' => 0, 'offset' => $offset]; + + /** + * @var $systemStaticModel Model\Universe\SystemStaticModel + */ + $systemStaticModel = Model\Universe\AbstractUniverseModel::getNew('SystemStaticModel'); + if(!empty($csvData = $systemStaticModel::getCSVData($systemStaticModel->getTable()))){ + $info['countAll'] = count($csvData); + if($length){ + $csvData = array_slice($csvData, $offset, $length); + } + $info['countChunk'] = count($csvData); + $cols = ['typeId' => [], 'systemId' => []]; + foreach($csvData as $data){ + $validColCount = 0; + $systemStaticModel->getById((int)$data['id'], 0); + $systemStaticModel->id = (int)$data['id']; + foreach($cols as $col => &$invalidIds){ + if($systemStaticModel->exists($col)){ + $colVal = (int)$data[$col]; + if(!in_array($colVal, $invalidIds)){ + $relModel = $systemStaticModel->rel($col); + $relModel->getById($colVal, 0); + if($relModel->valid()){ + $systemStaticModel->$col = $relModel; + $validColCount++; + }else{ + $invalidIds[] = $colVal; + break; + } + }else{ + break; + } + } + } + + if($validColCount == count($cols)){ + $systemStaticModel->save(); + } + $systemStaticModel->reset(); + + $info['count']++; + $info['offset']++; + } + } + + return $info; + } + + /** + * This function is just for setting up the cache table 'system_neighbour' which is used + * for system jump calculation. Call this function manually when CCP adds Systems/Stargates + * @param int $offset + * @param int $length + * @return array + */ + protected function setupSystemJumpTable(int $offset = 0, int $length = 0) : array { + $info = ['countAll' => 0, 'countChunk' => 0, 'count' => 0, 'offset' => $offset]; + $universeDB = $this->getDB('UNIVERSE'); + + $query = "SELECT SQL_CALC_FOUND_ROWS + `system`.`id` `systemId`, + `system`.`name` `systemName`, + `system`.`constellationId` `constellationId`, + ROUND( `system`.`securityStatus`, 4) `trueSec`, + `constellation`.`regionId` `regionId`, + ( + SELECT + GROUP_CONCAT( NULLIF(`sys_inner`.`id`, NULL) SEPARATOR ':') + FROM + `stargate` INNER JOIN + `system` `sys_inner` ON + `sys_inner`.`id` = `stargate`.`destinationSystemId` + WHERE + `stargate`.`systemId` = `system`.`id` + ) `jumpNodes` + FROM + `system` INNER JOIN + `constellation` ON + `constellation`.`id` = `system`.`constellationId` + WHERE + `constellation`.`regionId` != :regionIdJove1 AND + `constellation`.`regionId` != :regionIdJove2 AND + `constellation`.`regionId` != :regionIdJove3 AND + ( + `system`.`security` = :ns OR + `system`.`security` = :ls OR + `system`.`security` = :hs + ) + HAVING + `jumpNodes` IS NOT NULL + "; + + $args = [ + ':regionIdJove1' => 10000017, + ':regionIdJove2' => 10000019, + ':regionIdJove3' => 10000004, + ':ns' => '0.0', + ':ls' => 'L', + ':hs' => 'H' + ]; + + if($length){ + $query .= ' LIMIT :limit'; + $args[':limit'] = $length; + + if($offset){ + $query .= ' OFFSET :offset'; + $args[':offset'] = $offset; + } + } + + $rows = $universeDB->exec($query, $args); + + if(!empty($countRes = $universeDB->exec("SELECT FOUND_ROWS() `count`")) && isset($countRes[0]['count'])){ + $info['countAll'] = (int)$countRes[0]['count']; + } + + if($info['countChunk'] = count($rows)){ + $placeholderStr = function(string $str) : string { + return ':' . $str; + }; + + $updateRule = function(string $str) : string { + return $str . " = VALUES(" . $str . ")"; + }; + + $universeDB->begin(); + foreach($rows as $row){ + $info['count']++; + $info['offset']++; + + if(!$row['jumpNodes']){ + // should never happen! + continue; + } + + $columns = array_keys($row); + $columnsQuoted = array_map($universeDB->quotekey, $columns); + $placeholder = array_map($placeholderStr, $columns); + $args = array_combine($placeholder, $row); + + $updateSql = array_map($updateRule, $columns); + + $sql = "INSERT INTO + system_neighbour(" . implode(', ', $columnsQuoted) . ") + VALUES(" . implode(', ', $placeholder) . ") + ON DUPLICATE KEY UPDATE + " . implode(', ', $updateSql); + + $universeDB->exec($sql, $args); + } + $universeDB->commit(); + } + + return $info; + } + +} \ No newline at end of file diff --git a/app/Controller/Api/Statistic.php b/app/Controller/Api/Statistic.php new file mode 100644 index 000000000..796880237 --- /dev/null +++ b/app/Controller/Api/Statistic.php @@ -0,0 +1,300 @@ + 10 will get prefixed with "0" + * -> e.g. year 2016, week 2 => "201602" + * @param int $year + * @param int $week + * @return string + */ + protected function concatYearWeek($year, $week){ + return strval($year) . str_pad($week, 2, 0, STR_PAD_LEFT); + } + + /** + * get max count of weeks in a year + * @param $year + * @return int + */ + protected function getIsoWeeksInYear($year){ + $week = 0; + try{ + $date = new \DateTime; + $date->setISODate($year, 53); + $week = ($date->format('W') === '53' ? 53 : 52); + }catch(\Exception $e){} + return $week; + } + + /** + * get number of weeks for a given period + * @param string $period + * @param int $year + * @return int + */ + protected function getWeekCount($period, $year){ + $weeksInYear = $this->getIsoWeeksInYear($year); + + switch($period){ + case 'yearly': + $weekCount = $weeksInYear; + break; + case 'monthly': + $weekCount = 4; + break; + case 'weekly': + default: + $weekCount = 1; + break; + } + + return $weekCount; + } + + /** + * calculate calendar week and year for a given offset (weekCount) + * -> count forward OR backward + * @param int $year + * @param int $week + * @param int $weekCount + * @param bool $backwards + * @return array + */ + protected function calculateYearWeekOffset($year, $week, $weekCount, $backwards = false){ + $offset = [ + 'year' => (int)$year, + 'week' => (int)$week + ]; + + $weeksInYear = $this->getIsoWeeksInYear($year); + + // just for security... + if($offset['week'] > $weeksInYear){ + $offset['week'] = $weeksInYear; + }elseif($offset['week'] <= 0){ + $offset['week'] = 1; + } + + for($i = 1; $i < $weekCount; $i++){ + + if($backwards){ + // calculate backward + $offset['week']--; + + if($offset['week'] <= 0){ + // year change -> reset yearWeeks + $offset['year']--; + $offset['week'] = $this->getIsoWeeksInYear($offset['year']); + } + }else{ + // calculate forward + $offset['week']++; + + if($offset['week'] > $weeksInYear){ + // year change -> reset yearWeeks + $offset['week'] = 1; + $offset['year']++; + $weeksInYear = $this->getIsoWeeksInYear($offset['year']); + } + } + } + + return $offset; + } + + /** + * query statistic data for "activity log" + * -> group result by characterId + * @param CharacterModel $character + * @param int $typeId + * @param int $yearStart + * @param int $weekStart + * @param int $yearEnd + * @param int $weekEnd + * @return array + */ + protected function queryStatistic(CharacterModel $character, $typeId, $yearStart, $weekStart, $yearEnd, $weekEnd){ + $data = []; + + // can be either "characterId" || "corporationId" || "allianceId" + // -> is required (>0) to limit the result to only accessible data for the given character! + $objectId = 0; + + // add map-"typeId" (private/corp/ally) condition ------------------------------------------------------------- + // check if "LOG_ACTIVITY_ENABLED" is active for a given "typeId" + $sqlMapType = ""; + + switch($typeId){ + case 2: + if( Config::getMapsDefaultConfig('private')['log_activity_enabled'] ){ + $sqlMapType .= " AND `character`.`id` = :objectId "; + $objectId = $character->_id; + } + break; + case 3: + if( + Config::getMapsDefaultConfig('corporation')['log_activity_enabled'] && + $character->hasCorporation() + ){ + $sqlMapType .= " AND `character`.`corporationId` = :objectId "; + $objectId = $character->get('corporationId', true); + } + break; + case 4: + if( + Config::getMapsDefaultConfig('alliance')['log_activity_enabled'] && + $character->hasAlliance() + ){ + $sqlMapType .= " AND `character`.`allianceId` = :objectId "; + $objectId = $character->get('allianceId', true); + } + break; + } + + if($objectId > 0){ + + $queryData = [ + ':active' => 1, + ':objectId' => $objectId + ]; + + // date offset condition ---------------------------------------------------------------------------------- + $sqlDateOffset = " AND CONCAT(`log`.`year`, LPAD(`log`.`week`, 2, 0) ) BETWEEN :yearWeekStart AND :yearWeekEnd "; + + $queryData[':yearWeekStart'] = $this->concatYearWeek($yearStart, $weekStart); + $queryData[':yearWeekEnd'] = $this->concatYearWeek($yearEnd, $weekEnd); + + // build query -------------------------------------------------------------------------------------------- + $sql = "SELECT + `log`.`year`, + `log`.`week`, + `log`.`characterId`, + `character`.`name`, + `character`.`lastLogin`, + SUM(`log`.`mapCreate`) `mapCreate`, + SUM(`log`.`mapUpdate`) `mapUpdate`, + SUM(`log`.`mapDelete`) `mapDelete`, + SUM(`log`.`systemCreate`) `systemCreate`, + SUM(`log`.`systemUpdate`) `systemUpdate`, + SUM(`log`.`systemDelete`) `systemDelete`, + SUM(`log`.`connectionCreate`) `connectionCreate`, + SUM(`log`.`connectionUpdate`) `connectionUpdate`, + SUM(`log`.`connectionDelete`) `connectionDelete`, + SUM(`log`.`signatureCreate`) `signatureCreate`, + SUM(`log`.`signatureUpdate`) `signatureUpdate`, + SUM(`log`.`signatureDelete`) `signatureDelete` + FROM + `activity_log` `log` INNER JOIN + `character` ON + `character`.`id` = `log`.`characterId` + WHERE + `log`.`active` = :active + " . $sqlMapType . " + " . $sqlDateOffset . " + GROUP BY + `log`.`year`, + `log`.`week`, + `log`.`characterId` + ORDER BY + `log`.`year` DESC, `log`.`week` DESC"; + + $result = $this->getDB()->exec($sql, $queryData); + + if( !empty($result) ){ + // group result by characterId + foreach ($result as $key => &$entry) { + $tmp = $entry; + unset($tmp['characterId']); + unset($tmp['name']); + unset($tmp['lastLogin']); + $data[$entry['characterId']]['name'] = $entry['name']; + $data[$entry['characterId']]['lastLogin'] = strtotime($entry['lastLogin']); + $data[$entry['characterId']]['weeks'][ $entry['year'] . $entry['week'] ] = $tmp; + } + } + } + + return $data; + } + + /** + * get statistics data + * @param \Base $f3 + * @throws \Exception + */ + public function getData(\Base $f3){ + $postData = (array)$f3->get('POST'); + $return = (object) []; + + $period = $postData['period']; + $typeId = (int)$postData['typeId']; + $yearStart = (int)$postData['year']; + $weekStart = (int)$postData['week']; + + $currentYear = (int)date('o'); + $currentWeek = (int)date('W'); + + if( + $yearStart && + $weekStart + ){ + $weekCount = $this->getWeekCount($period, $yearStart); + }else{ + // if start date is not set -> calculate it from current data + $tmpYear = $currentYear; + if($period == 'yearly'){ + $tmpYear--; + } + $weekCount = $this->getWeekCount($period, $tmpYear); + $offsetStart = $this->calculateYearWeekOffset($currentYear, $currentWeek, $weekCount, true); + $yearStart = $offsetStart['year']; + $weekStart = $offsetStart['week']; + } + + // date offset for statistics query + $offset = $this->calculateYearWeekOffset($yearStart, $weekStart, $weekCount); + + $activeCharacter = $this->getCharacter(); + + $return->statistics = $this->queryStatistic($activeCharacter, $typeId, $yearStart, $weekStart, $offset['year'], $offset['week']); + $return->period = $period; + $return->typeId = $typeId; + $return->weekCount = $weekCount; + $return->yearWeeks = [ + $yearStart => $this->getIsoWeeksInYear($yearStart), + ($yearStart + 1) => $this->getIsoWeeksInYear($yearStart + 1) + ]; + + // pagination offset + $offsetNext = $this->calculateYearWeekOffset($yearStart, $weekStart, $weekCount + 1); + $offsetPrev = $this->calculateYearWeekOffset($yearStart, $weekStart, $weekCount + 1, true); + + // check if "next" button is available (not in future) + $currentCurrentDataConcat = intval($this->concatYearWeek($currentYear, $currentWeek)); + $offsetNextDateConcat = intval($this->concatYearWeek($offsetNext['year'], $offsetNext['week'])); + if($offsetNextDateConcat <= $currentCurrentDataConcat){ + $return->next = $offsetNext; + } + + $return->prev = $offsetPrev; + $return->start = ['year' => $yearStart, 'week' => $weekStart]; + $return->offset = $offset; + + echo json_encode($return); + } +} \ No newline at end of file diff --git a/app/Controller/Api/System.php b/app/Controller/Api/System.php new file mode 100644 index 000000000..8368d1b55 --- /dev/null +++ b/app/Controller/Api/System.php @@ -0,0 +1,94 @@ +get('POST'); + + $return = (object) []; + $return->error = []; + $return->destData = []; + + if(!empty($destData = (array)$postData['destData'])){ + $activeCharacter = $this->getCharacter(); + + $return->clearOtherWaypoints = (bool)$postData['clearOtherWaypoints']; + $return->first = (bool)$postData['first']; + + if($accessToken = $activeCharacter->getAccessToken()){ + $options = [ + 'clearOtherWaypoints' => $return->clearOtherWaypoints, + 'addToBeginning' => $return->first, + ]; + + foreach($destData as $data){ + $response = $f3->ccpClient()->send('setWaypoint', (int)$data['id'], $accessToken, $options); + + if(empty($response)){ + $return->destData[] = $data; + }else{ + $error = (object) []; + $error->type = 'error'; + $error->text = $response['error']; + $return->error[] = $error; + } + } + + } + } + + echo json_encode($return); + } + + /** + * send Rally Point poke + * @param \Base $f3 + * @throws \Exception + */ + public function pokeRally(\Base $f3){ + $rallyData = (array)$f3->get('POST'); + $systemId = (int)$rallyData['systemId']; + $return = (object) []; + + if($systemId){ + $activeCharacter = $this->getCharacter(); + + /** + * @var $system Pathfinder\SystemModel + */ + $system = Pathfinder\AbstractPathfinderModel::getNew('SystemModel'); + $system->getById($systemId); + + if($system->hasAccess($activeCharacter)){ + $rallyData['pokeDesktop'] = $rallyData['pokeDesktop'] === '1'; + $rallyData['pokeMail'] = $rallyData['pokeMail'] === '1'; + $rallyData['pokeSlack'] = $rallyData['pokeSlack'] === '1'; + $rallyData['pokeDiscord'] = $rallyData['pokeDiscord'] === '1'; + $rallyData['message'] = trim($rallyData['message']); + + $system->sendRallyPoke($rallyData, $activeCharacter); + } + } + + echo json_encode($return); + } + +} + diff --git a/app/Controller/Api/Universe.php b/app/Controller/Api/Universe.php new file mode 100644 index 000000000..907cb62dc --- /dev/null +++ b/app/Controller/Api/Universe.php @@ -0,0 +1,128 @@ +get('POST'); + $categories = (array)$postData['categories']; + $universeNameData = []; + + if( + array_key_exists('arg1', $params) && + !empty($search = strtolower($params['arg1'])) && + !empty($categories) + ){ + $universeNameData = Ccp\Universe::searchUniverseNameData($categories, $search); + } + + echo json_encode($universeNameData); + } + + /** + * search systems by name + * @param \Base $f3 + * @param $params + * @throws \Exception + */ + public function systems(\Base $f3, $params){ + $getData = (array)$f3->get('GET'); + $page = isset($getData['page']) ? (int)max($getData['page'],1) : 1; + $search = isset($params['arg1']) ? (string)$params['arg1'] : ''; + $morePages = false; + $count = 0; + + $return = (object) []; + $return->results = []; + + // some "edge cases" for testing trueSec rounding... + //$searchToken = 'H472-N'; // -0.000001 -> 0.0 + //$searchToken = 'X1E-OQ'; // -0.099426 -> -0.10 + //$searchToken = 'BKK4-H'; // -0.049954 -> -0.05 + //$searchToken = 'Uhtafal'; // 0.499612 -> 0.5 (HS) + //$searchToken = 'Oshaima'; // 0.453128 -> 0.5 (HS) + //$searchToken = 'Ayeroilen'; // 0.446568 -> 0.4 (LS) + //$searchToken = 'Enderailen'; // 0.448785 -> 0.4 (LS) + //$searchToken = 'Neziel'; // 0.449943 -> 0.4 (LS) + //$searchToken = 'Naga'; // 0.033684 -> 0.1 (LS) + + if( strlen($search) >= 3 ){ + $offset = ($page - 1) * self::PAGE_SIZE_SYSTEMS; + $system = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); + + $filter = [ + 'id LIKE :id OR name LIKE :name', + ':id' => $search . '%', // -> match first + ':name' => '%' . $search . '%' // -> match between + ]; + $options = [ + 'order' => 'name', + 'offset' => $offset, + 'limit' => self::PAGE_SIZE_SYSTEMS + ]; + $count = $system->count($filter); + $endCount = $offset + self::PAGE_SIZE_SYSTEMS; + $morePages = $endCount < $count; + + $systems = $system->find($filter, $options); + if($systems){ + foreach($systems as $system){ + if($systemData = $system->fromIndex()){ + $return->results[] = $systemData; + } + } + } + } + + $return->pagination = ['more' => $morePages, 'count' => $count]; + + echo json_encode($return); + } + + /** + * get system data for all systems within a constellation + * @param \Base $f3 + * @param array $params + * @throws \Exception + */ + public function constellationData(\Base $f3, $params){ + $constellationId = isset($params['arg1']) ? (int)$params['arg1'] : 0; + + $return = (object) []; + $return->error = []; + $return->systemsData = []; + + $constellation = Model\Universe\AbstractUniverseModel::getNew('ConstellationModel'); + $constellation->getById($constellationId); + if($constellation->valid() && $constellation->systems){ + /** + * @var Model\Universe\SystemModel $system + */ + foreach($constellation->systems as $system){ + if($systemData = $system->fromIndex()){ + $return->systemsData[] = $systemData; + } + } + } + + echo json_encode($return); + } +} \ No newline at end of file diff --git a/app/Controller/Api/User.php b/app/Controller/Api/User.php new file mode 100644 index 000000000..368864521 --- /dev/null +++ b/app/Controller/Api/User.php @@ -0,0 +1,388 @@ +getUser()){ + // check if character belongs to current user + // -> If there is already a logged in user! (e.g. multi character use) + $currentUser = $this->getUser(); + $timezone = $this->getF3()->get('getTimeZone')(); + + $sessionCharacters = [ + [ + 'ID' => $character->_id, + 'NAME' => $character->name, + 'TIME' => (new \DateTime('now', $timezone))->getTimestamp() + ] + ]; + + if( + is_null($currentUser) || + $currentUser->_id !== $user->_id + ){ + // user has changed OR new user ----------------------------------------------------------------------- + //-> set user/character data to session + $this->getF3()->set(self::SESSION_KEY_USER, [ + 'ID' => $user->_id, + 'NAME' => $user->name + ]); + }else{ + // user has NOT changed ------------------------------------------------------------------------------- + $sessionCharacters = $character::mergeSessionCharacterData($sessionCharacters); + } + + $this->getF3()->set(self::SESSION_KEY_CHARACTERS, $sessionCharacters); + + $character->updateCloneData(); + $character->updateRoleData(); + + // save user login information ---------------------------------------------------------------------------- + $character->touch('lastLogin'); + $character->save(); + + // write login log ---------------------------------------------------------------------------------------- + self::getLogger('CHARACTER_LOGIN')->write( + sprintf(self::LOG_LOGGED_IN, + $user->_id, + $user->name, + $character->_id, + $character->name + ) + ); + + // set temp character data -------------------------------------------------------------------------------- + // -> pass character data over for next http request (reroute()) + $this->setTempCharacterData($character->_id); + + $login = true; + } + + return $login; + } + + /** + * validate cookie character information + * -> return character data (if valid) + * @param \Base $f3 + * @throws \Exception + */ + public function getCookieCharacter(\Base $f3){ + $data = $f3->get('POST'); + $cookieName = (string)$data['cookie']; + + $return = (object) []; + $return->ccpImageServer = Config::getPathfinderData('api.ccp_image_server'); + $return->error = []; + + if( !empty($cookieData = $this->getCookieByName($cookieName) )){ + // cookie data is valid -> validate data against DB (security check!) + // -> add characters WITHOUT permission to log in too! + if( !empty($characters = $this->getCookieCharacters(array_slice($cookieData, 0, 1, true), false)) ){ + // character is valid and allowed to login + $return->character = reset($characters)->getData(); + // get Session status for character + if($activeCharacter = $this->getCharacter(0)){ + if($activeUser = $activeCharacter->getUser()){ + if($sessionCharacterData = $activeUser->findSessionCharacterData($return->character->id)){ + $return->character->hasActiveSession = true; + } + } + } + }else{ + $characterError = (object) []; + $characterError->type = 'warning'; + $characterError->text = 'This can happen through "invalid cookies(SSO)", "login restrictions", "ESI problems".'; + $return->error[] = $characterError; + } + } + + echo json_encode($return); + } + + /** + * get captcha image and store key to session + * @param \Base $f3 + */ + public function getCaptcha(\Base $f3){ + $data = $f3->get('POST'); + + $return = (object) []; + $return->error = []; + + // check if reason for captcha generation is valid + if( + isset($data['reason']) && + in_array( $data['reason'], self::$captchaReason) + ){ + $reason = $data['reason']; + + $im = imagecreatetruecolor(1, 1); + $colorText = imagecolorallocate($im, 102, 200, 79); + $colorBG = imagecolorallocate($im, 49, 51, 53); + + $img = new \Image(); + $imgDump = $img->captcha( + 'fonts/oxygen-bold-webfont.ttf', + 14, + 6, + $reason, + '', + $colorText, + $colorBG + )->dump(); + + $return->img = $f3->base64( $imgDump, 'image/png'); + }else{ + $captchaError = (object) []; + $captchaError->type = 'error'; + $captchaError->text = 'Could not create captcha image'; + $return->error[] = $captchaError; + } + + echo json_encode($return); + } + + /** + * log the current user out + clear character system log data + * @param \Base $f3 + * @throws \Exception + */ + public function logout(\Base $f3){ + $data = $f3->get('POST'); + $deleteCookie = (bool)$data['deleteCookie']; + + $this->logoutCharacter($f3, false, true, true, $deleteCookie, 200); + } + + /** + * remote open ingame information window (character, corporation or alliance) Id + * -> the type is auto-recognized by CCP + * @param \Base $f3 + * @throws \Exception + */ + public function openIngameWindow(\Base $f3){ + $data = $f3->get('POST'); + + $return = (object) []; + $return->error = []; + + if( $targetId = (int)$data['targetId']){ + $activeCharacter = $this->getCharacter(); + + $response = $f3->ccpClient()->send('openWindow', $targetId, $activeCharacter->getAccessToken()); + + if(empty($response)){ + $return->targetId = $targetId; + }else{ + $error = (object) []; + $error->type = 'error'; + $error->text = $response['error']; + $return->error[] = $error; + } + } + + echo json_encode($return); + } + + /** + * update user account data + * -> a fresh user automatically generated on first login with a new character + * -> see SSO login + * @param \Base $f3 + * @throws \Exception + */ + public function saveAccount(\Base $f3){ + $data = $f3->get('POST'); + + $return = (object)[]; + $return->error = []; + + $captcha = $f3->get(self::SESSION_CAPTCHA_ACCOUNT_UPDATE); + + // reset captcha -> forces user to enter new one + $f3->clear(self::SESSION_CAPTCHA_ACCOUNT_UPDATE); + + $newUserData = null; + + if(isset($data['formData'])){ + $formData = $data['formData']; + + try{ + if($activeCharacter = $this->getCharacter()){ + $user = $activeCharacter->getUser(); + + // captcha is send -> check captcha --------------------------------------------------------------- + if(isset($formData['captcha']) && !empty($formData['captcha'])){ + if($formData['captcha'] === $captcha){ + // change/set sensitive user data requires captcha! + + // set username + if(isset($formData['name']) && !empty($formData['name'])){ + $user->name = $formData['name']; + } + + // set email + if( + isset($formData['email']) && + isset($formData['email_confirm']) && + !empty($formData['email']) && + !empty($formData['email_confirm']) && + $formData['email'] == $formData['email_confirm'] + ){ + $user->email = $formData['email']; + } + + // save/update user model + // this will fail if model validation fails! + $user->save(); + + }else{ + // captcha was send but not valid -> return error + $captchaError = (object)[]; + $captchaError->type = 'error'; + $captchaError->text = 'Captcha does not match'; + $return->error[] = $captchaError; + } + } + + // sharing config --------------------------------------------------------------------------------- + if(isset($formData['share'])){ + $privateSharing = (int)$formData['privateSharing']; + $corporationSharing = (int)$formData['corporationSharing']; + $allianceSharing = (int)$formData['allianceSharing']; + + // update private/corp/ally + $corporation = $activeCharacter->getCorporation(); + $alliance = $activeCharacter->getAlliance(); + + if(is_object($corporation)){ + $corporation->shared = $corporationSharing; + $corporation->save(); + } + + if(is_object($alliance)){ + $alliance->shared = $allianceSharing; + $alliance->save(); + } + + $activeCharacter->shared = $privateSharing; + $activeCharacter->save(); + } + + // character config ------------------------------------------------------------------------------- + if(isset($formData['character'])){ + $activeCharacter->copyfrom($formData, ['logLocation', 'selectLocation']); + + $activeCharacter->save(); + } + + // get fresh updated user object + $newUserData = $user->getData(); + } + + }catch(Exception\ValidationException $e){ + $return->error[] = $e->getError(); + }catch(Exception\RegistrationException $e){ + $return->error[] = $e->getError(); + } + + // return new/updated user data + $return->userData = $newUserData; + } + + echo json_encode($return); + } + + /** + * delete current user account from DB + * @param \Base $f3 + * @throws \Exception + */ + public function deleteAccount(\Base $f3){ + $data = $f3->get('POST.formData'); + $return = (object) []; + + $captcha = $f3->get(self::SESSION_CAPTCHA_ACCOUNT_DELETE); + + // reset captcha -> forces user to enter new one + $f3->clear(self::SESSION_CAPTCHA_ACCOUNT_DELETE); + + if( + isset($data['captcha']) && + !empty($data['captcha']) && + $data['captcha'] === $captcha + ){ + $activeCharacter = $this->getCharacter(); + $user = $activeCharacter->getUser(); + + if($user){ + // save log + self::getLogger('DELETE_ACCOUNT')->write( + sprintf(self::LOG_DELETE_ACCOUNT, $user->id, $user->name) + ); + + $this->logoutCharacter($f3, true, true, true, true, 200); + $user->erase(); + } + }else{ + // captcha not valid -> return error + $captchaError = (object) []; + $captchaError->type = 'error'; + $captchaError->text = 'Captcha does not match'; + $return->error[] = $captchaError; + } + + echo json_encode($return); + } + + +} \ No newline at end of file diff --git a/app/Controller/AppController.php b/app/Controller/AppController.php new file mode 100644 index 000000000..b9880f4cd --- /dev/null +++ b/app/Controller/AppController.php @@ -0,0 +1,75 @@ +set('tplPageTitle', Config::getPathfinderData('name')); + + // main page content + $f3->set('tplPageContent', Config::getPathfinderData('view.login')); + + // body element class + $f3->set('tplBodyClass', 'pf-landing'); + + // JS main file + $f3->set('tplJsView', 'login'); + + if($return = parent::beforeroute($f3, $params)){ + // href for SSO Auth + $f3->set('tplAuthType', $f3->get('BASE') . $f3->alias( 'sso', ['action' => 'requestAuthorization'] )); + + // characters from cookies + $f3->set('cookieCharacters', $this->getCookieByName(self::COOKIE_PREFIX_CHARACTER, true)); + $f3->set('getCharacterGrid', function($characters){ + return ( ((12 / count($characters)) <= 3) ? 3 : (12 / count($characters)) ); + }); + } + + return $return; + } + + /** + * event handler after routing + * @param \Base $f3 + */ + public function afterroute(\Base $f3){ + parent::afterroute($f3); + + // clear all SSO related temp data + if($f3->exists(Ccp\Sso::SESSION_KEY_SSO)){ + $f3->clear(Ccp\Sso::SESSION_KEY_SSO); + } + } + + /** + * show main login (index) page + * @param \Base $f3 + */ + public function init(\Base $f3){ + $resource = Resource::instance(); + $resource->register('script', 'app/login'); + $resource->register('script', 'app/mappage', 'prefetch'); + $resource->register('image', 'sso/signature.png'); + $resource->register('image', 'sso/gameplay.png'); + } + +} \ No newline at end of file diff --git a/app/Controller/Ccp/Sso.php b/app/Controller/Ccp/Sso.php new file mode 100644 index 000000000..542e007be --- /dev/null +++ b/app/Controller/Ccp/Sso.php @@ -0,0 +1,556 @@ + cf. Controller->getCookieCharacters() ( equivalent cookie based login) + * @param \Base $f3 + */ + public function requestAdminAuthorization($f3){ + // store browser tabId to be "targeted" after login + $f3->set(self::SESSION_KEY_SSO_FROM, 'admin'); + + $scopes = self::getScopesByAuthType('admin'); + $this->rerouteAuthorization($f3, $scopes, 'admin'); + } + + /** + * redirect user to CCP SSO page and request authorization + * -> cf. Controller->getCookieCharacters() ( equivalent cookie based login) + * @param \Base $f3 + * @throws \Exception + */ + public function requestAuthorization($f3){ + $params = $f3->get('GET'); + + if( + isset($params['characterId']) && + ( $activeCharacter = $this->getCharacter() ) + ){ + // authentication restricted to a characterId ------------------------------------------------------------- + // restrict login to this characterId e.g. for character switch on map page + $characterId = (int)trim((string)$params['characterId']); + + /** + * @var $character Pathfinder\CharacterModel + */ + $character = Pathfinder\AbstractPathfinderModel::getNew('CharacterModel'); + $character->getById($characterId, 0); + + // check if character is valid and exists + if( + $character->valid() && + $character->hasUserCharacter() && + ($activeCharacter->getUser()->_id === $character->getUser()->_id) + ){ + // requested character belongs to current user + // -> update character vom ESI (e.g. corp changed,..) + $updateStatus = $character->updateFromESI(); + + if( empty($updateStatus) ){ + + // make sure character data is up2date! + // -> this is not the case if e.g. userCharacters was removed "ownerHash" changed... + $character->getById($character->_id); + + if( + $character->hasUserCharacter() && + ($character->isAuthorized() === 'OK') + ){ + if($this->loginByCharacter($character)){ + // set "login" cookie + $this->setLoginCookie($character); + + // route to "map" + $f3->reroute(['map', ['*' => '']]); + } + } + } + } + + // redirect to map map page on successful login + $f3->set(self::SESSION_KEY_SSO_FROM, 'map'); + } + + // redirect to CCP SSO ---------------------------------------------------------------------------------------- + $scopes = self::getScopesByAuthType(); + $this->rerouteAuthorization($f3, $scopes); + } + + /** + * redirect user to CCPs SSO page + * @param \Base $f3 + * @param array $scopes + * @param string $rootAlias + */ + private function rerouteAuthorization(\Base $f3, array $scopes = [], string $rootAlias = 'login'){ + if( !empty( Controller\Controller::getEnvironmentData('CCP_SSO_CLIENT_ID') ) ){ + // used for "state" check between request and callback + $state = bin2hex( openssl_random_pseudo_bytes(12) ); + $f3->set(self::SESSION_KEY_SSO_STATE, $state); + + $urlParams = [ + 'response_type' => 'code', + 'redirect_uri' => Controller\Controller::getEnvironmentData('URL') . Controller\Controller::getEnvironmentData('BASE') . $f3->build('/sso/callbackAuthorization'), + 'client_id' => Controller\Controller::getEnvironmentData('CCP_SSO_CLIENT_ID'), + 'scope' => implode(' ', $scopes), + 'state' => $state + ]; + + $ssoAuthUrl = $f3->ssoClient()->getUrl(); + $ssoAuthUrl .= $f3->ssoClient()->getAuthorizationEndpointURI(); + $ssoAuthUrl .= '?' . http_build_query($urlParams, '', '&', PHP_QUERY_RFC3986 ); + + $f3->status(302); + $f3->reroute($ssoAuthUrl); + }else{ + // SSO clientId missing + $f3->set(self::SESSION_KEY_SSO_ERROR, self::ERROR_CCP_CLIENT_ID); + self::getSSOLogger()->write(self::ERROR_CCP_CLIENT_ID); + $f3->reroute([$rootAlias, ['*' => '']]); + } + } + + /** + * callback handler for CCP SSO user Auth + * -> see requestAuthorization() + * @param \Base $f3 + * @throws \Exception + */ + public function callbackAuthorization($f3){ + $getParams = (array)$f3->get('GET'); + + // users can log in either from @login (new user) or @map (existing user) root alias + // -> or from /admin page + // -> in case login fails, users should be redirected differently + $rootAlias = 'login'; + if( !empty($f3->get(self::SESSION_KEY_SSO_FROM)) ){ + $rootAlias = $f3->get(self::SESSION_KEY_SSO_FROM); + } + + if($f3->exists(self::SESSION_KEY_SSO_STATE)){ + // check response and validate 'state' + if( + isset($getParams['code']) && + isset($getParams['state']) && + !empty($getParams['code']) && + !empty($getParams['state']) && + $f3->get(self::SESSION_KEY_SSO_STATE) === $getParams['state'] + ){ + // clear 'state' for new next login request + $f3->clear(self::SESSION_KEY_SSO_STATE); + $f3->clear(self::SESSION_KEY_SSO_FROM); + + $accessData = $this->getSsoAccessData($getParams['code']); + + if(isset($accessData->accessToken, $accessData->esiAccessTokenExpires, $accessData->refreshToken)){ + // login succeeded -> get basic character data for current login + $verificationCharacterData = $this->verifyCharacterData($accessData->accessToken); + + if( !empty($verificationCharacterData) ){ + + // check if login is restricted to a characterID + + // verification available data. Data is needed for "ownerHash" check + + // get character data from ESI + $characterData = $this->getCharacterData((int)$verificationCharacterData['characterId']); + + if( isset($characterData->character) ){ + // add "ownerHash" and SSO tokens + $characterData->character['ownerHash'] = $verificationCharacterData['characterOwnerHash']; + $characterData->character['esiAccessToken'] = $accessData->accessToken; + $characterData->character['esiAccessTokenExpires'] = $accessData->esiAccessTokenExpires; + $characterData->character['esiRefreshToken'] = $accessData->refreshToken; + $characterData->character['esiScopes'] = $verificationCharacterData['scopes']; + + // add/update static character data + $characterModel = $this->updateCharacter($characterData); + + if( !is_null($characterModel) ){ + // check if character is authorized to log in + if( ($authStatus = $characterModel->isAuthorized()) === 'OK'){ + // character is authorized to log in + // -> update character log (current location,...) + $characterModel = $characterModel->updateLog(); + + // connect character with current user + if(is_null($user = $this->getUser())){ + // connect character with existing user (no changes) + if(is_null($user = $characterModel->getUser())){ + // no user found (new character) -> create new user and connect to character + /** + * @var $user Pathfinder\UserModel + */ + $user = Pathfinder\AbstractPathfinderModel::getNew('UserModel'); + $user->name = $characterModel->name; + $user->save(); + } + } + + /** + * @var $userCharactersModel Pathfinder\UserCharacterModel + */ + if( is_null($userCharactersModel = $characterModel->userCharacter) ){ + $userCharactersModel = $characterModel->rel('userCharacter'); + $userCharactersModel->characterId = $characterModel; + } + + // user might have changed + $userCharactersModel->userId = $user; + $userCharactersModel->save(); + + // get updated character model + $characterModel = $userCharactersModel->getCharacter(); + + // login by character + if($this->loginByCharacter($characterModel)){ + // set "login" cookie + $this->setLoginCookie($characterModel); + + // -> pass current character data to target page + $this->setTempCharacterData($characterModel->_id); + + // route to "map" + if($rootAlias == 'admin'){ + $f3->reroute([$rootAlias, ['*' => '']]); + }else{ + $f3->reroute(['map', ['*' => '']]); + } + }else{ + $f3->set(self::SESSION_KEY_SSO_ERROR, sprintf(self::ERROR_LOGIN_FAILED, $characterModel->name)); + } + }else{ + // character is not authorized to log in + $f3->set(self::SESSION_KEY_SSO_ERROR, + sprintf(self::ERROR_CHARACTER_FORBIDDEN, $characterModel->name, Pathfinder\CharacterModel::AUTHORIZATION_STATUS[$authStatus]) + ); + } + } + }else{ + // failed to load characterData from API + $f3->set(self::SESSION_KEY_SSO_ERROR, self::ERROR_CHARACTER_DATA); + } + }else{ + // failed to verify character by CCP SSO + $f3->set(self::SESSION_KEY_SSO_ERROR, self::ERROR_CHARACTER_VERIFICATION); + } + }else{ + // SSO "accessData" missing (e.g. timeout) + $f3->set(self::SESSION_KEY_SSO_ERROR, sprintf(self::ERROR_SERVICE_TIMEOUT, self::SSO_TIMEOUT)); + } + }else{ + // invalid SSO response + $f3->set(self::SESSION_KEY_SSO_ERROR, sprintf(self::ERROR_LOGIN_FAILED, 'Invalid response')); + } + } + + $f3->reroute([$rootAlias, ['*' => '']]); + } + + /** + * login by cookie name + * @param \Base $f3 + * @throws \Exception + */ + public function login(\Base $f3){ + $data = (array)$f3->get('GET'); + $cookieName = (string)$data['cookie']; + $character = null; + + if( !empty($cookieName) ){ + if( !empty($cookieData = $this->getCookieByName($cookieName) )){ + // cookie data is valid -> validate data against DB (security check!) + if( !empty($characters = $this->getCookieCharacters(array_slice($cookieData, 0, 1, true))) ){ + // character is valid and allowed to login + $character = $characters[$cookieName]; + } + } + } + + if(is_object($character)){ + // login by character + if($this->loginByCharacter($character)){ + // route to "map" + $f3->reroute(['map', ['*' => '']]); + }else{ + $f3->set(self::SESSION_KEY_SSO_ERROR, sprintf(self::ERROR_LOGIN_FAILED, $character->name)); + } + }else{ + $f3->set(self::SESSION_KEY_SSO_ERROR, self::ERROR_COOKIE_LOGIN); + } + + // on error -> route back to login form + $f3->reroute(['login']); + } + + /** + * get a valid "access_token" for oAuth 2.0 verification + * -> if $authCode is set -> request NEW "access_token" + * -> else check for existing (not expired) "access_token" + * -> else try to refresh auth and get fresh "access_token" + * @param bool $authCode + * @return null|\stdClass + */ + protected function getSsoAccessData($authCode){ + $accessData = null; + + if( !empty($authCode) ){ + // Authentication Code is set -> request new "accessToken" + $accessData = $this->verifyAuthorizationCode($authCode); + }else{ + // Unable to get Token -> trigger error + self::getSSOLogger()->write(sprintf(self::ERROR_ACCESS_TOKEN, $authCode)); + } + + return $accessData; + } + + /** + * verify authorization code, and get an "access_token" data + * @param string $authCode + * @return \stdClass + */ + protected function verifyAuthorizationCode(string $authCode){ + $requestParams = [ + 'grant_type' => 'authorization_code', + 'code' => $authCode + ]; + + return $this->requestAccessData($requestParams); + } + + /** + * get new "access_token" by an existing "refresh_token" + * -> if "access_token" is expired, this function gets a fresh one + * @param string $refreshToken + * @return \stdClass + */ + public function refreshAccessToken(string $refreshToken){ + $requestParams = [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken + ]; + + return $this->requestAccessData($requestParams); + } + + /** + * request an "access_token" AND "refresh_token" data + * -> this can either be done by sending a valid "authorization code" + * OR by providing a valid "refresh_token" + * @param array $requestParams + * @return \stdClass + */ + protected function requestAccessData(array $requestParams) : \stdClass { + $accessData = (object) []; + $accessData->accessToken = null; + $accessData->refreshToken = null; + $accessData->esiAccessTokenExpires = 0; + + $authCodeRequestData = $this->getF3()->ssoClient()->send('getAccess', $this->getAuthorizationData(), $requestParams); + + if( !empty($authCodeRequestData) ){ + if( !empty($authCodeRequestData['accessToken']) ){ + // accessToken is required for endpoints that require Auth + $accessData->accessToken = $authCodeRequestData['accessToken']; + } + + if( !empty($authCodeRequestData['expiresIn']) ){ + // expire time for accessToken + try{ + $accessTokenExpires = $this->getF3()->get('getDateTime')(); + $accessTokenExpires->add(new \DateInterval('PT' . (int)$authCodeRequestData['expiresIn'] . 'S')); + + $accessData->esiAccessTokenExpires = $accessTokenExpires->format('Y-m-d H:i:s'); + }catch(\Exception $e){ + $this->getF3()->error(500, $e->getMessage(), $e->getTrace()); + } + } + + if( !empty($authCodeRequestData['refreshToken']) ){ + // this token is used to refresh/get a new access_token when expires + $accessData->refreshToken = $authCodeRequestData['refreshToken']; + } + }else{ + self::getSSOLogger()->write(sprintf(self::ERROR_ACCESS_TOKEN, print_r($requestParams, true))); + } + + return $accessData; + } + + /** + * verify character data by "access_token" + * -> get some basic information (like character id) + * -> if more character information is required, use ESI "characters" endpoints request instead + * @param string $accessToken + * @return array + */ + public function verifyCharacterData(string $accessToken) : array { + $characterData = $this->getF3()->ssoClient()->send('getVerifyCharacter', $accessToken); + + if( !empty($characterData) ){ + // convert string with scopes to array + $characterData['scopes'] = Lib\Util::convertScopesString($characterData['scopes']); + }else{ + self::getSSOLogger()->write(sprintf(self::ERROR_VERIFY_CHARACTER, __METHOD__)); + } + + return $characterData; + } + + /** + * get character data + * @param int $characterId + * @return \stdClass + * @throws \Exception + */ + public function getCharacterData(int $characterId) : \stdClass{ + $characterData = (object) []; + + if($characterId){ + $characterDataBasic = $this->getF3()->ccpClient()->send('getCharacter', $characterId); + if( !empty($characterDataBasic) ){ + // remove some "unwanted" data -> not relevant for Pathfinder + $characterData->character = array_filter($characterDataBasic, function($key){ + return in_array($key, ['id', 'name', 'securityStatus']); + }, ARRAY_FILTER_USE_KEY); + + $characterData->corporation = null; + $characterData->alliance = null; + + if($corporationId = (int)$characterDataBasic['corporation']['id']){ + /** + * @var $corporation Pathfinder\CorporationModel + */ + $corporation = Pathfinder\AbstractPathfinderModel::getNew('CorporationModel'); + $corporation->getById($corporationId, 0); + if($corporation->valid()){ + $characterData->corporation = $corporation; + } + } + + if($allianceId = (int)$characterDataBasic['alliance']['id']){ + /** + * @var $alliance Pathfinder\AllianceModel + */ + $alliance = Pathfinder\AbstractPathfinderModel::getNew('AllianceModel'); + $alliance->getById($allianceId, 0); + if($alliance->valid()){ + $characterData->alliance = $alliance; + } + } + } + } + + return $characterData; + } + + /** + * update character + * @param \stdClass $characterData + * @return Pathfinder\CharacterModel|null + * @throws \Exception + */ + protected function updateCharacter(\stdClass $characterData) : ?Pathfinder\CharacterModel { + $character = null; + + if(!empty($characterData->character)){ + /** + * @var $character Pathfinder\CharacterModel + */ + $character = Pathfinder\AbstractPathfinderModel::getNew('CharacterModel'); + $character->getById((int)$characterData->character['id'], 0); + $character->copyfrom($characterData->character, [ + 'id', 'name', 'ownerHash', 'esiAccessToken', 'esiAccessTokenExpires', 'esiRefreshToken', 'esiScopes', 'securityStatus' + ]); + + $character->corporationId = $characterData->corporation; + $character->allianceId = $characterData->alliance; + $character->save(); + } + + return $character; + } + + /** + * get data for HTTP "Authorization:" Header + * -> This header is required for any Auth-required endpoints! + * @return array + */ + protected function getAuthorizationData() : array { + return [ + Controller\Controller::getEnvironmentData('CCP_SSO_CLIENT_ID'), + Controller\Controller::getEnvironmentData('CCP_SSO_SECRET_KEY'), + 'basic' + ]; + } + + /** + * get CCP SSO url from configuration file + * -> throw error if url is broken/missing + * @return string + */ + static function getSsoUrlRoot() : string { + $url = ''; + if( \Audit::instance()->url(self::getEnvironmentData('CCP_SSO_URL')) ){ + $url = self::getEnvironmentData('CCP_SSO_URL'); + }else{ + $error = sprintf(self::ERROR_CCP_SSO_URL, __METHOD__); + self::getSSOLogger()->write($error); + \Base::instance()->error(502, $error); + } + + return $url; + } + + /** + * get logger for SSO logging + * @return \Log + */ + static function getSSOLogger() : \Log { + return parent::getLogger('SSO'); + } +} \ No newline at end of file diff --git a/app/Controller/Ccp/Universe.php b/app/Controller/Ccp/Universe.php new file mode 100644 index 000000000..fe6a1e3b7 --- /dev/null +++ b/app/Controller/Ccp/Universe.php @@ -0,0 +1,321 @@ + 93 systems) + ]; + $regionIds = $f3->ccpClient()->send('getUniverseRegions'); + $regionIds = array_intersect($regionsWhitelist, $regionIds); + + $region = Model\Universe\AbstractUniverseModel::getNew('RegionModel'); + foreach($regionIds as $regionId){ + $region->loadById($regionId); + $region->loadConstellationsData(); + + foreach((array)$region->constellations as $constellation){ + $constellation->loadSystemsData(); + } + + $region->reset(); + } + }*/ + + /* currently not used + protected function setupConstellations(\Base $f3){ + $constellationsWhitelist = [ + 20000014 // Mal (11 systems) + ]; + $constellationIds = $f3->ccpClient()->send('getUniverseConstellations'); + $constellationIds = array_intersect($constellationsWhitelist, $constellationIds); + $constellation = Model\Universe\AbstractUniverseModel::getNew('ConstellationModel'); + foreach($constellationIds as $constellationId){ + $constellation->loadById($constellationId); + $constellation->loadSystemsData(); + $constellation->reset(); + } + }*/ + + /** + * setup categories + all dependencies (e.g. groups, types) + * id 2 -> Celestial (>100 groups -> >1000 types) + * id 6 -> Ship (46 groups -> 4xx types) + * id 65 -> Structure (10 groups -> 33 types) + * @param array $categoriesWhitelist + * @return array + * @throws \Exception + */ + protected function setupCategories(array $categoriesWhitelist = []) : array { + $info = []; + $categoryIds = Model\Universe\CategoryModel::getUniverseCategories(); + $categoryIds = array_intersect($categoriesWhitelist, $categoryIds); + foreach($categoryIds as $categoryId){ + $info[$categoryId] = $this->setupCategory($categoryId); + } + return $info; + } + + /** + * setup category + all dependencies (e.g. groups, types) + * -> $length = 0 -> setup all groups + * @param int $categoryId + * @param int $offset + * @param int $length + * @return array + * @throws \Exception + */ + public function setupCategory(int $categoryId, int $offset = 0, int $length = 0) : array { + $info = ['countAll' => 0, 'countChunk' => 0, 'count' => 0, 'offset' => $offset, 'groupTypes' => []]; + + if($categoryId){ + /** + * @var $category Model\Universe\CategoryModel + */ + $category = Model\Universe\AbstractUniverseModel::getNew('CategoryModel'); + $category->loadById($categoryId); + $info = $category->loadGroupsData($offset, $length); + } + + return $info; + } + + /** + * setup groups + all dependencies (e.g. types) + * id 6 -> Sun (29 types) + * id 7 -> Planet (9 types) + * id 10 -> Stargate (17 types) + * id 988 -> Wormhole (89 types) + * @param array $groupsWhitelist + * @return array + * @throws \Exception + */ + protected function setupGroups(array $groupsWhitelist = []) : array { + $info = []; + $groupIds = Model\Universe\GroupModel::getUniverseGroups(); + $groupIds = array_intersect($groupsWhitelist, $groupIds); + foreach($groupIds as $groupId){ + $info[$groupId] = $this->setupGroup($groupId); + } + return $info; + } + + /** + * setup group + all dependencies (e.g. types) + * @param int $groupId + * @param int $offset + * @param int $length + * @param bool $storeDogmaAttributes + * @return array + * @throws \Exception + */ + public function setupGroup(int $groupId, int $offset = 0, int $length = 0, bool $storeDogmaAttributes = false) : array { + $info = ['countAll' => 0, 'countChunk' => 0, 'count' => 0, 'offset' => $offset]; + + if($groupId){ + /** + * @var $group Model\Universe\GroupModel + */ + $group = Model\Universe\AbstractUniverseModel::getNew('GroupModel'); + $group->storeDogmaAttributes = $storeDogmaAttributes; + $group->loadById($groupId); + $info = $group->loadTypesData($offset, $length); + } + + return $info; + } + + // system search index methods ==================================================================================== + + /** + * build search index from all systems data + * @param int $offset + * @param int $length + * @return array + * @throws \Exception + */ + public function buildSystemsIndex(int $offset = 0, int $length = 10) : array { + $systemIds = $this->getSystemIds(); + $systemsAll = count($systemIds); + $systemIds = array_slice($systemIds, $offset, $length); + + /** + * @var $system Model\Universe\SystemModel + */ + $system = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); + $indexData = []; + foreach($systemIds as $systemId){ + $system->getById($systemId, 0); + if($hashKeyId = $system->getHashKey()){ + $indexData[$hashKeyId] = $system->getData(); + } + $system->reset(); + // offset must increase otherwise we get a endless loop + // -> see /setup ajax build loop function + $offset++; + } + + $this->getF3()->mset($indexData, '', $system::CACHE_INDEX_EXPIRE_KEY); + + // ... add hashKeys for all table rows to tableIndex as well + $system::buildTableIndex($system, array_keys($indexData)); + + return ['countAll' => $systemsAll, 'countBuild' => count($systemIds), 'offset' => $offset]; + } + + /** + * get systemIds for all systems + * @param bool $ignoreCache + * @return array + * @throws \Exception + */ + public function getSystemIds(bool $ignoreCache = false) : array { + $f3 = $this->getF3(); + $systemIds = []; + if($ignoreCache || !$f3->exists(self::SESSION_KEY_SYSTEM_IDS, $systemIds)){ + /** + * @var $system Model\Universe\SystemModel + */ + $system = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); + if($systems = $system->find()){ + $systemIds = $systems->getAll('id'); + if(count($systemIds)){ + sort($systemIds, SORT_NUMERIC); + $f3->set(self::SESSION_KEY_SYSTEM_IDS, $systemIds); + } + } + } + + return $systemIds ? : []; + } + + /** + * get complete system index (all systems) + * @param bool $all + * @return array + */ + public function getSystemsIndex(bool $all = false) : array { + $index = []; + $cacheKeyTable = Model\Universe\AbstractUniverseModel::generateHashKeyTable('system'); + if($this->getF3()->exists($cacheKeyTable,$cacheKeys)){ + foreach((array)$cacheKeys as $cacheKeyRow){ + if(($data = $this->get($cacheKeyRow)) && is_object($data)){ + $index[] = $all ? $data : $data->id; + } + } + } + return $index; + } + + /** + * clear complete systems search index for all systems + */ + public function clearSystemsIndex(){ + $cacheKeyTable = Model\Universe\AbstractUniverseModel::generateHashKeyTable('system'); + if($this->getF3()->exists($cacheKeyTable,$cacheKeys)){ + foreach((array)$cacheKeys as $cacheKeyRow) { + $this->clear($cacheKeyRow); + } + $this->getF3()->clear($cacheKeyTable); + } + } + + /** + * look for existing systemData in index + * -> if not exists -> try to build + * @param int $systemId + * @return null|\stdClass + * @throws \Exception + */ + public function getSystemData(int $systemId) : ?\stdClass { + $data = null; + if($systemId){ + // ...check index for data + $cacheKeyRow = Model\Universe\AbstractUniverseModel::generateHashKeyRow('system', $systemId); + if(!$data = $this->get($cacheKeyRow)){ + // .. try to build index + /** + * @var $system Model\Universe\SystemModel + */ + $system = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); + if($system->getById($systemId)){ + $data = $system->buildIndex(); + } + } + } + return $data; + } + + /** + * look for existing cacheKey data + * @param string $cacheKey + * @return null|\stdClass + */ + private function get(string $cacheKey) : ?\stdClass { + $data = null; + if($this->getF3()->exists($cacheKey,$value)) { + if(is_string($value) && strpos($value, Model\Universe\AbstractUniverseModel::CACHE_KEY_PREFIX) === 0) { + // value references an other cacheKey that holds data + return $this->get($value); + }elseif( !empty((array)$value) ){ + // stdClass data is not empty + $data = (object)$value; + } + } + return $data; + } + + /** + * clear cacheKey + * @param string $cacheKey + */ + private function clear(string $cacheKey){ + if($this->getF3()->exists($cacheKey,$value)) { + if(is_string($value) && strpos($value, Model\Universe\AbstractUniverseModel::CACHE_KEY_PREFIX) === 0) { + // value references another cacheKey -> clear that one as well + $this->clear($value); + } + $this->getF3()->clear($cacheKey); + } + } + + /** + * search universeName data by search term + * @param array $categories + * @param string $search + * @param bool $strict + * @return array + */ + public static function searchUniverseNameData(array $categories, string $search, bool $strict = false) : array { + $f3 = \Base::instance(); + $universeNameData = []; + if( !empty($categories) && !empty($search)){ + $universeIds = $f3->ccpClient()->send('search', $categories, $search, $strict); + if(isset($universeIds['error'])){ + // ESI error + $universeNameData = $universeIds; + }elseif( !empty($universeIds) ){ + $universeIds = Util::arrayFlattenByValue($universeIds); + $universeNameData = $f3->ccpClient()->send('getUniverseNames', $universeIds); + } + } + return $universeNameData; + } + +} \ No newline at end of file diff --git a/app/Controller/Controllerr.php b/app/Controller/Controllerr.php new file mode 100644 index 000000000..594609511 --- /dev/null +++ b/app/Controller/Controllerr.php @@ -0,0 +1,1017 @@ + if it is a "graceful" logout (e.g. user clicks "logout" button, we use 200) + */ + const DEFAULT_STATUS_LOGOUT = 403; + + // cookie specific keys (names) + const COOKIE_NAME_STATE = 'cookie'; + const COOKIE_PREFIX_CHARACTER = 'char'; + + // log text + const ERROR_SESSION_SUSPECT = 'id: [%45s], ip: [%45s], User-Agent: [%s]'; + const ERROR_TEMP_CHARACTER_ID = 'Invalid temp characterId: %s'; + + const NOTIFICATION_TYPES = ['danger', 'warning', 'info', 'success']; + /** + * @var \Base + */ + protected $f3; + + /** + * @var string template for render + */ + protected $template; + + /** + * @param string $template + */ + protected function setTemplate($template){ + $this->template = $template; + } + + /** + * @return string + */ + protected function getTemplate(){ + return $this->template; + } + + /** + * get $f3 base object + * @return \Base + */ + protected function getF3() : \Base { + return \Base::instance(); + } + + /** + * get DB connection + * @param string $alias + * @return SQL|null + */ + protected function getDB(string $alias = 'PF') : ?SQL { + return $this->getF3()->DB->getDB($alias); + } + + /** + * event handler for all "views" + * some global template variables are set in here + * @param \Base $f3 + * @param $params + * @return bool + */ + function beforeroute(\Base $f3, $params) : bool { + // init user session + $this->initSession($f3); + + if($f3->get('AJAX')){ + header('Content-Type: application/json'); + + // send "maintenance" Header -> e.g. before server update + if($modeMaintenance = (int)Config::getPathfinderData('login.mode_maintenance')){ + header('Pf-Maintenance: ' . $modeMaintenance); + } + }else{ + $f3->set('tplResource', $this->initResource($f3)); + + $this->setTemplate(Config::getPathfinderData('view.index')); + + + $f3->set('tplImage', Format\Image::instance()); + } + + return true; + } + + /** + * event handler after routing + * -> render view + * @param \Base $f3 + */ + public function afterroute(\Base $f3){ + // send preload/prefetch headers + $resource = Resource::instance(); + if($resource->getOption('output') === 'header'){ + header($resource->buildHeader(), false); + } + + if($file = $this->getTemplate()){ + // Ajax calls don´t need a page render.. + // this happens on client side + echo \Template::instance()->render($file); + } + } + + /** + * init new Session handler + * @param \Base $f3 + */ + protected function initSession(\Base $f3){ + $session = null; + + if( + $f3->get('SESSION_CACHE') === 'mysql' && + ($db = $f3->DB->getDB('PF')) instanceof SQL + ){ + if(!headers_sent() && session_status() != PHP_SESSION_ACTIVE){ + /** + * callback() for suspect sessions + * @param \DB\SQL\Session $session + * @param string $sid + * @return bool + */ + $onSuspect = function($session, $sid){ + self::getLogger('SESSION_SUSPECT')->write( sprintf( + self::ERROR_SESSION_SUSPECT, + $sid, + $session->ip(), + $session->agent() + )); + // .. continue with default onSuspect() handler + // -> destroy session + return false; + }; + + new Mysql\Session($db, 'sessions', true, $onSuspect); + } + } + } + + /** + * init new Resource handler + * @param \Base $f3 + * @return Resource + */ + protected function initResource(\Base $f3){ + $resource = Resource::instance(); + $resource->setOption('basePath', $f3->get('BASE')); + $resource->setOption('filePath', [ + 'style' => sprintf('/%scss/%s', $f3->get('UI'), Config::getPathfinderData('version')), + 'script' => sprintf('/%sjs/%s', $f3->get('UI'), Config::getPathfinderData('version')), + 'font' => sprintf('/%sfonts', $f3->get('UI')), + 'document' => sprintf('/%stemplates', $f3->get('UI')), + 'image' => sprintf('/%simg/%s', $f3->get('UI'), Config::getPathfinderData('version')), + 'favicon' => $f3->get('FAVICON') + ], true); + + $resource->register('style', 'pathfinder'); + + $resource->register('script', 'lib/require'); + $resource->register('script', 'app'); + + $resource->register('font', 'oxygen-regular-webfont'); + $resource->register('font', 'oxygen-bold-webfont'); + $resource->register('font', 'fa-regular-400'); + $resource->register('font', 'fa-solid-900'); + $resource->register('font', 'fa-brands-400'); + + $resource->register('url', self::getEnvironmentData('CCP_SSO_URL'), 'prerender'); + $resource->register('url', Config::getPathfinderData('api.ccp_image_server'), 'dns-prefetch'); + $resource->register('url', '//i.ytimg.com', 'dns-prefetch'); // YouTube tiny embed domain + + return $resource; + } + + /** + * get cookies "state" information + * -> whether user accepts cookies + * @return bool + */ + protected function getCookieState() : bool { + return (bool)count( $this->getCookieByName(self::COOKIE_NAME_STATE) ); + } + + /** + * search for existing cookies + * -> either a specific cookie by its name + * -> or get multiple cookies by their name (search by prefix) + * @param $cookieName + * @param bool $prefix + * @return array + */ + protected function getCookieByName($cookieName, $prefix = false) : array { + $data = []; + + if(!empty($cookieName)){ + $cookieData = (array)$this->getF3()->get('COOKIE'); + if($prefix === true){ + // look for multiple cookies with same prefix + foreach($cookieData as $name => $value){ + if(strpos($name, $cookieName) === 0){ + $data[$name] = $value; + } + } + }elseif(isset($cookieData[$cookieName])){ + // look for a single cookie + $data[$cookieName] = $cookieData[$cookieName]; + } + } + + return $data; + } + + /** + * set/update logged in cookie by character model + * -> store validation data in DB + * @param Pathfinder\CharacterModel $character + * @throws \Exception + */ + protected function setLoginCookie(Pathfinder\CharacterModel $character){ + if( $this->getCookieState() ){ + $expireSeconds = (int)Config::getPathfinderData('login.cookie_expire'); + $expireSeconds *= 24 * 60 * 60; + + $timezone = $this->getF3()->get('getTimeZone')(); + $expireTime = new \DateTime('now', $timezone); + + // add cookie expire time + $expireTime->add(new \DateInterval('PT' . $expireSeconds . 'S')); + + // unique "selector" -> to facilitate database look-ups (small size) + // -> This is preferable to simply using the database id field, + // which leaks the number of active users on the application + $selector = bin2hex( openssl_random_pseudo_bytes(12) ); + + // generate unique "validator" (strong encryption) + // -> plaintext set to user (cookie), hashed version of this in DB + $size = openssl_cipher_iv_length('aes-256-cbc'); + $validator = bin2hex(openssl_random_pseudo_bytes($size) ); + + // generate unique cookie token + $token = hash('sha256', $validator); + + // get unique cookie name for this character + $name = $character->getCookieName(); + + $authData = [ + 'characterId' => $character, + 'selector' => $selector, + 'token' => $token, + 'expires' => $expireTime->format('Y-m-d H:i:s') + ]; + + $authenticationModel = $character->rel('characterAuthentications'); + $authenticationModel->copyfrom($authData); + $authenticationModel->save(); + + $cookieValue = implode(':', [$selector, $validator]); + + // get cookie name -> save new one OR update existing cookie + $cookieName = 'COOKIE.' . self::COOKIE_PREFIX_CHARACTER . '_' . $name; + $this->getF3()->set($cookieName, $cookieValue, $expireSeconds); + } + } + + /** + * get characters from given cookie data + * -> validate cookie data + * -> validate characters + * -> cf. Sso->requestAuthorization() ( equivalent DB based login) + * + * @param array $cookieData + * @param bool $checkAuthorization + * @return Pathfinder\CharacterModel[] + * @throws \Exception + */ + protected function getCookieCharacters($cookieData = [], $checkAuthorization = true) : array { + $characters = []; + + if( + $this->getCookieState() && + !empty($cookieData) + ){ + /** + * @var $characterAuth Pathfinder\CharacterAuthenticationModel + */ + $characterAuth = Pathfinder\AbstractPathfinderModel::getNew('CharacterAuthenticationModel'); + + $timezone = $this->getF3()->get('getTimeZone')(); + $currentTime = new \DateTime('now', $timezone); + + foreach($cookieData as $name => $value){ + // remove invalid cookies + $invalidCookie = false; + + $data = explode(':', $value); + if(count($data) === 2){ + // cookie data is well formatted + $characterAuth->getByForeignKey('selector', $data[0], ['limit' => 1]); + + // validate "scope hash" + // -> either "normal" scopes OR "admin" scopes + // "expire data" and "validate token" + if( !$characterAuth->dry() ){ + if( + strtotime($characterAuth->expires) >= $currentTime->getTimestamp() && + hash_equals($characterAuth->token, hash('sha256', $data[1])) + ){ + // cookie information is valid + // -> try to update character information from ESI + // e.g. Corp has changed, this also ensures valid "access_token" + /** + * @var $character Pathfinder\CharacterModel + */ + $updateStatus = $characterAuth->characterId->updateFromESI(); + + if( empty($updateStatus) ){ + // make sure character data is up2date! + // -> this is not the case if e.g. userCharacters was removed "ownerHash" changed... + $character = $characterAuth->rel('characterId'); + $character->getById( $characterAuth->get('characterId', true) ); + + // check ESI scopes + $scopeHash = Util::getHashFromScopes($character->esiScopes); + + if( + $scopeHash === Util::getHashFromScopes(self::getScopesByAuthType()) || + $scopeHash === Util::getHashFromScopes(self::getScopesByAuthType('admin')) + ){ + // check if character still has user (is not the case of "ownerHash" changed + // check if character is still authorized to log in (e.g. corp/ally or config has changed + // -> do NOT remove cookie on failure. This can be a temporary problem (e.g. ESI is down,..) + if( $character->hasUserCharacter() ){ + $authStatus = $character->isAuthorized(); + + if( + $authStatus == 'OK' || + !$checkAuthorization + ){ + $character->virtual( 'authStatus', $authStatus); + } + + $characters[$name] = $character; + } + }else{ + // outdated/invalid ESI scopes + $characterAuth->erase(); + $invalidCookie = true; + } + }else{ + $invalidCookie = true; + } + }else{ + // clear existing authentication data from DB + $characterAuth->erase(); + $invalidCookie = true; + } + }else{ + $invalidCookie = true; + } + $characterAuth->reset(); + }else{ + $invalidCookie = true; + } + + // remove invalid cookie + if($invalidCookie){ + $this->getF3()->clear('COOKIE.' . $name); + } + } + } + + return $characters; + } + + /** + * get current character from session data + * @param int $ttl + * @return Pathfinder\CharacterModel|null + * @throws \Exception + */ + protected function getSessionCharacter(int $ttl = AbstractModel::DEFAULT_SQL_TTL) : ?Pathfinder\CharacterModel { + $character = null; + if($user = $this->getUser($ttl)){ + $header = self::getRequestHeaders(); + $requestedCharacterId = (int)$header['Pf-Character']; + if( !$this->getF3()->get('AJAX') ){ + $requestedCharacterId = (int)$_COOKIE['old_char_id']; + if(!$requestedCharacterId){ + $tempCharacterData = (array)$this->getF3()->get(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA); + if((int)$tempCharacterData['ID'] > 0){ + $requestedCharacterId = (int)$tempCharacterData['ID']; + } + } + } + + $character = $user->getSessionCharacter($requestedCharacterId, $ttl); + } + + return $character; + } + + /** + * get current character + * @param int $ttl + * @return Pathfinder\CharacterModel|null + * @throws \Exception + */ + public function getCharacter(int $ttl = AbstractModel::DEFAULT_SQL_TTL) : ?Pathfinder\CharacterModel { + return $this->getSessionCharacter($ttl); + } + + /** + * get current user + * @param int $ttl + * @return Pathfinder\UserModel|null + * @throws \Exception + */ + public function getUser($ttl = AbstractModel::DEFAULT_SQL_TTL) : ?Pathfinder\UserModel { + $user = null; + + if($this->getF3()->exists(Api\User::SESSION_KEY_USER_ID, $userId)){ + /** + * @var $userModel Pathfinder\UserModel + */ + $userModel = Pathfinder\AbstractPathfinderModel::getNew('UserModel'); + $userModel->getById($userId, $ttl); + + if( + !$userModel->dry() && + $userModel->hasUserCharacters() + ){ + $user = $userModel; + } + } + + return $user; + } + + /** + * set temp login character data (required during HTTP redirects on login) + * @param int $characterId + * @throws \Exception + */ + protected function setTempCharacterData(int $characterId){ + if($characterId > 0){ + $tempCharacterData = [ + 'ID' => $characterId + ]; + $this->getF3()->set(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA, $tempCharacterData); + }else{ + throw new \Exception( sprintf(self::ERROR_TEMP_CHARACTER_ID, $characterId) ); + } + } + + /** + * log out current character or all active characters (multiple browser tabs) + * -> send response data to client + * @param \Base $f3 + * @param bool $all + * @param bool $deleteSession + * @param bool $deleteLog + * @param bool $deleteCookie + * @param int $statusCode + * @throws \Exception + */ + protected function logoutCharacter( + \Base $f3, + bool $all = false, + bool $deleteSession = true, + bool $deleteLog = true, + bool $deleteCookie = false, + int $statusCode = self::DEFAULT_STATUS_LOGOUT + ){ + $sessionCharacterData = (array)$f3->get(Api\User::SESSION_KEY_CHARACTERS); + + if($sessionCharacterData){ + $activeCharacterId = ($activeCharacter = $this->getCharacter()) ? $activeCharacter->_id : 0; + /** + * @var $character Pathfinder\CharacterModel + */ + $character = Pathfinder\AbstractPathfinderModel::getNew('CharacterModel'); + $characterIds = []; + foreach($sessionCharacterData as $characterData){ + if($characterData['ID'] === $activeCharacterId){ + $characterIds[] = $activeCharacter->_id; + $activeCharacter->logout($deleteSession, $deleteLog, $deleteCookie); + }elseif($all){ + $character->getById($characterData['ID']); + $characterIds[] = $character->_id; + $character->logout($deleteSession, $deleteLog, $deleteCookie); + } + $character->reset(); + } + + if($characterIds){ + // broadcast logout information to webSocket server + $f3->webSocket()->write('characterLogout', $characterIds); + } + } + + if($f3->get('AJAX')){ + $f3->status($statusCode); + + $return = (object) []; + $return->reroute = rtrim(self::getEnvironmentData('URL'), '/') . $f3->alias('login'); + $return->error[] = $this->getErrorObject($statusCode, Config::getHttpStatusByCode($statusCode), 'Access denied: User not found'); + + echo json_encode($return); + }else{ + // redirect to landing page + $f3->reroute(['login']); + } + } + + /** + * get EVE server status from ESI + * @param \Base $f3 + * @throws \Exception + */ + public function getEveServerStatus(\Base $f3){ + $ttl = 60; + $esiStatusVersion = 'latest'; + $cacheKey = 'eve_server_status'; + + if(!$exists = $f3->exists($cacheKey, $return)){ + $return = (object) []; + $return->error = []; + + /** + * @var $client CcpClient + */ + if($client = $f3->ccpClient()){ + $return->server = [ + 'name' => strtoupper(self::getEnvironmentData('CCP_ESI_DATASOURCE')), + 'status' => 'offline', + 'statusColor' => 'red', + ]; + $return->api = [ + 'name' => 'ESI API', + 'status' => 'offline', + 'statusColor' => 'red', + 'url' => $client->getUrl(), + 'timeout' => $client->getTimeout(), + 'connectTimeout' => $client->getConnectTimeout(), + 'readTimeout' => $client->getReadTimeout(), + 'proxy' => ($proxy = $client->getProxy()) ? : 'false', + 'verify' => $client->getVerify(), + 'debug' => $client->getDebugRequests(), + 'dataSource' => $client->getDataSource(), + 'statusVersion' => $esiStatusVersion, + 'routes' => [] + ]; + + $serverStatus = $client->send('getServerStatus'); + if( !isset($serverStatus['error']) ){ + $statusData = $serverStatus['status']; + // calculate time diff since last server restart + $timezone = $f3->get('getTimeZone')(); + $dateNow = new \DateTime('now', $timezone); + $dateServerStart = new \DateTime($statusData['startTime']); + $interval = $dateNow->diff($dateServerStart); + $startTimestampFormat = $interval->format('%hh %im'); + if($interval->days > 0){ + $startTimestampFormat = $interval->days . 'd ' . $startTimestampFormat; + } + + $statusData['name'] = $return->server['name']; + $statusData['status'] = 'online'; + $statusData['statusColor'] = 'green'; + $statusData['startTime'] = $startTimestampFormat; + $return->server = $statusData; + }else{ + $return->error[] = (new PathfinderException($serverStatus['error'], 500))->getError(); + } + + $apiStatus = $client->send('getStatus', 'latest', true); + if( !isset($apiStatus['error']) ){ + // find top status + $status = 'OK'; + $color = 'green'; + foreach($apiStatus['status'] as &$statusData){ + if('red' == $statusData['status']){ + $status = 'unstable'; + $color = $statusData['status'] = 'orange'; // red is already in use for fatal API errors (e.g. no response at all, or offline) + break; + } + if('yellow' == $statusData['status']){ + $status = 'degraded'; + $color = $statusData['status']; + } + } + + $return->api['status'] = $status; + $return->api['statusColor'] = $color; + $return->api['routes'] = $apiStatus['status']; + }else{ + $return->error[] = (new PathfinderException($apiStatus['error'], 500))->getError(); + } + + if(empty($return->error)){ + $f3->set($cacheKey, $return, $ttl); + } + } + } + + if(empty($return->error)){ + $f3->expire(Config::ttlLeft($exists, $ttl)); + } + + echo json_encode($return); + } + + /** + * @param int $code + * @param string $status + * @param string $text + * @param null $trace + * @return \stdClass + */ + protected function getErrorObject(int $code, string $status = '', string $text = '', $trace = null) : \stdClass { + $object = (object) []; + $object->type = 'error'; + $object->code = $code; + $object->status = empty($status) ? @constant('Base::HTTP_' . $code) : $status; + if(!empty($text)){ + $object->text = $text; + } + if(!empty($trace)){ + $object->trace = $trace; + } + return $object; + } + + /** + * @param string $title + * @param string $text + * @param string $type + * @return \stdClass + */ + protected function getNotificationObject(string $title, $text = '', $type = 'danger') : \stdClass { + $notification = (object) []; + $notification->type = in_array($type, self::NOTIFICATION_TYPES) ? $type : 'danger'; + $notification->title = $title; + $notification->text = $text; + return $notification; + } + + /** + * get a program URL by alias + * -> if no $alias given -> get "default" route (index.php) + * @param null $alias + * @return bool|string + */ + protected function getRouteUrl($alias = null){ + $url = false; + + if(!empty($alias)){ + // check given alias is a valid (registered) route + if(array_key_exists($alias, $this->getF3()->get('ALIASES'))){ + $url = $this->getF3()->alias($alias); + } + }elseif($this->getF3()->get('ALIAS')){ + // get current URL + $url = $this->getF3()->alias( $this->getF3()->get('ALIAS') ); + }else{ + // get main (index.php) URL + $url = $this->getF3()->alias('login'); + } + + return $url; + } + + /** + * get a custom userAgent string for API calls + * @return string + */ + protected function getUserAgent() : string { + $userAgent = ''; + $userAgent .= Config::getPathfinderData('name'); + $userAgent .= ' - ' . Config::getPathfinderData('version'); + $userAgent .= ' | ' . Config::getPathfinderData('contact'); + $userAgent .= ' (' . $_SERVER['SERVER_NAME'] . ')'; + + return $userAgent; + } + + /** + * print error information in CLI mode + * @param \stdClass $error + */ + protected function echoErrorCLI(\stdClass $error){ + echo '[' . date('H:i:s') . '] ───────────────────────────' . PHP_EOL; + foreach(get_object_vars($error) as $key => $value){ + $row = str_pad(' ',2 ) . str_pad($key . ':',10 ); + if($key == 'trace'){ + $value = preg_replace("/\r\n|\r|\n/", "\n" . str_pad(' ',12 ), $value); + $row .= PHP_EOL . str_pad(' ',12 ) . $value; + }else{ + $row .= $value; + } + echo $row . PHP_EOL; + } + } + + /** + * onError() callback function + * -> on AJAX request -> return JSON with error information + * -> on HTTP request -> render error page + * @param \Base $f3 + * @return bool + */ + public function showError(\Base $f3){ + + if(!headers_sent()){ + // collect error info ------------------------------------------------------------------------------------- + $errorData = $f3->get('ERROR'); + $exception = $f3->get('EXCEPTION'); + + if($exception instanceof PathfinderException){ + // ... handle Pathfinder exceptions (e.g. validation Exceptions,..) + $error = $exception->getError(); + }else{ + // ... handle error $f3->error() calls + $error = $this->getErrorObject( + $errorData['code'], + $errorData['status'], + $errorData['text'], + $f3->get('DEBUG') >= 1 ? $errorData['trace'] : null + ); + } + + // check if error is a PDO Exception ---------------------------------------------------------------------- + if(strpos(strtolower( $f3->get('ERROR.text') ), 'duplicate') !== false){ + preg_match_all('/\'([^\']+)\'/', $f3->get('ERROR.text'), $matches, PREG_SET_ORDER); + + if(count($matches) === 2){ + $error->field = $matches[1][1]; + $error->text = 'Value "' . $matches[0][1] . '" already exists'; + } + } + + // set response status ------------------------------------------------------------------------------------ + if(!empty($error->code)){ + $f3->status($error->code); + } + + if($f3->get('CLI')){ + $this->echoErrorCLI($error); + // no further processing (no HTML output) + return false; + }elseif($f3->get('AJAX')){ + $return = (object) []; + $return->error[] = $error; + echo json_encode($return); + }else{ + // non AJAX (e.g. GET/POST) + // recursively clear existing output buffers + while(ob_get_level()){ + ob_end_clean(); + } + + $f3->set('tplPageTitle', 'ERROR - ' . $error->code); + // set error data for template rendering + $error->redirectUrl = $this->getRouteUrl(); + $f3->set('errorData', $error); + + // 4xx/5xx error -> set error page template + if( preg_match('/^4[0-9]{2}$/', $error->code) ){ + $f3->set('tplPageContent', Config::getPathfinderData('STATUS.4XX') ); + }elseif( preg_match('/^5[0-9]{2}$/', $error->code) ){ + $f3->set('tplPageContent', Config::getPathfinderData('STATUS.5XX')); + } + + // stop script - die(); after this fkt is done + // -> unload() fkt is still called + $f3->set('HALT', true); + } + } + + return true; + } + + /** + * Callback for framework "unload" + * -> this function is called on each request! + * -> configured in config.ini + * @param \Base $f3 + * @return bool + */ + public function unload(\Base $f3){ + // store all user activities that are buffered for logging in this request + // this should work even on non HTTP200 responses + $this->logActivities(); + + return true; + } + + /** + * store activity log data to DB + */ + protected function logActivities(){ + LogController::instance()->logActivities(); + Monolog::instance()->log(); + } + + /** + * simple counter with "static" store + * -> called within tpl render + * @return \Closure + */ + protected function counter() : \Closure { + $store = []; + + return function(string $action = 'increment', string $type = 'default', $val = 0) use (&$store){ + $return = null; + switch($action){ + case 'increment': $store[$type]++; break; + case 'add': $store[$type] += (int)$val; break; + case 'get': $return = $store[$type] ? : null; break; + case 'reset': unset($store[$type]); break; + } + return $return; + }; + } + + /** + * get controller by class name + * -> controller class is searched within all controller directories + * @param $className + * @return null|Controller + * @throws \Exception + */ + static function getController($className){ + $controller = null; + // add subNamespaces for controller classes + $subNamespaces = ['Api', 'Ccp']; + + for($i = 0; $i <= count($subNamespaces); $i++){ + $path = [__NAMESPACE__]; + $path[] = ( isset($subNamespaces[$i - 1]) ) ? $subNamespaces[$i - 1] : ''; + $path[] = $className; + $classPath = implode('\\', array_filter($path)); + + if(class_exists($classPath)){ + $controller = new $classPath(); + break; + } + } + + if( is_null($controller) ){ + throw new \Exception( sprintf('Controller class "%s" not found!', $className) ); + } + + return $controller; + } + + + /** + * get scope array by a "role" + * @param string $authType + * @return array + */ + static function getScopesByAuthType(string $authType = '') : array { + $scopes = array_filter((array)self::getEnvironmentData('CCP_ESI_SCOPES')); + switch($authType){ + case 'admin': + $scopesAdmin = array_filter((array)self::getEnvironmentData('CCP_ESI_SCOPES_ADMIN')); + $scopes = array_merge($scopes, $scopesAdmin); + break; + } + sort($scopes); + return $scopes; + } + + /** + * Helper function to return all headers because + * getallheaders() is not available under nginx + * @return array (string $key -> string $value) + */ + static function getRequestHeaders() : array { + $headers = []; + $headerPrefix = 'http_'; + $prefixLength = mb_strlen($headerPrefix); + $serverData = self::getServerData(); + + if( + function_exists('apache_request_headers') && + $serverData->type === 'apache' + ){ + // Apache WebServer + $headers = apache_request_headers(); + }else{ + // Other WebServer, e.g. Nginx + // Unfortunately this "fallback" does not work for me (Apache) + // Therefore we can´t use this for all servers + // https://github.com/exodus4d/pathfinder/issues/58 + foreach($_SERVER as $name => $value){ + $name = mb_strtolower($name); + if(mb_substr($name, 0, $prefixLength) == $headerPrefix){ + $headers[mb_convert_case(str_replace('_', '-', mb_substr($name, $prefixLength)), MB_CASE_TITLE)] = $value; + } + } + } + + return $headers; + } + + /** + * get some server information + * @param int $ttl cache time (default: 1h) + * @return \stdClass + */ + static function getServerData($ttl = 3600){ + $f3 = \Base::instance(); + $cacheKey = 'PF_SERVER_INFO'; + + if( !$f3->exists($cacheKey) ){ + $serverData = (object) []; + $serverData->type = 'unknown'; + $serverData->version = 'unknown'; + $serverData->requiredVersion = 'unknown'; + $serverData->phpInterfaceType = php_sapi_name(); + + if(strpos(strtolower($_SERVER['SERVER_SOFTWARE']), 'nginx' ) !== false){ + // Nginx server + $serverSoftwareArgs = explode('/', strtolower( $_SERVER['SERVER_SOFTWARE']) ); + $serverData->type = reset($serverSoftwareArgs); + $serverData->version = end($serverSoftwareArgs); + $serverData->requiredVersion = $f3->get('REQUIREMENTS.SERVER.NGINX.VERSION'); + }elseif(strpos(strtolower($_SERVER['SERVER_SOFTWARE']), 'apache' ) !== false){ + // Apache server + $serverData->type = 'apache'; + $serverData->requiredVersion = $f3->get('REQUIREMENTS.SERVER.APACHE.VERSION'); + + // try to get the apache version... + if(function_exists('apache_get_version')){ + // function does not exists if PHP is running as CGI/FPM module! + $matches = preg_split('/[\s,\/ ]+/', strtolower( apache_get_version() ) ); + if(count($matches) > 1){ + $serverData->version = $matches[1]; + } + } + } + + // cache data for one day + $f3->set($cacheKey, $serverData, $ttl); + } + + return $f3->get($cacheKey); + } + + /** + * get the current registration status + * 0=registration stop |1=new registration allowed + * @return int + */ + static function getRegistrationStatus(){ + return (int)Config::getPathfinderData('registration.status'); + } + + /** + * get a Logger object by Hive key + * -> set in pathfinder.ini + * @param string $type + * @return \Log + */ + static function getLogger($type = 'DEBUG') : \Log { + return LogController::getLogger($type); + } + + /** + * removes illegal characters from a Hive-key that are not allowed + * @param $key + * @return string + */ + static function formatHiveKey($key) : string { + $illegalCharacters = ['-', ' ']; + return strtolower(str_replace($illegalCharacters, '', $key)); + } + + /** + * get environment specific configuration data + * @param string $key + * @return string|array|null + */ + static function getEnvironmentData($key){ + return Config::getEnvironmentData($key); + } + +} \ No newline at end of file diff --git a/app/Controller/LogController.php b/app/Controller/LogController.php new file mode 100644 index 000000000..b17044fa2 --- /dev/null +++ b/app/Controller/LogController.php @@ -0,0 +1,173 @@ + this buffered data can be stored somewhere (e.g. DB) before HTTP response + * -> should be cleared afterwards! + * @var array + */ + protected $activityLogBuffer = []; + + /** + * get columns from ActivityLogModel that can be uses as counter + * @return array + * @throws \Exception + */ + protected function getActivityLogColumns(): array{ + if(empty($this->activityLogColumns)){ + $f3 = \Base::instance(); + if(!$f3->exists(self::CACHE_KEY_ACTIVITY_COLUMNS, $this->activityLogColumns)){ + /** + * @var $activityLogModel Pathfinder\ActivityLogModel + */ + $activityLogModel = Pathfinder\AbstractPathfinderModel::getNew('ActivityLogModel'); + $this->activityLogColumns = $activityLogModel->getCountableColumnNames(); + $f3->set(self::CACHE_KEY_ACTIVITY_COLUMNS, self::CACHE_TTL_ACTIVITY_COLUMNS); + } + } + + return $this->activityLogColumns; + } + + /** + * buffered activity log data for this singleton LogController() class + * -> this buffered data can be stored somewhere (e.g. DB) before HTTP response + * -> should be cleared afterwards! + * @param MapLog $log + * @throws \Exception + */ + public function push(MapLog $log){ + $action = $log->getAction(); + + // check $action to be valid (table column exists) + if($action && in_array($action, $this->getActivityLogColumns())){ + if($mapId = $log->getChannelId()){ + $logData = $log->getData(); + if($characterId = (int)$logData['character']['id']){ + if($index = $this->getBufferedActivityIndex($characterId, $mapId)){ + $this->activityLogBuffer[$index][$action]++; + }else{ + $this->activityLogBuffer[] = [ + 'characterId' => $characterId, + 'mapId' => $mapId, + $action => 1 + ]; + } + } + } + } + } + + /** + * store all buffered activity log data to DB + */ + public function logActivities(){ + if( !empty($this->activityLogBuffer) ){ + $db = \Base::instance()->DB->getDB('PF'); + + $quoteStr = function($str) use ($db) { + return $db->quotekey($str); + }; + + $placeholderStr = function($str){ + return ':' . $str; + }; + + $updateRule = function($str){ + return $str . " = " . $str . " + VALUES(" . $str . ")"; + }; + + $year = (int)date('o'); + $yearWeek = (int)date('W'); + $db->begin(); + + foreach($this->activityLogBuffer as $activityData){ + $activityData['year'] = $year; + $activityData['week'] = $yearWeek; + + $columns = array_keys($activityData); + $columnsQuoted = array_map($quoteStr, $columns); + $placeholder = array_map($placeholderStr, $columns); + $args = array_combine($placeholder, $activityData); + + // "filter" columns that can be updated + $columnsForUpdate = array_diff($columns, ['year', 'week', 'characterId', 'mapId']); + $updateSql = array_map($updateRule, $columnsForUpdate); + + $sql = "INSERT DELAYED INTO + activity_log (" . implode(', ', $columnsQuoted) . ") VALUES( + " . implode(', ', $placeholder) . " + ) + ON DUPLICATE KEY UPDATE + updated = NOW(), + " . implode(', ', $updateSql) . " + "; + + $db->exec($sql, $args); + } + + $db->commit(); + + // clear activity data for this instance + $this->activityLogBuffer = []; + } + } + + /** + * get array key/index from "buffered activity log" array + * @param int $characterId + * @param int $mapId + * @return int + */ + private function getBufferedActivityIndex(int $characterId, int $mapId): int { + $activityKey = 0; + if($characterId > 0 && $mapId > 0 ){ + foreach($this->activityLogBuffer as $key => $activityData){ + if( + $activityData['characterId'] === $characterId && + $activityData['mapId'] === $mapId + ){ + $activityKey = (int)$key; + break; + } + } + } + + return $activityKey; + } + + /** + * get Logger instance + * @param string $type + * @return \Log + */ + public static function getLogger(string $type) : \Log { + $logFiles = Config::getPathfinderData('logfiles'); + $logFileName = empty($logFiles[$type]) ? 'error' : $logFiles[$type]; + return new \Log($logFileName . '.log'); + } + +} \ No newline at end of file diff --git a/app/Controller/MapController.php b/app/Controller/MapController.php new file mode 100644 index 000000000..fcab21e5f --- /dev/null +++ b/app/Controller/MapController.php @@ -0,0 +1,40 @@ +register('script', 'app/mappage'); + + $character = $this->getCharacter(); + + // characterId + $f3->set('tplCharacterId', $character->id); + + // page title + $f3->set('tplPageTitle', $character->name . ' | ' . Config::getPathfinderData('name')); + + // main page content + $f3->set('tplPageContent', false); + + // JS main file + $f3->set('tplJsView', 'mappage'); + } + +} \ No newline at end of file diff --git a/app/Controller/Setup.php b/app/Controller/Setup.php new file mode 100644 index 000000000..07ebd3faa --- /dev/null +++ b/app/Controller/Setup.php @@ -0,0 +1,2010 @@ + [], + 'BASE' => ['missingOk' => true], + 'URL' => [], + 'DEBUG' => [], + 'DB_PF_DNS' => [], + 'DB_PF_NAME' => [], + 'DB_PF_USER' => [], + 'DB_PF_PASS' => [], + 'DB_UNIVERSE_DNS' => [], + 'DB_UNIVERSE_NAME' => [], + 'DB_UNIVERSE_USER' => [], + 'DB_UNIVERSE_PASS' => [], + 'CCP_SSO_URL' => [], + 'CCP_SSO_CLIENT_ID' => [], + 'CCP_SSO_SECRET_KEY' => [], + 'CCP_SSO_DOWNTIME' => [], + 'CCP_ESI_URL' => [], + 'CCP_ESI_DATASOURCE' => [], + 'SMTP_HOST' => [], + 'SMTP_PORT' => [], + 'SMTP_SCHEME' => [], + 'SMTP_USER' => [], + 'SMTP_PASS' => [], + 'SMTP_FROM' => [], + 'SMTP_ERROR' => [] + ]; + + /** + * required database setup + * @var array + */ + protected $databases = [ + 'PF' => [ + 'info' => [], + 'models' => [ + 'Model\Pathfinder\CronModel', + 'Model\Pathfinder\UserModel', + 'Model\Pathfinder\AllianceModel', + 'Model\Pathfinder\CorporationModel', + 'Model\Pathfinder\MapModel', + 'Model\Pathfinder\MapScopeModel', + 'Model\Pathfinder\MapTypeModel', + 'Model\Pathfinder\SystemTypeModel', + 'Model\Pathfinder\SystemStatusModel', + 'Model\Pathfinder\RightModel', + 'Model\Pathfinder\RoleModel', + 'Model\Pathfinder\StructureModel', + + 'Model\Pathfinder\CharacterStatusModel', + 'Model\Pathfinder\ConnectionScopeModel', + 'Model\Pathfinder\StructureStatusModel', + + 'Model\Pathfinder\CharacterMapModel', + 'Model\Pathfinder\AllianceMapModel', + 'Model\Pathfinder\CorporationMapModel', + + 'Model\Pathfinder\CorporationRightModel', + 'Model\Pathfinder\CorporationStructureModel', + + 'Model\Pathfinder\UserCharacterModel', + 'Model\Pathfinder\CharacterModel', + 'Model\Pathfinder\CharacterAuthenticationModel', + 'Model\Pathfinder\CharacterLogModel', + + 'Model\Pathfinder\SystemModel', + + 'Model\Pathfinder\ConnectionModel', + 'Model\Pathfinder\ConnectionLogModel', + 'Model\Pathfinder\SystemSignatureModel', + + 'Model\Pathfinder\ActivityLogModel', + + 'Model\Pathfinder\SystemShipKillModel', + 'Model\Pathfinder\SystemPodKillModel', + 'Model\Pathfinder\SystemFactionKillModel', + 'Model\Pathfinder\SystemJumpModel' + ] + ], + 'UNIVERSE' => [ + 'info' => [], + 'models' => [ + 'Model\Universe\DogmaAttributeModel', + 'Model\Universe\TypeAttributeModel', + 'Model\Universe\TypeModel', + 'Model\Universe\GroupModel', + 'Model\Universe\CategoryModel', + 'Model\Universe\FactionModel', + 'Model\Universe\AllianceModel', + 'Model\Universe\CorporationModel', + 'Model\Universe\RaceModel', + 'Model\Universe\StationModel', + 'Model\Universe\StructureModel', + 'Model\Universe\StargateModel', + 'Model\Universe\StarModel', + 'Model\Universe\PlanetModel', + 'Model\Universe\SystemModel', + 'Model\Universe\ConstellationModel', + 'Model\Universe\RegionModel', + 'Model\Universe\SystemNeighbourModel', + 'Model\Universe\SystemStaticModel', + 'Model\Universe\SovereigntyMapModel', + 'Model\Universe\FactionWarSystemModel' + ] + ] + ]; + + /** + * database error + * @var bool + */ + protected $databaseHasError = false; + + /** + * event handler for all "views" + * some global template variables are set in here + * @param \Base $f3 + * @param array $params + * @return bool + */ + function beforeroute(\Base $f3, $params): bool { + $f3->set('tplResource', $this->initResource($f3)); + + // page title + $f3->set('tplPageTitle', 'Setup | ' . Config::getPathfinderData('name')); + + // main page content + $f3->set('tplPageContent', Config::getPathfinderData('view.setup')); + + // body element class + $f3->set('tplBodyClass', 'pf-landing'); + + // top navigation configuration + $f3->set('tplNavigation', $this->getNavigationConfig()); + + return true; + } + + /** + * @param \Base $f3 + */ + public function afterroute(\Base $f3) { + // js view (file) + $f3->set('tplJsView', 'setup'); + + $f3->set('tplCounter', $this->counter()); + + $f3->set('tplConvertBytes', function(){ + return call_user_func_array([Number::instance(), 'bytesToString'], func_get_args()); + }); + + // render view + echo \Template::instance()->render( Config::getPathfinderData('view.index') ); + } + + /** + * main setup route handler + * works as dispatcher for setup functions + * -> for security reasons all /setup "routes" are dispatched by GET params + * @param \Base $f3 + * @throws \Exception + */ + public function init(\Base $f3){ + $params = $f3->get('GET'); + + // enables automatic column fix + $fixColumns = false; + + switch($params['action']){ + case 'createDB': + $this->createDB($f3, $params['db']); + break; + case 'bootstrapDB': + $this->bootstrapDB($f3, $params['db']); + break; + case 'fixCols': + $fixColumns = true; + break; + case 'importTable': + $this->importTable($params['model']); + break; + case 'exportTable': + $this->exportTable($params['model']); + break; + case 'clearFiles': + $this->clearFiles((string)$params['path']); + break; + case 'flushRedisDb': + $this->flushRedisDb((string)$params['host'], (int)$params['port'], (int)$params['db']); + break; + case 'invalidateCookies': + $this->invalidateCookies($f3); + break; + } + + // ============================================================================================================ + // Template data + // ============================================================================================================ + + // Server ----------------------------------------------------------------------------------------------------- + // Server information + $f3->set('serverInformation', $this->getServerInformation($f3)); + + // Pathfinder directory config + $f3->set('directoryConfig', $this->getDirectoryConfig($f3)); + + // Server environment variables + $f3->set('checkSystemConfig', $this->checkSystemConfig($f3)); + + // Environment ------------------------------------------------------------------------------------------------ + // Server requirement + $f3->set('checkRequirements', $this->checkRequirements($f3)); + + // PHP config + $f3->set('checkPHPConfig', $this->checkPHPConfig($f3)); + + // Settings --------------------------------------------------------------------------------------------------- + // Pathfinder environment config + $f3->set('environmentInformation', $this->getEnvironmentInformation($f3)); + + // Pathfinder map default config + $f3->set('mapsDefaultConfig', $this->getMapsDefaultConfig($f3)); + + // Database --------------------------------------------------------------------------------------------------- + // Database config + $f3->set('checkDatabase', $this->checkDatabase($f3, $fixColumns)); + + // Redis ------------------------------------------------------------------------------------------------------ + // Redis information + $f3->set('checkRedisInformation', $this->checkRedisInformation($f3)); + + // Socket ----------------------------------------------------------------------------------------------------- + // WebSocket information + $f3->set('socketInformation', $this->getSocketInformation($f3)); + + // Cronjob ---------------------------------------------------------------------------------------------------- + $f3->set('cronConfig', $this->getCronConfig($f3)); + + // Administration --------------------------------------------------------------------------------------------- + // Index information + $f3->set('indexInformation', $this->getIndexData($f3)); + + // Filesystem (cache) size + $f3->set('checkDirSize', $this->checkDirSize($f3)); + } + + /** + * get top navigation configuration + * @return array + */ + protected function getNavigationConfig() : array { + return [ + 'server' => [ + 'icon' => 'fa-home' + ], + 'environment' => [ + 'icon' => 'fa-server' + ], + 'settings' => [ + 'icon' => 'fa-sliders-h' + ], + 'database' => [ + 'icon' => 'fa-database' + ], + 'cache' => [ + 'icon' => 'fa-hdd' + ], + 'socket' => [ + 'icon' => 'fa-exchange-alt' + ], + 'cronjob' => [ + 'icon' => 'fa-user-clock' + ], + 'administration' => [ + 'icon' => 'fa-wrench' + ] + ]; + } + + /** + * set environment information + * @param \Base $f3 + * @return array + */ + protected function getEnvironmentInformation(\Base $f3) : array { + $environmentData = []; + // exclude some sensitive data (e.g. database, passwords) + $excludeVars = [ + 'DB_PF_DNS', 'DB_PF_NAME', 'DB_PF_USER', 'DB_PF_PASS', + 'DB_UNIVERSE_DNS', 'DB_UNIVERSE_NAME', 'DB_UNIVERSE_USER', 'DB_UNIVERSE_PASS' + ]; + + // obscure some values + $obscureVars = ['CCP_SSO_CLIENT_ID', 'CCP_SSO_SECRET_KEY', 'SMTP_PASS']; + + foreach($this->environmentVars as $var => $options){ + if( !in_array($var, $excludeVars) ){ + $value = Config::getEnvironmentData($var); + $check = true; + + if(is_null($value) && !array_key_exists('missingOk', $options)){ + // variable missing + $check = false; + $value = '[missing]'; + }elseif( in_array($var, $obscureVars)){ + $value = Util::obscureString($value); + } + + $environmentData[$var] = [ + 'label' => $var, + 'value' => ((empty($value) && !is_int($value)) ? ' ' : $value), + 'check' => $check + ]; + } + } + + return $environmentData; + } + + /** + * get server information + * @param \Base $f3 + * @return array + */ + protected function getServerInformation(\Base $f3) : array { + return [ + 'time' => [ + 'label' => 'Time', + 'value' => date('Y/m/d H:i:s') . ' - (' . $f3->get('TZ') . ')' + ], + 'os' => [ + 'label' => 'OS', + 'value' => function_exists('php_uname') ? php_uname('s') : $_SERVER['OS'] + ], + 'name' => [ + 'label' => 'Host name', + 'value' => function_exists('php_uname') ? php_uname('n') : $_SERVER['SERVER_NAME'] + ], + 'release' => [ + 'label' => 'Release name', + 'value' => function_exists('php_uname') ? php_uname('r') : 'unknown' + ], + 'version' => [ + 'label' => 'Version info', + 'value' => function_exists('php_uname') ? php_uname('v') : 'unknown' + ], + 'machine' => [ + 'label' => 'Machine type', + 'value' => function_exists('php_uname') ? php_uname('m') : $_SERVER['PROCESSOR_ARCHITECTURE'] + ], + 'root' => [ + 'label' => 'Document root', + 'value' => $f3->get('ROOT') + ], + 'port' => [ + 'label' => 'Port', + 'value' => $f3->get('PORT') + ], + 'protocol' => [ + 'label' => 'Protocol - scheme', + 'value' => $f3->get('SERVER.SERVER_PROTOCOL') . ' - ' . $f3->get('SCHEME') + ] + ]; + } + + /** + * get information for used directories + * @param \Base $f3 + * @return array + */ + protected function getDirectoryConfig(\Base $f3) : array { + return [ + 'TEMP' => [ + 'label' => 'TEMP', + 'value' => $f3->get('TEMP'), + 'check' => true, + 'tooltip' => 'Temporary folder for pre compiled templates.', + 'chmod' => Util::filesystemInfo($f3->get('TEMP'))['chmod'] + ], + 'CACHE' => [ + 'label' => 'CACHE', + 'value' => $f3->get('CACHE'), + 'check' => true, + 'tooltip' => 'Cache backend. Support for Redis, Memcache, APC, WinCache, XCache and a filesystem-based (default) cache.', + 'chmod' => ((Config::parseDSN($f3->get('CACHE'), $confCache)) && $confCache['type'] == 'folder') ? + Util::filesystemInfo((string)$confCache['folder'])['chmod'] : '' + ], + 'API_CACHE' => [ + 'label' => 'API_CACHE', + 'value' => $f3->get('API_CACHE'), + 'check' => true, + 'tooltip' => 'Cache backend for API related cache data. Support for Redis and a filesystem-based (default) cache.', + 'chmod' => ((Config::parseDSN($f3->get('API_CACHE'), $confCacheApi)) && $confCacheApi['type'] == 'folder') ? + Util::filesystemInfo((string)$confCacheApi['folder'])['chmod'] : '' + ], + 'LOGS' => [ + 'label' => 'LOGS', + 'value' => $f3->get('LOGS'), + 'check' => true, + 'tooltip' => 'Folder for pathfinder logs (e.g. cronjob-, error-logs, ...).', + 'chmod' => Util::filesystemInfo($f3->get('LOGS'))['chmod'] + ], + 'UI' => [ + 'label' => 'UI', + 'value' => $f3->get('UI'), + 'check' => true, + 'tooltip' => 'Folder for public accessible resources (templates, js, css, images,..).', + 'chmod' => Util::filesystemInfo($f3->get('UI'))['chmod'] + ], + 'AUTOLOAD' => [ + 'label' => 'AUTOLOAD', + 'value' => $f3->get('AUTOLOAD'), + 'check' => true, + 'tooltip' => 'Autoload folder for PHP files.', + 'chmod' => Util::filesystemInfo($f3->get('AUTOLOAD'))['chmod'] + ], + 'FAVICON' => [ + 'label' => 'FAVICON', + 'value' => $f3->get('FAVICON'), + 'check' => true, + 'tooltip' => 'Folder for Favicons.', + 'chmod' => Util::filesystemInfo($f3->get('FAVICON'))['chmod'] + ], + 'HISTORY' => [ + 'label' => 'HISTORY [optional]', + 'value' => Config::getPathfinderData('history.log'), + 'check' => true, + 'tooltip' => 'Folder for log history files. (e.g. change logs for maps).', + 'chmod' => Util::filesystemInfo(Config::getPathfinderData('history.log'))['chmod'] + ], + 'CONFIG' => [ + 'label' => 'CONFIG PATH [optional]', + 'value' => implode(' ', (array)$f3->get('CONF')), + 'check' => true, + 'tooltip' => 'Folder for custom *.ini files. (e.g. when overwriting of default values in app/*.ini)' + ] + ]; + } + + /** + * check all required backend requirements + * (Fat Free Framework) + * @param \Base $f3 + * @return array + */ + protected function checkRequirements(\Base $f3) : array { + + $serverData = self::getServerData(0); + + $checkRequirements = [ + 'serverType' => [ + 'label' => 'Server type', + 'version' => $serverData->type, + 'check' => true + ], + 'serverVersion' => [ + 'label' => 'Server version', + 'required' => $serverData->requiredVersion, + 'version' => $serverData->version, + 'check' => version_compare( $serverData->version, $serverData->requiredVersion, '>='), + 'tooltip' => 'If not specified, please check your \'ServerTokens\' server config. (not critical)' + ], + 'phpInterface' => [ + 'label' => 'PHP interface type', + 'version' => $serverData->phpInterfaceType, + 'check' => empty($serverData->phpInterfaceType) ? false : true + ], + 'php' => [ + 'label' => 'PHP', + 'required' => number_format((float)$f3->get('REQUIREMENTS.PHP.VERSION'), 1, '.', ''), + 'version' => phpversion(), + 'check' => version_compare( phpversion(), $f3->get('REQUIREMENTS.PHP.VERSION'), '>=') + ], + 'php_bit' => [ + 'label' => 'php_int_size', + 'required' => ($f3->get('REQUIREMENTS.PHP.PHP_INT_SIZE') * 8 ) . '-bit', + 'version' => (PHP_INT_SIZE * 8) . '-bit', + 'check' => $f3->get('REQUIREMENTS.PHP.PHP_INT_SIZE') == PHP_INT_SIZE + ], + [ + 'label' => 'PHP extensions' + ], + 'pcre' => [ + 'label' => 'PCRE', + 'required' => $f3->get('REQUIREMENTS.PHP.PCRE_VERSION'), + 'version' => strstr(PCRE_VERSION, ' ', true), + 'check' => version_compare( strstr(PCRE_VERSION, ' ', true), $f3->get('REQUIREMENTS.PHP.PCRE_VERSION'), '>=') + ], + 'ext_pdo' => [ + 'label' => 'PDO', + 'required' => 'installed', + 'version' => extension_loaded('pdo') ? 'installed' : 'missing', + 'check' => extension_loaded('pdo') + ], + 'ext_pdoMysql' => [ + 'label' => 'PDO_MYSQL', + 'required' => 'installed', + 'version' => extension_loaded('pdo_mysql') ? 'installed' : 'missing', + 'check' => extension_loaded('pdo_mysql') + ], + 'ext_openssl' => [ + 'label' => 'OpenSSL', + 'required' => 'installed', + 'version' => extension_loaded('openssl') ? 'installed' : 'missing', + 'check' => extension_loaded('openssl') + ], + 'ext_xml' => [ + 'label' => 'XML', + 'required' => 'installed', + 'version' => extension_loaded('xml') ? 'installed' : 'missing', + 'check' => extension_loaded('xml') + ], + 'ext_gd' => [ + 'label' => 'GD Library (for Image plugin)', + 'required' => 'installed', + 'version' => (extension_loaded('gd') && function_exists('gd_info')) ? 'installed' : 'missing', + 'check' => (extension_loaded('gd') && function_exists('gd_info')) + ], + 'ext_curl' => [ + 'label' => 'cURL (for Web plugin)', + 'required' => 'installed', + 'version' => (extension_loaded('curl') && function_exists('curl_version')) ? 'installed' : 'missing', + 'check' => (extension_loaded('curl') && function_exists('curl_version')) + ], + 'ext_redis' => [ + 'label' => 'Redis [optional]', + 'required' => $f3->get('REQUIREMENTS.PHP.REDIS'), + 'version' => extension_loaded('redis') ? phpversion('redis') : 'missing', + 'check' => version_compare( phpversion('redis'), $f3->get('REQUIREMENTS.PHP.REDIS'), '>='), + 'tooltip' => 'Redis can replace the default file-caching mechanic. It is much faster!' + ], + [ + 'label' => 'LibEvent library [optional]' + ], + 'ext_event' => [ + 'label' => 'Event extension', + 'required' => $f3->get('REQUIREMENTS.PHP.EVENT'), + 'version' => extension_loaded('event') ? phpversion('event') : 'missing', + 'check' => version_compare( phpversion('event'), $f3->get('REQUIREMENTS.PHP.EVENT'), '>='), + 'tooltip' => 'LibEvent PHP extension. Optional performance boost for WebSocket configuration.' + ] + ]; + + if($serverData->type != 'nginx'){ + // default msg if module status not available + $modNotFoundMsg = 'Module status can not be identified. ' + . 'This can happen if PHP runs as \'FastCGI\'. Please check manual! '; + + // mod_rewrite check -------------------------------------------------------------------------------------- + $modRewriteCheck = false; + $modRewriteVersion = 'disabled'; + $modRewriteTooltip = false; + if(function_exists('apache_get_modules')){ + if(in_array('mod_rewrite',apache_get_modules())){ + $modRewriteCheck = true; + $modRewriteVersion = 'enabled'; + } + }else{ + // e.g. Nginx server + $modRewriteVersion = 'unknown'; + $modRewriteTooltip = $modNotFoundMsg; + } + + $checkRequirements['mod_rewrite'] = [ + 'label' => 'mod_rewrite', + 'required' => 'enabled', + 'version' => $modRewriteVersion, + 'check' => $modRewriteCheck, + 'tooltip' => $modRewriteTooltip + ]; + + // mod_headers check -------------------------------------------------------------------------------------- + $modHeadersCheck = false; + $modHeadersVersion = 'disabled'; + $modHeadersTooltip = false; + if(function_exists('apache_get_modules')){ + if(in_array('mod_headers',apache_get_modules())){ + $modHeadersCheck = true; + $modHeadersVersion = 'enabled'; + } + }else{ + // e.g. Nginx server + $modHeadersVersion = 'unknown'; + $modHeadersTooltip = $modNotFoundMsg; + } + + $checkRequirements['mod_headers'] = [ + 'label' => 'mod_headers', + 'required' => 'enabled', + 'version' => $modHeadersVersion, + 'check' => $modHeadersCheck, + 'tooltip' => $modHeadersTooltip + ]; + } + + return $checkRequirements; + } + + /** + * check PHP config (php.ini) + * @param \Base $f3 + * @return array + */ + protected function checkPHPConfig(\Base $f3): array { + $memoryLimit = (int)ini_get('memory_limit'); + $maxInputVars = (int)ini_get('max_input_vars'); + $maxExecutionTime = (int)ini_get('max_execution_time'); // 0 == infinite + $htmlErrors = (int)ini_get('html_errors'); + + return [ + 'exec' => [ + 'label' => 'exec()', + 'required' => $f3->get('REQUIREMENTS.PHP.EXEC'), + 'version' => function_exists('exec'), + 'check' => function_exists('exec') == $f3->get('REQUIREMENTS.PHP.EXEC'), + 'tooltip' => 'exec() funktion. Check "disable_functions" in php.ini' + ], + 'memoryLimit' => [ + 'label' => 'memory_limit', + 'required' => $f3->get('REQUIREMENTS.PHP.MEMORY_LIMIT'), + 'version' => $memoryLimit, + 'check' => $memoryLimit >= $f3->get('REQUIREMENTS.PHP.MEMORY_LIMIT'), + 'tooltip' => 'PHP default = 64MB.' + ], + 'maxInputVars' => [ + 'label' => 'max_input_vars', + 'required' => $f3->get('REQUIREMENTS.PHP.MAX_INPUT_VARS'), + 'version' => $maxInputVars, + 'check' => $maxInputVars >= $f3->get('REQUIREMENTS.PHP.MAX_INPUT_VARS'), + 'tooltip' => 'PHP default = 1000. Increase it in order to import larger maps.' + ], + 'maxExecutionTime' => [ + 'label' => 'max_execution_time', + 'required' => $f3->get('REQUIREMENTS.PHP.MAX_EXECUTION_TIME'), + 'version' => $maxExecutionTime, + 'check' => !$maxExecutionTime || $maxExecutionTime >= $f3->get('REQUIREMENTS.PHP.MAX_EXECUTION_TIME'), + 'tooltip' => 'PHP default = 30. Max execution time for PHP scripts.' + ], + 'htmlErrors' => [ + 'label' => 'html_errors', + 'required' => $f3->get('REQUIREMENTS.PHP.HTML_ERRORS'), + 'version' => $htmlErrors, + 'check' => (bool)$htmlErrors == (bool)$f3->get('REQUIREMENTS.PHP.HTML_ERRORS'), + 'tooltip' => 'Formatted HTML StackTrace on error.' + ], + [ + 'label' => 'Session' + ], + 'sessionSaveHandler' => [ + 'label' => 'save_handler', + 'version' => ini_get('session.save_handler'), + 'check' => true, + 'tooltip' => 'PHP Session save handler (Redis is preferred).' + ], + 'sessionSavePath' => [ + 'label' => 'session.save_path', + 'version' => ini_get('session.save_path'), + 'check' => true, + 'tooltip' => 'PHP Session save path (Redis is preferred).' + ], + 'sessionName' => [ + 'label' => 'session.name', + 'version' => ini_get('session.name'), + 'check' => true, + 'tooltip' => 'PHP Session name.' + ] + ]; + } + + /** + * check Redis (cache) config + * -> only visible if Redis is used as Cache backend + * @param \Base $f3 + * @return array + */ + protected function checkRedisInformation(\Base $f3): array { + $redisConfig = []; + + if( + extension_loaded('redis') && + class_exists('\Redis') + ){ + // collection of DSN specific $conf array (host, port, db,..) + $dsnData = []; + + /** + * @param int $dbNum + * @param string $tag + * @return string + */ + $getDbLabel = function(int $dbNum, string $tag) : string { + return ' db(' . $dbNum . ') : ' . $tag; + }; + + /** + * get client information for a Redis client + * @param \Redis $client + * @param array $conf + * @return array + */ + $getClientInfo = function(\Redis $client, array $conf) : array { + return [ + 'dsn' => [ + 'label' => 'DSN', + 'value' => $conf['host'] . ':' . $conf['port'] + ], + 'connected' => [ + 'label' => 'status', + 'value' => $client->isConnected() + ] + ]; + }; + + /** + * get status information for a Redis client + * @param \Redis $client + * @return array + */ + $getClientStats = function(\Redis $client) use ($f3) : array { + $redisStats = []; + + if($client->isConnected() && !$client->getLastError()){ + $redisServerInfo = (array)$client->info('SERVER'); + $redisClientsInfo = (array)$client->info('CLIENTS'); + $redisMemoryInfo = (array)$client->info('MEMORY'); + $redisStatsInfo = (array)$client->info('STATS'); + + $redisStats = [ + 'redisVersion' => [ + 'label' => 'redis_version', + 'required' => number_format((float)$f3->get('REQUIREMENTS.REDIS.VERSION'), 1, '.', ''), + 'version' => $redisServerInfo['redis_version'], + 'check' => version_compare( $redisServerInfo['redis_version'], $f3->get('REQUIREMENTS.REDIS.VERSION'), '>='), + 'tooltip' => 'Redis server version' + ], + 'maxMemory' => [ + 'label' => 'maxmemory', + 'required' => Number::instance()->bytesToString($f3->get('REQUIREMENTS.REDIS.MAX_MEMORY')), + 'version' => Number::instance()->bytesToString($redisMemoryInfo['maxmemory']), + 'check' => $redisMemoryInfo['maxmemory'] >= $f3->get('REQUIREMENTS.REDIS.MAX_MEMORY'), + 'tooltip' => 'Max memory limit for Redis' + ], + 'usedMemory' => [ + 'label' => 'used_memory', + 'version' => Number::instance()->bytesToString($redisMemoryInfo['used_memory']), + 'check' => $redisMemoryInfo['used_memory'] < $redisMemoryInfo['maxmemory'], + 'tooltip' => 'Current memory used by Redis' + ], + 'usedMemoryPeak' => [ + 'label' => 'used_memory_peak', + 'version' => Number::instance()->bytesToString($redisMemoryInfo['used_memory_peak']), + 'check' => $redisMemoryInfo['used_memory_peak'] <= $redisMemoryInfo['maxmemory'], + 'tooltip' => 'Peak memory used by Redis' + ], + 'maxmemoryPolicy' => [ + 'label' => 'maxmemory_policy', + 'required' => $f3->get('REQUIREMENTS.REDIS.MAXMEMORY_POLICY'), + 'version' => $redisMemoryInfo['maxmemory_policy'], + 'check' => $redisMemoryInfo['maxmemory_policy'] == $f3->get('REQUIREMENTS.REDIS.MAXMEMORY_POLICY'), + 'tooltip' => 'How Redis behaves if \'maxmemory\' limit reached' + ], + 'connectedClients' => [ + 'label' => 'connected_clients', + 'version' => $redisClientsInfo['connected_clients'], + 'check' => (bool)$redisClientsInfo['connected_clients'], + 'tooltip' => 'Number of client connections (excluding connections from replicas)' + ], + 'blockedClients' => [ + 'label' => 'blocked_clients', + 'version' => $redisClientsInfo['blocked_clients'], + 'check' => !(bool)$redisClientsInfo['blocked_clients'], + 'tooltip' => 'Number of clients pending on a blocking call (BLPOP, BRPOP, BRPOPLPUSH)' + ], + 'evictedKeys' => [ + 'label' => 'evicted_keys', + 'version' => $redisStatsInfo['evicted_keys'], + 'check' => !(bool)$redisStatsInfo['evicted_keys'], + 'tooltip' => 'Number of evicted keys due to maxmemory limit' + ], + [ + 'label' => 'Databases' + ] + ]; + } + + return $redisStats; + }; + + /** + * get database status for current selected db + * @param \Redis $client + * @param string $tag + * @return array + */ + $getDatabaseStatus = function(\Redis $client, string $tag) use ($getDbLabel) : array { + $redisDatabases = []; + if($client->isConnected() && !$client->getLastError()){ + $dbNum = $client->getDbNum(); + $dbSize = $client->dbSize(); + $redisDatabases = [ + 'db_' . $dbNum => [ + 'label' => $getDbLabel($dbNum, $tag), + 'version' => $dbSize . ' keys', + 'check' => $dbSize > 0, + 'tooltip' => 'Keys in db(' . $dbNum . ')', + 'task' => [ + [ + 'action' => http_build_query([ + 'action' => 'flushRedisDb', + 'host' => $client->getHost(), + 'port' => $client->getPort(), + 'db' => $dbNum + ]) . '#pf-setup-cache', + 'label' => 'Flush', + 'icon' => 'fa-trash', + 'btn' => 'btn-danger' . (($dbSize > 0) ? '' : ' disabled') + ] + ] + ] + ]; + } + + return $redisDatabases; + }; + + /** + * build (modify) $redisConfig with DNS $conf data + * @param array $conf + */ + $buildRedisConfig = function(array $conf) use (&$redisConfig, $getDbLabel, $getClientInfo, $getClientStats, $getDatabaseStatus){ + if($conf['type'] == 'redis'){ + // is Redis -> group all DNS by host:port + $uid = $conf['host'] . ':' . $conf['port']; + + $client = new \Redis(); + try{ + $client->pconnect($conf['host'], $conf['port'], 0.3); + if(!empty($conf['auth'])){ + $client->auth($conf['auth']); + } + + if(isset($conf['db'])) { + $client->select($conf['db']); + } + + $conf['db'] = $client->getDbNum(); + }catch(\RedisException $e){ + // connection failed, getLastError() is called further down + } + + if(!array_key_exists($uid, $redisConfig)){ + $redisConfig[$uid] = $getClientInfo($client, $conf); + $redisConfig[$uid]['status'] = $getClientStats($client) + $getDatabaseStatus($client, $conf['tag']); + }elseif(!array_key_exists($uidDb = 'db_' . $conf['db'], $redisConfig[$uid]['status'])){ + $redisConfig[$uid]['status'] += $getDatabaseStatus($client, $conf['tag']); + }else{ + $redisConfig[$uid]['status'][$uidDb]['label'] .= '; ' . $conf['tag']; + } + + if($error = $client->getLastError()){ + $redisConfig[$uid]['errors'][] = [ + 'label' => $getDbLabel((int)$conf['db'], $conf['tag']), + 'error' => $error + ]; + } + + $client->close(); + } + }; + + // potential Redis caches --------------------------------------------------------------------------------- + $redisCaches = [ + 'CACHE' => $f3->get('CACHE'), + 'API_CACHE' => $f3->get('API_CACHE') + ]; + + foreach($redisCaches as $tag => $dsn){ + if(Config::parseDSN($dsn, $conf)){ + $conf['tag'] = $tag; + $dsnData[] = $conf; + } + } + + // if Session handler is also Redis -> add this as well --------------------------------------------------- + // -> the DSN format is not the same, convert URL format into DSN + if( + strtolower(session_module_name()) == 'redis' && + ($parts = parse_url(strtolower(session_save_path()))) + ){ + // parse URL parameters + parse_str((string)$parts['query'], $params); + + $conf = [ + 'type' => 'redis', + 'host' => $parts['host'], + 'port' => $parts['port'], + 'db' => !empty($params['database']) ? (int)$params['database'] : 0, + 'auth' => !empty($params['auth']) ? $params['auth'] : null, + 'tag' => 'SESSION' + ]; + $dsnData[] = $conf; + } + + // sort all $dsnData by 'db' number ----------------------------------------------------------------------- + usort($dsnData, function($a, $b){ + return $a['db'] <=> $b['db']; + }); + + foreach($dsnData as $conf){ + $buildRedisConfig($conf); + } + } + + return $redisConfig; + } + + /** + * check system environment vars + * -> mostly relevant for development/build/deployment + * @param \Base $f3 + * @return array + */ + protected function checkSystemConfig(\Base $f3): array { + $systemConf = []; + if(function_exists('exec')){ + $gitOut = $composerOut = $nodeOut = $npmOut = []; + $gitStatus = $composerStatus = $nodeStatus = $npmStatus = 1; + + exec('git --version', $gitOut, $gitStatus); + exec('composer -V', $composerOut, $composerStatus); + exec('node -v', $nodeOut, $nodeStatus); + exec('npm -v', $npmOut, $npmStatus); + + $normalizeVersion = function($version): string { + return preg_replace("/[^0-9\.\s]/", '', (string)$version); + }; + + $systemConf = [ + 'git' => [ + 'label' => 'Git', + 'version' => $gitOut[0] ? 'installed' : 'missing', + 'check' => $gitStatus == 0, + 'tooltip' => 'Git # git --version : ' . $gitOut[0] + ], + 'composer' => [ + 'label' => 'Composer', + 'version' => $composerOut[0] ? 'installed' : 'missing', + 'check' => $composerStatus == 0, + 'tooltip' => 'Composer # composer -V : ' . $composerOut[0] + ], + 'node' => [ + 'label' => 'NodeJs', + 'required' => number_format((float)$f3->get('REQUIREMENTS.PATH.NODE'), 1, '.', ''), + 'version' => $normalizeVersion($nodeOut[0]) ?: 'missing', + 'check' => version_compare( $normalizeVersion($nodeOut[0]), number_format((float)$f3->get('REQUIREMENTS.PATH.NODE'), 1, '.', ''), '>='), + 'tooltip' => 'NodeJs # node -v' + ], + 'npm' => [ + 'label' => 'npm', + 'required' => $f3->get('REQUIREMENTS.PATH.NPM'), + 'version' => $normalizeVersion($npmOut[0]) ?: 'missing', + 'check' => version_compare( $normalizeVersion($npmOut[0]), $f3->get('REQUIREMENTS.PATH.NPM'), '>='), + 'tooltip' => 'npm # npm -v' + ] + ]; + } + + return $systemConf; + } + + /** + * get default map config + * @param \Base $f3 + * @return array + */ + protected function getMapsDefaultConfig(\Base $f3): array { + $matrix = \Matrix::instance(); + $mapsDefaultConfig = (array)Config::getMapsDefaultConfig(); + $matrix->transpose($mapsDefaultConfig); + + $mapConfig = ['mapTypes' => array_keys(reset($mapsDefaultConfig))]; + + foreach($mapsDefaultConfig as $option => $defaultConfig){ + $tooltip = ''; + switch($option){ + case 'lifetime': + $label = 'Map lifetime (days)'; + $tooltip = 'Unchanged/inactive maps get auto deleted afterwards (cronjob).'; + break; + case 'max_count': + $label = 'Max. maps count/user'; + break; + case 'max_shared': + $label = 'Map share limit/map'; + $tooltip = 'E.g. A Corp map can be shared with X other corps.'; + break; + case 'max_systems': + $label = 'Max. systems count/map'; + break; + case 'log_activity_enabled': + $label = ' Activity statistics'; + $tooltip = 'If "enabled", map admins can enable user statistics for a map.'; + break; + case 'log_history_enabled': + $label = ' History log files'; + $tooltip = 'If "enabled", map admins can pipe map logs to file. (one file per map)'; + break; + case 'send_history_slack_enabled': + $label = ' History log Slack'; + $tooltip = 'If "enabled", map admins can set a Slack channel were map logs get piped to.'; + break; + case 'send_rally_slack_enabled': + $label = ' Rally point poke Slack'; + $tooltip = 'If "enabled", map admins can set a Slack channel for rally point pokes.'; + break; + case 'send_history_discord_enabled': + $label = ' History log Discord'; + $tooltip = 'If "enabled", map admins can set a Discord channel were map logs get piped to.'; + break; + case 'send_rally_discord_enabled': + $label = ' Rally point poke Discord'; + $tooltip = 'If "enabled", map admins can set a Discord channel for rally point pokes.'; + break; + case 'send_rally_mail_enabled': + $label = ' Rally point poke Email'; + $tooltip = 'If "enabled", rally point pokes can be send by Email (SMTP config + recipient address required).'; + break; + default: + $label = 'unknown'; + } + + $mapsDefaultConfig[$option] = [ + 'label' => $label, + 'tooltip' => $tooltip, + 'data' => $defaultConfig + ]; + } + + $mapConfig['mapConfig'] = $mapsDefaultConfig; + + return $mapConfig; + } + + /** + * get database connection information + * @param \Base $f3 + * @param bool|false $exec + * @return array + */ + protected function checkDatabase(\Base $f3, $exec = false){ + + foreach($this->databases as $dbAlias => $dbData){ + + $dbLabel = ''; + $dbConfig = []; + + // DB connection status + $dbConnected = false; + // DB initialized as persistent connection + $dbPersistent = false; + // DB type (e.g. MySql,..) + $dbDriver = 'unknown'; + // enable database ::create() function on UI + $dbCreate = false; + // enable database ::setup() function on UI + $dbSetupEnable = false; + // check if everything is OK (connection, tables, columns, indexes,..) + $dbStatusCheckCount = 0; + // db queries for column fixes (types, indexes, unique) + $dbColumnQueries = []; + // tables that should exist in this DB + $requiredTables = []; + // get DB config + $dbConfigValues = Config::getDatabaseConfig($f3, $dbAlias); + // collection for errors + $dbErrors = []; + /** + * @var $db Sql + */ + $db = $f3->DB->getDB($dbAlias); + + // check config that does NOT require a valid DB connection + switch($dbAlias){ + case 'PF': $dbLabel = 'Pathfinder'; break; + case 'UNIVERSE': $dbLabel = 'EVE-Online universe'; break; + } + + $dbName = $dbConfigValues['NAME']; + $dbUser = $dbConfigValues['USER']; + $dbAlias = $dbConfigValues['ALIAS']; + + if($db){ + switch($dbAlias){ + case 'PF': + case 'UNIVERSE': + // enable (table) setup for this DB + $dbSetupEnable = true; + + // get table data from model + foreach($dbData['models'] as $model){ + $tableConfig = call_user_func(Config::withNamespace($model) . '::resolveConfiguration'); + $requiredTables[$tableConfig['table']] = [ + 'model' => $model, + 'name' => $tableConfig['table'], + 'fieldConf' => $tableConfig['fieldConf'], + 'exists' => false, + 'empty' => true, + 'requiredCharset' => $tableConfig['charset'], + 'requiredCollation' => $tableConfig['charset'] . '_unicode_ci', + 'foreignKeys' => [] + ]; + } + break; + } + + // db connect was successful + $dbConnected = true; + $dbPersistent = $db->pdo()->getAttribute(\PDO::ATTR_PERSISTENT); + $dbDriver = $db->driver(); + $dbConfig = $this->checkDBConfig($f3, $db); + + // get tables + $schema = new Schema($db); + $currentTables = $schema->getTables(); + + // check each table for changes + foreach($requiredTables as $requiredTableName => $data){ + $tableCharset = null; + $tableCollation = null; + $tableExists = false; + $tableRows = 0; + // Check if table status is OK (no errors/warnings,..) + $tableStatusCheckCount = 0; + + $currentColumns = []; + if(in_array($requiredTableName, $currentTables)){ + // Table exists + $tableExists = true; + // get existing table columns and column related constraints (if exists) + $tableModifierTemp = new Mysql\TableModifier($requiredTableName, $schema); + $currentColumns = $tableModifierTemp->getCols(true); + // get row count + $tableRows = $db->getRowCount($requiredTableName); + + $tableStatus = $db->getTableStatus($requiredTableName); + if( + !empty($tableStatus['Collation']) && + ($statusVal = strstr($tableStatus['Collation'], '_', true)) !== false + ){ + $tableCharset = $statusVal; + $tableCollation = $tableStatus['Collation']; + } + + // find deprecated columns that are no longer needed ------------------------------------------ + $deprecatedColumnNames = array_diff(array_keys($currentColumns), array_keys($data['fieldConf']), ['id']); + foreach($deprecatedColumnNames as $deprecatedColumnName){ + $requiredTables[$requiredTableName]['fieldConf'][$deprecatedColumnName]['deprecated'] = true; + $requiredTables[$requiredTableName]['fieldConf'][$deprecatedColumnName]['currentType'] = 'deprecated'; + //$requiredTables[$requiredTableName]['fieldConf'][$deprecatedColumnName]['statusCheck'] = false; + //$tableStatusCheckCount++; + + //$tableModifierTemp->dropColumn($deprecatedColumnName); + } + + //$buildStatus = $tableModifierTemp->build(false); + //$dbColumnQueries = array_merge($dbColumnQueries, (array)$buildStatus); + }else{ + // table missing + $dbStatusCheckCount++; + $tableStatusCheckCount++; + } + + foreach((array)$data['fieldConf'] as $columnName => $fieldConf){ + // if 'nullable' key not set in $fieldConf, Column was created with 'nullable' = true (Cortex default) + $fieldConf['nullable'] = isset($fieldConf['nullable']) ? (bool)$fieldConf['nullable'] : true; + + $columnStatusCheck = true; + $foreignKeyStatusCheck = true; + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['requiredType'] = $fieldConf['type']; + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['requiredNullable'] = ($fieldConf['nullable']) ? '1' : '0'; + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['requiredIndex'] = ($fieldConf['index']) ? '1' : '0'; + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['requiredUnique'] = ($fieldConf['unique']) ? '1' : '0'; + + if(array_key_exists($columnName, $currentColumns)){ + // column exists + + // get tableModifier -> possible column update + $tableModifier = new Mysql\TableModifier($requiredTableName, $schema); + + // get new column and copy Schema from existing column + $col = new Mysql\Column($columnName, $tableModifier); + $col->copyfrom($currentColumns[$columnName]); + + $currentColType = $currentColumns[$columnName]['type']; + $currentNullable = $currentColumns[$columnName]['nullable']; + $hasNullable = $currentNullable ? '1' : '0'; + $currentColIndexData = call_user_func(Config::withNamespace($data['model']) . '::indexExists', [$columnName]); + $currentColIndex = is_array($currentColIndexData); + $hasIndex = ($currentColIndex) ? '1' : '0'; + $hasUnique = ($currentColIndexData['unique']) ? '1' : '0'; + $changedType = false; + $changedNullable = false; + $changedUnique = false; + $changedIndex = false; + $addConstraints = []; + + // set (new) column information ----------------------------------------------------------- + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['exists'] = true; + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentType'] = $currentColType; + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentNullable'] = $hasNullable; + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentIndex'] = $hasIndex; + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentUnique'] = $hasUnique; + + // check constraint ----------------------------------------------------------------------- + if(isset($fieldConf['constraint'])){ + // add or update constraints + foreach((array)$fieldConf['constraint'] as $constraintData){ + $constraint = $col->newConstraint($constraintData); + + $foreignKeyExists = $col->constraintExists($constraint); + + // constraint information -> show in template + $requiredTables[$requiredTableName]['foreignKeys'][] = [ + 'exists' => $foreignKeyExists, + 'keyName' => $constraint->getConstraintName() + ]; + + if($foreignKeyExists){ + // drop constraint and re-add again at the and, in case something has changed + $col->dropConstraint($constraint); + }else{ + $tableStatusCheckCount++; + $foreignKeyStatusCheck = false; + } + + $addConstraints[] = $constraint; + } + } + + // check type changed --------------------------------------------------------------------- + if( + $fieldConf['type'] !== 'JSON' && + !$schema->isCompatible($fieldConf['type'], $currentColType) + ){ + // column type has changed + $changedType = true; + $columnStatusCheck = false; + $tableStatusCheckCount++; + } + + // check if column nullable changed ------------------------------------------------------- + if( $currentNullable != $fieldConf['nullable']){ + $changedNullable = true; + $columnStatusCheck = false; + $tableStatusCheckCount++; + } + + // check if column index changed ---------------------------------------------------------- + $indexUpdate = false; + $indexKey = (bool)$hasIndex; + $indexUnique = (bool)$hasUnique; + + if($currentColIndex != $fieldConf['index']){ + $changedIndex = true; + $columnStatusCheck = false; + $tableStatusCheckCount++; + + $indexUpdate = true; + $indexKey = (bool)$fieldConf['index']; + } + + // check if column unique changed --------------------------------------------------------- + if($currentColIndexData['unique'] != $fieldConf['unique']){ + $changedUnique = true; + $columnStatusCheck = false; + $tableStatusCheckCount++; + + $indexUpdate = true; + $indexUnique = (bool)$fieldConf['unique']; + } + + // build table with changed columns ------------------------------------------------------- + if(!$columnStatusCheck || !$foreignKeyStatusCheck){ + + if(!$columnStatusCheck ){ + // IMPORTANT: setType is always required! Even if type has not changed + $col->type($fieldConf['type']); + + // update "nullable" + if($changedNullable){ + $col->nullable($fieldConf['nullable']); + } + + // update/change/delete index/unique keys + if($indexUpdate){ + if($hasIndex){ + $tableModifier->dropIndex($columnName); + } + + if($indexKey){ + $tableModifier->addIndex($columnName, $indexUnique); + } + } + $tableModifier->updateColumn($columnName, $col); + } + + // (re-)add constraints !after! index update is done + // otherwise index update will fail if there are existing constraints + foreach($addConstraints as $constraint){ + $col->addConstraint($constraint); + } + + $buildStatus = $tableModifier->build($exec); + + if( + is_array($buildStatus) || + is_string($buildStatus) + ){ + // query strings for change available + $dbColumnQueries = array_merge($dbColumnQueries, (array)$buildStatus); + } + } + + // set (new) column information ----------------------------------------------------------- + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['changedType'] = $changedType; + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['changedNullable'] = $changedNullable; + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['changedUnique'] = $changedUnique; + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['changedIndex'] = $changedIndex; + + }elseif( + !isset($fieldConf['has-manny']) && + isset($fieldConf['type']) + ){ + // column not exists but it is required! + // columns that do not match this criteria ("mas-manny") are "virtual" fields + // and can be ignored + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentType'] = ''; + $columnStatusCheck = false; + $tableStatusCheckCount++; + } + $requiredTables[$requiredTableName]['fieldConf'][$columnName]['statusCheck'] = $columnStatusCheck; + } + + $dbStatusCheckCount += $tableStatusCheckCount; + $requiredTables[$requiredTableName]['currentCharset'] = $tableCharset; + $requiredTables[$requiredTableName]['currentCollation'] = $tableCollation; + $requiredTables[$requiredTableName]['rows'] = $tableRows; + $requiredTables[$requiredTableName]['exists'] = $tableExists; + $requiredTables[$requiredTableName]['statusCheckCount'] = $tableStatusCheckCount; + } + + }else{ + // DB connection failed + $dbStatusCheckCount++; + + foreach($f3->DB->getErrors($dbAlias, 10) as $dbException){ + $dbErrors[] = $dbException->getMessage(); + } + + // try to connect without! DB (-> offer option to create them) + // do not log errors (silent) + $f3->DB->setSilent(true); + $dbServer = $f3->DB->connectToServer($dbAlias); + $f3->DB->setSilent(false); + if(!is_null($dbServer)){ + // connection succeeded + $dbCreate = true; + $dbDriver = $dbServer->driver(); + } + } + + if($dbStatusCheckCount !== 0){ + $this->databaseHasError = true; + } + + // sort tables for better readability + ksort($requiredTables); + + $this->databases[$dbAlias]['info'] = [ + 'label' => $dbLabel, + 'host' => $dbConfigValues['SOCKET'] ? : $dbConfigValues['HOST'], + 'port' => $dbConfigValues['PORT'] && !$dbConfigValues['SOCKET'] ? $dbConfigValues['PORT'] : '', + 'driver' => $dbDriver, + 'name' => $dbName, + 'user' => $dbUser, + 'pass' => Util::obscureString((string)$dbConfigValues['PASS'], 8), + 'dbConfig' => $dbConfig, + 'dbCreate' => $dbCreate, + 'setupEnable' => $dbSetupEnable, + 'connected' => $dbConnected, + 'persistent' => $dbPersistent, + 'statusCheckCount' => $dbStatusCheckCount, + 'columnQueries' => $dbColumnQueries, + 'tableData' => $requiredTables, + 'errors' => $dbErrors + ]; + } + + if($exec){ + $f3->reroute('@setup'); + } + + return $this->databases; + } + + /** + * check MySQL params + * @param \Base $f3 + * @param Sql $db + * @return array + */ + protected function checkDBConfig(\Base $f3, Sql $db) : array { + $checkAll = true; + // some db like "Maria DB" have some strange version strings.... + $dbVersionString = $db->version(); + $dbVersionParts = explode('-', $dbVersionString); + $dbVersion = 'unknown'; + foreach($dbVersionParts as $dbVersionPart){ + // check if this is a valid version number + // hint: MariaDB´s version is NOT always the last valid version number + if( version_compare( $dbVersionPart, '1', '>' ) > 0 ){ + $dbVersion = $dbVersionPart; + } + } + + $dbConfig = [ + 'data' => [ + 'version' => [ + 'label' => 'DB version', + 'required' => $f3->get('REQUIREMENTS.MYSQL.VERSION'), + 'version' => $dbVersion, + 'check' => version_compare($dbVersion, $f3->get('REQUIREMENTS.MYSQL.VERSION'), '>=' ) ? : $checkAll = false + ] + ] + ]; + + $mySQLConfig = array_change_key_case((array)$f3->get('REQUIREMENTS.MYSQL.VARS')); + $mySQLConfigKeys = array_keys($mySQLConfig); + + $results = $db->exec("SHOW VARIABLES WHERE Variable_Name IN ('" . implode("','", $mySQLConfigKeys) . "')"); + + $getValue = function(string $param) use ($results) : string { + $match = array_filter($results, function($k) use ($param) : bool { + return strtolower($k['Variable_name']) == $param; + }); + return !empty($match) ? end(reset($match)) : 'unknown'; + }; + + $checkValue = function($requiredValue, $value) : bool { + $check = true; + if(!empty($requiredValue)){ + if(is_int($requiredValue)){ + $check = $requiredValue <= $value; + }else{ + $check = $requiredValue == $value; + } + } + return $check; + }; + + foreach($mySQLConfig as $param => $requiredValue){ + $value = $getValue($param); + $dbConfig['data'][] = [ + 'label' => $param, + 'required' => $requiredValue, + 'version' => $value, + 'check' => $checkValue($requiredValue, $value) ? : $checkAll = false + ]; + } + + $dbConfig['meta'] = [ + 'check' => $checkAll + ]; + + return $dbConfig; + } + + /** + * try to create a fresh database + * @param \Base $f3 + * @param string $dbAlias + */ + protected function createDB(\Base $f3, string $dbAlias){ + // check for valid key + if(!empty($this->databases[$dbAlias])){ + // disable logging (we expect the DB connect to fail -> no db created) + $f3->DB->setSilent(true); + // try to connect + $db = $f3->DB->getDB($dbAlias); + // enable logging + $f3->DB->setSilent(false, true); + if(is_null($db)){ + // try create new db + $db = $f3->DB->createDB($dbAlias); + if(is_null($db)){ + foreach($f3->DB->getErrors($dbAlias, 5) as $error){ + // ... no further error handling here -> check log files + //$error->getMessage() + } + } + } + } + } + + /** + * init the complete database + * - create tables + * - create indexes + * - set default static values + * @param \Base $f3 + * @param string $dbAlias + * @return array + */ + protected function bootstrapDB(\Base $f3, string $dbAlias) : array { + $checkTables = []; + if($db = $f3->DB->getDB($dbAlias)){ + // set some default config for this database + $requiredVars = Config::getRequiredDbVars($f3, $db->driver()); + $db->prepareDatabase($requiredVars['CHARACTER_SET_DATABASE'], $requiredVars['COLLATION_DATABASE']); + + // setup tables + foreach($this->databases[$dbAlias]['models'] as $modelClass){ + $checkTables[] = call_user_func(Config::withNamespace($modelClass) . '::setup', $db); + } + } + return $checkTables; + } + + /** + * get Socket information (TCP (internal)), (WebSocket (clients)) + * @param \Base $f3 + * @return array + * @throws \Exception + */ + protected function getSocketInformation(\Base $f3) : array { + $ttl = 0.6; + $task = 'healthCheck'; + $healthCheckToken = microtime(true); + + $statusTcp = [ + 'type' => 'danger', + 'label' => 'INIT CONNECTION…', + 'class' => 'txt-color-danger' + ]; + + $statusWeb = [ + 'type' => 'danger', + 'label' => 'INIT CONNECTION…', + 'class' => 'txt-color-danger' + ]; + + $statsTcp = false; + $statsWeb = false; + + $setStats = function(array $stats) use (&$statsTcp, &$statsWeb) { + if(!empty($stats['tcpSocket'])){ + $statsTcp = $stats['tcpSocket']; + } + if(!empty($stats['webSocket'])){ + $statsWeb = $stats['webSocket']; + } + }; + + // ping TCP Socket with "healthCheck" task + $f3->webSocket(['timeout' => $ttl]) + ->write($task, $healthCheckToken) + ->then( + function($payload) use ($task, $healthCheckToken, &$statusTcp, $setStats) { + if( + $payload['task'] == $task && + $payload['load'] == $healthCheckToken + ){ + $statusTcp['type'] = 'success'; + $statusTcp['label'] = 'PING OK'; + $statusTcp['class'] = 'txt-color-success'; + }else{ + $statusTcp['type'] = 'warning'; + $statusTcp['label'] = is_string($payload['load']) ? $payload['load'] : 'INVALID RESPONSE'; + $statusTcp['class'] = 'txt-color-warning'; + } + + // statistics (e.g. current connection count) + $setStats((array)$payload['stats']); + }, + function($payload) use (&$statusTcp, $setStats) { + $statusTcp['label'] = $payload['load']; + + // statistics (e.g. current connection count) + $setStats((array)$payload['stats']); + }); + + return [ + 'tcpSocket' => [ + 'label' => 'TCP-Socket (intern)', + 'icon' => 'fa-exchange-alt', + 'status' => $statusTcp, + 'stats' => $statsTcp, + 'data' => [ + [ + 'label' => 'HOST', + 'value' => Config::getEnvironmentData('SOCKET_HOST') ? : '[missing]', + 'check' => !empty( Config::getEnvironmentData('SOCKET_HOST') ) + ],[ + 'label' => 'PORT', + 'value' => Config::getEnvironmentData('SOCKET_PORT') ? : '[missing]', + 'check' => !empty( Config::getEnvironmentData('SOCKET_PORT') ) + ],[ + 'label' => 'URI', + 'value' => Config::getSocketUri() ? : '[missing]', + 'check' => !empty( Config::getSocketUri() ) + ],[ + 'label' => 'timeout (seconds)', + 'value' => $ttl, + 'check' => !empty( $ttl ) + ],[ + 'label' => 'uptime', + 'value' => Config::formatTimeInterval($statsTcp['startup'] ? : 0), + 'check' => $statsTcp['startup'] > 0 + ] + ], + 'token' => $healthCheckToken + ], + 'webSocket' => [ + 'label' => 'Web-Socket', + 'icon' => 'fa-random', + 'status' => $statusWeb, + 'stats' => $statsWeb, + 'data' => [ + [ + 'label' => 'URI', + 'value' => '', + 'check' => null // undefined + ] + ] + ] + ]; + } + + /** + * get cronjob config + * @param \Base $f3 + * @return array + */ + protected function getCronConfig(\Base $f3) : array { + $cron = Cron::instance(); + + $cronConf = [ + 'log' => [ + 'label' => 'LOG', + 'required' => $f3->get('REQUIREMENTS.CRON.LOG'), + 'version' => $f3->get('CRON.log'), + 'check' => $f3->get('CRON.log') == $f3->get('REQUIREMENTS.CRON.LOG'), + 'tooltip' => 'Write default cron.log' + ], + 'cli' => [ + 'label' => 'CLI', + 'required' => $f3->get('REQUIREMENTS.CRON.CLI'), + 'version' => $f3->get('CRON.cli'), + 'check' => $f3->get('CRON.cli') == $f3->get('REQUIREMENTS.CRON.CLI'), + 'tooltip' => 'Jobs can be triggered by CLI. Must be set on Unix where "crontab -e" config is used' + ], + 'web' => [ + 'label' => 'WEB', + 'version' => (int)$f3->get('CRON.web'), + 'check' => true, + 'tooltip' => 'Jobs can be triggered by URL. Could be useful if jobs should be triggered by e.g. 3rd party app. Secure "/cron" url if active!' + ], + 'silent' => [ + 'label' => 'SILENT', + 'version' => (int)$f3->get('CRON.silent'), + 'check' => true, + 'tooltip' => 'Write job execution status to STDOUT if job completes' + ] + ]; + + return [ + 'checkCronConfig' => $cronConf, + 'settings' => $f3->constants($cron, 'DEFAULT_'), + 'jobs' => $cron->getJobsConfig() + ]; + } + + /** + * get indexed (cache) data information + * @param \Base $f3 + * @return array + * @throws \Exception + */ + protected function getIndexData(\Base $f3) : array { + // active DB and tables are required for obtain index data + if(!$this->databaseHasError){ + /** + * @var $categoryUniverseModel Universe\CategoryModel + */ + $categoryUniverseModel = Universe\AbstractUniverseModel::getNew('CategoryModel'); + $categoryUniverseModel->getById(Config::ESI_CATEGORY_STRUCTURE_ID, 0); + $groupsCountStructure = $categoryUniverseModel->getGroupsCount(false); + $typesCountStructure = $categoryUniverseModel->getTypesCount(false); + + $categoryUniverseModel->getById(Config::ESI_CATEGORY_SHIP_ID, 0); + $groupsCountShip = $categoryUniverseModel->getGroupsCount(false); + $typesCountShip = $categoryUniverseModel->getTypesCount(false); + + /** + * @var $groupUniverseModel Universe\GroupModel + */ + + $groupUniverseModel = Universe\AbstractUniverseModel::getNew('GroupModel'); + $groupUniverseModel->getById(Config::ESI_GROUP_WORMHOLE_ID, 0); + $wormholeCount = $groupUniverseModel->getTypesCount(false); + + /** + * @var $systemNeighbourModel Universe\SystemNeighbourModel + */ + $systemNeighbourModel = Universe\AbstractUniverseModel::getNew('SystemNeighbourModel'); + + /** + * @var $systemStaticModel Universe\SystemStaticModel + */ + $systemStaticModel = Universe\AbstractUniverseModel::getNew('SystemStaticModel'); + + if(empty($systemCountAll = count(($universeController = new UniverseController())->getSystemIds(true)))){ + // no systems found in 'universe' DB. Clear potential existing system cache + $universeController->clearSystemsIndex(); + } + + $sum = function(int $carry, int $value) : int { + return $carry + $value; + }; + + $indexInfo = [ + 'Wormholes' => [ + 'task' => [ + [ + 'action' => 'buildIndex', + 'label' => 'Import', + 'icon' => 'fa-sync', + 'btn' => 'btn-primary' + ] + ], + 'label' => 'Wormholes data', + 'countBuild' => $wormholeCount, + 'countAll' => count(Universe\GroupModel::getUniverseGroupTypes(Config::ESI_GROUP_WORMHOLE_ID)), + 'tooltip' => 'import all wormhole types (e.g. L031) from ESI. Runtime: ~25s' + ], + 'Structures' => [ + 'task' => [ + [ + 'action' => 'buildIndex', + 'label' => 'Import', + 'icon' => 'fa-sync', + 'btn' => 'btn-primary' + ] + ], + 'label' => 'Structures data', + 'countBuild' => $groupsCountStructure, + 'countAll' => count(Universe\CategoryModel::getUniverseCategoryGroups(Config::ESI_CATEGORY_STRUCTURE_ID)), + 'tooltip' => 'import all structure types (e.g. Citadels) from ESI. Runtime: ~15s', + 'subCount' => [ + 'countBuild' => $typesCountStructure, + 'countAll' => array_reduce(array_map('count', Universe\CategoryModel::getUniverseCategoryTypes(Config::ESI_CATEGORY_STRUCTURE_ID)), $sum, 0), + ] + ], + 'Ships' => [ + 'task' => [ + [ + 'action' => 'buildIndex', + 'label' => 'Import', + 'icon' => 'fa-sync', + 'btn' => 'btn-primary' + ] + ], + 'label' => 'Ships data', + 'countBuild' => $groupsCountShip, + 'countAll' => count(Universe\CategoryModel::getUniverseCategoryGroups(Config::ESI_CATEGORY_SHIP_ID)), + 'tooltip' => 'import all ships from ESI. Runtime: ~2min', + 'subCount' => [ + 'countBuild' => $typesCountShip, + 'countAll' => array_reduce(array_map('count', Universe\CategoryModel::getUniverseCategoryTypes(Config::ESI_CATEGORY_SHIP_ID)), $sum, 0), + ] + ], + 'SystemStatic' => [ + 'task' => [ + [ + 'action' => 'buildIndex', + 'label' => 'Import', + 'icon' => 'fa-sync', + 'btn' => 'btn-primary' + ] + ], + 'label' => 'Wormhole statics data', + 'countBuild' => $systemStaticModel->getRowCount(), + 'countAll' => 3772, + 'tooltip' => 'import all static wormholes for systems. Runtime: ~25s' + ], + [ + 'label' => 'Build search index', + 'icon' => 'fa-search', + 'tooltip' => 'Search indexes are build from static EVE universe data (e.g. systems, stargate connections,…). Re-build if underlying data was updated.' + ], + 'Systems' => [ + 'task' => [ + [ + 'action' => 'clearIndex', + 'label' => 'Clear', + 'icon' => 'fa-trash', + 'btn' => 'btn-danger' + ],[ + 'action' => 'buildIndex', + 'label' => 'Build', + 'icon' => 'fa-sync', + 'btn' => 'btn-primary' + ] + ], + 'label' => 'Systems data index', + 'countBuild' => count($universeController->getSystemsIndex()), + 'countAll' => $systemCountAll, + 'tooltip' => 'Build up a static search index over all systems, found on DB. Runtime: ~5min' + ], + 'SystemNeighbour' => [ + 'task' => [ + [ + 'action' => 'clearIndex', + 'label' => 'Clear', + 'icon' => 'fa-trash', + 'btn' => 'btn-danger' + ],[ + 'action' => 'buildIndex', + 'label' => 'Build', + 'icon' => 'fa-sync', + 'btn' => 'btn-primary' + ] + ], + 'label' => 'Systems neighbour index', + 'countBuild' => $systemNeighbourModel->getRowCount(), + 'countAll' => (int)$f3->get('REQUIREMENTS.DATA.NEIGHBOURS'), + 'tooltip' => 'Build up a static search index for route search. This is used as fallback in case ESI is down. Runtime: ~10s' + ] + ]; + }else{ + $indexInfo = [ + [ + 'label' => 'Fix database errors first!', + 'class' => 'txt-color-danger text-center' + ] + ]; + } + + return $indexInfo; + } + + /** + * import table data from existing dump file (e.g *.csv) + * @param string $modelClass + * @return bool + * @throws \Exception + */ + protected function importTable($modelClass){ + $this->getDB('PF'); + return Pathfinder\AbstractPathfinderModel::getNew($modelClass)->importData(); + } + + /** + * export table data + * @param string $modelClass + * @throws \Exception + */ + protected function exportTable($modelClass){ + $this->getDB('PF'); + Pathfinder\AbstractPathfinderModel::getNew($modelClass)->exportData(); + } + + /** + * get cache folder size + * @param \Base $f3 + * @return array + */ + protected function checkDirSize(\Base $f3) : array { + // limit shown cache size. Reduce page load on big cache. In Bytes + $maxBytes = 10 * 1024 * 1024; // 10MB + $dirTemp = (string)$f3->get('TEMP'); + $cacheDsn = (string)$f3->get('CACHE'); + Config::parseDSN($cacheDsn, $conf); + // if 'CACHE' is e.g. redis=... -> show default dir for cache + $dirCache = $conf['type'] == 'folder' ? $conf['folder'] : $dirTemp . 'cache/'; + + $dirAll = [ + 'TEMP' => [ + 'label' => 'Temp dir', + 'path' => $dirTemp + ], + 'CACHE' => [ + 'label' => 'Cache dir', + 'path' => $dirCache + ] + ]; + + $maxHitAll = false; + $bytesAll = 0; + + foreach($dirAll as $key => $dirData){ + $maxHit = false; + $bytes = 0; + $files = Search::getFilesByMTime($dirData['path']); + foreach($files as $filename => $file) { + $bytes += $file->getSize(); + if($bytes > $maxBytes){ + $maxHit = $maxHitAll = true; + break; + } + } + $bytesAll += $bytes; + + $dirAll[$key]['size'] = ($maxHit ? '>' : '') . Number::instance()->bytesToString($bytes); + $dirAll[$key]['task'] = [ + [ + 'action' => http_build_query([ + 'action' => 'clearFiles', + 'path' => $dirData['path'] + ]), + 'label' => 'Delete files', + 'icon' => 'fa-trash', + 'btn' => 'btn-danger' . (($bytes > 0) ? '' : ' disabled') + ] + ]; + } + + return [ + 'sizeAll' => ($maxHitAll ? '>' : '') . Number::instance()->bytesToString($bytesAll), + 'dirAll' => $dirAll + ]; + } + + /** + * clear directory + * @param string $path + */ + protected function clearFiles(string $path){ + $files = Search::getFilesByMTime($path); + foreach($files as $filename => $file){ + /** + * @var $file \SplFileInfo + */ + if($file->isFile()){ + if($file->isWritable()){ + unlink($file->getRealPath()); + } + } + } + } + + /** + * clear all key in a specific Redis database + * @param string $host + * @param int $port + * @param int $db + */ + protected function flushRedisDb(string $host, int $port, int $db = 0){ + $client = new \Redis(); + $client->pconnect($host, $port, 0.3); + $client->select($db); + $client->flushDB(); + $client->close(); + } + + /** + * clear all character authentication (Cookie) data + * @param \Base $f3 + * @throws \Exception + */ + protected function invalidateCookies(\Base $f3){ + $this->getDB('PF'); + $authenticationModel = Pathfinder\AbstractPathfinderModel::getNew('CharacterAuthenticationModel'); + $results = $authenticationModel->find(); + if($results){ + foreach($results as $result){ + $result->erase(); + } + } + } +} \ No newline at end of file diff --git a/app/Cron/AbstractCron.php b/app/Cron/AbstractCron.php new file mode 100644 index 000000000..2413a8661 --- /dev/null +++ b/app/Cron/AbstractCron.php @@ -0,0 +1,211 @@ + generic information data + */ + const LOG_TEXT_BASE = '%4s/%-4s %6s done, %5s total, %8s peak, %9s exec'; + + /** + * default max_execution_time for cronJobs + * -> should be less then execution period + */ + const DEFAULT_MAX_EXECUTION_TIME = 50; + + /** + * default threshold time in seconds before a running script (e.g. a large loop) should stop + * -> so there is some time or e.g. logging,... left + */ + const DEFAULT_EXECUTION_TIME_THRESHOLD = 3; + + /** + * started jobs + * @var Pathfinder\CronModel[] + */ + protected $activeCron = []; + + /** + * disables log file write entry for some cronJobs + * -> either job runs too frequently, or no relevant data available for logging + * @var array + */ + protected $logDisabled = []; + + /** + * set max execution time for cronJbs + * -> Default CLI execution time is 0 == infinite! + * php.ini settings are ignored! http://php.net/manual/en/info.configuration.php#ini.max-execution-time + * @param int $time + */ + protected function setMaxExecutionTime(int $time = self::DEFAULT_MAX_EXECUTION_TIME){ + ini_set('max_execution_time', $time); + } + + /** + * get max execution time + * -> 0 means == infinite! + * @return int + */ + protected function getMaxExecutionTime() : int { + return (int)ini_get('max_execution_time'); + } + + /** + * checks execution time of a "long" running script + * -> returns false if execution time is close to maxExecutionTime + * @param float $timeTotalStart + * @param float|null $timeCheck + * @param int $timeThreshold + * @return bool + */ + protected function isExecutionTimeLeft(float $timeTotalStart, float $timeCheck = null, int $timeThreshold = self::DEFAULT_EXECUTION_TIME_THRESHOLD) : bool { + $timeLeft = true; + if($timeTotalMax = $this->getMaxExecutionTime()){ + $timeTotalMaxThreshold = $timeTotalStart + $timeTotalMax - $timeThreshold; + $timeCheck = $timeCheck ? : microtime(true); + if($timeCheck >= $timeTotalMaxThreshold){ + $timeLeft = false; + } + } + return $timeLeft; + } + + /** + * log cronjob exec state on start + * @param string $job + * @param bool $logging + */ + protected function logStart(string $job, bool $logging = true){ + $this->setMaxExecutionTime(); + + $cron = \Exodus4D\Pathfinder\Lib\Cron::instance(); + if(isset($cron->jobs[$job])){ + // set "start" date for current cronjob + $jobConf = $cron->getJobDataFromConf($cron->jobs[$job]); + $jobConf['lastExecStart'] = $_SERVER['REQUEST_TIME_FLOAT']; + if(($cronModel = $cron->registerJob($job, $jobConf)) instanceof Pathfinder\CronModel){ + $this->activeCron[$job] = $cronModel; + } + } + + if(!$logging){ + $this->logDisabled[] = $job; + } + } + + /** + * log cronjob exec state on finish + * @param string $job + * @param int $total + * @param int $count + * @param int $importCount + * @param int $offset + * @param string $logText + */ + protected function logEnd(string $job, int $total = 0, int $count = 0, int $importCount = 0, int $offset = 0, string $logText = ''){ + $execEnd = microtime(true); + $memPeak = memory_get_peak_usage(); + $state = [ + 'total' => $total, + 'count' => $count, + 'importCount' => $importCount, + 'offset' => $offset, + 'loop' => 1, + 'percent' => $total ? round(100 / $total * ($count + $offset), 1) : 100 + ]; + + if(isset($this->activeCron[$job])){ + if($lastState = $this->activeCron[$job]->lastExecState){ + if(isset($lastState['loop']) && $offset){ + $state['loop'] = (int)$lastState['loop'] + 1; + } + } + + $jobConf = [ + 'lastExecEnd' => $execEnd, + 'lastExecMemPeak' => $memPeak, + 'lastExecState' => $state + ]; + $this->activeCron[$job]->setData($jobConf); + $this->activeCron[$job]->save(); + unset($this->activeCron[$job]); + } + + if(!in_array($job, $this->logDisabled)){ + $this->writeLog($job, $memPeak, $execEnd, $state, $logText); + } + } + + /** + * get either CLI GET params OR + * check for params from last run -> incremental import + * @param string $job + * @return array + */ + protected function getParams(string $job) : array { + $params = []; + + // check for CLI GET params + $f3 = \Base::instance(); + if($getParams = (array)$f3->get('GET')){ + if(isset($getParams['offset'])){ + $params['offset'] = (int)$getParams['offset']; + } + if(isset($getParams['length']) && (int)$getParams['length'] > 0){ + $params['length'] = (int)$getParams['length']; + } + } + + // .. or check for logged params from last exec state (DB entry) + if(empty($params) && isset($this->activeCron[$job])){ + if($lastState = $this->activeCron[$job]->lastExecState){ + if(isset($lastState['offset'])){ + $params['offset'] = (int)$lastState['offset']; + } + if(isset($lastState['count'])){ + $params['offset'] = (int)$params['offset'] + (int)$lastState['count']; + } + if(isset($lastState['loop'])){ + $params['loop'] = (int)$lastState['loop']; + } + } + } + + return $params; + } + + /** + * write log file for $job + * @param string $job + * @param int $memPeak + * @param float $execEnd + * @param array $state + * @param string $logText for custom text + */ + private function writeLog(string $job, int $memPeak = 0, float $execEnd = 0, array $state = [], string $logText = ''){ + $percent = number_format($state['percent'], 1) . '%'; + $duration = number_format(round($execEnd - $_SERVER['REQUEST_TIME_FLOAT'], 3), 3) . 's'; + $log = new \Log('cron_' . $job . '.log'); + + $text = sprintf(self::LOG_TEXT_BASE, + $state['count'], $state['importCount'], $percent, $state['total'], + Number::instance()->bytesToString($memPeak), $duration + ); + + $text .= $logText ? $logText: ''; + $log->write($text); + } +} \ No newline at end of file diff --git a/app/Cron/Cache.php b/app/Cron/Cache.php new file mode 100644 index 000000000..9cc0caf46 --- /dev/null +++ b/app/Cron/Cache.php @@ -0,0 +1,82 @@ +get('PATHFINDER.CACHE.EXPIRE_MAX'); + return ($expireTime >= 0) ? $expireTime : self::CACHE_EXPIRE_MAX; + } + + /** + * clear expired cached files + * >> php index.php "/cron/deleteExpiredCacheData" + * @param \Base $f3 + */ + function deleteExpiredCacheData(\Base $f3){ + $this->logStart(__FUNCTION__); + + // cache dir (dir is recursively searched...) + $cacheDir = $f3->get('TEMP'); + + $filterTime = (int)strtotime('-' . $this->getExpireMaxTime($f3) . ' seconds'); + $expiredFiles = Search::getFilesByMTime($cacheDir, $filterTime, Search::DEFAULT_FILE_LIMIT); + + $totalFiles = 0; + $deletedFiles = 0; + $deletedSize = 0; + $notWritableFiles = 0; + $deleteErrors = 0; + foreach($expiredFiles as $filename => $file) { + /** + * @var $file \SplFileInfo + */ + if($file->isFile()){ + $totalFiles++; + if($file->isWritable()){ + $tmpSize = $file->getSize(); + if( unlink($file->getRealPath()) ){ + $deletedSize += $tmpSize; + $deletedFiles++; + }else{ + $deleteErrors++; + } + }else{ + $notWritableFiles++; + } + } + } + + // Log -------------------------------------------------------------------------------------------------------- + $total = $totalFiles; + $importCount = $total; + $count = $deletedFiles; + + $text = sprintf(self::LOG_TEXT, $deletedSize, $notWritableFiles, $deleteErrors); + $this->logEnd(__FUNCTION__, $total, $count, $importCount, 0, $text); + } + +} diff --git a/app/Cron/CcpSystemsUpdate.php b/app/Cron/CcpSystemsUpdate.php new file mode 100644 index 000000000..c9880f42c --- /dev/null +++ b/app/Cron/CcpSystemsUpdate.php @@ -0,0 +1,190 @@ + 'system_jumps', + 'ship_kills' => 'system_kills_ships', + 'pod_kills' => 'system_kills_pods', + 'npc_kills' => 'system_kills_factions' + ]; + + /** + * checks if a table exists in DB or not + * @param Sql $db + * @param string $table + * @return bool + */ + protected function tableExists (Sql $db, string $table) : bool { + return !empty($db->exec('SHOW TABLES LIKE :table', [':table' => $table])); + } + + /** + * check all system log tables for the correct number of system entries that will be locked + * @param \Base $f3 + * @return int[] + */ + private function prepareSystemLogTables(\Base $f3) : array { + $systemIds = []; + + // get all available systems from "universe" DB + $universeDB = $f3->DB->getDB('UNIVERSE'); + + if($this->tableExists($universeDB, 'system')){ + $systemsData = $universeDB->exec(' + SELECT + `id` + FROM + `system` + WHERE + `security` = :ns OR + `security` = :ls OR + `security` = :hs + ', + [':ns' => '0.0', ':ls' => 'L', ':hs' => 'H'] + ); + + $systemIds = array_map('intval', array_column($systemsData, 'id')); + sort($systemIds, SORT_NUMERIC); + + $pfDB = $f3->DB->getDB('PF'); + + // insert systems into each log table if not exist + foreach($this->logTables as $tableName){ + $pfDB->begin(); + // insert systems into jump log table + $sqlInsertSystem = "INSERT IGNORE INTO " . $tableName . " (`systemId`) VALUES (:systemId)"; + + foreach($systemIds as $systemId){ + $pfDB->exec($sqlInsertSystem, [ + ':systemId' => $systemId + ], 0, false); + } + + $pfDB->commit(); + } + } + + return $systemIds; + } + + + /** + * imports all relevant map stats from CCPs API + * >> php index.php "/cron/importSystemData" + * @param \Base $f3 + */ + function importSystemData(\Base $f3){ + $this->logStart(__FUNCTION__); + $params = $this->getParams(__FUNCTION__); + + + // prepare system jump log table ------------------------------------------------------------------------------ + $time_start = microtime(true); + $systemIds = $this->prepareSystemLogTables($f3); + $time_end = microtime(true); + $execTimePrepareSystemLogTables = $time_end - $time_start; + + $total = count($systemIds); + $offset = ($params['offset'] > 0 && $params['offset'] < $total) ? $params['offset'] : 0; + $systemIds = array_slice($systemIds, $offset, $params['length']); + $importCount = count($systemIds); + $count = 0; + + // switch DB for data import.. + /** + * @var $pfDB Sql + */ + $pfDB = $f3->DB->getDB('PF'); + + // get current jump data -------------------------------------------------------------------------------------- + $time_start = microtime(true); + $jumpData = $f3->ccpClient()->send('getUniverseJumps'); + $time_end = microtime(true); + $execTimeGetJumpData = $time_end - $time_start; + + // get current kill data -------------------------------------------------------------------------------------- + $time_start = microtime(true); + $killData = $f3->ccpClient()->send('getUniverseKills'); + $time_end = microtime(true); + $execTimeGetKillData = $time_end - $time_start; + + // merge both results + $systemValues = array_replace_recursive($jumpData, $killData); + + // update system log tables ----------------------------------------------------------------------------------- + $time_start = microtime(true); + + $logTableCount = count($this->logTables); + $logTableCounter = 0; + foreach($this->logTables as $key => $tableName){ + $logTableCounter++; + $pfDB->begin(); + + $sqlUpdateColumn = vsprintf("SELECT `systemId`, IF (lastUpdatedValue, lastUpdatedValue, DEFAULT (lastUpdatedValue)) AS `updateColumn` FROM %s", [ + $pfDB->quotekey($tableName) + ]); + $resUpdateColumns = $pfDB->exec($sqlUpdateColumn, null, 0); + $resUpdateColumns = array_column($resUpdateColumns, 'updateColumn', 'systemId'); + + foreach($systemIds as $systemId){ + $column = 1; + if(isset($resUpdateColumns[$systemId])){ + $column = (int)$resUpdateColumns[$systemId]; + $column = (++$column > 24) ? 1 : $column; + } + + // update data (if available) + $currentData = 0; + if(isset($systemValues[$systemId][$key])){ + $currentData = (int)$systemValues[$systemId][$key]; + } + + $sql = vsprintf("UPDATE %s SET `updated` = NOW(), %s = :value, `lastUpdatedValue` = :updateColumn WHERE systemId = :systemId", [ + $pfDB->quotekey($tableName), + $pfDB->quotekey('value' . $column) + ]); + + $pfDB->exec($sql, [ + ':systemId' => $systemId, + ':updateColumn' => $column, + ':value' => $currentData + ] , 0); + + // system import is done if ALL related tables (4) were updated + if($logTableCounter === $logTableCount){ + $count++; + } + } + + $pfDB->commit(); + } + + $time_end = microtime(true); + $execTimeUpdateTables = $time_end - $time_start; + + // Log -------------------------------------------------------------------------------------------------------- + $text = sprintf(self::LOG_TEXT, $execTimePrepareSystemLogTables, $execTimeGetJumpData, $execTimeGetKillData, $execTimeUpdateTables); + $this->logEnd(__FUNCTION__, $total, $count, $importCount, $offset, $text); + } +} \ No newline at end of file diff --git a/app/Cron/CharacterUpdate.php b/app/Cron/CharacterUpdate.php new file mode 100644 index 000000000..53eb59215 --- /dev/null +++ b/app/Cron/CharacterUpdate.php @@ -0,0 +1,158 @@ +get('PATHFINDER.CACHE.CHARACTER_LOG_INACTIVE'); + return ($logInactiveTime >= 0) ? $logInactiveTime : self::CHARACTER_LOG_INACTIVE; + } + + /** + * delete all character log data that have not changed since X seconds + * -> see deactivateLogData() + * >> php index.php "/cron/deleteLogData" + * @param \Base $f3 + * @throws \Exception + */ + function deleteLogData(\Base $f3){ + $this->logStart(__FUNCTION__, false); + $logInactiveTime = $this->getCharacterLogInactiveTime($f3); + + /** + * @var $characterLogModel Pathfinder\CharacterLogModel + */ + $characterLogModel = Pathfinder\AbstractPathfinderModel::getNew('CharacterLogModel'); + + // find character logs that were not checked recently and update + $characterLogs = $characterLogModel->find([ + 'TIMESTAMPDIFF(SECOND, updated, NOW() ) > :lifetime', + ':lifetime' => $logInactiveTime + ], [ + 'order' => 'updated asc', + 'limit' => self::CHARACTERS_UPDATE_LOGS_MAX + ]); + + $total = 0; + $count = 0; + + if(is_object($characterLogs)){ + $total = count($characterLogs); + foreach($characterLogs as $characterLog){ + /** + * @var $characterLog Pathfinder\CharacterLogModel + */ + if(is_object($characterLog->characterId)){ + if($accessToken = $characterLog->characterId->getAccessToken()){ + if($characterLog->characterId->isOnline($accessToken)){ + // force characterLog as "updated" even if no changes were made + $characterLog->touch('updated'); + $characterLog->save(); + }else{ + $characterLog->erase(); + } + }else{ + // no valid $accessToken. (e.g. ESI is down; or invalid `refresh_token` found + $characterLog->erase(); + } + }else{ + // character_log does not have a character assigned -> delete + $characterLog->erase(); + } + + $count++; + } + } + + $importCount = $total; + + $this->logEnd(__FUNCTION__, $total, $count, $importCount); + } + + /** + * clean up outdated character data e.g. kicked until status + * >> php index.php "/cron/cleanUpCharacterData" + * @param \Base $f3 + * @throws \Exception + */ + function cleanUpCharacterData(\Base $f3){ + $this->logStart(__FUNCTION__, false); + + /** + * @var $characterModel Pathfinder\CharacterModel + */ + $characterModel = Pathfinder\AbstractPathfinderModel::getNew('CharacterModel'); + + $characters = $characterModel->find([ + 'active = :active AND TIMESTAMPDIFF(SECOND, kicked, NOW() ) > 0', + ':active' => 1 + ]); + + if(is_object($characters)){ + foreach($characters as $character){ + /** + * @var $character Pathfinder\CharacterModel + */ + $character->kick(); + $character->save(); + } + } + + $this->logEnd(__FUNCTION__); + } + + /** + * delete expired character authentication data + * authentication data is used for cookie based login + * >> php index.php "/cron/deleteAuthenticationData" + * @param \Base $f3 + * @throws \Exception + */ + function deleteAuthenticationData(\Base $f3){ + $this->logStart(__FUNCTION__, false); + + /** + * @var $authenticationModel Pathfinder\CharacterAuthenticationModel + */ + $authenticationModel = Pathfinder\AbstractPathfinderModel::getNew('CharacterAuthenticationModel'); + + // find expired authentication data + $authentications = $authenticationModel->find([ + '(expires - NOW()) <= 0' + ]); + + if(is_object($authentications)){ + foreach($authentications as $authentication){ + $authentication->erase(); + } + } + + $this->logEnd(__FUNCTION__); + } + +} \ No newline at end of file diff --git a/app/Cron/MapHistory.php b/app/Cron/MapHistory.php new file mode 100644 index 000000000..70da7d97f --- /dev/null +++ b/app/Cron/MapHistory.php @@ -0,0 +1,131 @@ +get('PATHFINDER.HISTORY.LOG_SIZE_THRESHOLD'); + return ($logSize >= 0) ? ($logSize * 1024 * 1024) : self::LOG_SIZE_THRESHOLD; + } + + /** + * get max log entries (lines) after truncate + * @param \Base $f3 + * @return int + */ + protected function getMaxLogLines(\Base $f3) : int { + $logLines = (int)$f3->get('PATHFINDER.HISTORY.LOG_LINES'); + return ($logLines >= 0) ? $logLines : self::LOG_LINES; + } + + /** + * truncate map history log files and keep size small + * >> php index.php "/cron/truncateMapHistoryLogFiles" + * @param \Base $f3 + */ + function truncateMapHistoryLogFiles(\Base $f3){ + $this->logStart(__FUNCTION__); + + $largeFiles = 0; + $notWritableFiles = 0; + $readErrors = 0; + $writeErrors = 0; + $renameErrors = 0; + $deleteErrors = 0; + $truncatedFileNames = []; + + if($f3->exists('PATHFINDER.HISTORY.LOG', $dir)){ + $fileHandler = FileHandler::instance(); + + $dir = $f3->fixslashes('./' . $dir . 'map/'); + $files = Search::getFilesBySize($dir, $this->getMaxLogSize($f3)); + + // sort by file size + $files = new SortingIterator($files, function( \SplFileInfo $a, \SplFileInfo $b){ + return $b->getSize() - $a->getSize(); + }); + + // limit files count for truncate + $files = new \LimitIterator($files, 0, self::LOG_COUNT); + + foreach($files as $filename => $file){ + /** + * @var $file \SplFileInfo + */ + if($file->isFile()){ + $largeFiles++; + if($file->isWritable()){ + // read newest logs from large files (reverse order) -> new log entries were appended... + $rowsData = $fileHandler->readFileReverse($file->getRealPath(), 0, self::LOG_LINES); + if(!empty($rowsData)){ + // create temp file... + $temp = tempnam(sys_get_temp_dir(), 'map_'); + // write newest logs into temp file... + $fileSizeNew = file_put_contents($temp, implode(PHP_EOL, array_reverse($rowsData)) . PHP_EOL, LOCK_EX); + if($fileSizeNew){ + // move temp file from PHP temp dir into Pathfinders history log dir... + // ... overwrite old log file with new file + if(rename($temp, $file->getRealPath())){ + $truncatedFileNames[] = $file->getFilename(); + // map history logs should be writable for non cronjob user too + @chmod($file->getRealPath(), 0666); + }else{ + $renameErrors++; + } + }else{ + $writeErrors++; + } + }else{ + $readErrors++; + } + }else{ + $notWritableFiles++; + } + } + } + } + + $importCount = $total = $largeFiles; + $count = count($truncatedFileNames); + + // Log -------------------------------------------------------------------------------------------------------- + $text = sprintf(self::LOG_TEXT, $largeFiles, implode(', ', $truncatedFileNames), $notWritableFiles, $readErrors, $writeErrors, $renameErrors, $deleteErrors); + $this->logEnd(__FUNCTION__, $total, $count, $importCount, 0, $text); + } +} \ No newline at end of file diff --git a/app/Cron/MapUpdate.php b/app/Cron/MapUpdate.php new file mode 100644 index 000000000..1d6196362 --- /dev/null +++ b/app/Cron/MapUpdate.php @@ -0,0 +1,230 @@ +> php index.php "/cron/deactivateMapData" + * @param \Base $f3 + */ + function deactivateMapData(\Base $f3){ + $this->logStart(__FUNCTION__, false); + $privateMapLifetime = (int)Config::getMapsDefaultConfig('private.lifetime'); + + if($privateMapLifetime > 0){ + if($pfDB = $f3->DB->getDB('PF')){ + $sqlDeactivateExpiredMaps = "UPDATE map SET + active = 0 + WHERE + map.active = 1 AND + map.typeId = 2 AND + TIMESTAMPDIFF(DAY, map.updated, NOW() ) > :lifetime"; + + $pfDB->exec($sqlDeactivateExpiredMaps, ['lifetime' => $privateMapLifetime]); + } + } + + $this->logEnd(__FUNCTION__); + } + + /** + * delete all deactivated maps + * >> php index.php "/cron/deleteMapData" + * @param \Base $f3 + * @throws \Exception + */ + function deleteMapData(\Base $f3){ + $this->logStart(__FUNCTION__); + $total = 0; + + if($pfDB = $f3->DB->getDB('PF')){ + $sqlDeleteDisabledMaps = "SELECT + id + FROM + map + WHERE + map.active = 0 AND + TIMESTAMPDIFF(DAY, map.updated, NOW() ) > :deletion_time"; + + $disabledMaps = $pfDB->exec($sqlDeleteDisabledMaps, ['deletion_time' => self::DAYS_UNTIL_MAP_DELETION]); + + if($total = $pfDB->count()){ + $mapModel = Pathfinder\AbstractPathfinderModel::getNew('MapModel'); + foreach($disabledMaps as $data){ + $mapModel->getById( (int)$data['id'], 3, false ); + if($mapModel->valid()){ + $mapModel->erase(); + } + $mapModel->reset(); + } + } + } + + $count = $importCount = $total; + + // Log -------------------------------------------------------------------------------------------------------- + $text = sprintf(self::LOG_TEXT_MAPS_DELETED, $total); + $this->logEnd(__FUNCTION__, $total, $count, $importCount, 0, $text); + } + + /** + * delete expired EOL connections + * >> php index.php "/cron/deleteEolConnections" + * @param \Base $f3 + * @throws \Exception + */ + function deleteEolConnections(\Base $f3){ + $this->logStart(__FUNCTION__, false); + $eolExpire = (int)$f3->get('PATHFINDER.CACHE.EXPIRE_CONNECTIONS_EOL'); + + $total = 0; + $count = 0; + if($eolExpire > 0){ + if($pfDB = $f3->DB->getDB('PF')){ + $sql = "SELECT + `con`.`id` + FROM + `connection` `con` INNER JOIN + `map` ON + `map`.`id` = `con`.`mapId` + WHERE + `map`.`deleteEolConnections` = :deleteEolConnections AND + TIMESTAMPDIFF(SECOND, `con`.`eolUpdated`, NOW() ) > :expire_time + "; + + $connectionsData = $pfDB->exec($sql, [ + 'deleteEolConnections' => 1, + 'expire_time' => $eolExpire + ]); + + if($connectionsData){ + $total = count($connectionsData); + /** + * @var $connection Pathfinder\ConnectionModel + */ + $connection = Pathfinder\AbstractPathfinderModel::getNew('ConnectionModel'); + foreach($connectionsData as $data){ + $connection->getById( (int)$data['id'] ); + if($connection->valid()){ + $connection->erase(); + $count++; + } + } + } + } + } + + $importCount = $total; + + $this->logEnd(__FUNCTION__, $total, $count, $importCount); + } + + /** + * delete expired WH connections after max lifetime for wormholes is reached + * >> php index.php "/cron/deleteExpiredConnections" + * @param \Base $f3 + * @throws \Exception + */ + function deleteExpiredConnections(\Base $f3){ + $this->logStart(__FUNCTION__, false); + + $total = 0; + $count = 0; + + $whExpire = (int)$f3->get('PATHFINDER.CACHE.EXPIRE_CONNECTIONS_WH'); + + if($whExpire > 0){ + if($pfDB = $f3->DB->getDB('PF')){ + $sql = "SELECT + `con`.`id` + FROM + `connection` `con` INNER JOIN + `map` ON + `map`.`id` = `con`.`mapId` + WHERE + `map`.`deleteExpiredConnections` = :deleteExpiredConnections AND + `con`.`scope` = :scope AND + TIMESTAMPDIFF(SECOND, `con`.`created`, NOW() ) > :expire_time + "; + + $connectionsData = $pfDB->exec($sql, [ + 'deleteExpiredConnections' => 1, + 'scope' => 'wh', + 'expire_time' => $whExpire + ]); + + if($connectionsData){ + $total = count($connectionsData); + /** + * @var $connection Pathfinder\ConnectionModel + */ + $connection = Pathfinder\AbstractPathfinderModel::getNew('ConnectionModel'); + foreach($connectionsData as $data){ + $connection->getById( (int)$data['id'] ); + if($connection->valid()){ + $connection->erase(); + $count++; + } + } + } + } + } + + $importCount = $total; + + $this->logEnd(__FUNCTION__, $total, $count, $importCount); + } + + /** + * delete all expired signatures on "inactive" systems + * >> php index.php "/cron/deleteSignatures" + * @param \Base $f3 + */ + function deleteSignatures(\Base $f3){ + $this->logStart(__FUNCTION__, false); + $signatureExpire = (int)$f3->get('PATHFINDER.CACHE.EXPIRE_SIGNATURES'); + + $count = 0; + if($signatureExpire > 0){ + if($pfDB = $f3->DB->getDB('PF')){ + $sqlDeleteExpiredSignatures = "DELETE `sigs` FROM + `system_signature` `sigs` INNER JOIN + `system` ON + `system`.`id` = `sigs`.`systemId` + WHERE + `system`.`active` = 0 AND + TIMESTAMPDIFF(SECOND, `sigs`.`updated`, NOW() ) > :lifetime + "; + + $count = $pfDB->exec($sqlDeleteExpiredSignatures, ['lifetime' => $signatureExpire]); + } + } + + $importCount = $total = $count; + + $this->logEnd(__FUNCTION__, $total, $count, $importCount); + } + +} \ No newline at end of file diff --git a/app/Cron/StatisticsUpdate.php b/app/Cron/StatisticsUpdate.php new file mode 100644 index 000000000..b2dc743ef --- /dev/null +++ b/app/Cron/StatisticsUpdate.php @@ -0,0 +1,52 @@ + older than 1 year + * >> php index.php "/cron/deleteStatisticsData" + * @param \Base $f3 + */ + function deleteStatisticsData(\Base $f3){ + $this->logStart(__FUNCTION__); + + $currentYear = (int)date('o'); + $currentWeek = (int)date('W'); + $expiredYear = $currentYear - 1; + + $pfDB = $f3->DB->getDB('PF'); + + $queryData = [ + 'yearWeekEnd' => strval($expiredYear) . str_pad($currentWeek, 2, 0, STR_PAD_LEFT) + ]; + + $sql = "DELETE FROM + activity_log + WHERE + CONCAT(`year`, `week`) < :yearWeekEnd"; + + $pfDB->exec($sql, $queryData); + + $deletedLogsCount = $pfDB->count(); + + // Log -------------------------------------------------------------------------------------------------------- + $total = $count = $importCount = $deletedLogsCount; + + $text = sprintf(self::LOG_TEXT_STATISTICS, $deletedLogsCount); + $this->logEnd(__FUNCTION__, $total, $count, $importCount, 0, $text); + } +} \ No newline at end of file diff --git a/app/Cron/Universe.php b/app/Cron/Universe.php new file mode 100644 index 000000000..2caa9368b --- /dev/null +++ b/app/Cron/Universe.php @@ -0,0 +1,396 @@ +echoFlush(); + } + + /** + * echo configuration + */ + private function echoConfig(){ + echo 'config ───────────────────────────────────────────────────────────────────────────────────────────────────────' . PHP_EOL; + echo 'max_execution_time : ' . ini_get('max_execution_time') . PHP_EOL; + echo 'memory_limit : ' . ini_get('memory_limit') . PHP_EOL; + $this->echoFlush(); + } + + /** + * echo information + * @param int $total + * @param int $offset + * @param int $importCount + * @param array $ids + */ + private function echoInfo(int $total, int $offset, int $importCount, array $ids){ + echo 'info ─────────────────────────────────────────────────────────────────────────────────────────────────────────' . PHP_EOL; + echo 'all data : ' . $total . PHP_EOL; + echo 'import offset : ' . $offset . PHP_EOL; + echo 'import count : ' . $importCount . PHP_EOL; + echo 'import chunk : ' . implode(',', $ids) . PHP_EOL; + $this->echoFlush(); + } + + /** + * echo start + */ + private function echoStart(){ + echo 'start ────────────────────────────────────────────────────────────────────────────────────────────────────────' . PHP_EOL; + $this->echoFlush(); + } + + /** + * echo loop start information + * @param int $count + * @param int $importCount + * @param int $id + */ + private function echoLoading(int $count, int $importCount, int $id){ + echo '[' . date('H:i:s') . '] loading... ' . $this->formatCounterValue($count) . '/' . $importCount . ' id: ' . $this->formatIdValue($id) . PHP_EOL; + $this->echoFlush(); + } + + /** + * echo loop finish information + * @param int $importCount + * @param int $id + * @param float $timeLoopStart + * @param float $timeTotalStart + */ + private function echoLoaded(int $importCount, int $id, float $timeLoopStart, float $timeTotalStart){ + $time = microtime(true); + echo '[' . date('H:i:s') . '] loaded ' . str_pad('', strlen($importCount), ' ') . ' id: ' . $this->formatIdValue($id) . + ' memory: ' . $this->formatMemoryValue(memory_get_usage()) . + ' time: ' . $this->formatSeconds($time - $timeLoopStart) . + ' total: ' . $this->formatSeconds($time - $timeTotalStart) . PHP_EOL; + $this->echoFlush(); + } + + /** + * echo finish information + * @param int $count + * @param int $importCount + * @param float $timeTotalStart + */ + private function echoFinish(int $count, int $importCount, float $timeTotalStart){ + echo 'finished ─────────────────────────────────────────────────────────────────────────────────────────────────────' . PHP_EOL; + echo '[' . date('H:i:s') . '] ' . $this->formatCounterValue($count) . '/' . $importCount . + ' peak: ' . $this->formatMemoryValue(memory_get_peak_usage ()) . + ' total: ' . $this->formatSeconds(microtime(true) - $timeTotalStart) . PHP_EOL; + $this->echoFlush(); + } + + /** + * imports static universe data from ESI + * >> php index.php "/cron/setup?type=system&offset=0&length=5" + * @param \Base $f3 + * @throws \Exception + */ + function setup(\Base $f3){ + $params = (array)$f3->get('GET'); + $type = (string)$params['type']; + $paramOffset = (int)$params['offset']; + $paramLength = (int)$params['length']; + $timeTotalStart = microtime(true); + $msg = ''; + + $ids = []; + $importCount = 0; + $count = 0; + $modelClass = ''; + $setupModel = function(Model\Universe\AbstractUniverseModel &$model, int $id){}; + + switch($type){ + case 'system': + // load systems + dependencies (planets, star, types,...) + $ids = $f3->ccpClient()->send('getUniverseSystems'); + $modelClass = 'SystemModel'; + $setupModel = function(Model\Universe\SystemModel &$model, int $id){ + $model->loadById($id); + $model->loadPlanetsData(); + }; + break; + case 'stargate': + // load all stargates. Systems must be present first! + $ids = $f3->ccpClient()->send('getUniverseSystems'); + $modelClass = 'SystemModel'; + $setupModel = function(Model\Universe\SystemModel &$model, int $id){ + $model->loadById($id); + $model->loadStargatesData(); + }; + break; + case 'station': + $ids = $f3->ccpClient()->send('getUniverseSystems'); + $modelClass = 'SystemModel'; + $setupModel = function(Model\Universe\SystemModel &$model, int $id){ + if($model->getById($id)){ + $model->loadStationsData(); + }else{ + echo 'NOT VALID ' . $id . PHP_EOL; + die(); + } + }; + break; + case 'sovereignty': + // load sovereignty map data. Systems must be present first! + $sovData = $f3->ccpClient()->send('getSovereigntyMap'); + $ids = !empty($sovData = $sovData['map']) ? array_keys($sovData): []; + $modelClass = 'SystemModel'; + $setupModel = function(Model\Universe\SystemModel &$model, int $id) use ($sovData) { + if($model->getById($id)){ + $model->updateSovereigntyData($sovData[$id]); + }else{ + echo 'NOT VALID ' . $id . PHP_EOL; + die(); + } + }; + break; + case 'faction_war_systems': + $fwSystems = $f3->ccpClient()->send('getFactionWarSystems'); + $ids = !empty($fwSystems = $fwSystems['systems']) ? array_keys($fwSystems): []; + $modelClass = 'SystemModel'; + $setupModel = function(Model\Universe\SystemModel &$model, int $id) use ($fwSystems) { + if($model->getById($id)){ + $model->updateFactionWarData($fwSystems[$id]); + }else{ + echo 'NOT VALID ' . $id . PHP_EOL; + die(); + } + }; + break; + case 'index_system': + // setup system index, Systems must be present first! + $ids = $f3->ccpClient()->send('getUniverseSystems'); + $modelClass = 'SystemModel'; + $setupModel = function(Model\Universe\SystemModel &$model, int $id){ + $model->getById($id); // no loadById() here! would take "forever" when system not exists and must be build up first... + $model->buildIndex(); + }; + break; + default: + $msg = 'Model is not valid'; + } + + if($modelClass){ + $this->echoParams($type, $paramOffset, $paramLength); + $this->echoConfig(); + + $total = count($ids); + $offset = ($paramOffset < 0) ? 0 : (($paramOffset >= $total) ? $total : $paramOffset); + $length = ($paramLength < 0) ? 0 : $paramLength; + sort($ids, SORT_NUMERIC); + $ids = array_slice($ids, $offset, $length); + $importCount = count($ids); + $count = 0; + + $this->echoInfo($total, $offset, $importCount, $ids); + $this->echoStart(); + + /** + * @var $model Model\Universe\SystemModel + */ + $model = Model\Universe\AbstractUniverseModel::getNew($modelClass); + foreach($ids as $id){ + $timeLoopStart = microtime(true); + $this->echoLoading(++$count, $importCount, $id); + $setupModel($model, $id); + $model->reset(); + $this->echoLoaded($importCount, $id, $timeLoopStart, $timeTotalStart); + } + + $this->echoFinish($count, $importCount, $timeTotalStart); + } + + // Log -------------------------------------------------------------------------------------------------------- + $log = new \Log('cron_' . __FUNCTION__ . '.log'); + $log->write(sprintf(self::LOG_TEXT, __FUNCTION__, $type, + $this->formatCounterValue($count), $importCount, $this->formatMemoryValue(memory_get_peak_usage ()), + $this->formatSeconds(microtime(true) - $timeTotalStart), $msg)); + } + + /** + * update Sovereignty system data from ESI + * -> this updates Faction warfare data as well + * >> php index.php "/cron/updateSovereigntyData" + * @param \Base $f3 + * @throws \Exception + */ + function updateSovereigntyData(\Base $f3){ + $this->logStart(__FUNCTION__); + $params = $this->getParams(__FUNCTION__); + + $timeTotalStart = microtime(true); + $msg = ''; + + /** + * @var $system Model\Universe\SystemModel + */ + $system = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); + + $sovData = $f3->ccpClient()->send('getSovereigntyMap'); + $fwSystems = $f3->ccpClient()->send('getFactionWarSystems'); + $fwSystems = $fwSystems['systems']; + $ids = !empty($sovData = $sovData['map']) ? array_keys($sovData): []; + sort($ids, SORT_NUMERIC); + + $total = count($ids); + $offset = ($params['offset'] > 0 && $params['offset'] < $total) ? $params['offset'] : 0; + $ids = array_slice($ids, $offset, $params['length']); + $importCount = count($ids); + $count = 0; + + $changes = []; + foreach($ids as $id){ + // skip wormhole systems -> can not have sov data + // -> even though they are returned from sovereignty/map endpoint?! + if( + $system->getById($id, 0) && + strpos($system->security, 'C') === false + ){ + if($changedSovData = $system->updateSovereigntyData($sovData[$id])){ + $changes['sovereignty'][] = $id; + } + + $changedFwData = false; + if(is_array($fwSystems[$id])){ + if($changedFwData = $system->updateFactionWarData($fwSystems[$id])){ + $changes['factionWarfare'][] = $id; + } + } + + if($changedSovData || $changedFwData){ + $system->buildIndex(); + } + } + $system->reset(); + + $count++; + + // stop loop if runtime gets close to "max_execution_time" + // -> we need some time for writing *.log file + if(!$this->isExecutionTimeLeft($timeTotalStart)){ + $msg = 'Script execution stopped due to "max_execution_time" limit reached'; + break; + } + } + + $changedIds = array_reduce($changes, function(array $reducedIds, array $changedIds) : array { + return array_unique(array_merge($reducedIds, $changedIds)); + }, []); + + // Log -------------------------------------------------------------------------------------------------------- + $text = sprintf(self::LOG_TEXT_SOV_FW, + count($changedIds), count($changes['sovereignty'] ? : []), count($changes['factionWarfare'] ? : []), + $msg); + + $this->logEnd(__FUNCTION__, $total, $count, $importCount, $offset, $text); + } + + /** + * update static universe system data from ESI + * -> updates small chunk of systems at once + * >> php index.php "/cron/updateUniverseSystems" + * @param \Base $f3 + * @throws \Exception + */ + function updateUniverseSystems(\Base $f3){ + $this->logStart(__FUNCTION__); + /** + * @var $systemModel Model\Universe\SystemModel + * @var $system Model\Universe\SystemModel + */ + $systemModel = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); + $systems = $systemModel->find( null, ['order' => 'updated', 'limit' => 2]); + if($systems){ + foreach ($systems as $system){ + $system->updateModel(); + } + } + + $this->logEnd(__FUNCTION__); + } + +} \ No newline at end of file diff --git a/app/Data/File/FileHandler.php b/app/Data/File/FileHandler.php new file mode 100644 index 000000000..e6ff1ddf9 --- /dev/null +++ b/app/Data/File/FileHandler.php @@ -0,0 +1,89 @@ +setFlags(\SplFileObject::DROP_NEW_LINE | \SplFileObject::READ_AHEAD | \SplFileObject::SKIP_EMPTY); + + foreach( new \LimitIterator($file, 0, $limit) as $i => $rowData){ + if(is_callable($rowParser)){ + // custom parser for row data -> manipulate $data by ref + $rowParser($rowData, $data); + }else{ + $data[] = $rowData; + } + } + }else{ + \Base::instance()->error(500, sprintf(self::ERROR_STREAM_READABLE, $sourceFile)); + } + } + + return $data; + } + + /** + * validate offset + * @param int $offset + * @return int + */ + public static function validateOffset(int $offset): int{ + if( + $offset < self::LOG_FILE_OFFSET_MIN || + $offset > self::LOG_FILE_OFFSET_MAX + ){ + $offset = self::LOG_FILE_OFFSET; + } + return $offset; + } + + /** + * validate limit + * @param int $limit + * @return int + */ + public static function validateLimit(int $limit): int{ + if( + $limit < self::LOG_FILE_LIMIT_MIN || + $limit > self::Log_File_LIMIT_MAX + ){ + $limit = self::LOG_FILE_LIMIT; + } + return $limit; + } +} \ No newline at end of file diff --git a/app/Data/File/ReverseSplFileObject.php b/app/Data/File/ReverseSplFileObject.php new file mode 100644 index 000000000..829ae0279 --- /dev/null +++ b/app/Data/File/ReverseSplFileObject.php @@ -0,0 +1,219 @@ + 'start with 2nd last line') + * @var int + */ + protected $offset = 0; + + /** + * total lines found in file + * @var int + */ + protected $lineCount = 0; + + /** + * empty lines found in file + * @var int + */ + protected $lineCountEmpty = 0; + + /** + * current pointer position + * @var int + */ + protected $pointer = 0; + + /** + * position increments when valid row data found + * @var + */ + protected $position; + + /** + * control characters + * @var array + */ + protected $eol = ["\r", "\n"]; + + public function __construct($sourceFile, $offset = 0){ + parent::__construct($sourceFile); + + // set total line count of the file + $this->setLineCount(); + + //Seek to the first position of the file and record its position + //Should be 0 + $this->fseek(0); + $this->begin = $this->ftell(); + $this->offset = $offset; + + //Seek to the last position from the end of the file + //This varies depending on the file + $this->fseek($this->pointer, SEEK_END); + } + + /** + * reverse rewind file. + */ + public function rewind(){ + //Set the line position to 0 - First Line + $this->position = 0; + + //Reset the file pointer to the end of the file minus 1 character. "0" == false + $this->fseek(-1, SEEK_END); + + $this->findLineBegin(); + //... File pointer is now at the beginning of the last line that contains data + + // add custom line offset + if($this->offset){ + // calculate offset start line + $offsetLine = $this->lineCount - $this->lineCountEmpty - $this->offset; + + if($offsetLine > 0){ + // row is zero based + $offsetIndex = $offsetLine - 1; + + parent::seek($offsetIndex); + // seek() sets pointer to next line... set it back to previous + $this->fseek(-2, SEEK_CUR); + + $this->findLineBegin(); + //... File pointer is now at the beginning of the last line that contains data from $offset + }else{ + // negative offsetLine -> invalid! + $this->pointer = $this->begin -1; + } + + } + } + + /** + * Return the current line after the file pointer + * @return string + */ + public function current(){ + return trim($this->fgets()); + } + + /** + * Return the current key of the line we're on + * These go in reverse order + * @return mixed + */ + public function key(){ + return $this->position; + } + + /** + * move one line up + */ + public function next(){ + //Step the file pointer back one step to the last letter of the previous line + --$this->pointer; + if($this->pointer < $this->begin){ + return; + } + + $this->fseek($this->pointer); + + $this->findLineBegin(); + + //File pointer is now on the next previous line + //Increment the line position + ++$this->position; + } + + /** + * Check the current file pointer to make sure we are not at the beginning of the file + * @return bool + */ + public function valid(){ + return ($this->pointer >= $this->begin); + } + + /** + * seek to previous lines + * @param int $lineCount + */ + public function seek($lineCount){ + for($i = 0; $i < $lineCount; $i++){ + $this->next(); + } + } + + /** + * move pointer to line begin + * -> skip line breaks + */ + private function findLineBegin(){ + //Check the character over and over till we hit another new line + $c = $this->fgetc(); + + // skip empty lines + while(in_array($c, $this->eol)){ + $this->fseek(-2, SEEK_CUR); + if(!$this->pointer = $this->ftell()){ + break; + } + $c = $this->fgetc(); + + $this->lineCountEmpty++; + } + + //Check the last character to make sure it is not a new line + while(!in_array($c, $this->eol)){ + $this->fseek(-2, SEEK_CUR); + if(!$this->pointer = $this->ftell()){ + break; + } + $c = $this->fgetc(); + } + } + + /** + * set total line count. No matter if there are empty lines in between + */ + private function setLineCount(){ + // Store flags and position + $flags = $this->getFlags(); + $currentPointer = $this->ftell(); + + // Prepare count by resetting flags as READ_CSV for example make the tricks very slow + $this->setFlags(null); + + // Go to the larger INT we can as seek will not throw exception, errors, notice if we go beyond the bottom line + //$this->seek(PHP_INT_MAX); + parent::seek(PHP_INT_MAX); + + // We store the key position + // As key starts at 0, we add 1 + $this->lineCount = parent::key() + 1; + + // We move to old position + // As seek method is longer with line number < to the max line number, it is better to count at the beginning of iteration + //parent::seek($currentPointer); + $this->fseek($currentPointer); + + // Re set flags + $this->setFlags($flags); + } +} \ No newline at end of file diff --git a/app/Data/Filesystem/Search.php b/app/Data/Filesystem/Search.php new file mode 100644 index 000000000..2872faa6f --- /dev/null +++ b/app/Data/Filesystem/Search.php @@ -0,0 +1,90 @@ +isFile() || // allow recursion + ( + strpos($current->getFilename(), '.') !== 0 && // skip e.g. ".gitignore" + $current->getMTime() < $mTime // filter last modification date + ) + ){ + return true; + } + return false; + }; + + return self::getFilesByCallback($dir, $filterCallback, $limit); + } + + /** + * recursive file filter by size + * @param string $dir + * @param int $size + * @param int $limit + * @return \Traversable + */ + static function getFilesBySize(string $dir, int $size = 0, int $limit = self::DEFAULT_FILE_LIMIT) : \Traversable { + + $filterCallback = function($current, $key, $iterator) use ($size) { + /** + * @var $current \RecursiveDirectoryIterator + */ + if ( + !$current->isFile() || // allow recursion + ( + strpos($current->getFilename(), '.') !== 0 && // skip e.g. ".gitignore" + $current->getSize() > $size // filter file size + ) + ){ + return true; + } + return false; + }; + + return self::getFilesByCallback($dir, $filterCallback, $limit); + } + + /** + * @param string $dir + * @param \Closure $filterCallback + * @param int $limit + * @return \Traversable + */ + private static function getFilesByCallback(string $dir, \Closure $filterCallback, int $limit = self::DEFAULT_FILE_LIMIT) : \Traversable { + $files = new \ArrayIterator(); + if(is_dir($dir)){ + $directory = new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS ); + $files = new \RecursiveCallbackFilterIterator($directory, $filterCallback); + } + return new \LimitIterator($files, 0, $limit); + } +} \ No newline at end of file diff --git a/app/Data/Mapper/AbstractIterator.php b/app/Data/Mapper/AbstractIterator.php new file mode 100644 index 000000000..f81b02e49 --- /dev/null +++ b/app/Data/Mapper/AbstractIterator.php @@ -0,0 +1,126 @@ + overwrite in child classes (late static binding) + * @var array + */ + protected static $map = []; + + /** + * remove unmapped values from Array + * -> see $map + * @var bool + */ + protected static $removeUnmapped = true; + + /** + * AbstractIterator constructor. + * @param $data + */ + function __construct($data){ + parent::__construct($data, \RecursiveIteratorIterator::SELF_FIRST); + } + + /** + * map iterator + * @return array + */ + public function getData(){ + iterator_apply($this, 'self::recursiveIterator', [$this]); + + return iterator_to_array($this, true); + } + + /** + * convert array keys to camelCase + * @param $array + * @return array + */ + protected function camelCaseKeys($array){ + return Util::arrayChangeKeys($array, [\Base::instance(), 'camelcase']); + } + + /** + * recursive iterator function called on every node + * @param AbstractIterator $iterator + * @return AbstractIterator + */ + static function recursiveIterator(AbstractIterator $iterator){ + + $keyWhitelist = array_keys(static::$map); + + while($iterator->valid()){ + + if( isset(static::$map[$iterator->key()]) ){ + $mapValue = static::$map[$iterator->key()]; + + // check for mapping key + if( + $iterator->hasChildren() && + Util::is_assoc($iterator->current()) + ){ + // recursive call for child elements + $iterator->offsetSet($iterator->key(), forward_static_call(array('self', __METHOD__), $iterator->getChildren())->getArrayCopy()); + $iterator->next(); + }elseif(is_array($mapValue)){ + // a -> array mapping + $parentKey = array_keys($mapValue)[0]; + $entryKey = array_values($mapValue)[0]; + + // check if key already exists + if($iterator->offsetExists($parentKey)){ + $currentValue = $iterator->offsetGet($parentKey); + // add new array entry + $currentValue[$entryKey] = $iterator->current(); + $iterator->offsetSet($parentKey, $currentValue); + }else{ + $iterator->offsetSet($parentKey, [$entryKey => $iterator->current()]); + $keyWhitelist[] = $parentKey; + } + + $iterator->offsetUnset($iterator->key()); + }elseif(is_object($mapValue)){ + // a -> a (format by function) + $formatFunction = $mapValue; + $iterator->offsetSet($iterator->key(), call_user_func($formatFunction, $iterator)); + + // just value change no key change + $iterator->next(); + }elseif($mapValue !== $iterator->key()){ + // a -> b mapping (key changed) + $iterator->offsetSet($mapValue, $iterator->current()); + $iterator->offsetUnset($iterator->key()); + $keyWhitelist[] = $mapValue; + }else{ + // a -> a (no changes) + $iterator->next(); + } + + }elseif( + static::$removeUnmapped && + !in_array($iterator->key(), $keyWhitelist) + ){ + $iterator->offsetUnset($iterator->key()); + }else{ + $iterator->next(); + } + + } + + return $iterator; + } + +} \ No newline at end of file diff --git a/app/Data/Mapper/SortingIterator.php b/app/Data/Mapper/SortingIterator.php new file mode 100644 index 000000000..ceb4b7f3f --- /dev/null +++ b/app/Data/Mapper/SortingIterator.php @@ -0,0 +1,20 @@ +uasort($callback); + } +} \ No newline at end of file diff --git a/app/Db/Sql/Mysql/Session.php b/app/Db/Sql/Mysql/Session.php new file mode 100644 index 000000000..cce602ecd --- /dev/null +++ b/app/Db/Sql/Mysql/Session.php @@ -0,0 +1,46 @@ + We use this "custom" SQl rather than the default in parent::__construct() + // because of the defaults 'data' column type TEXT + $dbName = $db->name(); + + $sql = "CREATE TABLE IF NOT EXISTS "; + $sql .= $dbName ? $db->quotekey($dbName,FALSE) . "." : ""; + $sql .= $db->quotekey($table,FALSE) . " ("; + $sql .= $db->quotekey('session_id') . " VARCHAR(255),"; + $sql .= $db->quotekey('data') . " MEDIUMTEXT,"; + $sql .= $db->quotekey('ip') . " VARCHAR(45),"; + $sql .= $db->quotekey('agent') . " VARCHAR(300),"; + $sql .= $db->quotekey('stamp') . " INT(11),"; + $sql .= "PRIMARY KEY (" . $db->quotekey('session_id') . ")"; + $sql .= ");"; + + $db->exec($sql); + } + + // $force = false for parent constructor -> skip default create SQL + parent::__construct($db, $table, false, $onsuspect, $key); + } +} \ No newline at end of file diff --git a/app/Db/Sql/Mysql/tablemodifier.php b/app/Db/Sql/Mysql/tablemodifier.php new file mode 100644 index 000000000..7e3312efa --- /dev/null +++ b/app/Db/Sql/Mysql/tablemodifier.php @@ -0,0 +1,337 @@ + if §constraint is passed, constraints are limited to that column + * @param null| Constraint $constraint + * @return Constraint[] + */ + public function listConstraint($constraint = null){ + + $constraintName = '%'; + $keys = []; + if($constraint instanceof Constraint){ + // list constraints for given column in this table + $constraintName = $constraint->getConstraintName() . '%'; + $keys = $constraint->getKeys(); + } + + $this->db->exec("USE information_schema"); + $constraintsData = $this->db->exec(" + SELECT + * + FROM + referential_constraints + WHERE + constraint_schema = :db AND + table_name = :table AND + constraint_name LIKE :constraint_name + ", [ + ':db' => $this->db->name(), + ':table' => $this->name, + ':constraint_name' => $constraintName + ]); + // switch back to current DB + $this->db->exec("USE " . $this->db->quotekey($this->db->name())); + + $constraints = []; + foreach($constraintsData as $data){ + $constraints[$data['CONSTRAINT_NAME']] = new Constraint($this, $keys, $data['REFERENCED_TABLE_NAME'] ); + } + + return $constraints; + } + + /** + * checks whether a constraint name exists or not + * -> does not check constraint params + * @param Constraint $constraint + * @return bool + */ + public function constraintExists($constraint){ + $constraints = $this->listConstraint(); + return array_key_exists($constraint->getConstraintName(), $constraints); + } + + /** + * drop foreign key constraint + * @param Constraint $constraint + */ + public function dropConstraint($constraint){ + if($constraint->isValid()){ + $this->queries[] = "ALTER TABLE " . $this->db->quotekey($this->name) . " + DROP FOREIGN KEY " . $this->db->quotekey($constraint->getConstraintName()) . ";"; + }else{ + trigger_error(sprintf(self::TEXT_ConstraintNotValid, 'table: ' . $this->name . ' constraintName: ' . $constraint->getConstraintName())); + } + } + + /** + * Add/Update foreign key constraint + * @param Constraint $constraint + */ + public function addConstraint($constraint){ + + if($constraint->isValid()){ + $this->queries[] = " + ALTER TABLE " . $this->db->quotekey($this->name) . " + ADD CONSTRAINT " . $this->db->quotekey($constraint->getConstraintName()) . " + FOREIGN KEY (" . implode(', ', $constraint->getKeys()) . ") + REFERENCES " . $this->db->quotekey($constraint->getReferencedTable()) . " (" . implode(', ', $constraint->getReferencedCols()) . ") + ON DELETE " . $constraint->getOnDelete() . " + ON UPDATE " . $constraint->getOnUpdate() . ";"; + }else{ + trigger_error(sprintf(self::TEXT_ConstraintNotValid, 'table: ' . $this->name . ' constraintName: ' . $constraint->getConstraintName())); + } + } +} + +/** + * Class Column + * @package DB\SQL\MySQL + */ +class Column extends SQL\Column { + + const TEXT_TableNameMissing = 'Table name missing for FOREIGN KEY in `%s`'; + + /** + * drop constraint from this column + * @param Constraint $constraint + */ + public function dropConstraint(Constraint $constraint){ + $this->table->dropConstraint($constraint); + } + + /** + * add constraint to this column + * @param Constraint $constraint + */ + public function addConstraint(Constraint $constraint){ + $this->table->addConstraint($constraint); + } + + /** + * @param Constraint $constraint + * @return mixed + */ + public function constraintExists(Constraint $constraint){ + return $this->table->constraintExists($constraint); + } + + /** + * get a new column based constraint + * $constraintData['table'] => referenceTable name (required) + * $constraintData['id'] => referenceColumns (optional) default: ['id'] + * $constraintData['on-delete'] => ON DELETE action (optional) default: see \DB\SQL\MySQL\Constraint const + * $constraintData['on-update'] => ON UPDATE action (optional) default: see \DB\SQL\MySQL\Constraint const + * + * @param array $constraintData + * @return Constraint + */ + public function newConstraint($constraintData){ + + $constraint = null; + + if(isset($constraintData['table'])){ + if(isset($constraintData['column'])){ + $constraintData['column'] = (array)$constraintData['column']; + }else{ + $constraintData['column'] = ['id']; + } + + $constraint = new Constraint($this->table, $this->name, $constraintData['table'], $constraintData['column']); + + if(isset($constraintData['on-delete'])){ + $constraint->setOnDelete($constraintData['on-delete']); + } + + if(isset($constraintData['on-update'])){ + $constraint->setOnUpdate($constraintData['on-update']); + } + + }else{ + trigger_error(sprintf(self::TEXT_TableNameMissing, $this->table->name . '->' . $this->name)); + } + + return $constraint; + } +} + +class Constraint { + + // available actions + const ACTIONS_DELETE = ['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION']; + const ACTIONS_UPDATE = ['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION']; + + // default actions + const ACTION_DELETE = 'RESTRICT'; + const ACTION_UPDATE = 'RESTRICT'; + + const TEXT_ActionNotSupported = 'Constraint action `%s` is not supported.'; + + protected $table; + protected $keys = []; + protected $referencedTable = ''; + protected $referencedCols = []; + protected $onDelete = self::ACTION_DELETE; + protected $onUpdate = self::ACTION_UPDATE; + + /** + * Constraint constructor. + * @param SQL\TableBuilder $table + * @param array $keys + * @param string $referencedTable + * @param array $referencedCols + */ + public function __construct(SQL\TableBuilder $table, $keys = [], $referencedTable = '', $referencedCols = ['id']){ + $this->table = &$table; + $this->setKeys($keys); + $this->setReferencedTable($referencedTable); + $this->setReferencedCols($referencedCols); + } + + /** + * @param mixed $keys + */ + public function setKeys($keys){ + $this->keys = (array)$keys; + } + + /** + * @param mixed $referencedTable + */ + public function setReferencedTable($referencedTable){ + $this->referencedTable = $referencedTable; + } + + /** + * @param mixed $referencedCols + */ + public function setReferencedCols($referencedCols){ + $this->referencedCols = (array)$referencedCols; + } + + /** + * @param string $onDelete + */ + public function setOnDelete($onDelete){ + if( in_array($onDelete, self::ACTIONS_DELETE) ){ + $this->onDelete = $onDelete; + }else{ + trigger_error(sprintf(self::TEXT_ActionNotSupported, $onDelete)); + } + } + + /** + * @param string $onUpdate + */ + public function setOnUpdate($onUpdate){ + if( in_array($onUpdate, self::ACTIONS_UPDATE) ){ + $this->onUpdate = $onUpdate; + }else{ + trigger_error(sprintf(self::TEXT_ActionNotSupported, $onUpdate)); + } + } + + /** + * @return array + */ + public function getKeys(){ + return $this->keys; + } + + /** + * @return string + */ + public function getReferencedTable(){ + return $this->referencedTable; + } + + /** + * @return array + */ + public function getReferencedCols(){ + return $this->referencedCols; + } + + /** + * @return string + */ + public function getOnDelete(){ + return $this->onDelete; + } + + /** + * @return string + */ + public function getOnUpdate(){ + return $this->onUpdate; + } + + /** + * get a constraint name for this table. + * This can either be used to generate unique constraint names for foreign keys in parent tables + * or generate a "part" of a name. e.g. for db-Query all constraints of this table (ignore columns) + * by "LIKE" selecting "information_schema" + * -> To get a certain constraint or generate a unique constraint, ALL params are required! + * @return string + */ + public function getConstraintName(){ + $constraintName = 'fk_' . $this->table->name; + + if(!empty($this->getKeys())){ + $constraintName .= '___' . implode('__', $this->getKeys()); + if(!empty($this->getReferencedTable())){ + $constraintName .= '___' . $this->getReferencedTable(); + if(!empty($this->getReferencedCols())){ + $constraintName .= '___' . implode('__', $this->getReferencedCols()); + } + } + } + + return $constraintName; + } + + /** + * checks if constraint is valid + * -> all required members must be set! + * @return bool + */ + public function isValid(){ + $valid = false; + + if( + !empty($this->getKeys()) && + !empty($this->getReferencedTable()) && + !empty($this->getReferencedCols()) + ){ + $valid = true; + } + + return $valid; + } + + +} \ No newline at end of file diff --git a/app/Exception/ConfigException.php b/app/Exception/ConfigException.php new file mode 100644 index 000000000..d61d89e40 --- /dev/null +++ b/app/Exception/ConfigException.php @@ -0,0 +1,21 @@ + 500 + ]; + +} \ No newline at end of file diff --git a/app/Exception/DatabaseException.php b/app/Exception/DatabaseException.php new file mode 100644 index 000000000..79ac3fe39 --- /dev/null +++ b/app/Exception/DatabaseException.php @@ -0,0 +1,28 @@ + 500 + ]; + + /** + * DatabaseException constructor. + * @param string $message + */ + public function __construct(string $message){ + parent::__construct($message, 1500); + } +} \ No newline at end of file diff --git a/app/Exception/DateException.php b/app/Exception/DateException.php new file mode 100644 index 000000000..823c49a4b --- /dev/null +++ b/app/Exception/DateException.php @@ -0,0 +1,20 @@ + 500 // invalid DateRange + ]; +} \ No newline at end of file diff --git a/app/Exception/PathfinderException.php b/app/Exception/PathfinderException.php new file mode 100644 index 000000000..a65164f29 --- /dev/null +++ b/app/Exception/PathfinderException.php @@ -0,0 +1,67 @@ + can be specified by using custom Exception codes + */ + const DEFAULT_RESPONSECODE = 500; + + /** + * lists all exception codes + * @var array + */ + protected $codes = [ + 0 => self::DEFAULT_RESPONSECODE + ]; + + /** + * PathfinderException constructor. + * @param string $message + * @param int $code + */ + public function __construct(string $message, int $code = 0){ + if( !array_key_exists($code, $this->codes) ){ + // exception code not specified by child class + $code = 0; + } + parent::__construct($message, $code); + } + + /** + * get error object + * @return \stdClass + */ + public function getError() : \stdClass { + $error = (object) []; + $error->type = 'error'; + $error->code = $this->getResponseCode(); + $error->status = Config::getHttpStatusByCode($this->getResponseCode()); + $error->text = $this->getMessage(); + if(\Base::instance()->get('DEBUG') >= 1){ + $error->trace = preg_split('/\R/', $this->getTraceAsString()); // no $this->>getTrace() here -> to much data + } + return $error; + } + + /** + * returns the HTTP response code for the client from exception + * -> if Exception is not handled/catched 'somewhere' this code is used by the final onError handler + * @return int + */ + public function getResponseCode() : int { + return $this->codes[$this->getCode()]; + } +} \ No newline at end of file diff --git a/app/Exception/RegistrationException.php b/app/Exception/RegistrationException.php new file mode 100644 index 000000000..bd0057b2d --- /dev/null +++ b/app/Exception/RegistrationException.php @@ -0,0 +1,46 @@ + 403 + ]; + + /** + * form field name that causes this exception + * @var string + */ + private $field; + + /** + * RegistrationException constructor. + * @param string $message + * @param string $field + */ + public function __construct(string $message, string $field = ''){ + parent::__construct($message, 2000); + $this->field = $field; + } + + /** + * get error object + * @return \stdClass + */ + public function getError() : \stdClass { + $error = parent::getError(); + $error->field = $this->field; + return $error; + } +} \ No newline at end of file diff --git a/app/Exception/ValidationException.php b/app/Exception/ValidationException.php new file mode 100644 index 000000000..b28e1b804 --- /dev/null +++ b/app/Exception/ValidationException.php @@ -0,0 +1,46 @@ + 422 + ]; + + /** + * table column that triggers the exception + * @var string + */ + private $field; + + /** + * ValidationException constructor. + * @param string $message + * @param string $field + */ + public function __construct(string $message, string $field = ''){ + parent::__construct($message, 2000); + $this->field = $field; + } + + /** + * get error object + * @return \stdClass + */ + public function getError() : \stdClass { + $error = parent::getError(); + $error->field = $this->field; + return $error; + } +} \ No newline at end of file diff --git a/app/Lib/Api/AbstractClient.php b/app/Lib/Api/AbstractClient.php new file mode 100644 index 000000000..6579fa52b --- /dev/null +++ b/app/Lib/Api/AbstractClient.php @@ -0,0 +1,367 @@ + can be Redis, Filesystem or Array cachePool + * -> used by e.g. GuzzleCacheMiddleware + * @var CacheItemPoolInterface|null + */ + protected $cachePool = null; + + /** + * @param \Base $f3 + * @return ApiInterface|null + */ + abstract protected function getClient(\Base $f3) : ?ApiInterface; + + /** + * get userAgent + * @return string + */ + protected function getUserAgent() : string { + $userAgent = ''; + $userAgent .= Config::getPathfinderData('name'); + $userAgent .= ' - ' . Config::getPathfinderData('version'); + $userAgent .= ' | ' . Config::getPathfinderData('contact'); + $userAgent .= ' (' . $_SERVER['SERVER_NAME'] . ')'; + return $userAgent; + } + + /** + * returns a new Log object used within the Api for logging + * @return \Closure + */ + protected function newLog() : \Closure { + return function(string $action, string $level = 'warning') : Logging\LogInterface { + $log = new Logging\ApiLog($action, $level); + $log->addHandler('stream', 'json', $this->getStreamConfig($action)); + return $log; + }; + } + + /** + * returns a new instance of PSR-6 compatible CacheItemPoolInterface + * -> this Cache backend will be used across Guzzle Middleware + * e.g. GuzzleCacheMiddleware + * @see http://www.php-cache.com + * @param \Base $f3 + * @return \Closure + */ + protected function getCachePool(\Base $f3) : \Closure { + // determine cachePool options + $poolConfig = $this->getCachePoolConfig($f3); + + return function() use ($poolConfig) : ?CacheItemPoolInterface { + // an active CachePool should be re-used + // -> no need for e.g. a new Redis->pconnect() + // and/or re-init when it is used the next time + if(!is_null($this->cachePool)){ + return $this->cachePool; + } + + // Redis is preferred option (best performance) ----------------------------------------------------------- + if( + $poolConfig['type'] == 'redis' && + extension_loaded('redis') && + class_exists('\Redis') && + class_exists(RedisCachePool::class) + ){ + $client = new \Redis(); + if( + $client->pconnect( + $poolConfig['host'], + $poolConfig['port'], + Config::REDIS_OPT_TIMEOUT, + null, + Config::REDIS_OPT_RETRY_INTERVAL, + Config::REDIS_OPT_READ_TIMEOUT + ) + ){ + if(!empty($poolConfig['auth'])){ + $client->auth($poolConfig['auth']); + } + + if(isset($poolConfig['tag'])){ + $name = 'pathfinder|php|tag:' . strtolower($poolConfig['tag']) . '|pid:' . getmypid(); + $client->client('setname', $name); + } + + if(isset($poolConfig['db'])){ + $client->select($poolConfig['db']); + } + $poolRedis = new RedisCachePool($client); + + // RedisCachePool supports "Hierarchy" store slots + // -> "Hierarchy" support is required to use it in a NamespacedCachePool + // This helps to separate keys by a namespace + // @see http://www.php-cache.com/en/latest/ + $this->cachePool = new NamespacedCachePool($poolRedis, static::CLIENT_NAME); + + register_shutdown_function([$this,'unloadCache'], $client); + } + } + + // Filesystem is second option and fallback for failed Redis pool ----------------------------------------- + if( + is_null($this->cachePool) && + in_array($poolConfig['type'], ['redis', 'folder']) && + class_exists(FilesystemCachePool::class) + ){ + $filesystemAdapter = new Local(\Base::instance()->get('ROOT')); + $filesystem = new Filesystem($filesystemAdapter); + $poolFilesystem = new FilesystemCachePool($filesystem, $poolConfig['folder']); + + $this->cachePool = $poolFilesystem; + } + + // Array cache pool fallback (not persistent) ------------------------------------------------------------- + if( + is_null($this->cachePool) && + in_array($poolConfig['type'], ['redis', 'folder', 'array']) && + class_exists(ArrayCachePool::class) + ){ + $this->cachePool = new ArrayCachePool(2000); + } + + return $this->cachePool; + }; + } + + /** + * get cachePool config from [D]ata [S]ource [N]ame string + * @param \Base $f3 + * @return array + */ + protected function getCachePoolConfig(\Base $f3) : array { + $tag = 'API_CACHE'; + $dsn = (string)$f3->get($tag); + + // fallback + $conf = ['type' => 'array']; + + if(!empty($folder = (string)$f3->get('TEMP'))){ + // filesystem (better than 'array' cache) + $conf = [ + 'type' => 'folder', + 'folder' => $folder . 'cache/' + ]; + } + + // redis or filesystem -> overwrites $conf + Config::parseDSN($dsn, $conf); + + // tag name is used as alias name e.g. for debugging + // -> e.g. for Redis https://redis.io/commands/client-setname + $conf['tag'] = $tag; + + return $conf; + } + + /** + * return callback function that expects a $request and checks + * whether it should be logged (in case of errors) + * @param \Base $f3 + * @return \Closure + */ + protected function isLoggable(\Base $f3) : \Closure { + return function(RequestInterface $request) use ($f3) : bool { + // we need the timestamp for $request that should be checked + // -> we assume $request was "recently" send. -> current server time is used for check + $requestTime = $f3->get('getDateTime')(); + // ... "interpolate" time to short interval + // -> this might help to re-use sequential calls of this method + Util::roundToInterval($requestTime); + // check if request was send within ESI downTime range + // -> errors during downTime should not be logged + $inDowntimeRange = Config::inDownTimeRange($requestTime); + + return !$inDowntimeRange; + }; + } + + /** + * get Logger + * @param string $ype + * @return \Log + */ + protected function getLogger(string $ype = 'ERROR') : \Log { + return LogController::getLogger($ype); + } + /** + * get error msg for missing $this->client class + * @param string $class + * @return string + */ + protected function getMissingClassError(string $class) : string { + return sprintf(Config::ERROR_CLASS_NOT_EXISTS_COMPOSER, $class); + } + + /** + * get error msg for undefined method in $this->client class + * @param string $class + * @param string $method + * @return string + */ + protected function getMissingMethodError(string $class, string $method) : string { + return sprintf(Config::ERROR_METHOD_NOT_EXISTS_COMPOSER, $method, $class); + } + + /** + * get config for stream logging + * @param string $logFileName + * @param bool $abs + * @return \stdClass + */ + protected function getStreamConfig(string $logFileName, bool $abs = false) : \stdClass { + $f3 = \Base::instance(); + + $config = (object) []; + $config->stream = ''; + if( $f3->exists('LOGS', $dir) ){ + $config->stream .= $abs ? $f3->get('ROOT') . '/' : './'; + $config->stream .= $dir . $logFileName . '.log'; + $config->stream = $f3->fixslashes($config->stream); + } + return $config; + } + + /** + * unload function + * @param \Redis $client + */ + public function unloadCache(\Redis $client){ + if($client->isConnected()){ + $client->close(); + } + } + + /** + * call request API data + * @param string $name + * @param array $arguments + * @return array|mixed + */ + public function __call(string $name, array $arguments = []){ + $return = []; + if(is_object($this->client)){ + if(method_exists($this->client, $name)){ + $return = call_user_func_array([$this->client, $name], $arguments); + }else{ + $errorMsg = $this->getMissingMethodError(get_class($this->client), $name); + $this->getLogger('ERROR')->write($errorMsg); + \Base::instance()->error(501, $errorMsg); + } + }else{ + \Base::instance()->error(501, self::ERROR_CLIENT_INVALID); + } + + return $return; + } + + /** + * init web client on __invoke() + * -> no need to init client on __construct() + * maybe it is never used... + * @return AbstractClient + */ + function __invoke() : self { + $f3 = \Base::instance(); + + if( + !($this->client instanceof ApiInterface) && + ($this->getClient($f3) instanceof ApiInterface) + ){ + // web client not initialized + $client = $this->getClient($f3); + $client->setTimeout(5); + $client->setConnectTimeout(5); + $client->setUserAgent($this->getUserAgent()); + $client->setDecodeContent('gzip, deflate'); + + $client->setDebugLevel($f3->get('DEBUG')); + $client->setNewLog($this->newLog()); + $client->setIsLoggable($this->isLoggable($f3)); + + $client->setLogStats(true); // add cURL stats (e.g. transferTime) to loggable requests + $client->setLogCache(true); // add cache info (e.g. from cached) to loggable requests + $client->setLogAllStatus(false); // log all requests regardless of response HTTP status code + $client->setLogRequestHeaders(false); // add request HTTP headers to loggable requests + $client->setLogResponseHeaders(false); // add response HTTP headers to loggable requests + $client->setLogFile('esi_requests'); + + $client->setRetryLogFile('esi_retry_requests'); + + $client->setCacheDebug(true); + $client->setCachePool($this->getCachePool($f3)); + + + //$client->setProxy('127.0.0.1:8888'); // use local proxy server for debugging requests + + // disable SSL certificate verification -> allow proxy to decode(view) request + //$client->setVerify(false); + + //$client->setDebugRequests(true); + + $this->client = $client; + } + + return $this; + } +} \ No newline at end of file diff --git a/app/Lib/Api/CcpClient.php b/app/Lib/Api/CcpClient.php new file mode 100644 index 000000000..0d7c8933c --- /dev/null +++ b/app/Lib/Api/CcpClient.php @@ -0,0 +1,44 @@ +setDataSource(Config::getEnvironmentData('CCP_ESI_DATASOURCE')); + }else{ + $this->getLogger()->write($this->getMissingClassError(Client::class)); + } + + return $client; + } +} \ No newline at end of file diff --git a/app/Lib/Api/EveScoutClient.php b/app/Lib/Api/EveScoutClient.php new file mode 100644 index 000000000..1d124a33a --- /dev/null +++ b/app/Lib/Api/EveScoutClient.php @@ -0,0 +1,38 @@ +getLogger()->write($this->getMissingClassError(Client::class)); + } + + return $client; + } +} \ No newline at end of file diff --git a/app/Lib/Api/GitHubClient.php b/app/Lib/Api/GitHubClient.php new file mode 100644 index 000000000..d87d6c23d --- /dev/null +++ b/app/Lib/Api/GitHubClient.php @@ -0,0 +1,43 @@ +getLogger()->write($this->getMissingClassError(Client::class)); + } + + return $client; + } +} \ No newline at end of file diff --git a/app/Lib/Api/SsoClient.php b/app/Lib/Api/SsoClient.php new file mode 100644 index 000000000..30c473273 --- /dev/null +++ b/app/Lib/Api/SsoClient.php @@ -0,0 +1,40 @@ +getLogger()->write($this->getMissingClassError(Client::class)); + } + + return $client; + } +} \ No newline at end of file diff --git a/app/Lib/Config.php b/app/Lib/Config.php new file mode 100644 index 000000000..000b28c77 --- /dev/null +++ b/app/Lib/Config.php @@ -0,0 +1,686 @@ + use "," as delimiter in config files/data + */ + const ARRAY_KEYS = ['CCP_ESI_SCOPES', 'CCP_ESI_SCOPES_ADMIN']; + + /** + * custom HTTP status codes + */ + const + HTTP_422 = 'Unprocessable Entity'; + + /** + * all environment data + * @var array + */ + private $serverConfigData = []; + + /** + * Config constructor. + * @param \Base $f3 + */ + public function __construct(\Base $f3){ + // set server data + // -> CGI params (Nginx) + // -> .htaccess (Apache) + $this->setServerData(); + // set environment data + $this->setAllEnvironmentData($f3); + // set hive configuration variables + // -> overwrites default configuration + $this->setHiveVariables($f3); + + // set global getter for \DateTimeZone + $f3->set('getTimeZone', function() use ($f3) : \DateTimeZone { + return new \DateTimeZone( $f3->get('TZ') ); + }); + + // set global getter for new \DateTime + $f3->set('getDateTime', function(string $time = 'now', ?\DateTimeZone $timeZone = null) use ($f3) : \DateTime { + $timeZone = $timeZone ? : $f3->get('getTimeZone')(); + return new \DateTime($time, $timeZone); + }); + + // database connection pool ----------------------------------------------------------------------------------- + $f3->set(Pool::POOL_NAME, Pool::instance( + function(string $alias) use ($f3) : array { + // get DB config by alias for new connections + return self::getDatabaseConfig($f3, $alias); + }, + function(string $schema) use ($f3) : array { + // get DB requirement vars from requirements.ini + return self::getRequiredDbVars($f3, $schema); + } + )); + + // lazy init Web Api clients ---------------------------------------------------------------------------------- + $f3->set(SsoClient::CLIENT_NAME, SsoClient::instance()); + $f3->set(CcpClient::CLIENT_NAME, CcpClient::instance()); + $f3->set(GitHubClient::CLIENT_NAME, GitHubClient::instance()); + $f3->set(EveScoutClient::CLIENT_NAME, EveScoutClient::instance()); + + // Socket connectors ------------------------------------------------------------------------------------------ + $f3->set(TcpSocket::SOCKET_NAME, function(array $options = ['timeout' => 1]) : SocketInterface { + return AbstractSocket::factory(TcpSocket::class, self::getSocketUri(), $options); + }); + } + + /** + * get environment configuration data + * @param \Base $f3 + * @return array|null + */ + protected function getAllEnvironmentData(\Base $f3){ + if(!$f3->exists(self::HIVE_KEY_ENVIRONMENT, $environmentData)){ + $environmentData = $this->setAllEnvironmentData($f3); + } + + return $environmentData; + } + + /** + * set/overwrite some global framework variables original set in config.ini + * -> can be overwritten in environments.ini OR ENV-Vars + * -> see: https://github.com/exodus4d/pathfinder/issues/175 + * that depend on environment settings + * @param \Base $f3 + */ + protected function setHiveVariables(\Base $f3){ + // hive keys that can be overwritten + $hiveKeys = ['BASE', 'URL', 'DEBUG', 'CACHE']; + + foreach($hiveKeys as $key){ + if( !is_null( $var = self::getEnvironmentData($key)) ){ + $f3->set($key,$var); + } + } + } + + /** + * set all environment configuration data + * @param \Base $f3 + * @return array|mixed|null + */ + protected function setAllEnvironmentData(\Base $f3){ + $environmentData = null; + + if( !empty($this->serverConfigData['ENV']) ){ + // get environment config from $_SERVER data + $environmentData = (array)$this->serverConfigData['ENV']; + + // some environment variables should be parsed as array + array_walk($environmentData, function(&$item, $key){ + $item = (in_array($key, self::ARRAY_KEYS)) ? explode(',', $item) : $item; + }); + + $environmentData['ENVIRONMENT_CONFIG'] = 'PHP: environment variables'; + }else{ + // get environment data from *.ini file config + $customConfDir = $f3->get('CONF'); + + // check "custom" ini dir, of not found check default ini dir + foreach($customConfDir as $type => $path){ + $envConfFile = $path . 'environment.ini'; + $f3->config($envConfFile, true); + + if( + $f3->exists(self::HIVE_KEY_ENVIRONMENT) && + ($environment = $f3->get(self::HIVE_KEY_ENVIRONMENT . '.SERVER')) && + ($environmentData = $f3->get(self::HIVE_KEY_ENVIRONMENT . '.' . $environment)) + ){ + $environmentData['ENVIRONMENT_CONFIG'] = 'Config: ' . $envConfFile; + break; + } + } + } + + if( !is_null($environmentData) ){ + ksort($environmentData); + $f3->set(self::HIVE_KEY_ENVIRONMENT, $environmentData); + } + + return $environmentData; + } + + /** + * get/extract all server data passed to PHP + * this can be done by either: + * OS Environment variables: + * -> add to /etc/environment + * OR: + * Nginx (server config): + * -> FastCGI syntax + * fastcgi_param PF-ENV-DEBUG 3; + */ + protected function setServerData(){ + $data = []; + foreach($_SERVER as $key => $value){ + if(strpos($key, self::PREFIX_KEY . self::ARRAY_DELIMITER) === 0){ + $path = explode( self::ARRAY_DELIMITER, $key); + // remove prefix + array_shift($path); + + $tmp = &$data; + foreach($path as $segment){ + $tmp[$segment] = (array)$tmp[$segment]; + $tmp = &$tmp[$segment]; + } + + // type cast values + // (e.g. '1.2' => (float); '4' => (int),...) + $tmp = is_numeric($value) ? $value + 0 : $value; + } + } + + $this->serverConfigData = $data; + } + + /** + * get a environment variable by hive key + * @param $key + * @return string|null + */ + static function getEnvironmentData($key){ + $hiveKey = self::HIVE_KEY_ENVIRONMENT . '.' . $key; + \Base::instance()->exists($hiveKey, $data); + return $data; + } + + /** + * get database config values + * @param \Base $f3 + * @param string $alias + * @return array + */ + static function getDatabaseConfig(\Base $f3, string $alias) : array { + $alias = strtoupper($alias); + + $config = [ + 'ALIAS' => $alias, + 'SCHEME' => 'mysql', + 'HOST' => 'localhost', + 'PORT' => 3306, + 'SOCKET' => null, + 'NAME' => self::getEnvironmentData('DB_' . $alias . '_NAME'), + 'USER' => self::getEnvironmentData('DB_' . $alias . '_USER'), + 'PASS' => self::getEnvironmentData('DB_' . $alias . '_PASS') + ]; + + $pdoReg = '/^(?[[:alpha:]]+):((host=(?[a-zA-Z0-9-_\.]*))|(unix_socket=(?[a-zA-Z0-9\/]*\.sock)))((;dbname=(?\w*))|(;port=(?\d*))){0,2}/'; + if(preg_match($pdoReg, self::getEnvironmentData('DB_' . $alias . '_DNS'), $matches)){ + // remove unnamed matches + $matches = array_intersect_key($matches, $config); + // remove empty matches + $matches = array_filter($matches); + // merge matches with default config + $config = array_merge($config, $matches); + } + + // connect options -------------------------------------------------------------------------------------------- + $options = [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + \PDO::ATTR_TIMEOUT => $f3->get('REQUIREMENTS.MYSQL.PDO_TIMEOUT') + ]; + + if($config['SCHEME'] == 'mysql'){ + $options[\PDO::MYSQL_ATTR_COMPRESS] = true; + $options[\PDO::MYSQL_ATTR_INIT_COMMAND] = implode(',', [ + "SET NAMES " . self::getRequiredDbVars($f3, $config['SCHEME'])['CHARACTER_SET_CONNECTION'] . " COLLATE " . self::getRequiredDbVars($f3, $config['SCHEME'])['COLLATION_CONNECTION'], + "@@session.time_zone = '+00:00'", + "@@session.default_storage_engine = " . self::getRequiredDbVars($f3, $config['SCHEME'])['DEFAULT_STORAGE_ENGINE'] + ]); + } + + if(self::getPathfinderData('experiments.persistent_db_connections')){ + $options[\PDO::ATTR_PERSISTENT] = true; + } + + $config['OPTIONS'] = $options; + + return $config; + } + + /** + * get required MySQL variables from requirements.ini + * @param \Base $f3 + * @param string $schema + * @return array + */ + static function getRequiredDbVars(\Base $f3, string $schema) : array { + return $f3->exists('REQUIREMENTS[' . strtoupper($schema) . '][VARS]', $vars) ? $vars : []; + } + + /** + * get SMTP config values + * @return \stdClass + */ + static function getSMTPConfig() : \stdClass{ + $config = new \stdClass(); + $config->host = self::getEnvironmentData('SMTP_HOST'); + $config->port = self::getEnvironmentData('SMTP_PORT'); + $config->scheme = self::getEnvironmentData('SMTP_SCHEME'); + $config->username = self::getEnvironmentData('SMTP_USER'); + $config->password = self::getEnvironmentData('SMTP_PASS'); + $config->from = [ + self::getEnvironmentData('SMTP_FROM') => self::getPathfinderData('name') + ]; + return $config; + } + + /** + * validates an SMTP config + * @param \stdClass $config + * @return bool + */ + static function isValidSMTPConfig(\stdClass $config) : bool { + // validate email from either an configured array or plain string + $validateMailConfig = function($mailConf = null) : bool { + $email = null; + if(is_array($mailConf)){ + reset($mailConf); + $email = key($mailConf); + }elseif(is_string($mailConf)){ + $email = $mailConf; + } + return \Audit::instance()->email($email); + }; + + return ( + !empty($config->host) && + !empty($config->username) && + $validateMailConfig($config->from) && + $validateMailConfig($config->to) + ); + } + + /** + * get email for notifications by hive key + * @param $key + * @return mixed + */ + static function getNotificationMail($key){ + return self::getPathfinderData('notification' . ($key ? '.' . $key : '')); + } + + /** + * get map default config values for map types (private/corp/ally) + * -> read from pathfinder.ini + * @param string $mapType + * @return mixed + */ + static function getMapsDefaultConfig($mapType = ''){ + if( $mapConfig = self::getPathfinderData('map' . ($mapType ? '.' . $mapType : '')) ){ + $mapConfig = Util::arrayChangeKeyCaseRecursive($mapConfig); + } + + return $mapConfig; + } + + /** + * get Plugin config from `plugin.ini` + * @param string|null $key + * @param bool $checkEnabled + * @return array|null + */ + static function getPluginConfig(?string $key, bool $checkEnabled = true) : ?array { + $isEnabled = $checkEnabled ? + filter_var(\Base::instance()->get( + self::HIVE_KEY_PLUGIN . '.' . strtoupper($key) . '_ENABLED'), + FILTER_VALIDATE_BOOLEAN + ) : + true; + + $data = null; + if($isEnabled){ + $hiveKey = self::HIVE_KEY_PLUGIN . '.' . strtoupper($key); + $data = (array)\Base::instance()->get($hiveKey); + } + return $data; + } + + /** + * use this function to "validate" the socket connection. + * The result will be CACHED for a few seconds! + * This function is intended to pre-check a Socket connection if it MIGHT exists. + * No data will be send to the Socket, this function just validates if a socket is available + * -> see pingDomain() + * @param string $uri + * @return bool + */ + static function validSocketConnect(string $uri) : bool{ + $valid = false; + $f3 = \Base::instance(); + + if( !$f3->exists(self::CACHE_KEY_SOCKET_VALID, $valid) ){ + if( $socketUrl = self::getSocketUri() ){ + // get socket URI parts -> not elegant... + $domain = parse_url( $socketUrl, PHP_URL_SCHEME) . '://' . parse_url( $socketUrl, PHP_URL_HOST); + $port = parse_url( $socketUrl, PHP_URL_PORT); + // check connection -> get ms + $status = self::pingDomain($domain, $port); + if($status >= 0){ + // connection OK + $valid = true; + }else{ + // connection error/timeout + $valid = false; + } + }else{ + // requirements check failed or URL not valid + $valid = false; + } + + $f3->set(self::CACHE_KEY_SOCKET_VALID, $valid, self::CACHE_TTL_SOCKET_VALID); + } + + return $valid; + } + + /** + * get response time for a host in ms or -1 on error/timeout + * @param string $domain + * @param int $port + * @param int $timeout + * @return int + */ + static function pingDomain(string $domain, int $port, $timeout = 1) : int { + $startTime = microtime(true); + $file = @fsockopen ($domain, $port, $errno, $errstr, $timeout); + $stopTime = microtime(true); + + if (!$file){ + // Site is down + $status = -1; + }else { + fclose($file); + $status = ($stopTime - $startTime) * 1000; + $status = floor($status); + } + return $status; + } + + /** + * get URI for TCP socket + * @return bool|string + */ + static function getSocketUri(){ + $uri = false; + + if( + ( $ip = self::getEnvironmentData('SOCKET_HOST') ) && + ( $port = self::getEnvironmentData('SOCKET_PORT') ) + ){ + $uri = 'tcp://' . $ip . ':' . $port; + } + return $uri; + } + + /** + * @param string $key + * @return null|mixed + */ + static function getPathfinderData($key = ''){ + $hiveKey = self::HIVE_KEY_PATHFINDER . ($key ? '.' . strtoupper($key) : ''); + if( !\Base::instance()->exists($hiveKey, $data) ){ + $data = null; + } + return $data; + } + + /** + * get HTTP status by HTTP return code + * -> either from F3 or from self::Config constants + * @param int $code + * @return string + */ + static function getHttpStatusByCode(int $code) : string { + if(empty($status = @constant('Base::HTTP_' . $code))){ + $status = @constant('self::HTTP_' . $code); + } + return $status; + } + + /** + * parse [D]ata [S]ource [N]ame string from *.ini into $conf parts + * -> $dsn = redis=localhost:6379:2 + * $conf = ['type' => 'redis', 'host' => 'localhost', 'port' => 6379, 'db' => 2] + * -> some $conf values might be NULL if not found in $dsn! + * -> some missing values become defaults + * @param string $dsn + * @param array|null $conf + * @return bool + */ + static function parseDSN(string $dsn, ?array &$conf = []) : bool { + // reset reference + if($matches = (bool)preg_match('/^(\w+)\h*=\h*(.+)/', strtolower(trim($dsn)), $parts)){ + $conf['type'] = $parts[1]; + if($conf['type'] == 'redis'){ + [$conf['host'], $conf['port'], $conf['db'], $conf['auth']] = explode(':', $parts[2]) + [1 => 6379, 2 => null, 3 => null]; + }elseif($conf['type'] == 'folder'){ + $conf['folder'] = $parts[2]; + } + // int cast numeric values + $conf = array_map(function($val){ + return is_numeric($val) ? intval($val) : $val; + }, $conf); + } + return $matches; + } + + /** + * check if a given DateTime() is within downTime range: downtime + 10m + * -> can be used for prevent logging errors during downTime + * @param \DateTime|null $dateCheck + * @return bool + */ + static function inDownTimeRange(\DateTime $dateCheck = null) : bool { + $inRange = false; + // default daily downtime 00:00am + $downTimeParts = [0, 0]; + if( !empty($downTime = (string)self::getEnvironmentData('CCP_SSO_DOWNTIME')) ){ + $parts = array_map('intval', explode(':', $downTime)); + if(count($parts) === 2){ + // well formatted DOWNTIME found in config files + $downTimeParts = $parts; + } + } + + try{ + // downTime Range is 10m + $downtimeLength = self::DOWNTIME_LENGTH + (2 * self::DOWNTIME_BUFFER); + $timezone = \Base::instance()->get('getTimeZone')(); + + // if not set -> use current time + $dateCheck = is_null($dateCheck) ? new \DateTime('now', $timezone) : $dateCheck; + $dateDowntimeStart = new \DateTime('now', $timezone); + $dateDowntimeStart->setTime($downTimeParts[0],$downTimeParts[1]); + $dateDowntimeStart->sub(new \DateInterval('PT' . self::DOWNTIME_BUFFER . 'M')); + + $dateDowntimeEnd = clone $dateDowntimeStart; + $dateDowntimeEnd->add(new \DateInterval('PT' . $downtimeLength . 'M')); + + $dateRange = new DateRange($dateDowntimeStart, $dateDowntimeEnd); + $inRange = $dateRange->inRange($dateCheck); + }catch(\Exception $e){ + $f3 = \Base::instance(); + $f3->error(500, $e->getMessage(), $e->getTrace()); + } + + return $inRange; + } + + /** + * format timeInterval in seconds into human readable string + * @param int $seconds + * @return string + * @throws \Exception + */ + static function formatTimeInterval(int $seconds = 0) : string { + $dtF = new \DateTime('@0'); + $dtT = new \DateTime("@" . $seconds); + $diff = $dtF->diff($dtT); + + $format = ($d = $diff->format('%d')) ? $d . 'd ' : ''; + $format .= ($h = $diff->format('%h')) ? $h . 'h ' : ''; + $format .= ($i = $diff->format('%i')) ? $i . 'm ' : ''; + $format .= ($s = $diff->format('%s')) ? $s . 's' : ''; + return $format; + } + + /** + * @param $fromExists + * @param int $ttlMax + * @return int + */ + static function ttlLeft($fromExists, int $ttlMax) : int { + $ttlMax = max($ttlMax, 0); + if($fromExists){ + // == true || array + if(is_array($fromExists)){ + return max(min((int)ceil(round(array_sum($fromExists) - microtime(true), 4)), $ttlMax), 0); + }else{ + return 0; + } + }else{ + // == false + return $ttlMax; + } + } + + /** + * @param string|null $class + * @return string + */ + static function withNamespace(?string $class) : string { + $path = [\Base::instance()->get('NAMESPACE')]; + if($class){ + $path[] = $class; + } + return implode('\\', $path); + } +} \ No newline at end of file diff --git a/app/Lib/Cron.php b/app/Lib/Cron.php new file mode 100644 index 000000000..026e82610 --- /dev/null +++ b/app/Lib/Cron.php @@ -0,0 +1,161 @@ + cronJobs that exceed avg. exec. time + DEFAULT_BUFFER_EXEC_TIME show warnings + */ + const DEFAULT_BUFFER_EXEC_TIME = 20; + + /** + * execution memory buffer in percent + * -> cronJobs that exceed avg. mem. peak + DEFAULT_BUFFER_MEM_PEAK show warnings + */ + const DEFAULT_BUFFER_MEM_PEAK = 20; + + /** + * extends parent::isDue() + * -> adds check for "paused" jobs + * @param string $job + * @param int $time + * @return bool + */ + public function isDue($job, $time){ + if($isDue = parent::isDue($job, $time)){ + // check if job is not paused + if($job = $this->getJob($job)){ + if($job->valid() && $job->isPaused){ + $isDue = false; + } + } + } + return $isDue; + } + + public function execute($job, $async = true) { + return parent::execute($job, $async); + } + + /** + * @param $name + * @return string + */ + public function __get($name){ + if(in_array($name, ['jobs'])){ + return $this->$name; + }else{ + return parent::__get($name); + } + } + + /** + * @param array $jobConf + * @return array + */ + public function getJobDataFromConf(array $jobConf) : array { + return ['handler' => $jobConf[0], 'expr' => $jobConf[1]]; + } + + /** + * get all configured cronjobs (read from cron.ini) + * @param array $names + * @return array + */ + public function getJobsConfig(array $names = []) : array { + $config = []; + + $jobs = array_filter($this->jobs, function(string $name) use ($names) : bool { + return !empty($names) ? in_array($name, $names) : true; + }, ARRAY_FILTER_USE_KEY ); + + foreach($jobs as $name => $jobConf){ + $jobConf = $this->getJobDataFromConf($jobConf); + if($job = $this->registerJob($name, $jobConf)){ + // get job config from DB + $config[$name] = $job->getData(); + }else{ + // job registration failed (e.g. DB connect failed) -> return min config from cron.ini + $jobConf = (object)$jobConf; + $jobConf->status = ['dbError' => Pathfinder\CronModel::STATUS['dbError']]; + $jobConf->history = []; + $config[$name] = $jobConf; + } + $config[$name]->exprPreset = $this->checkPreset($config[$name]->expr); + } + ksort($config); + return $config; + } + + /** + * @param string $name + * @param array $jobConf + * @return mixed|void + */ + public function registerJob(string $name, array $jobConf){ + // 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(); + } + } + \Base::instance()->DB->setSilent(false); + } + + /** + * find CronModel by job $name + * @param string $name + * @return Pathfinder\CronModel|null + */ + public function getJob(string $name) : ?Pathfinder\CronModel { + $job = null; + try{ + /** + * @var $job Pathfinder\CronModel + */ + $jobModel = Pathfinder\AbstractPathfinderModel::getNew('CronModel'); + // we need to check if table exists here + // if not we get an error for later insert/update SQL actions + // -> e.g. if job is triggered manually on CLI + if($jobModel->tableExists()){ + $jobModel->getByForeignKey('name', $name); + $job = $jobModel; + } + }catch(\Exception $e){ + // Cron DB table not exists or other DB issues... + } + + return $job; + } + + /** + * check expression for a preset + * @param string $expr + * @return bool + */ + protected function checkPreset(string $expr){ + if(preg_match('/^@(\w+)$/', $expr,$m)){ + if(!isset($this->presets[$m[1]])) + return false; + return $this->presets[$m[1]]; + } + return false; + } +} \ No newline at end of file diff --git a/app/Lib/DateRange.php b/app/Lib/DateRange.php new file mode 100644 index 000000000..f349c7f1b --- /dev/null +++ b/app/Lib/DateRange.php @@ -0,0 +1,55 @@ +from = $from; + $this->to = $to; + } else { + $this->from = $to; + $this->to = $from; + } + } + } + + /** + * check if DateTime $dateCheck is within this range + * @param \DateTime $dateCheck + * @return bool + */ + public function inRange(\DateTime $dateCheck) : bool { + return $dateCheck >= $this->from && $dateCheck <= $this->to; + } +} \ No newline at end of file diff --git a/app/Lib/Db/Pool.php b/app/Lib/Db/Pool.php new file mode 100644 index 000000000..3798ea904 --- /dev/null +++ b/app/Lib/Db/Pool.php @@ -0,0 +1,236 @@ +getConfig = $getConfig; + $this->requiredVars = $requiredVars; + } + + /** + * set "silent" mode (no error logging) + * -> optional clear $this->errors + * @param bool $silent + * @param bool $clearErrors + */ + public function setSilent(bool $silent, bool $clearErrors = false){ + $this->silent = $silent; + if($clearErrors){ + $this->errors = []; + } + } + + /** + * @return bool + */ + public function isSilent() : bool { + return $this->silent; + } + + /** + * connect to the DB server itself -> NO database is used + * -> can be used to check if a certain DB exists without connecting to it directly + * @param string $alias + * @return Sql|null + */ + public function connectToServer(string $alias) : ?Sql { + $config = ($this->getConfig)($alias); + $config['NAME'] = ''; + return $this->newDB($config); + } + + /** + * tries to create a database if not exists + * -> DB user needs rights to create a DB + * @param string $alias + * @return Sql|null + */ + public function createDB(string $alias) : ?Sql { + $db = null; + $config = ($this->getConfig)($alias); + // remove database from $dsn (we want to crate it) + $newDbName = $config['NAME']; + if(!empty($newDbName)){ + $config['NAME'] = ''; + + $db = $this->newDB($config); + if(!is_null($db)){ + $schema = new Schema($db); + if(!in_array($newDbName, $schema->getDatabases())){ + $db->exec("CREATE DATABASE IF NOT EXISTS + `" . $newDbName . "` DEFAULT CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci;"); + $db->exec("USE `" . $newDbName . "`"); + + // check if DB create was successful + $dbCheck = $db->exec("SELECT DATABASE()"); + if( + !empty($dbCheck[0]) && + !empty($checkDbName = reset($dbCheck[0])) && + $checkDbName == $newDbName + ){ + // prepare new created DB + $requiredVars = ($this->requiredVars)($db->driver()); + $db->prepareDatabase($requiredVars['CHARACTER_SET_DATABASE'], $requiredVars['COLLATION_DATABASE']); + } + } + } + } + + return $db; + } + + /** + * get active connection from store or init new connection + * @param string $alias + * @return Sql|null + */ + public function getDB(string $alias) : ?Sql { + if(!isset($this->connectionStore[$alias])){ + $db = $this->newDB(($this->getConfig)($alias)); + if(!is_null($db)){ + $this->connectionStore[$alias] = $db; + } + return $db; + }else{ + return $this->connectionStore[$alias]; + } + } + + /** + * get last recent Exceptions from error history + * @param string $alias + * @param int $limit + * @return \Exception[] + */ + public function getErrors(string $alias, int $limit = 1) : array { + return array_slice((array)$this->errors[$alias] , 0, $limit); + } + + /** + * build PDO DNS connect string from DB config array + * -> Hint: dbName is not part of the DNS we need -> passed as extra parameter + * @param array $config + * @return string + */ + protected function buildDnsFromConfig(array $config) : string { + $dns = $config['SCHEME'] . ':'; + $dns .= $config['SOCKET'] ? 'unix_socket=' . $config['SOCKET'] : 'host=' . $config['HOST']; + $dns .= $config['PORT'] && !$config['SOCKET'] ? ';port=' . $config['PORT'] : ''; + $dns .= $config['NAME'] ? ';dbname=' . $config['NAME'] : ''; + return $dns; + } + + /** + * @param array $config + * @return Sql|null + */ + protected function newDB(array $config) : ?Sql { + $db = null; + + if($config['SCHEME'] == 'mysql'){ + try{ + $db = new Sql($this->buildDnsFromConfig($config), $config['USER'], $config['PASS'], $config['OPTIONS']); + }catch(\PDOException $e){ + $this->pushError($config['ALIAS'], $e); + + if(!$this->isSilent()){ + self::getLogger()->write($e); + } + } + }else{ + // unsupported DB type + $this->pushError($config['ALIAS'], new ConfigException( + sprintf(self::ERROR_SCHEME, $config['SCHEME'], $config['ALIAS'])) + ); + } + + return $db; + } + + /** + * push new Exception into static error history + * @param string $alias + * @param \Exception $e + */ + protected function pushError(string $alias, \Exception $e){ + if(!is_array($this->errors[$alias])){ + $this->errors[$alias] = []; + } + + // prevent adding same errors twice + if(!empty($this->errors[$alias])){ + /** + * @var $lastError \Exception + */ + $lastError = array_values($this->errors[$alias])[0]; + if($lastError->getMessage() === $e->getMessage()){ + return; + } + } + + array_unshift($this->errors[$alias], $e); + if(count($this->errors[$alias]) > 5){ + $this->errors[$alias] = array_pop($this->errors[$alias]); + } + } + + /** + * @return \Log + */ + static function getLogger() : \Log { + return LogController::getLogger('ERROR'); + } +} \ No newline at end of file diff --git a/app/Lib/Db/Sql.php b/app/Lib/Db/Sql.php new file mode 100644 index 000000000..3153e4ac2 --- /dev/null +++ b/app/Lib/Db/Sql.php @@ -0,0 +1,118 @@ +dsn; + } + + /** + * get all table names + * @return array|bool + */ + public function getTables(){ + $schema = new Schema($this); + return $schema->getTables(); + } + + /** + * checks whether a table exists or not + * @param string $table + * @return bool + */ + public function tableExists(string $table) : bool { + return in_array($table, $this->getTables()); + } + + /** + * get current row (data) count for an existing table + * -> returns 0 if table not exists or empty + * @param string $table + * @return int + */ + public function getRowCount(string $table) : int { + $count = 0; + if($this->tableExists($table)){ + $countRes = $this->exec("SELECT COUNT(*) `num` FROM " . $this->quotekey($table)); + if(isset($countRes[0]['num'])){ + $count = (int)$countRes[0]['num']; + } + } + return $count; + } + + /** + * @param string|null $table + * @return array|null + */ + public function getTableStatus(?string $table) : ?array { + $status = null; + $sql = "SHOW TABLE STATUS"; + $args = null; + if(!empty($table)){ + $sql .= " LIKE :table"; + $args = [ + ':table' => $table + ]; + } + + if(!empty($statusRes = $this->exec($sql, $args))){ + if(!empty($table)){ + $status = reset($statusRes); + }else{ + $status = $statusRes; + } + } + + return $status; + } + + /** + * set some default config for this DB + * @param string $characterSetDatabase + * @param string $collationDatabase + */ + public function prepareDatabase(string $characterSetDatabase, string $collationDatabase){ + if($this->name() && $characterSetDatabase && $collationDatabase){ + // set/change default "character set" and "collation" + $this->exec('ALTER DATABASE ' . $this->quotekey($this->name()) + . ' CHARACTER SET ' . $characterSetDatabase + . ' COLLATE ' . $collationDatabase + ); + } + } + + /** + * @see https://fatfreeframework.com/3.6/sql#exec + * @param array|string $cmds + * @param null $args + * @param int $ttl + * @param bool $log (we use false as default parameter) + * @param bool $stamp + * @return array|FALSE|int + */ + function exec($cmds, $args = null, $ttl = 0, $log = false, $stamp = false) { + return parent::exec($cmds, $args, $ttl, $log, $stamp); + } +} \ No newline at end of file diff --git a/app/Lib/Format/Image.php b/app/Lib/Format/Image.php new file mode 100644 index 000000000..90a79ad58 --- /dev/null +++ b/app/Lib/Format/Image.php @@ -0,0 +1,58 @@ + [ + 'variant' => 'logo', + 'size' => 64 // 64 is less 'blurry' with CSS downscale to 32 than native 32 + ], + 'corporations' => [ + 'variant' => 'logo', + 'size' => 64 // 64 is less 'blurry' with CSS downscale to 32 than native 32 + ], + 'characters' => [ + 'variant' => 'portrait', + 'size' => 32 // 32 is fine here, no visual difference to 64 + ], + 'types' => [ + 'variant' => 'icon', // 'render' also works, 64px size is max for 'icon' + 'size' => 64 // 64 is less 'blurry' with CSS downscale to 32 than native 32 + ] + ]; + + /** + * build image server src URL + * @param string $resourceType + * @param int $resourceId + * @param int|null $size + * @param string|null $resourceVariant + * @return string|null + */ + public function eveSrcUrl(string $resourceType, int $resourceId, ?int $size = null, ?string $resourceVariant = null) : ?string { + $url = null; + if( + $resourceId && + ($serviceUrl = rtrim(Config::getPathfinderData('api.ccp_image_server'), '/')) && + ($defaults = static::DEFAULT_EVE_SRC_CONFIG[$resourceType]) + ){ + $parts = [$serviceUrl, $resourceType, $resourceId, $resourceVariant ? : $defaults['variant']]; + $url = implode('/', $parts); + + $params = ['size' => $size ? : $defaults['size']]; + $url .= '?' . http_build_query($params); + } + + return $url; + } +} \ No newline at end of file diff --git a/app/Lib/Format/Number.php b/app/Lib/Format/Number.php new file mode 100644 index 000000000..edb664001 --- /dev/null +++ b/app/Lib/Format/Number.php @@ -0,0 +1,24 @@ +setChannelData($channelData); + + // add log processor -> remove $channelData from log + $processorClearChannelData = function($record){ + $record['context'] = array_diff_key($record['context'], $this->getChannelData()); + return $record; + }; + + // init processorConfig. IMPORTANT: first processor gets executed at the end! + $this->processorConfig = ['clearChannelData' => $processorClearChannelData] + $this->processorConfig; + } + + /** + * @param array $channelData + */ + protected function setChannelData(array $channelData){ + $this->channelData = $channelData; + } + + /** + * @return array + */ + public function getChannelData() : array{ + return $this->channelData; + } + + /** + * @return int + */ + public function getChannelId() : int{ + return (int)$this->getChannelData()['channelId']; + } + + /** + * @return string + */ + public function getChannelName() : string{ + return (string)$this->getChannelData()['channelName']; + } + + /** + * @return array + */ + public function getData() : array{ + $data['main'] = parent::getData(); + + if(!empty($channelLogData = $this->getChannelData())){ + $channelData['channel'] = $channelLogData; + $data = $channelData + $data; + } + + return $data; + } + + /** + * @return array + */ + public function getContext(): array{ + $context = parent::getContext(); + + // add temp data (e.g. used for $message placeholder replacement + $context += $this->getChannelData(); + + return $context; + } +} \ No newline at end of file diff --git a/app/Lib/Logging/AbstractCharacterLog.php b/app/Lib/Logging/AbstractCharacterLog.php new file mode 100644 index 000000000..889a0433d --- /dev/null +++ b/app/Lib/Logging/AbstractCharacterLog.php @@ -0,0 +1,86 @@ + remove $channelData from log + $processorAddThumbData = function($record){ + $record['extra']['thumb']['url'] = $this->getThumbUrl(); + return $record; + }; + + // init processorConfig. IMPORTANT: first processor gets executed at the end! + $this->processorConfig = ['addThumbData' => $processorAddThumbData] + $this->processorConfig; + } + + /** + * CharacterModel $character + * @param CharacterModel $character + * @return LogInterface + */ + public function setCharacter(CharacterModel $character): LogInterface{ + $this->character = $character; + return $this; + } + + /** + * @return CharacterModel + */ + public function getCharacter(): CharacterModel{ + return $this->character; + } + + /** + * @return array + */ + public function getData() : array{ + $data = parent::getData(); + + if(is_object($character = $this->getCharacter())){ + $characterData['character'] = [ + 'id' => $character->_id, + 'name' => $character->name + ]; + $data = $characterData + $data; + } + + return $data; + } + + /** + * get character thumbnailUrl + * @return string + */ + protected function getThumbUrl(): string { + $url = ''; + if(is_object($character = $this->getCharacter())){ + $url = Config::getPathfinderData('api.ccp_image_server') . '/Character/' . $character->_id . '_128.jpg'; + } + + return $url; + } + +} \ No newline at end of file diff --git a/app/Lib/Logging/AbstractLog.php b/app/Lib/Logging/AbstractLog.php new file mode 100644 index 000000000..f551a68bb --- /dev/null +++ b/app/Lib/Logging/AbstractLog.php @@ -0,0 +1,629 @@ + check Monolog::HANDLER and Monolog::FORMATTER + * @var array + */ + protected $handlerConfig = ['stream' => 'line']; + + /** + * log Processors, array with either callable functions or Processor class with __invoce() method + * -> functions used to add "extra" data to a log + * @var array + */ + protected $processorConfig = ['psr' => null]; + + /** + * some handler need individual configuration parameters + * -> see $handlerConfig end getHandlerParams() + * @var array + */ + 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 + * @var array + */ + protected $handlerGroups = []; + + /** + * @var string + */ + protected $message = ''; + + /** + * @var string + */ + protected $action = ''; + + /** + * @var string + */ + protected $channelType = ''; + + /** + * log level from self::LEVEL + * -> private - use setLevel() to set + * @var string + */ + private $level = 'debug'; + + /** + * log tag from self::TAG + * -> private - use setTag() to set + * @var string + */ + private $tag = 'default'; + + /** + * log data (main log data) + * @var array + */ + private $data = []; + + /** + * (optional) temp data for logger (will not be stored with the log entry) + * @var array + */ + private $tmpData = []; + + /** + * buffer multiple logs with the same chanelType and store all at once + * @var bool + */ + private $buffer = true; + + + /** + * AbstractLog constructor. + * @param string $action + */ + public function __construct(string $action){ + $this->setF3(); + $this->action = $action; + + // add custom log processor callback -> add "extra" (meta) data + $f3 = $this->f3; + $processorExtraData = function($record) use (&$f3){ + $record['extra'] = [ + 'path' => $f3->get('PATH'), + 'ip' => $f3->get('IP') + ]; + return $record; + }; + + // add log processor -> remove §tempData from log + $processorClearTempData = function($record){ + $record['context'] = array_diff_key($record['context'], $this->getTempData()); + return $record; + }; + + // init processorConfig. IMPORTANT: first processor gets executed at the end! + $this->processorConfig = ['cleaTempData' => $processorClearTempData] + [ 'addExtra' => $processorExtraData] + $this->processorConfig; + } + + /** + * set $f3 base object + */ + public function setF3(){ + $this->f3 = \Base::instance(); + } + + /** + * @param $message + */ + public function setMessage(string $message){ + $this->message = $message; + } + + /** + * @param string $level + * @throws \Exception + */ + public function setLevel(string $level){ + if( in_array($level, self::LEVEL)){ + $this->level = $level; + }else{ + throw new \Exception( sprintf(self::ERROR_LEVEL, $level)); + } + } + + /** + * @param string $tag + * @throws \Exception + */ + public function setTag(string $tag){ + if( in_array($tag, self::TAG)){ + $this->tag = $tag; + }else{ + throw new \Exception( sprintf(self::ERROR_TAG, $tag)); + } + } + + /** + * @param array $data + * @return LogInterface + */ + public function setData(array $data) : LogInterface { + $this->data = $data; + return $this; + } + + /** + * @param array $data + * @return LogInterface + */ + public function setTempData(array $data) : LogInterface { + $this->tmpData = $data; + return $this; + } + + /** + * add new Handler by $handlerKey + * set its default Formatter by $formatterKey + * @param string $handlerKey + * @param string|null $formatterKey + * @param \stdClass|null $handlerParams + * @return LogInterface + */ + public function addHandler(string $handlerKey, string $formatterKey = null, \stdClass $handlerParams = null) : LogInterface { + if(!$this->hasHandlerKey($handlerKey)){ + $this->handlerConfig[$handlerKey] = $formatterKey; + // add more configuration params for the new handler + if(!is_null($handlerParams)){ + $this->handlerParamsConfig[$handlerKey] = $handlerParams; + } + } + return $this; + } + + /** + * add new handler for Log() grouping + * @param string $handlerKey + * @return LogInterface + */ + public function addHandlerGroup(string $handlerKey) : LogInterface { + if( + $this->hasHandlerKey($handlerKey) && + !$this->hasHandlerGroupKey($handlerKey) + ){ + $this->handlerGroups[] = $handlerKey; + } + return $this; + } + + /** + * @return array + */ + public function getHandlerConfig() : array { + return $this->handlerConfig; + } + + /** + * get __construct() parameters for a given $handlerKey + * @param string $handlerKey + * @return array + * @throws \Exception + */ + public function getHandlerParams(string $handlerKey) : array { + if($this->hasHandlerKey($handlerKey)){ + switch($handlerKey){ + case 'stream': $params = $this->getHandlerParamsStream(); + break; + case 'mail': $params = $this->getHandlerParamsMail(); + break; + case 'socket': $params = $this->getHandlerParamsSocket(); + break; + case 'slackMap': + case 'slackRally': + case 'discordMap': + case 'discordRally': + $params = $this->getHandlerParamsSlack($handlerKey); + break; + default: + throw new \Exception(sprintf(self::ERROR_HANDLER_PARAMS, $handlerKey)); + } + }else{ + throw new \Exception(sprintf(self::ERROR_HANDLER_KEY, $handlerKey, implode(', ', array_flip($this->handlerConfig)))); + } + + return $params; + } + + /** + * @return array + */ + public function getHandlerParamsConfig() : array { + return $this->handlerParamsConfig; + } + + /** + * @return array + */ + public function getProcessorConfig() : array { + 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 { + return $this->message; + } + + /** + * @return string + */ + public function getAction() : string { + return $this->action; + } + + /** + * @return string + */ + public function getChannelType() : string { + return $this->channelType; + } + + /** + * @return string + */ + public function getChannelName() : string { + return $this->getChannelType(); + } + + /** + * @return string + */ + public function getLevel() : string { + return $this->level; + } + + /** + * @return string + */ + public function getTag() : string { + return $this->tag; + } + + /** + * @return array + */ + public function getData() : array { + return $this->data; + } + /** + * @return array + */ + public function getContext() : array { + $context = [ + 'data' => $this->getData(), + 'tag' => $this->getTag() + ]; + + // add temp data (e.g. used for $message placeholder replacement + $context += $this->getTempData(); + + return $context; + } + + /** + * @return array + */ + protected function getTempData() : array { + return $this->tmpData; + } + + /** + * @return array + */ + public function getHandlerGroups() : array { + return $this->handlerGroups; + } + + /** + * get unique hash for this kind of logs (channel) and same $handlerGroups + * @return string + */ + public function getGroupHash() : string { + $groupName = $this->getChannelName(); + if($this->isGrouped()){ + $groupName .= '_' . implode('_', $this->getHandlerGroups()); + } + + return $this->f3->hash($groupName); + } + + /** + * @param string $handlerKey + * @return bool + */ + public function hasHandlerKey(string $handlerKey) : bool { + return array_key_exists($handlerKey, $this->handlerConfig); + } + + /** + * @param string $handlerKey + * @return 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 { + return $this->buffer; + } + + /** + * @return bool + */ + public function isGrouped() : bool { + return !empty($this->getHandlerGroups()); + } + + /** + * remove all group handlers and their config params + */ + public function removeHandlerGroups(){ + foreach($this->getHandlerGroups() as $handlerKey){ + $this->removeHandlerGroup($handlerKey); + } + } + + /** + * @param string $handlerKey + */ + public function removeHandlerGroup(string $handlerKey){ + unset($this->handlerConfig[$handlerKey]); + unset($this->handlerParamsConfig[$handlerKey]); + } + + // Handler parameters for Monolog\Handler\* instances ------------------------------------------------------------- + + /** + * @return array + */ + protected function getHandlerParamsStream() : array { + $params = []; + if( !empty($conf = $this->handlerParamsConfig['stream']) ){ + $params[] = $conf->stream; + $params[] = Logger::toMonologLevel($this->getLevel()); // min level that is handled; + $params[] = true; // bubble + $params[] = 0666; // permissions (default 644) + } + + return $params; + } + + /** + * get __construct() parameters for SwiftMailerHandler() call + * @return array + */ + protected function getHandlerParamsMail() : array { + $params = []; + if( !empty($conf = $this->handlerParamsConfig['mail']) ){ + $transport = (new \Swift_SmtpTransport()) + ->setHost($conf->host) + ->setPort($conf->port) + ->setEncryption($conf->scheme) + ->setUsername($conf->username) + ->setPassword($conf->password) + ->setStreamOptions([ + 'ssl' => [ + 'allow_self_signed' => true, + 'verify_peer' => false + ] + ]); + + $mailer = new \Swift_Mailer($transport); + + // callback function used instead of Swift_Message() object + // -> we want the formatted/replaced message as subject + $messageCallback = function($content, $records) use ($conf){ + $subject = 'No Subject'; + if(!empty($records)){ + // build subject from first record -> remove "markdown" + $subject = str_replace(['*', '_'], '', $records[0]['message']); + } + + $jsonData = @json_encode($records, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + + $message = (new \Swift_Message()) + ->setSubject($subject) + ->addPart($jsonData) + ->setFrom($conf->from) + ->setTo($conf->to) + ->setContentType('text/html') + ->setCharset('utf-8') + ->setMaxLineLength(1000); + + if($conf->addJson){ + $jsonAttachment = (new \Swift_Attachment()) + ->setFilename('data.json') + ->setContentType('application/json') + ->setBody($jsonData); + $message->attach($jsonAttachment); + } + + return $message; + }; + + $params[] = $mailer; + $params[] = $messageCallback; + $params[] = Logger::toMonologLevel($this->getLevel()); // min level that is handled + $params[] = true; // bubble + } + + return $params; + } + + /** + * get __construct() parameters for SocketHandler() call + * @return array + */ + protected function getHandlerParamsSocket() : array { + $params = []; + if( !empty($conf = $this->handlerParamsConfig['socket']) ){ + // meta data (required by receiver socket) + $meta = [ + 'logType' => 'mapLog', + 'stream'=> $conf->streamConf->stream + ]; + + $params[] = $conf->dsn; + $params[] = Logger::toMonologLevel($this->getLevel()); + $params[] = true; + $params[] = $meta; + } + + return $params; + } + + /** + * get __construct() params for SlackWebhookHandler() call + * @param string $handlerKey + * @return array + */ + protected function getHandlerParamsSlack(string $handlerKey) : array { + $params = []; + if( !empty($conf = $this->handlerParamsConfig[$handlerKey]) ){ + $params[] = $conf->slackWebHookURL; + $params[] = $conf->slackChannel; + $params[] = $conf->slackUsername; + $params[] = true; // $useAttachment + $params[] = $conf->slackIcon; + $params[] = true; // $includeContext + $params[] = false; // $includeExtra + $params[] = Logger::toMonologLevel($this->getLevel()); // min level that is handled + $params[] = true; // $bubble + //$params[] = ['extra', 'context.tag']; // $excludeFields + $params[] = []; // $excludeFields + } + + 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 + */ + public function buffer(){ + if( !empty($this->handlerParamsConfig) ){ + Monolog::instance()->push($this); + } + } + + +} diff --git a/app/Lib/Logging/ApiLog.php b/app/Lib/Logging/ApiLog.php new file mode 100644 index 000000000..bfb9183e9 --- /dev/null +++ b/app/Lib/Logging/ApiLog.php @@ -0,0 +1,49 @@ + final handler will be set dynamic for per instance + * @var array + */ + protected $handlerConfig = [ + //'stream' => 'json' + ]; + + /** + * @var string + */ + protected $channelType = 'api'; + + /** + * ApiLog constructor. + * @param string $action + * @param string $level + * @throws \Exception + */ + public function __construct(string $action, string $level){ + parent::__construct($action); + + $this->setLevel($level); + } + + /** + * overwrites parent + * -> we need unique channelNames for different $actions within same $channelType + * -> otherwise logs would be bundled into the first log file handler + * @return string + */ + public function getChannelName(): string{ + return $this->getChannelType() . '_' . $this->getAction(); + } +} \ No newline at end of file diff --git a/app/Lib/Logging/DefaultLog.php b/app/Lib/Logging/DefaultLog.php new file mode 100644 index 000000000..6a23a2aef --- /dev/null +++ b/app/Lib/Logging/DefaultLog.php @@ -0,0 +1,19 @@ + $record['message'], + 'tplGreeting' => \Markdown::instance()->convert(str_replace('*', '', $record['message'])), + 'message' => false, + 'tplText2' => false, + 'tplClosing' => 'Fly save!', + 'actionPrimary' => false, + 'appName' => Config::getPathfinderData('name'), + 'appUrl' => Config::getEnvironmentData('URL'), + 'appHost' => $_SERVER['HTTP_HOST'], + 'appContact' => Config::getPathfinderData('contact'), + 'appMail' => Config::getPathfinderData('email'), + ]; + + $tplData = array_replace_recursive($tplDefaultData, (array)$record['context']['data']['main']); + + return \Template::instance()->render('templates/mail/basic_inline.html', 'text/html', $tplData); + } + + /** + * @param array $records + * @return mixed|string + */ + public function formatBatch(array $records){ + $message = ''; + foreach ($records as $key => $record) { + $message .= $this->format($record); + } + + return $message; + } + +} \ No newline at end of file diff --git a/app/Lib/Logging/Handler/AbstractMapWebhookHandler.php b/app/Lib/Logging/Handler/AbstractMapWebhookHandler.php new file mode 100644 index 000000000..2e34095d5 --- /dev/null +++ b/app/Lib/Logging/Handler/AbstractMapWebhookHandler.php @@ -0,0 +1,99 @@ +getTimestamp(); + $text = ''; + + if ( + $this->useAttachment && + !empty( $attachmentsData = $record['context']['data']) + ) { + + // convert non grouped data (associative array) to multi dimensional (sequential) array + // -> see "group" records + $attachmentsData = Util::is_assoc($attachmentsData) ? [$attachmentsData] : $attachmentsData; + + $thumbData = (array)$record['extra']['thumb']; + + $postData['attachments'] = []; + + foreach($attachmentsData as $attachmentData){ + $channelData = (array)$attachmentData['channel']; + $characterData = (array)$attachmentData['character']; + $formatted = (string)$attachmentData['formatted']; + + // get "message" from $formatted + $msgParts = explode('|', $formatted, 2); + + // build main text from first Attachment (they belong to same channel) + if(!empty($channelData)){ + $text = "*Map '" . $channelData['channelName'] . "'* _#" . $channelData['channelId'] . "_ *changed*"; + } + + $attachment = [ + 'title' => !empty($msgParts[0]) ? $msgParts[0] : 'No Title', + //'pretext' => '', + 'text' => !empty($msgParts[1]) ? sprintf('```%s```', $msgParts[1]) : '', + 'fallback' => !empty($msgParts[1]) ? $msgParts[1] : 'No Fallback', + 'color' => $this->getAttachmentColor($tag), + 'fields' => [], + 'mrkdwn_in' => ['fields', 'text'], + 'footer' => 'Pathfinder API', + //'footer_icon'=> '', + 'ts' => $timestamp + ]; + + $attachment = $this->setAuthor($attachment, $characterData); + $attachment = $this->setThumb($attachment, $thumbData); + + + // set 'field' array ---------------------------------------------------------------------------------- + if ($this->includeExtra) { + $attachment['fields'][] = $this->generateAttachmentField('', 'Meta data:', false, false); + + if(!empty($record['extra']['path'])){ + $attachment['fields'][] = $this->generateAttachmentField('Path', $record['extra']['path'], true); + } + + if(!empty($tag)){ + $attachment['fields'][] = $this->generateAttachmentField('Tag', $tag, true); + } + + if(!empty($record['level_name'])){ + $attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name'], true); + } + + if(!empty($record['extra']['ip'])){ + $attachment['fields'][] = $this->generateAttachmentField('IP', $record['extra']['ip'], true); + } + } + + $postData['attachments'][] = $attachment; + } + } + + $postData['text'] = empty($text) ? $postData['text'] : $text; + + + return $postData; + } +} \ No newline at end of file diff --git a/app/Lib/Logging/Handler/AbstractRallyWebhookHandler.php b/app/Lib/Logging/Handler/AbstractRallyWebhookHandler.php new file mode 100644 index 000000000..c8ca5ddff --- /dev/null +++ b/app/Lib/Logging/Handler/AbstractRallyWebhookHandler.php @@ -0,0 +1,155 @@ +getTimestamp(); + $text = ''; + + if ( + $this->useAttachment && + !empty( $attachmentsData = $record['context']['data']) + ){ + // convert non grouped data (associative array) to multi dimensional (sequential) array + // -> see "group" records + $attachmentsData = Util::is_assoc($attachmentsData) ? [$attachmentsData] : $attachmentsData; + + $thumbData = (array)$record['extra']['thumb']; + + $postData['attachments'] = []; + + foreach($attachmentsData as $attachmentData){ + $characterData = (array)$attachmentData['character']; + + $text = 'No Title'; + if( !empty($attachmentData['formatted']) ){ + $text = $attachmentData['formatted']; + } + + $attachment = [ + 'title' => !empty($attachmentData['main']['message']) ? 'Message' : '', + //'pretext' => '', + 'text' => !empty($attachmentData['main']['message']) ? sprintf('```%s```', $attachmentData['main']['message']) : '', + 'fallback' => !empty($attachmentData['main']['message']) ? $attachmentData['main']['message'] : 'No Fallback', + 'color' => $this->getAttachmentColor($tag), + 'fields' => [], + 'mrkdwn_in' => ['fields', 'text'], + 'footer' => 'Pathfinder API', + //'footer_icon'=> '', + 'ts' => $timestamp + ]; + + $attachment = $this->setAuthor($attachment, $characterData); + $attachment = $this->setThumb($attachment, $thumbData); + + // set 'field' array ---------------------------------------------------------------------------------- + if ($this->includeContext) { + if(!empty($objectData = $attachmentData['object'])){ + if(!empty($objectData['objAlias'])){ + // System alias + $attachment['fields'][] = $this->generateAttachmentField('Alias', $objectData['objAlias']); + } + + if(!empty($objectData['objName'])){ + // System name + $attachment['fields'][] = $this->generateAttachmentField('System', $objectData['objName']); + } + + if(!empty($objectData['objRegion'])){ + // System region + $attachment['fields'][] = $this->generateAttachmentField('Region', $objectData['objRegion']); + } + + if(isset($objectData['objIsWormhole'])){ + // Is wormhole + $attachment['fields'][] = $this->generateAttachmentField('Wormhole', $objectData['objIsWormhole'] ? 'Yes' : 'No'); + } + + if(!empty($objectData['objSecurity'])){ + // System security + $attachment['fields'][] = $this->generateAttachmentField('Security', $objectData['objSecurity']); + } + + if(!empty($objectData['objEffect'])){ + // System effect + $attachment['fields'][] = $this->generateAttachmentField('Effect', $objectData['objEffect']); + } + + if(!empty($objectData['objTrueSec'])){ + // System trueSec + $attachment['fields'][] = $this->generateAttachmentField('TrueSec', $objectData['objTrueSec']); + } + + if(!empty($objectData['objCountPlanets'])){ + // System planet count + $attachment['fields'][] = $this->generateAttachmentField('Planets', $objectData['objCountPlanets']); + } + + if(!empty($objectData['objDescription'])){ + // System description + $attachment['fields'][] = $this->generateAttachmentField('System description', '```' . $this->htmlToMarkdown($objectData['objDescription']) . '```', false, false); + } + + if(!empty($objectData['objUrl'])){ + // System deeeplink + $attachment['fields'][] = $this->generateAttachmentField('', $objectData['objUrl'] , false, false); + } + } + } + + if($this->includeExtra){ + if(!empty($record['extra']['path'])){ + $attachment['fields'][] = $this->generateAttachmentField('Path', $record['extra']['path'], true); + } + + if(!empty($tag)){ + $attachment['fields'][] = $this->generateAttachmentField('Tag', $tag, true); + } + + if(!empty($record['level_name'])){ + $attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name'], true); + } + + if(!empty($record['extra']['ip'])){ + $attachment['fields'][] = $this->generateAttachmentField('IP', $record['extra']['ip'], true); + } + } + + $postData['attachments'][] = $attachment; + } + } + + $postData['text'] = empty($text) ? $postData['text'] : $text; + + return $postData; + } + + /** + * convert $html into Markdown + * @param $html + * @return string + */ + protected function htmlToMarkdown($html){ + $converter = new HtmlConverter(); + $converter->getConfig()->setOption('strip_tags', true); + return $converter->convert($html); + } +} \ No newline at end of file diff --git a/app/Lib/Logging/Handler/AbstractWebhookHandler.php b/app/Lib/Logging/Handler/AbstractWebhookHandler.php new file mode 100644 index 000000000..50869218e --- /dev/null +++ b/app/Lib/Logging/Handler/AbstractWebhookHandler.php @@ -0,0 +1,260 @@ +webhookUrl = $webhookUrl; + $this->channel = $channel; + $this->username = $username; + $this->userIcon = trim($iconEmoji, ':'); + $this->useAttachment = $useAttachment; + $this->includeContext = $includeContext; + $this->includeExtra = $includeExtra; + $this->excludeFields = $excludeFields; + + parent::__construct($level, $bubble); + + } + + /** + * format + * @param array $record + * @return array + */ + protected function getSlackData(array $record): array { + $postData = []; + + if ($this->username) { + $postData['username'] = $this->username; + } + + if ($this->channel) { + $postData['channel'] = $this->channel; + } + + $postData['text'] = (string)$record['message']; + + if ($this->userIcon) { + if (filter_var($this->userIcon, FILTER_VALIDATE_URL)) { + $postData['icon_url'] = $this->userIcon; + } else { + $postData['icon_emoji'] = ":{$this->userIcon}:"; + } + } + + return $postData; + } + + /** + * {@inheritdoc} + * + * @param array $record + */ + protected function write(array $record) : void { + $record = $this->excludeFields($record); + + $postData = $this->getSlackData($record); + + $postData = $this->cleanAttachments($postData); + + $postString = json_encode($postData); + + $ch = curl_init(); + $options = [ + CURLOPT_URL => $this->webhookUrl, + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_POSTFIELDS => $postString + ]; + if (defined('CURLOPT_SAFE_UPLOAD')) { + $options[CURLOPT_SAFE_UPLOAD] = true; + } + + curl_setopt_array($ch, $options); + + Handler\Curl\Util::execute($ch); + } + + /** + * @param array $postData + * @return array + */ + protected function cleanAttachments(array $postData): array{ + $attachmentCount = count($postData['attachments']); + if( $attachmentCount > $this->maxAttachments){ + $text = 'To many attachments! ' . ($attachmentCount - $this->maxAttachments) . ' of ' . $attachmentCount . ' attachments not visible'; + $postData['attachments'] = array_slice($postData['attachments'], 0, $this->maxAttachments); + + $attachment = [ + 'title' => $text, + 'fallback' => $text, + 'color' => $this->getAttachmentColor('information') + ]; + + $postData['attachments'][] = $attachment; + } + + return $postData; + } + + /** + * @param array $attachment + * @param array $characterData + * @return array + */ + protected function setAuthor(array $attachment, array $characterData): array { + if( !empty($characterData['id']) && !empty($characterData['name'])){ + $attachment['author_name'] = $characterData['name'] . ' #' . $characterData['id']; + $attachment['author_link'] = Config::getPathfinderData('api.z_killboard') . '/character/' . $characterData['id'] . '/'; + $attachment['author_icon'] = Config::getPathfinderData('api.ccp_image_server') . '/Character/' . $characterData['id'] . '_32.jpg'; + } + + return $attachment; + } + + /** + * @param array $attachment + * @param array $thumbData + * @return array + */ + protected function setThumb(array $attachment, array $thumbData): array { + if( !empty($thumbData['url'])) { + $attachment['thumb_url'] = $thumbData['url']; + } + + return $attachment; + } + + /** + * @param $title + * @param $value + * @param bool $format + * @param bool $short + * @return array + */ + protected function generateAttachmentField($title, $value, $format = false, $short = true){ + return [ + 'title' => $title, + 'value' => !empty($value) ? ( $format ? sprintf('`%s`', $value) : $value ) : '', + 'short' => $short + ]; + } + + /** + * @param string $tag + * @return string + */ + protected function getAttachmentColor(string $tag): string { + switch($tag){ + case 'information': $color = '#428bca'; break; + case 'success': $color = '#4f9e4f'; break; + case 'warning': $color = '#e28a0d'; break; + case 'danger': $color = '#a52521'; break; + default: $color = '#313335'; break; + } + return $color; + } + + /** + * Get a copy of record with fields excluded according to $this->excludeFields + * @param array $record + * @return array + */ + private function excludeFields(array $record){ + foreach($this->excludeFields as $field){ + $keys = explode('.', $field); + $node = &$record; + $lastKey = end($keys); + foreach($keys as $key){ + if(!isset($node[$key])){ + break; + } + if($lastKey === $key){ + unset($node[$key]); + break; + } + $node = &$node[$key]; + } + } + + return $record; + } +} \ No newline at end of file diff --git a/app/Lib/Logging/Handler/DiscordMapWebhookHandler.php b/app/Lib/Logging/Handler/DiscordMapWebhookHandler.php new file mode 100644 index 000000000..41f8c1a9d --- /dev/null +++ b/app/Lib/Logging/Handler/DiscordMapWebhookHandler.php @@ -0,0 +1,14 @@ +metaData = $metaData; + + parent::__construct($connectionString, $level, $bubble); + } + + /** + * overwrite default handle() + * -> change data structure after processor() calls and before formatter() calls + * @param array $record + * @return bool + */ + public function handle(array $record) : bool { + if (!$this->isHandling($record)) { + return false; + } + + $record = $this->processRecord($record); + + $record = [ + 'task' => 'logData', + 'load' => [ + 'meta' => $this->metaData, + 'log' => $record + ] + ]; + + $record['formatted'] = $this->getFormatter()->format($record); + + $this->write($record); + + return false === $this->bubble; + } +} \ No newline at end of file diff --git a/app/Lib/Logging/LogCollection.php b/app/Lib/Logging/LogCollection.php new file mode 100644 index 000000000..721a3972a --- /dev/null +++ b/app/Lib/Logging/LogCollection.php @@ -0,0 +1,188 @@ + no default is set + * @var array + */ + protected $handlerConfig = []; + + /** + * processors for this collection + * -> no default is set + * @var array + */ + protected $processorConfig = []; + + /** + * @var null|\SplObjectStorage + */ + private $collection = null; + + /** + * LogCollection constructor. + * @param string $action + */ + public function __construct(string $action){ + parent::__construct($action); + + $this->collection = new \SplObjectStorage(); + } + + /** + * get first Log from Collection + * @return AbstractLog + * @throws \Exception + */ + protected function getPrimaryLog(): AbstractLog{ + $this->collection->rewind(); + if($this->collection->valid()){ + /** + * @var $log AbstractLog + */ + $log = $this->collection->current(); + }else{ + throw new \Exception( self::ERROR_EMPTY); + } + + return $log; + } + + /** + * add a new log object to this collection + * @param AbstractLog $log + * @throws \Exception + */ + public function addLog(AbstractLog $log){ + if(!$this->collection->contains($log)){ + if(!$this->collection->count()){ + // first log sets the default for this collection + $this->channelType = $log->getChannelType(); + + // get relevant handlerKeys for this collection + $handlerGroups = array_flip($log->getHandlerGroups()); + + // remove handlers that are not relevant for this collection + $handlerConfig = $log->getHandlerConfig(); + $handlerConfigGroup = array_intersect_key($handlerConfig, $handlerGroups); + + // remove handlersParams that are not relevant for this collection + $handlerParamsConfig = $log->getHandlerParamsConfig(); + $handlerParamsConfigGroup = array_intersect_key($handlerParamsConfig, $handlerGroups); + + // add all handlers that are relevant for this collection + foreach($handlerConfigGroup as $handlerKey => $formatterKey){ + $handlerParams = array_key_exists($handlerKey, $handlerParamsConfigGroup) ? $handlerParamsConfigGroup[$handlerKey] : null; + $this->addHandler($handlerKey, $formatterKey, $handlerParams); + } + + // add processors for this collection + $this->processorConfig = $log->getProcessorConfig(); + } + + $this->setMessage($log->getMessage()); + $this->setTag($log->getTag()); + + $this->collection->attach($log); + } + } + + /** + * @param string $message + */ + public function setMessage(string $message){ + $currentMessage = parent::getMessage(); + if(empty($currentMessage)){ + $newMessage = $message; + }elseif($message !== $currentMessage){ + $newMessage = 'multi changes'; + }else{ + $newMessage = $currentMessage ; + } + + parent::setMessage($newMessage); + } + + /** + * @param string $tag + * @throws \Exception + */ + public function setTag(string $tag){ + $currentTag = parent::getTag(); + switch($currentTag){ + case 'default': + // no specific tag set so far... set new + $newTag = $tag; break; + case 'information': + // do not change "information" tag (mixed tag logs in this collection) + $newTag = $currentTag; break; + default: + // set mixed tag -> "information" + $newTag = ($tag !== $currentTag) ? 'information': $tag; + } + + parent::setTag($newTag); + } + + /** + * get log data for all logs in this collection + * @return array + */ + public function getData() : array{ + $this->collection->rewind(); + $data = []; + while($this->collection->valid()){ + $data[] = $this->collection->current()->getData(); + $this->collection->next(); + } + return $data; + } + + /** + * @return string + * @throws \Exception + */ + public function getChannelName() : string{ + return $this->getPrimaryLog()->getChannelName(); + } + + /** + * @return string + * @throws \Exception + */ + public function getLevel() : string{ + return $this->getPrimaryLog()->getLevel(); + } + + /** + * @return bool + * @throws \Exception + */ + public function hasBuffer() : bool{ + return $this->getPrimaryLog()->hasBuffer(); + } + + /** + * @return array + * @throws \Exception + */ + public function getTempData() : array{ + return $this->getPrimaryLog()->getTempData(); + } + + + +} \ No newline at end of file diff --git a/app/Lib/Logging/LogInterface.php b/app/Lib/Logging/LogInterface.php new file mode 100644 index 000000000..e5346dd6b --- /dev/null +++ b/app/Lib/Logging/LogInterface.php @@ -0,0 +1,71 @@ + final handler will be set dynamic for per instance + * @var array + */ + protected $handlerConfig = [ + //'stream' => 'json', + //'socket' => 'json', + //'slackMap' => 'json' + ]; + + /** + * @var string + */ + protected $channelType = 'map'; + + /** + * @var bool + */ + protected $logActivity = false; + + /** + * MapLog constructor. + * @param string $action + * @param array $objectData + * @throws \Exception + */ + public function __construct(string $action, array $objectData){ + parent::__construct($action, $objectData); + + $this->setLevel('info'); + $this->setTag($this->getTagFromAction()); + } + + /** + * get log tag depending on log action + * @return string + */ + public function getTagFromAction(){ + $tag = parent::getTag(); + $actionParts = $this->getActionParts(); + switch($actionParts[1]){ + case 'create': $tag = 'success'; break; + case 'update': $tag = 'warning'; break; + case 'delete': $tag = 'danger'; break; + } + + return $tag; + } + + /** + * @return string + */ + public function getChannelName() : string { + return $this->getChannelType() . '_' . $this->getChannelId(); + } + + /** + * @return string + */ + public function getMessage() : string { + return $this->getActionParts()[0] . " '{objName}'"; + } + + /** + * @return array + */ + public function getData() : array { + $data = parent::getData(); + + // add system, connection, signature data ------------------------------------------------- + if(!empty($tempLogData = $this->getTempData())){ + $objectData['object'] = $tempLogData; + $data = $objectData + $data; + } + + // add human readable changes to string --------------------------------------------------- + $data['formatted'] = $this->formatData($data); + + return $data; + } + + /** + * @param array $data + * @return string + */ + protected function formatData(array $data) : string { + $actionParts = $this->getActionParts(); + $objectString = !empty($data['object']) ? "'" . $data['object']['objName'] . "'" . ' #' . $data['object']['objId'] : ''; + $string = ucfirst($actionParts[1]) . 'd ' . $actionParts[0] . " " . $objectString; + + // format changed columns (recursive) --------------------------------------------- + switch($actionParts[1]){ + case 'create': + case 'update': + $formatChanges = function(array $changes) use (&$formatChanges) : string { + $string = ''; + foreach($changes as $field => $value){ + if(is_array($value)){ + $string .= $field . ": "; + $string .= $formatChanges($value); + $string .= next( $changes ) ? " , " : ''; + }else{ + if(is_numeric($value)){ + $formattedValue = $value; + }elseif(is_null($value)){ + $formattedValue = "NULL"; + }elseif(empty($value)){ + $formattedValue = "' '"; + }elseif(is_string($value)){ + $formattedValue = "'" . $this->f3->clean($value) . "'"; + }else{ + $formattedValue = (string)$value; + } + + $string .= $formattedValue; + if($field == 'old'){ + $string .= " ➜ "; + } + } + } + return $string; + }; + + $string .= ' | ' . $formatChanges($data['main']); + break; + } + + return $string; + } + + /** + * split $action "CamelCase" wise + * @return array + */ + protected function getActionParts() : array { + return array_map('strtolower', preg_split('/(?=[A-Z])/', $this->getAction())); + } + + /** + * @param bool $logActivity + */ + public function logActivity(bool $logActivity){ + $this->logActivity = $logActivity; + } + + public function buffer(){ + parent::buffer(); + + if($this->logActivity){ + // map logs should also used for "activity" logging + LogController::instance()->push($this); + } + } + +} \ No newline at end of file diff --git a/app/Lib/Logging/RallyLog.php b/app/Lib/Logging/RallyLog.php new file mode 100644 index 000000000..ebfc3efb8 --- /dev/null +++ b/app/Lib/Logging/RallyLog.php @@ -0,0 +1,111 @@ + final handler will be set dynamic for per instance + * @var array + */ + protected $handlerConfig = [ + // 'slackRally' => 'json', + // 'mail' => 'html' + ]; + + /** + * @var string + */ + protected $channelType = 'rally'; + + /** + * RallyLog constructor. + * @param string $action + * @param array $objectData + * @throws \Exception + */ + public function __construct(string $action, array $objectData){ + parent::__construct($action, $objectData); + + $this->setLevel('notice'); + $this->setTag('information'); + } + + /** + * @return string + */ + protected function getThumbUrl() : string{ + $url = ''; + if(is_object($character = $this->getCharacter())){ + $characterLog = $character->getLog(); + if($characterLog && !empty($characterLog->shipTypeId)){ + $url = Config::getPathfinderData('api.ccp_image_server') . '/Render/' . $characterLog->shipTypeId . '_64.png'; + }else{ + $url = parent::getThumbUrl(); + } + } + + return $url; + } + + /** + * @return string + */ + public function getMessage() : string{ + return "*New RallyPoint system '{objName}'* _#{objId}_ *map '{channelName}'* _#{channelId}_ "; + } + + /** + * @return array + */ + public function getData() : array{ + $data = parent::getData(); + + // add system ----------------------------------------------------------------------------- + if(!empty($tempLogData = $this->getTempData())){ + $objectData['object'] = $tempLogData; + $data = $objectData + $data; + } + + // add human readable changes to string --------------------------------------------------- + $data['formatted'] = $this->formatData($data); + + return $data; + } + + /** + * @param array $data + * @return string + */ + protected function formatData(array $data): string{ + $string = ''; + + if( + !empty($data['object']) && + !empty($data['channel']) + ){ + $replace = [ + '{objName}' => $data['object']['objName'], + '{objId}' => $data['object']['objId'], + '{channelName}' => $data['channel']['channelName'], + '{channelId}' => $data['channel']['channelId'] + ]; + $string = str_replace(array_keys($replace), array_values($replace), $this->getMessage()); + } + + return $string; + } + + + +} \ No newline at end of file diff --git a/app/Lib/Logging/UserLog.php b/app/Lib/Logging/UserLog.php new file mode 100644 index 000000000..d4e3813df --- /dev/null +++ b/app/Lib/Logging/UserLog.php @@ -0,0 +1,42 @@ + final handler will be set dynamic for per instance + * @var array + */ + protected $handlerConfig = [ + // 'mail' => 'html' + ]; + + /** + * @var string + */ + protected $channelType = 'user'; + + /** + * UserLog constructor. + * @param string $action + * @param array $objectData + * @throws \Exception + */ + public function __construct(string $action, array $objectData){ + parent::__construct($action, $objectData); + + $this->setLevel('notice'); + $this->setTag('information'); + } + + +} \ No newline at end of file diff --git a/app/Lib/Monolog.php b/app/Lib/Monolog.php new file mode 100644 index 000000000..ff6d22206 --- /dev/null +++ b/app/Lib/Monolog.php @@ -0,0 +1,241 @@ + 'Monolog\Formatter\LineFormatter', + 'json' => 'Monolog\Formatter\JsonFormatter', + 'html' => 'Monolog\Formatter\HtmlFormatter', + 'mail' => 'Exodus4D\Pathfinder\Lib\Logging\Formatter\MailFormatter' + ]; + + /** + * available handlers + */ + const HANDLER = [ + 'stream' => 'Monolog\Handler\StreamHandler', + 'mail' => 'Monolog\Handler\SwiftMailerHandler', + 'socket' => 'Exodus4D\Pathfinder\Lib\Logging\Handler\SocketHandler', + 'slackMap' => 'Exodus4D\Pathfinder\Lib\Logging\Handler\SlackMapWebhookHandler', + 'slackRally' => 'Exodus4D\Pathfinder\Lib\Logging\Handler\SlackRallyWebhookHandler', + 'discordMap' => 'Exodus4D\Pathfinder\Lib\Logging\Handler\DiscordMapWebhookHandler', + 'discordRally' => 'Exodus4D\Pathfinder\Lib\Logging\Handler\DiscordRallyWebhookHandler' + ]; + + /** + * available processors + */ + const PROCESSOR = [ + 'psr' => 'Monolog\Processor\PsrLogMessageProcessor' + ]; + + /** + * @var Logging\LogCollection[][]|Logging\MapLog[][] + */ + private $logs = [ + 'solo' => [], + 'groups' => [] + ]; + + public function __construct(){ + if(!class_exists(Logger::class)){ + LogController::getLogger('ERROR')->write(sprintf(Config::ERROR_CLASS_NOT_EXISTS_COMPOSER, Logger::class)); + } + } + + /** + * buffer log object, add to objectStorage collection + * -> this buffered data can be stored/logged somewhere (e.g. DB/file) at any time + * -> should be cleared afterwards! + * @param Logging\AbstractLog $log + * @throws \Exception + */ + public function push(Logging\AbstractLog $log){ + // check whether $log should be "grouped" by common handlers + if($log->isGrouped()){ + $groupHash = $log->getGroupHash(); + + if(!isset($this->logs['groups'][$groupHash])){ + // create new log collection + // $this->logs['groups'][$groupHash] = new Logging\LogCollection($log->getChannelName()); + $this->logs['groups'][$groupHash] = new Logging\LogCollection('mapDelete'); + } + $this->logs['groups'][$groupHash]->addLog($log); + + // remove "group" handler from $log + // each log should only be logged once per handler! + $log->removeHandlerGroups(); + } + + $this->logs['solo'][] = $log; + } + + /** + * bulk process all stored logs -> send to Monolog lib + */ + public function log(){ + + foreach($this->logs as $logType => $logs){ + foreach($logs as $logKey => $log){ + $groupHash = $log->getGroupHash(); + $level = Logger::toMonologLevel($log->getLevel()); + + // add new logger to Registry if not already exists + if(Registry::hasLogger($groupHash)){ + $logger = Registry::getInstance($groupHash); + }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); + + // configure new $logger -------------------------------------------------------------------------- + // get Monolog Handler with Formatter config + // -> $log could have multiple handler with different Formatters + $handlerConf = $log->getHandlerConfig(); + foreach($handlerConf as $handlerKey => $formatterKey){ + // get Monolog Handler class + $handlerParams = $log->getHandlerParams($handlerKey); + $handler = $this->getHandler($handlerKey, $handlerParams); + + // get Monolog Formatter + $formatter = $this->getFormatter((string)$formatterKey); + if( $formatter instanceof FormatterInterface){ + $handler->setFormatter($formatter); + } + + if($log->hasBuffer()){ + // wrap Handler into bufferHandler + // -> bulk save all logs for this $logger + $bufferHandler = new BufferHandler($handler); + $logger->pushHandler($bufferHandler); + }else{ + $logger->pushHandler($handler); + } + } + + // get Monolog Processor config + $processorConf = $log->getProcessorConfig(); + foreach($processorConf as $processorKey => $processorCallback){ + if(is_callable($processorCallback)){ + // custom Processor callback function + $logger->pushProcessor($processorCallback); + }else{ + // get Monolog Processor class + $processorParams = $log->getProcessorParams($processorKey); + $processor = $this->getProcessor($processorKey, $processorParams); + $logger->pushProcessor($processor); + } + } + + Registry::addLogger($logger, $groupHash); + } + + $logger->addRecord($level, $log->getMessage(), $log->getContext()); + } + } + + // clear log object storage + $this->logs['groups'] = []; + $this->logs['solo'] = []; + } + + /** + * get Monolog Formatter instance by key + * @param string $formatKey + * @return FormatterInterface|null + * @throws \Exception + */ + private function getFormatter(string $formatKey){ + $formatter = null; + if(!empty($formatKey)){ + if(array_key_exists($formatKey, self::FORMATTER)){ + $formatClass = self::FORMATTER[$formatKey]; + $formatter = new $formatClass(); + }else{ + throw new \Exception(sprintf(self::ERROR_FORMATTER, $formatKey)); + } + } + + return $formatter; + } + + /** + * get Monolog Handler instance by key + * @param string $handlerKey + * @param array $handlerParams + * @return HandlerInterface + * @throws \Exception + */ + private function getHandler(string $handlerKey, array $handlerParams = []) : HandlerInterface{ + if(array_key_exists($handlerKey, self::HANDLER)){ + $handlerClass = self::HANDLER[$handlerKey]; + $handler = new $handlerClass(...$handlerParams); + }else{ + throw new \Exception(sprintf(self::ERROR_HANDLER, $handlerKey)); + } + + return $handler; + } + + /** + * get Monolog Processor instance by key + * @param string $processorKey + * @param array $processorParams + * @return ProcessorInterface + * @throws \Exception + */ + private function getProcessor(string $processorKey, array $processorParams = []) : ProcessorInterface { + if(array_key_exists($processorKey, self::PROCESSOR)){ + $ProcessorClass = self::PROCESSOR[$processorKey]; + $processor = new $ProcessorClass(...$processorParams); + }else{ + throw new \Exception(sprintf(self::ERROR_PROCESSOR, $processorKey)); + } + + return $processor; + } + + +} \ No newline at end of file diff --git a/app/Lib/PriorityCacheStore.php b/app/Lib/PriorityCacheStore.php new file mode 100644 index 000000000..85add008c --- /dev/null +++ b/app/Lib/PriorityCacheStore.php @@ -0,0 +1,124 @@ + truncate store after 10 inserts. Max store entries: + * DEFAULT_ENTRY_LIMIT + DEFAULT_CLEANUP_INTERVAL - 1 + */ + const DEFAULT_CLEANUP_INTERVAL = 10; + + /** + * @var int + */ + protected $entryLimit; + + /** + * @var int + */ + protected $cleanupInterval; + + /** + * @var array + */ + protected $store; + + /** + * @var \SplPriorityQueue + */ + protected $priorityQueue; + + /** + * @var int + */ + protected $priority = 0; + + /** + * PriorityCacheStore constructor. + * @param int $entryLimit + * @param int $cleanupInterval + */ + function __construct(int $entryLimit = self::DEFAULT_ENTRY_LIMIT, int $cleanupInterval = self::DEFAULT_CLEANUP_INTERVAL){ + $this->cleanupInterval = $cleanupInterval; + $this->entryLimit = $entryLimit; + $this->store = []; + $this->priorityQueue = new \SplPriorityQueue (); + $this->priorityQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); + } + + /** + * @param $key + * @param $data + */ + public function set($key, $data){ + if(!$this->exists($key)){ + $this->priorityQueue->insert($key, $this->priority--); + } + + $this->store[$key] = $data; + + // check cleanup interval and cleanup Store + $this->cleanupInterval(); + } + + /** + * @param $key + * @return mixed|null + */ + public function get($key){ + return $this->exists($key) ? $this->store[$key] : null; + } + + /** + * @param $key + * @return bool + */ + public function exists($key){ + return isset($this->store[$key]); + } + + public function cleanupInterval() : void { + if( + !$this->priorityQueue->isEmpty() && $this->cleanupInterval && + ($this->priorityQueue->count() % $this->cleanupInterval === 0) + ){ + $this->cleanup(); + } + } + + public function cleanup(){ + while( + $this->entryLimit < $this->priorityQueue->count() && + $this->priorityQueue->valid() + ){ + if($this->exists($key = $this->priorityQueue->extract()['data'])){ + unset($this->store[$key]); + } + } + } + + public function clear(){ + $limit = $this->entryLimit; + $this->entryLimit = 0; + $this->cleanup(); + // restore entryLimit for next data + $this->entryLimit = $limit; + } + + /** + * @return string + */ + public function __toString(){ + return 'Store count: ' . count($this->store) . ' priorityQueue count: ' . $this->priorityQueue->count(); + } +} \ No newline at end of file diff --git a/app/Lib/Resource.php b/app/Lib/Resource.php new file mode 100644 index 000000000..7a9b6ba16 --- /dev/null +++ b/app/Lib/Resource.php @@ -0,0 +1,259 @@ + 'style', + 'script' => 'script', + 'font' => 'font', + 'document' => 'document', + 'image' => 'image', + 'url' => '' + ]; + + /** + * default link "type" attributes + */ + const ATTR_TYPE = [ + 'font' => 'font/woff2' + ]; + + /** + * default additional attributes by $group + */ + const ATTR_ADD = [ + 'font' => ['crossorigin' => 'anonymous'] + ]; + + /** + * BASE path + * @var string + */ + private $basePath = ''; + + /** + * absolute file path -> use setOption() for update + * @var array + */ + private $filePath = [ + 'style' => '', + 'script' => '', + 'font' => '', + 'document' => '', + 'image' => '', + 'favicon' => '', + 'url' => '' + ]; + + /** + * default file extensions by $group + * -> used if no fileExtension found in $file + * @var array + */ + private $fileExt = [ + 'style' => 'css', + 'script' => 'js', + 'document' => 'html', + 'font' => 'woff2' + ]; + + /** + * output type + * -> 'inline' -> render inline HTML tags + * -> 'header' -> send "Link" HTTP Header with request + * @see buildLinks() + * @see buildHeader() + * @var string + */ + private $output = 'inline'; + + /** + * resource file cache + * @var array + */ + private $resources = []; + + /** + * set or extend option + * @param string $option + * @param $value + * @param bool $extend + */ + public function setOption(string $option, $value, bool $extend = false){ + $this->$option = ($extend && is_array($value) && is_array($this->$option)) ? array_merge($this->$option, $value) : $value; + } + + /** + * get option + * @param string $option + * @return mixed|null + */ + public function getOption(string $option){ + return isset($this->$option) ? $this->$option : null; + } + + /** + * register new resource $file + * @param string $group + * @param string $file + * @param string $rel + */ + public function register(string $group, string $file, string $rel = self::ATTR_REL){ + $this->resources[$group][$file] = ['options' => ['rel' => $rel]]; + } + + /** + * get resource path/file.ext + * @param string $group + * @param string $file + * @return string + */ + public function getLink(string $group, string $file) : string { + // $group 'url' expect full qualified URLs + $link = ($group == 'url' ? '' : $this->getPath($group) . '/') . $file; + // add extension if not already part of the file + // -> allows switching between extensions (e.g. .jpg, .png) for the same image + $link .= empty(pathinfo($file, PATHINFO_EXTENSION)) ? '.' . $this->getFileExtension($group) : ''; + return $link; + } + + /** + * get resource path + * @param string $group + * @return string + */ + public function getPath(string $group) : string { + return rtrim($this->basePath, '/\\') . $this->filePath[$group]; + } + + /** + * build inline HTML tags for resources + * @return string + */ + public function buildLinks(){ + $this->build(); + $links = []; + foreach($this->resources as $group => $resources){ + foreach($resources as $file => $conf){ + $resourceHeader = ' $value){ + $resourceHeader .= ' ' . $attr . '="' . $value . '"'; + // insert href attr after rel attr -> better readability + if($attr == 'rel'){ + $resourceHeader .= ' href="' . $conf['link'] . '"'; + } + } + $links[] = $resourceHeader . '>'; + } + } + return "\n\t" . implode("\n\t", $links); + } + + /** + * build HTTP header for resource preload + * -> all registered resources combined in a single header + * @link https://www.nginx.com/blog/nginx-1-13-9-http2-server-push/#automatic-push + * @return string + */ + public function buildHeader() : string { + $this->build(); + $headers = []; + foreach($this->resources as $group => $resources){ + foreach($resources as $file => $conf){ + $resourceHeader = '<' . $conf['link'] . '>'; + foreach($conf['options'] as $attr => $value){ + $resourceHeader .= '; ' . $attr . '="' . $value . '"'; + } + $headers[] = $resourceHeader; + } + } + return 'Link: ' . implode(', ', $headers); + } + + /** + * build resource data + * -> add missing attributes to resources + */ + protected function build(){ + foreach($this->resources as $group => &$resources){ + foreach($resources as $file => &$conf){ + if(empty($conf['link'])){ + $conf['link'] = $this->getLink($group, $file); + } + + if( empty($conf['options']['rel']) ){ + $conf['options']['rel'] = self::ATTR_REL; + } + if( empty($conf['options']['as']) && !empty($attrAs = $this->getLinkAttrAs($group)) ){ + $conf['options']['as'] = $attrAs; + } + if( empty($conf['options']['type']) && !empty($attrType = $this->getLinkAttrType($group)) ){ + $conf['options']['type'] = $attrType; + } + + if( !empty($additionalAttr = $this->getAdditionalAttrs($group)) ){ + $conf['options'] = $conf['options'] + $additionalAttr; + } + } + } + unset($resources); // unset ref + } + + /** + * get 'as' attribute (potential destination) by resource $group + * @link https://w3c.github.io/preload/#as-attribute + * @param string $group + * @return string + */ + protected function getLinkAttrAs(string $group) : string { + return isset(self::ATTR_AS[$group]) ? self::ATTR_AS[$group] : ''; + } + + /** + * get 'type' attribute by resource $group + * @link https://w3c.github.io/preload/#early-fetch-of-critical-resources + * @param string $group + * @return string + */ + protected function getLinkAttrType(string $group) : string { + return isset(self::ATTR_TYPE[$group]) ? self::ATTR_TYPE[$group] : ''; + } + + /** + * get additional attributes by $group + * -> e.g. or fonts + * @param string $group + * @return array + */ + protected function getAdditionalAttrs(string $group) : array { + return isset(self::ATTR_ADD[$group]) ? self::ATTR_ADD[$group] : []; + } + + /** + * get file extension by $group + * -> e.g. or fonts + * @param string $group + * @return string + */ + protected function getFileExtension(string $group) : string { + return isset($this->fileExt[$group]) ? $this->fileExt[$group] : ''; + } +} \ No newline at end of file diff --git a/app/Lib/Socket/AbstractSocket.php b/app/Lib/Socket/AbstractSocket.php new file mode 100644 index 000000000..94daba1de --- /dev/null +++ b/app/Lib/Socket/AbstractSocket.php @@ -0,0 +1,261 @@ + throw OverflowException on exceed + */ + const JSON_DECODE_MAX_LENGTH = 65536 * 4; + + /** + * @var EventLoop\LoopInterface|null + */ + private $loop; + + /** + * Socket URI + * @var string + */ + protected $uri; + + /** + * Socket Options + * @var array + */ + protected $options; + + /** + * AbstractSocket constructor. + * @param string $uri + * @param array $options + */ + public function __construct(string $uri, array $options = []){ + $this->uri = $uri; + $this->options = $options; + } + + /** + * @return Socket\ConnectorInterface + */ + abstract protected function getConnector() : Socket\ConnectorInterface; + + /** + * @return EventLoop\LoopInterface + */ + protected function getLoop(): EventLoop\LoopInterface { + if(!($this->loop instanceof EventLoop\LoopInterface)){ + $this->loop = EventLoop\Factory::create(); + } + + return $this->loop; + } + + /** + * connect to socket + * @return Promise\PromiseInterface + */ + protected function connect() : Promise\PromiseInterface { + $deferred = new Promise\Deferred(); + + $this->getConnector() + ->connect($this->uri) + ->then($this->initConnection()) + ->then( + function(Socket\ConnectionInterface $connection) use ($deferred) { + $deferred->resolve($connection); + }, + function(\Exception $e) use ($deferred) { + $deferred->reject($e); + }); + + return $deferred->promise(); + } + + /** + * @param string $task + * @param null $load + * @return Promise\PromiseInterface + */ + public function write(string $task, $load = null) : Promise\PromiseInterface { + $deferred = new Promise\Deferred(); + $payload = $this->newPayload($task, $load); + + $this->connect() + ->then( + function(Socket\ConnectionInterface $connection) use ($payload, $deferred) { + return (new Promise\FulfilledPromise($connection)) + ->then($this->initWrite($payload)) + ->then($this->initRead()) + ->then($this->initClose($connection)) + ->then( + function($payload) use ($deferred) { + // we got valid data from socketServer -> check if $payload contains an error + if(is_array($payload) && $payload['task'] == 'error'){ + // ... wrap error payload in a rejectedPromise + $deferred->reject( + new Promise\RejectedPromise( + new \Exception($payload['load']) + ) + ); + }else{ + // good response + $deferred->resolve($payload); + } + }, + function(\Exception $e) use ($deferred) { + $deferred->reject($e); + }); + }, + function(\Exception $e) use ($deferred) { + // connection error + $deferred->reject($e); + }); + + $this->getLoop()->run(); + + return $deferred->promise() + ->otherwise( + // final exception handler for rejected promises -> convert to payload array + // -> No socket related Exceptions should be thrown down the chain + function(\Exception $e){ + return new Promise\RejectedPromise( + $this->newPayload('error', $e->getMessage()) + ); + }); + } + + /** + * set connection events + * @return callable + */ + protected function initConnection() : callable { + return function(Socket\ConnectionInterface $connection) : Promise\PromiseInterface { + $deferred = new Promise\Deferred(); + + /* connection event callbacks should be added here (if needed) + $connection->on('end', function(){ + echo "pf: connection on end" . PHP_EOL; + }); + + $connection->on('error', function(\Exception $e) { + echo "pf: connection on error: " . $e->getMessage() . PHP_EOL; + }); + + $connection->on('close', function(){ + echo "pf: connection on close" . PHP_EOL; + }); + */ + + $deferred->resolve($connection); + //$deferred->reject(new \RuntimeException('lala')); + + return $deferred->promise(); + }; + } + + /** + * write payload to connection + * @param $payload + * @return callable + */ + protected function initWrite($payload) : callable { + return function(Socket\ConnectionInterface $connection) use ($payload) : Promise\PromiseInterface { + $deferred = new Promise\Deferred(); + + $streamEncoded = new NDJson\Encoder($connection); + + $streamEncoded->on('error', function(\Exception $e) use ($deferred) { + $deferred->reject($e); + }); + + if($streamEncoded->write($payload)){ + $deferred->resolve($connection); + } + + return $deferred->promise(); + }; + } + + /** + * read response data from connection + * @return callable + */ + protected function initRead() : callable { + return function(Socket\ConnectionInterface $connection) : Promise\PromiseInterface { + // new empty stream for processing JSON + $stream = new Stream\ThroughStream(); + + $streamDecoded = new NDJson\Decoder($stream, true, 512, 0, self::JSON_DECODE_MAX_LENGTH); + + // promise get resolved on first emit('data') + $promise = Promise\Stream\first($streamDecoded); + + // register on('data') for main input stream + $connection->once('data', function ($chunk) use ($stream) { + // send current data chunk to processing stream -> resolves promise + $stream->emit('data', [$chunk]); + }); + + return $promise; + }; + } + + /** + * close connection + * @param Socket\ConnectionInterface $connection + * @return callable + */ + protected function initClose(Socket\ConnectionInterface $connection) : callable { + return function($payload) use ($connection) : Promise\PromiseInterface { + $deferred = new Promise\Deferred(); + $deferred->resolve($payload); + + //$connection->close(); + return $deferred->promise(); + }; + } + /** + * get new payload + * @param string $task + * @param null $load + * @return array + */ + protected function newPayload(string $task, $load = null) : array { + return [ + 'task' => $task, + 'load' => $load + ]; + } + + /** + * use this function to create new Socket instances + * @param string $class + * @param string $uri + * @param array $options + * @return SocketInterface + */ + public static function factory(string $class, string $uri, array $options = []) : SocketInterface { + if(class_exists($class) && $uri){ + return new $class($uri, $options); + }else{ + // invalid Socket requirements -> return NullSocket + return new NullSocket($uri); + } + } +} \ No newline at end of file diff --git a/app/Lib/Socket/NullSocket.php b/app/Lib/Socket/NullSocket.php new file mode 100644 index 000000000..7702f6424 --- /dev/null +++ b/app/Lib/Socket/NullSocket.php @@ -0,0 +1,38 @@ +getLoop(), $this->options); + } + + /** + * write to NullSocket can not be performed + * @param string $task + * @param null $load + * @return Promise\PromiseInterface + */ + public function write(string $task, $load = null) : Promise\PromiseInterface { + return new Promise\RejectedPromise(); + } +} \ No newline at end of file diff --git a/app/Lib/Socket/SocketInterface.php b/app/Lib/Socket/SocketInterface.php new file mode 100644 index 000000000..238c97a9b --- /dev/null +++ b/app/Lib/Socket/SocketInterface.php @@ -0,0 +1,30 @@ +getLoop(), $this->options); + } + +} \ No newline at end of file diff --git a/app/Lib/Util.php b/app/Lib/Util.php new file mode 100644 index 000000000..ec9d3324c --- /dev/null +++ b/app/Lib/Util.php @@ -0,0 +1,202 @@ + recursive + * @param $arr + * @param int $case + * @return array + */ + static function arrayChangeKeyCaseRecursive($arr, $case = CASE_LOWER){ + if(is_array($arr)){ + $arr = array_map( function($item){ + if( is_array($item) ) + $item = self::arrayChangeKeyCaseRecursive($item); + return $item; + }, array_change_key_case((array)$arr, $case)); + } + + return $arr; + } + + /** + * flatten multidimensional array ignore keys + * @param array $array + * @return array + */ + static function arrayFlattenByValue(array $array) : array { + $return = []; + array_walk_recursive($array, function($value) use (&$return) { $return[] = $value; }); + return $return; + } + + /** + * flatten multidimensional array merge keys + * -> overwrites duplicate keys! + * @param array $array + * @return array + */ + static function arrayFlattenByKey(array $array) : array { + $return = []; + array_walk_recursive($array, function($value, $key) use (&$return) { $return[$key] = $value; }); + return $return; + } + + /** + * transforms array with assoc. arrays as values + * into assoc. array where $key column data is used for its key + * @param array $array + * @param string $key + * @param bool $unsetKey + * @return array + */ + static function arrayGetBy(array $array, string $key, bool $unsetKey = true) : array { + // we can remove $key from nested arrays + return array_map(function($val) use ($key, $unsetKey) : array { + if($unsetKey){ + unset($val[$key]); + } + return $val; + }, array_column($array, null, $key)); + } + + /** + * checks whether an array is associative or not (sequential) + * @param mixed $array + * @return bool + */ + static function is_assoc($array) : bool { + $isAssoc = false; + if( + is_array($array) && + array_keys($array) !== range(0, count($array) - 1) + ){ + $isAssoc = true; + } + + return $isAssoc; + } + + /** + * convert array keys by a custom callback + * @param $arr + * @param $callback + * @return array + */ + static function arrayChangeKeys($arr, $callback){ + return array_combine( + array_map(function ($key) use ($callback){ + return $callback($key); + }, array_keys($arr)), $arr + ); + } + + /** + * convert a string with multiple scopes into an array + * @param string $scopes + * @return array|null + */ + static function convertScopesString($scopes){ + $scopes = array_filter( + array_map('strtolower', + (array)explode(' ', $scopes) + ) + ); + + if($scopes){ + sort($scopes); + }else{ + $scopes = null; + } + + return $scopes; + } + + /** + * obsucre string e.g. password (hide last characters) + * @param string $string + * @param int $maxHideChars + * @return string + */ + static function obscureString(string $string, int $maxHideChars = 10) : string { + $formatted = ''; + $length = mb_strlen((string)$string); + if($length > 0){ + $hideChars = ($length < $maxHideChars) ? $length : $maxHideChars; + $formatted = substr_replace($string, str_repeat('_', min(3, $length)), -$hideChars) . + ' [' . $length . ']'; + } + return $formatted; + } + + /** + * get hash from an array of ESI scopes + * @param array $scopes + * @return string + */ + static function getHashFromScopes($scopes) : string { + $scopes = (array)$scopes; + sort($scopes); + return md5(serialize($scopes)); + } + + /** + * get some information about a $source file/dir + * @param string|null $source + * @return array + */ + static function filesystemInfo(?string $source) : array { + $info = []; + if(is_dir($source)){ + $info['isDir'] = true; + }elseif(is_file($source)){ + $info['isFile'] = true; + } + if(!empty($info)){ + $info['chmod'] = substr(sprintf('%o', fileperms($source)), -4); + } + return $info; + } + + /** + * round DateTime to interval + * @param \DateTime $dateTime + * @param string $type + * @param int $interval + * @param string $round + */ + static function roundToInterval(\DateTime &$dateTime, string $type = 'sec', int $interval = 5, string $round = 'floor'){ + $hours = $minutes = $seconds = 0; + + $roundInterval = function(string $format, int $interval, string $round) : int { + return call_user_func($round, $format / $interval) * $interval; + }; + + switch($type){ + case 'hour': + $hours = $roundInterval($dateTime->format('H'), $interval, $round); + break; + case 'min': + $hours = $dateTime->format('H'); + $minutes = $roundInterval($dateTime->format('i'), $interval, $round); + break; + case 'sec': + $hours = $dateTime->format('H'); + $minutes = $dateTime->format('i'); + $seconds = $roundInterval($dateTime->format('s'), $interval, $round); + break; + } + + $dateTime->setTime($hours, $minutes, $seconds); + } +} \ No newline at end of file diff --git a/app/Model/AbstractModell.php b/app/Model/AbstractModell.php new file mode 100644 index 000000000..f87e8ad5d --- /dev/null +++ b/app/Model/AbstractModell.php @@ -0,0 +1,1156 @@ + leave this at a higher value + * @var int + */ + protected $ttl = 60; + + /** + * caching for relational data + * @var int + */ + protected $rel_ttl = 0; + + /** + * ass static columns for this table + * -> can be overwritten in child models + * @var bool + */ + protected $addStaticFields = true; + + /** + * enables table truncate + * -> see truncate(); + * -> CAUTION! if set to true truncate() will clear ALL rows! + * @var bool + */ + protected $allowTruncate = false; + + /** + * enables change for "active" column + * -> see setActive(); + * -> $this->active = false; will NOT work (prevent abuse)! + * @var bool + */ + private $allowActiveChange = false; + + /** + * getData() cache key prefix + * -> do not change, otherwise cached data is lost + * @var string + */ + private $dataCacheKeyPrefix = 'DATACACHE'; + + /** + * enables data export for this table + * -> can be overwritten in child models + * @var bool + */ + public static $enableDataExport = false; + + /** + * enables data import for this table + * -> can be overwritten in child models + * @var bool + */ + public static $enableDataImport = false; + + /** + * collection for validation errors + * @var array + */ + protected $validationError = []; + + + /** + * default charset for table + */ + const DEFAULT_CHARSET = 'utf8mb4'; + + /** + * default caching time of field schema - seconds + */ + const DEFAULT_TTL = 86400; + + /** + * default TTL for getData(); cache - seconds + */ + const DEFAULT_CACHE_TTL = 120; + + /** + * default TTL or temp table data read from *.csv file + * -> used during data import + */ + const DEFAULT_CACHE_CSV_TTL = 120; + + /** + * cache key prefix name for "full table" indexing + * -> used e.g. for a "search" index; or "import" index for *.csv imports + */ + const CACHE_KEY_PREFIX = 'INDEX'; + + /** + * cache key name for temp data import from *.csv files per table + */ + const CACHE_KEY_CSV_PREFIX = 'CSV'; + + /** + * default TTL for SQL query cache + */ + const DEFAULT_SQL_TTL = 3; + + /** + * data from Universe tables is static and does not change frequently + * -> refresh static data after X days + */ + const CACHE_MAX_DAYS = 60; + + /** + * class not exists error + */ + const ERROR_INVALID_MODEL_CLASS = 'Model class (%s) not found'; + + /** + * AbstractModel constructor. + * @param null $db + * @param null $table + * @param null $fluid + * @param int $ttl + * @param string $charset + */ + public function __construct($db = null, $table = null, $fluid = null, $ttl = self::DEFAULT_TTL, $charset = self::DEFAULT_CHARSET){ + + if(!is_object($db)){ + $db = self::getF3()->DB->getDB(static::DB_ALIAS); + } + + if(is_null($db)){ + // no valid DB connection found -> break on error + self::getF3()->set('HALT', true); + } + + // set charset -> used during table setup() + $this->charset = $charset; + + $this->addStaticFieldConfig(); + + parent::__construct($db, $table, $fluid, $ttl); + + // insert events ------------------------------------------------------------------------------------ + $this->beforeinsert(function($self, $pkeys){ + return $self->beforeInsertEvent($self, $pkeys); + }); + + $this->afterinsert(function($self, $pkeys){ + $self->afterInsertEvent($self, $pkeys); + }); + + // update events ------------------------------------------------------------------------------------ + $this->beforeupdate(function($self, $pkeys){ + return $self->beforeUpdateEvent($self, $pkeys); + }); + + $this->afterupdate(function($self, $pkeys){ + $self->afterUpdateEvent($self, $pkeys); + }); + + // erase events ------------------------------------------------------------------------------------- + $this->beforeerase(function($self, $pkeys){ + return $self->beforeEraseEvent($self, $pkeys); + }); + + $this->aftererase(function($self, $pkeys){ + $self->afterEraseEvent($self, $pkeys); + }); + } + + /** + * checks whether table exists on DB + * @return bool + */ + public function tableExists() : bool { + return is_object($this->db) ? $this->db->tableExists($this->table) : false; + } + + /** + * clear existing table Schema cache + * @return bool + */ + public function clearSchemaCache() : bool { + $f3 = self::getF3(); + $cache=\Cache::instance(); + if( + $f3->CACHE && is_object($this->db) && + $cache->exists($hash = $f3->hash($this->db->getDSN() . $this->table) . '.schema') + ){ + return (bool)$cache->clear($hash); + } + return false; + } + + /** + * @param string $key + * @param mixed $val + * @return mixed + * @throws ValidationException + */ + public function set($key, $val){ + if(is_string($val)){ + $val = trim($val); + } + + if( + !$this->dry() && + $key != 'updated' + ){ + if($this->exists($key)){ + // get raw column data (no objects) + $currentVal = $this->get($key, true); + + if(is_object($val)){ + if( + is_subclass_of($val, 'Model\AbstractModel') && + $val->_id != $currentVal + ){ + // relational object changed + $this->touch('updated'); + } + }elseif($val != $currentVal){ + // non object value + $this->touch('updated'); + } + } + } + + if(!$this->validateField($key, $val)){ + $this->throwValidationException($key); + } + + return parent::set($key, $val); + } + + /** + * setter for "active" status + * -> default: keep current "active" status + * -> can be overwritten + * @param bool $active + * @return mixed + */ + public function set_active($active){ + if($this->allowActiveChange){ + // allowed to set/change -> reset "allowed" property + $this->allowActiveChange = false; + }else{ + // not allowed to set/change -> keep current status + $active = $this->active; + } + return $active; + } + + /** + * get static fields for this model instance + * @return array + */ + protected function getStaticFieldConf() : array { + $staticFieldConfig = []; + + // static tables (fixed data) do not require them... + if($this->addStaticFields){ + $staticFieldConfig = [ + 'created' => [ + 'type' => Schema::DT_TIMESTAMP, + 'default' => Schema::DF_CURRENT_TIMESTAMP, + 'index' => true + ], + 'updated' => [ + 'type' => Schema::DT_TIMESTAMP, + 'default' => Schema::DF_CURRENT_TIMESTAMP, + 'index' => true + ] + ]; + } + + return $staticFieldConfig; + } + + /** + * extent the fieldConf Array with static fields for each table + */ + private function addStaticFieldConfig(){ + $this->fieldConf = array_merge($this->getStaticFieldConf(), $this->fieldConf); + } + + /** + * validates a table column based on validation settings + * @param string $key + * @param $val + * @return bool + */ + protected function validateField(string $key, $val) : bool { + $valid = true; + if($fieldConf = $this->fieldConf[$key]){ + if($method = $this->fieldConf[$key]['validate']){ + if( !is_string($method)){ + $method = $key; + } + $method = 'validate_' . $method; + if(method_exists($this, $method)){ + // validate $key (column) with this method... + $valid = $this->$method($key, $val); + }else{ + self::getF3()->error(501, 'Method ' . get_class($this) . '->' . $method . '() is not implemented'); + } + } + } + + return $valid; + } + + /** + * validates a model field to be a valid relational model + * @param $key + * @param $val + * @return bool + * @throws ValidationException + */ + protected function validate_notDry($key, $val) : bool { + $valid = true; + if($colConf = $this->fieldConf[$key]){ + if(isset($colConf['belongs-to-one'])){ + if( (is_int($val) || ctype_digit($val)) && (int)$val > 0){ + $valid = true; + }elseif( is_a($val, $colConf['belongs-to-one']) && !$val->dry() ){ + $valid = true; + }else{ + $valid = false; + $msg = 'Validation failed: "' . get_class($this) . '->' . $key . '" must be a valid instance of ' . $colConf['belongs-to-one']; + $this->throwValidationException($key, $msg); + } + } + } + + return $valid; + } + + /** + * validates a model field to be not empty + * @param $key + * @param $val + * @return bool + */ + protected function validate_notEmpty($key, $val) : bool { + $valid = false; + if($colConf = $this->fieldConf[$key]){ + switch($colConf['type']){ + case Schema::DT_INT: + case Schema::DT_FLOAT: + if( (is_int($val) || ctype_digit($val)) && (int)$val > 0){ + $valid = true; + } + break; + case Schema::DT_VARCHAR128: + case Schema::DT_VARCHAR256: + case Schema::DT_VARCHAR512: + if(!empty($val)){ + $valid = true; + } + break; + default: + } + } + + return $valid; + } + + /** + * get key for for all objects in this table + * @return string + */ + private function getTableCacheKey() : string { + return $this->dataCacheKeyPrefix .'.' . strtoupper($this->table); + } + + /** + * get the cache key for this model + * ->do not set a key if the model is not saved! + * @param string $dataCacheTableKeyPrefix + * @return null|string + */ + protected function getCacheKey(string $dataCacheTableKeyPrefix = '') : ?string { + $cacheKey = null; + + // set a model unique cache key if the model is saved + if($this->_id > 0){ + $cacheKey = $this->getTableCacheKey(); + + // check if there is a given key prefix + // -> if not, use the standard key. + // this is useful for caching multiple data sets according to one row entry + if(!empty($dataCacheTableKeyPrefix)){ + $cacheKey .= '.' . $dataCacheTableKeyPrefix . '_'; + }else{ + $cacheKey .= '.ID_'; + } + $cacheKey .= (string)$this->_id; + } + + return $cacheKey; + } + + /** + * get cached data from this model + * @param string $dataCacheKeyPrefix - optional key prefix + * @return mixed|null + */ + protected function getCacheData($dataCacheKeyPrefix = ''){ + $cacheData = null; + // table cache exists + // -> check cache for this row data + if(!is_null($cacheKey = $this->getCacheKey($dataCacheKeyPrefix))){ + self::getF3()->exists($cacheKey, $cacheData); + } + return $cacheData; + } + + /** + * update/set the getData() cache for this object + * @param $cacheData + * @param string $dataCacheKeyPrefix + * @param int $data_ttl + */ + public function updateCacheData($cacheData, string $dataCacheKeyPrefix = '', int $data_ttl = self::DEFAULT_CACHE_TTL){ + $cacheDataTmp = (array)$cacheData; + + // check if data should be cached + // and cacheData is not empty + if( + $data_ttl > 0 && + !empty($cacheDataTmp) + ){ + $cacheKey = $this->getCacheKey($dataCacheKeyPrefix); + if(!is_null($cacheKey)){ + self::getF3()->set($cacheKey, $cacheData, $data_ttl); + } + } + } + + /** + * unset the getData() cache for this object + * -> see also clearCacheDataWithPrefix(), for more information + */ + public function clearCacheData(){ + $this->clearCache($this->getCacheKey()); + } + + /** + * unset object cached data by prefix + * -> primarily used by object cache with multiple data caches + * @param string $dataCacheKeyPrefix + */ + public function clearCacheDataWithPrefix(string $dataCacheKeyPrefix = ''){ + $this->clearCache($this->getCacheKey($dataCacheKeyPrefix)); + } + + /** + * unset object cached data (if exists) + * @param $cacheKey + */ + private function clearCache($cacheKey){ + if(!empty($cacheKey)){ + $f3 = self::getF3(); + if($f3->exists($cacheKey)){ + $f3->clear($cacheKey); + } + } + } + + /** + * throw validation exception for a model property + * @param string $col + * @param string $msg + * @throws ValidationException + */ + protected function throwValidationException(string $col, string $msg = ''){ + $msg = empty($msg) ? 'Validation failed: "' . $col . '".' : $msg; + throw new ValidationException($msg, $col); + } + + /** + * @param string $msg + * @throws DatabaseException + */ + protected function throwDbException(string $msg){ + throw new DatabaseException($msg); + } + + /** + * checks whether this model is active or not + * each model should have an "active" column + * @return bool + */ + public function isActive() : bool { + return (bool)$this->active; + } + + /** + * set active state for a model + * -> do NOT use $this->active for status change! + * -> this will not work (prevent abuse) + * @param bool $active + */ + public function setActive(bool $active){ + // enables "active" change for this model + $this->allowActiveChange = true; + $this->active = $active; + } + + /** + * get single dataSet by id + * @param int $id + * @param int $ttl + * @param bool $isActive + * @return bool + */ + public function getById(int $id, int $ttl = self::DEFAULT_SQL_TTL, bool $isActive = true) : bool { + return $this->getByForeignKey($this->primary, $id, ['limit' => 1], $ttl, $isActive); + } + + /** + * get dataSet by foreign column (single result) + * @param string $key + * @param $value + * @param array $options + * @param int $ttl + * @param bool $isActive + * @return bool + */ + public function getByForeignKey(string $key, $value, array $options = [], int $ttl = 0, bool $isActive = true) : bool { + $filters = [self::getFilter($key, $value)]; + + if($isActive && $this->exists('active')){ + $filters[] = self::getFilter('active', true); + } + + $this->filterRel(); + + return $this->load($this->mergeFilter($filters), $options, $ttl); + } + + /** + * apply filter() for relations + * -> overwrite in child classes + * @see https://github.com/ikkez/f3-cortex#filter + */ + protected function filterRel() : void {} + + /** + * get first model from a relation that matches $filter + * @param string $key + * @param array $filter + * @return mixed|null + */ + protected function relFindOne(string $key, array $filter){ + $relModel = null; + $relFilter = []; + if($this->exists($key, true)){ + $fieldConf = $this->getFieldConfiguration(); + if(array_key_exists($key, $fieldConf)){ + if(array_key_exists($type = 'has-many', $fieldConf[$key])){ + $fromConf = $fieldConf[$key][$type]; + $relFilter = self::getFilter($fromConf[1], $this->getRaw($fromConf['relField'])); + } + } + + /** + * @var $relModel self|bool + */ + $relModel = $this->rel($key)->findone($this->mergeFilter([$relFilter, $this->mergeWithRelFilter($key, $filter)])); + } + + return $relModel ? : null; + } + + /** + * get all models from a relation that match $filter + * @param string $key + * @param array $filter + * @return CortexCollection|null + */ + protected function relFind(string $key, array $filter) : ?CortexCollection { + $relModel = null; + $relFilter = []; + if($this->exists($key, true)){ + $fieldConf = $this->getFieldConfiguration(); + if(array_key_exists($key, $fieldConf)){ + if(array_key_exists($type = 'has-many', $fieldConf[$key])){ + $fromConf = $fieldConf[$key][$type]; + $relFilter = self::getFilter($fromConf[1], $this->getRaw($fromConf['relField'])); + } + } + + /** + * @var $relModel CortexCollection|bool + */ + $relModel = $this->rel($key)->find($this->mergeFilter([$relFilter, $this->mergeWithRelFilter($key, $filter)])); + } + + return $relModel ? : null; + } + + /** + * Event "Hook" function + * can be overwritten + * return false will stop any further action + * @param self $self + * @param $pkeys + * @return bool + */ + public function beforeInsertEvent($self, $pkeys) : bool { + if($this->exists('updated')){ + $this->touch('updated'); + } + return true; + } + + /** + * Event "Hook" function + * can be overwritten + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterInsertEvent($self, $pkeys){ + } + + /** + * Event "Hook" function + * can be overwritten + * return false will stop any further action + * @param self $self + * @param $pkeys + * @return bool + */ + public function beforeUpdateEvent($self, $pkeys) : bool { + return true; + } + + /** + * Event "Hook" function + * can be overwritten + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterUpdateEvent($self, $pkeys){ + } + + /** + * Event "Hook" function + * can be overwritten + * @param self $self + * @param $pkeys + * @return bool + */ + public function beforeEraseEvent($self, $pkeys) : bool { + return true; + } + + /** + * Event "Hook" function + * can be overwritten + * @param self $self + * @param $pkeys + */ + public function afterEraseEvent($self, $pkeys){ + } + + /** + * function should be overwritten in parent classes + * @return bool + */ + public function isValid() : bool { + return true; + } + + /** + * get row count in this table + * @return int + */ + public function getRowCount() : int { + return is_object($this->db) ? $this->db->getRowCount($this->getTable()) : 0; + } + + /** + * truncate all table rows + * -> Use with Caution!!! + */ + public function truncate(){ + if($this->allowTruncate && is_object($this->db)){ + $this->db->exec("TRUNCATE " . $this->getTable()); + } + } + + /** + * format dateTime column + * @param $column + * @param string $format + * @return false|null|string + */ + public function getFormattedColumn($column, $format = 'Y-m-d H:i'){ + return $this->get($column) ? date($format, strtotime( $this->get($column) )) : null; + } + + /** + * export and download table data as *.csv + * this is primarily used for static tables + * @param array $fields + * @return bool + */ + public function exportData(array $fields = []) : bool { + $status = false; + + if(static::$enableDataExport){ + $tableModifier = static::getTableModifier(); + $headers = $tableModifier->getCols(); + + if($fields){ + // columns to export -> reIndex keys + $headers = array_values(array_intersect($headers, $fields)); + } + + // just get the records with existing columns + // -> no "virtual" fields or "new" columns + $this->fields($headers); + $allRecords = $this->find(); + + if($allRecords){ + $tableData = $allRecords->castAll(0); + + // format data -> "id" must be first key + foreach($tableData as &$rowData){ + $rowData = [$this->primary => $rowData['_id']] + $rowData; + unset($rowData['_id']); + } + + $sheet = \Sheet::instance(); + $data = $sheet->dumpCSV($tableData, $headers); + + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Content-Type: text/csv;charset=UTF-8'); + header('Content-Disposition: attachment;filename=' . $this->getTable() . '.csv'); + echo $data; + exit(); + } + } + + return $status; + } + + /** + * read *.csv file for a $table name + * -> 'group' by $getByKey column name and return array + * @param string $table + * @param string $getByKey + * @return array + */ + public static function getCSVData(string $table, string $getByKey = 'id') : array { + $hashKeyTableCSV = static::generateHashKeyTable($table, static::CACHE_KEY_PREFIX . '_' . self::CACHE_KEY_CSV_PREFIX); + + if( + !self::getF3()->exists($hashKeyTableCSV, $tableData) && + !empty($tableData = Util::arrayGetBy(self::loadCSV($table), $getByKey, false)) + ){ + self::getF3()->set($hashKeyTableCSV, $tableData, self::DEFAULT_CACHE_CSV_TTL); + } + + return $tableData; + } + + /** + * load data from *.csv file + * @param string $fileName + * @return array + */ + protected static function loadCSV(string $fileName) : array { + $tableData = []; + + // rtrim(); for arrays (removes empty values) from the end + $rtrim = function($array = [], $lengthMin = false) : array { + $length = key(array_reverse(array_diff($array, ['']), 1))+1; + $length = $length < $lengthMin ? $lengthMin : $length; + return array_slice($array, 0, $length); + }; + + if($fileName){ + $filePath = self::getF3()->get('EXPORT') . 'csv/' . $fileName . '.csv'; + if(is_file($filePath)){ + $handle = @fopen($filePath, 'r'); + $keys = array_map('lcfirst', fgetcsv($handle, 0, ';')); + $keys = $rtrim($keys); + + if(count($keys) > 0){ + while (!feof($handle)) { + $tableData[] = array_combine($keys, $rtrim(fgetcsv($handle, 0, ';'), count($keys))); + } + }else{ + self::getF3()->error(500, 'File could not be read'); + } + }else{ + self::getF3()->error(404, 'File not found: ' . $filePath); + } + } + + return $tableData; + } + + /** + * import table data from a *.csv file + * @return array|bool + */ + public function importData(){ + $status = false; + + if( + static::$enableDataImport && + !empty($tableData = self::loadCSV($this->getTable())) + ){ + // import row data + $status = $this->importStaticData($tableData); + $this->getF3()->status(202); + } + + return $status; + } + + /** + * insert/update static data into this table + * WARNING: rows will be deleted if not part of $tableData ! + * @param array $tableData + * @return array + */ + protected function importStaticData(array $tableData = []) : array { + $rowIDs = []; + $addedCount = 0; + $updatedCount = 0; + $deletedCount = 0; + + $tableModifier = static::getTableModifier(); + $fields = $tableModifier->getCols(); + + foreach($tableData as $rowData){ + // search for existing record and update columns + $this->getById($rowData['id'], 0); + if($this->dry()){ + $addedCount++; + }else{ + $updatedCount++; + } + $this->copyfrom($rowData, $fields); + $this->save(); + $rowIDs[] = $this->_id; + $this->reset(); + } + + // remove old data + $oldRows = $this->find('id NOT IN (' . implode(',', $rowIDs) . ')'); + if($oldRows){ + foreach($oldRows as $oldRow){ + $oldRow->erase(); + $deletedCount++; + } + } + return ['added' => $addedCount, 'updated' => $updatedCount, 'deleted' => $deletedCount]; + } + + /** + * get "default" logging object for this kind of model + * -> can be overwritten + * @param string $action + * @return Logging\LogInterface + */ + protected function newLog(string $action = '') : Logging\LogInterface{ + return new Logging\DefaultLog($action); + } + + /** + * get formatter callback function for parsed logs + * @return null + */ + protected function getLogFormatter(){ + return null; + } + + /** + * add new validation error + * @param ValidationException $e + */ + protected function setValidationError(ValidationException $e){ + $this->validationError[] = $e->getError(); + } + + /** + * get all validation errors + * @return array + */ + public function getErrors() : array { + return $this->validationError; + } + + /** + * checks whether data is outdated and should be refreshed + * @return bool + */ + protected function isOutdated() : bool { + $outdated = true; + if($this->valid()){ + try{ + $timezone = $this->getF3()->get('getTimeZone')(); + $currentTime = new \DateTime('now', $timezone); + $updateTime = \DateTime::createFromFormat( + 'Y-m-d H:i:s', + $this->updated, + $timezone + ); + $interval = $updateTime->diff($currentTime); + if($interval->days < self::CACHE_MAX_DAYS){ + $outdated = false; + } + }catch(\Exception $e){ + self::getF3()->error($e->getCode(), $e->getMessage(), $e->getTrace()); + } + } + return $outdated; + } + + /** + * @return mixed + */ + public function save(){ + $return = false; + try{ + $return = parent::save(); + }catch(ValidationException $e){ + $this->setValidationError($e); + }catch(DatabaseException $e){ + self::getF3()->error($e->getResponseCode(), $e->getMessage(), $e->getTrace()); + } + + return $return; + } + + /** + * @return string + */ + public function __toString() : string { + return $this->getTable(); + } + + /** + * @param string $argument + * @return \ReflectionClass + * @throws \ReflectionException + */ + protected static function refClass($argument = self::class) : \ReflectionClass { + return new \ReflectionClass($argument); + } + + /** + * get the framework instance + * @return \Base + */ + public static function getF3() : \Base { + return \Base::instance(); + } + + /** + * get model data as array + * @param $data + * @return array + */ + public static function toArray($data) : array { + return json_decode(json_encode($data), true); + } + + /** + * get new filter array representation + * -> $suffix can be used fore unique placeholder, + * in case the same $key is used with different $values in the same query + * @param string $key + * @param mixed $value + * @param string $operator + * @param string $suffix + * @return array + */ + public static function getFilter(string $key, $value, string $operator = '=', string $suffix = '') : array { + $placeholder = ':' . implode('_', array_filter([$key, $suffix])); + return [$key . ' ' . $operator . ' ' . $placeholder, $placeholder => $value]; + } + + /** + * stores data direct into the Cache backend (e.g. Redis) + * $f3->set() used the same code. The difference is, that $f3->set() + * also loads data into the Hive. + * This can result in high RAM usage if a great number of key->values should be stored in Cache + * (like the search index for system data) + * @param string $key + * @param $data + * @param int $ttl + */ + public static function setCacheValue(string $key, $data, int $ttl = 0){ + $cache = \Cache::instance(); + $cache->set(self::getF3()->hash($key).'.var', $data, $ttl); + } + + /** + * check whether a cache $key exists + * -> §val (reference) get updated with the cache data + * -> equivalent to $f3->exists() + * @param string $key + * @param null $val + * @return bool + */ + public static function existsCacheValue(string $key, &$val = null){ + $cache = \Cache::instance(); + return $cache->exists(self::getF3()->hash($key).'.var',$val); + } + + /** + * debug log function + * @param string $text + * @param string $type + */ + public static function log($text, $type = 'DEBUG'){ + Controller\LogController::getLogger($type)->write($text); + } + + /** + * get tableModifier class for this table + * @return bool|Mysql\TableModifier + */ + public static function getTableModifier(){ + $df = parent::resolveConfiguration(); + $schema = new Schema($df['db']); + return $schema->alterTable($df['table']); + } + + /** + * Check whether a (multi)-column index exists or not on a table + * related to this model + * @param array $columns + * @return bool|array + */ + public static function indexExists(array $columns = []){ + $tableModifier = self::getTableModifier(); + $df = parent::resolveConfiguration(); + + $check = false; + $indexKey = $df['table'] . '___' . implode('__', $columns); + $indexList = $tableModifier->listIndex(); + if(array_key_exists( $indexKey, $indexList)){ + $check = $indexList[$indexKey]; + } + + return $check; + } + + /** + * set a multi-column index for this table + * @param array $columns Column(s) to be indexed + * @param bool $unique Unique index + * @param int $length index length for text fields in mysql + * @return bool + */ + public static function setMultiColumnIndex(array $columns = [], $unique = false, $length = 20) : bool { + $status = false; + $tableModifier = self::getTableModifier(); + + if( self::indexExists($columns) === false ){ + $tableModifier->addIndex($columns, $unique, $length); + $buildStatus = $tableModifier->build(); + if($buildStatus === 0){ + $status = true; + } + } + + return $status; + } + + /** + * factory for all Models + * @param string $className + * @param int $ttl + * @return AbstractModel|null + * @throws \Exception + */ + public static function getNew(string $className, int $ttl = self::DEFAULT_TTL) : ?self { + $model = null; + $className = self::refClass(static::class)->getNamespaceName() . '\\' . $className; + if(class_exists($className)){ + $model = new $className(null, null, null, $ttl); + }else{ + throw new \Exception(sprintf(self::ERROR_INVALID_MODEL_CLASS, $className)); + } + return $model; + } + + /** + * generate hashKey for a complete table + * -> should hold hashKeys for multiple rows + * @param string $table + * @param string $prefix + * @return string + */ + public static function generateHashKeyTable(string $table, string $prefix = self::CACHE_KEY_PREFIX ) : string { + return $prefix . '_' . strtolower($table); + } + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + $status = parent::setup($db, $table, $fields); + + // set static default data + if($status === true && property_exists(static::class, 'tableData')){ + $model = self::getNew(self::refClass(static::class)->getShortName(), 0); + $model->importStaticData(static::$tableData); + } + + return $status; + } + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/AbstractMapTrackingModel.php b/app/Model/Pathfinder/AbstractMapTrackingModel.php new file mode 100644 index 000000000..510a86a82 --- /dev/null +++ b/app/Model/Pathfinder/AbstractMapTrackingModel.php @@ -0,0 +1,117 @@ + [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\CharacterModel', + 'constraint' => [ + [ + 'table' => 'character', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'updatedCharacterId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\CharacterModel', + 'constraint' => [ + [ + 'table' => 'character', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ] + ]; + + /** + * get static character fields for this model instance + * @return array + */ + protected function getStaticFieldConf(): array{ + return array_merge(parent::getStaticFieldConf(), $this->trackingFieldConf); + } + + /** + * log character activity create/update/delete events + * @param string $action + */ + protected function logActivity($action){ + // check if activity logging is enabled for this object + if($this->enableActivityLogging){ + // check for field changes + if( + mb_stripos(mb_strtolower($action), 'delete') !== false || + !empty($this->fieldChanges) + ){ + $this->newLog($action)->setCharacter($this->updatedCharacterId)->setData($this->fieldChanges)->buffer(); + } + } + } + + /** + * validates all required columns of this class + * @return bool + * @throws Exception\DatabaseException + */ + public function isValid(): bool { + if($valid = parent::isValid()){ + foreach($this->trackingFieldConf as $key => $colConf){ + if($this->exists($key)){ + $valid = $this->validateField($key, $this->$key); + if(!$valid){ + break; + } + }else{ + $valid = false; + $this->throwDbException('Missing table column "' . $this->getTable(). '.' . $key . '"'); + break; + } + } + } + + return $valid; + } + + /** + * get log file data + * @return array + */ + public function getLogData(): array { + return []; + } + + /** + * save connection + * @param CharacterModel $characterModel + * @return ConnectionModel|false + */ + public function save(CharacterModel $characterModel = null){ + if($this->dry()){ + $this->createdCharacterId = $characterModel; + } + $this->updatedCharacterId = $characterModel; + + return parent::save(); + } + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/AbstractPathfinderModell.php b/app/Model/Pathfinder/AbstractPathfinderModell.php new file mode 100644 index 000000000..4f487c18e --- /dev/null +++ b/app/Model/Pathfinder/AbstractPathfinderModell.php @@ -0,0 +1,97 @@ + fields that should be checked need an "activity-log" flag + * in $fieldConf config + * @var bool + */ + protected $enableActivityLogging = true; + + /** + * changed fields (columns) on update/insert + * -> e.g. for character "activity logging" + * @var array + */ + protected $fieldChanges = []; + + /** + * change default "activity logging" status + * -> enable/disable + * @param $status + */ + public function setActivityLogging(bool $status){ + $this->enableActivityLogging = $status; + } + + /** + * @param bool $mapper + * @return NULL|void + */ + public function reset($mapper = true){ + $this->fieldChanges = []; + parent::reset($mapper); + } + + /** + * function should be overwritten in child classes with access restriction + * @param CharacterModel $characterModel + * @return bool + */ + public function hasAccess(CharacterModel $characterModel) : bool { + return true; + } + + /** + * get old and new value from field, in case field is configured with 'activity-log' + * @return array + */ + protected function getFieldChanges() : array { + $changes = []; + + if($this->enableActivityLogging){ + // filter fields, where "activity" (changes) should be logged + $fieldConf = array_filter($this->fieldConf, function($fieldConf, $key){ + return isset($fieldConf['activity-log']) ? (bool)$fieldConf['activity-log'] : false; + }, ARRAY_FILTER_USE_BOTH); + + if($fieldKeys = array_keys($fieldConf)){ + // model has fields where changes should be logged + $schema = $this->getMapper()->schema(); + foreach($fieldKeys as $key){ + if($this->changed($key)){ + $changes[$key] = [ + 'old' => $schema[$key]['initial'], + 'new' => $schema[$key]['value'] + ]; + } + } + } + } + + return $changes; + } + + /** + * @return mixed|void + */ + public function save(){ + // save changed field value BEFORE ->save() it called! + // parent::save() resets the schema and old values get replaced with new values + $this->fieldChanges = $this->getFieldChanges(); + + return parent::save(); + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/AbstractSystemApiBasicModel.php b/app/Model/Pathfinder/AbstractSystemApiBasicModel.php new file mode 100644 index 000000000..89728c643 --- /dev/null +++ b/app/Model/Pathfinder/AbstractSystemApiBasicModel.php @@ -0,0 +1,99 @@ +addStaticKillFieldConfig(); + + parent::__construct($db, $table, $fluid, $ttl); + } + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object)[]; + $data->systemId = $this->getRaw('systemId'); + $data->values = $this->getValues(); + $data->updated = strtotime($this->updated); + + return $data; + } + + /** + * get all "valX" column data as array + * -> "start" (most recent) value is stored in column name stored in "lastUpdatedValue" column + * @return array + */ + protected function getValues() : array { + $valueColumnNames = range(1, static::DATA_COLUMN_COUNT); + $preFixer = function(&$value, $key, $prefix){ + $value = $prefix . $value; + }; + array_walk($valueColumnNames, $preFixer, static::DATA_COLUMN_PREFIX); + + $valueColumns = array_intersect_key($this->cast(null, 0), array_flip($valueColumnNames)); + $lastUpdatedValue = $this->lastUpdatedValue ? : 1; + + // bring values in correct order based on "lastUpdatedValue" + $valueColumnsA = array_slice($valueColumns, $lastUpdatedValue - static::DATA_COLUMN_COUNT , null, true); + $valueColumnsB = array_slice($valueColumns, 0, $lastUpdatedValue, true); + + return array_values($valueColumnsA + $valueColumnsB); + } + + /** + * extent the fieldConf Array with static fields for each table + */ + private function addStaticKillFieldConfig(){ + if(is_array($this->fieldConf)){ + $staticFieldConfig = []; + + $staticFieldConfig['lastUpdatedValue'] = [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 1, + 'index' => true + ]; + + for($i = 1; $i <= static::DATA_COLUMN_COUNT; $i++){ + $staticFieldConfig[static::DATA_COLUMN_PREFIX . $i] = [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ]; + } + + $this->fieldConf = array_merge($this->fieldConf, $staticFieldConfig); + } + } + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/ActivityLogModel.php b/app/Model/Pathfinder/ActivityLogModel.php new file mode 100644 index 000000000..bfe47e39f --- /dev/null +++ b/app/Model/Pathfinder/ActivityLogModel.php @@ -0,0 +1,205 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'characterId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\CharacterModel', + 'constraint' => [ + [ + 'table' => 'character', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'mapId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\MapModel', + 'constraint' => [ + [ + 'table' => 'map', + 'on-delete' => 'SET NULL' // keep log data on map delete + ] + ] + ], + + // map actions ----------------------------------------------------- + + 'mapCreate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + 'counter' => true + ], + 'mapUpdate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + 'counter' => true + ], + 'mapDelete' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + 'counter' => true + ], + + // system actions ----------------------------------------------------- + + 'systemCreate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + 'counter' => true + ], + 'systemUpdate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + 'counter' => true + ], + 'systemDelete' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + 'counter' => true + ], + + // connection actions ------------------------------------------------- + + 'connectionCreate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + 'counter' => true + ], + 'connectionUpdate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + 'counter' => true + ], + 'connectionDelete' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + 'counter' => true + ], + + // signature actions ------------------------------------------------- + + 'signatureCreate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + 'counter' => true + ], + 'signatureUpdate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + 'counter' => true + ], + 'signatureDelete' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + 'counter' => true + ], + ]; + + /** + * ActivityLogModel constructor. + * @param null $db + * @param null $table + * @param null $fluid + * @param int $ttl + */ + public function __construct($db = NULL, $table = NULL, $fluid = NULL, $ttl = 0){ + $this->addStaticDateFieldConfig(); + + parent::__construct($db, $table, $fluid, $ttl); + } + + /** + * extent the fieldConf Array with static fields for each table + */ + private function addStaticDateFieldConfig(){ + if(is_array($this->fieldConf)){ + $staticFieldConfig = [ + 'year' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => date('o'), // 01.01 could be week 53 -> NOT "current" year! + 'index' => true + ], + 'week' => [ // week in year [1-53] + 'type' => Schema::DT_TINYINT, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + ]; + $this->fieldConf = array_merge($staticFieldConfig, $this->fieldConf); + } + } + + /** + * get all table columns that are used as "counter" columns + * @return array + */ + public function getCountableColumnNames(): array { + $fieldConf = $this->getFieldConfiguration(); + + $filterCounterColumns = function($key, $value){ + return isset($value['counter']) ? $key : false; + }; + + return array_values(array_filter(array_map($filterCounterColumns, array_keys($fieldConf), $fieldConf))); + } + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['year', 'week', 'characterId', 'mapId'], true); + if($status === true){ + $status = parent::setMultiColumnIndex(['year', 'week', 'characterId']); + } + } + return $status; + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/AllianceMapModel.php b/app/Model/Pathfinder/AllianceMapModel.php new file mode 100644 index 000000000..e13a39de3 --- /dev/null +++ b/app/Model/Pathfinder/AllianceMapModel.php @@ -0,0 +1,76 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'allianceId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\AllianceModel', + 'constraint' => [ + [ + 'table' => 'alliance', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'mapId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\MapModel', + 'constraint' => [ + [ + 'table' => 'map', + 'on-delete' => 'CASCADE' + ] + ] + ] + ]; + + /** + * see parent + */ + public function clearCacheData(){ + // clear map cache + $this->mapId->clearCacheData(); + } + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['allianceId', 'mapId'], true); + } + return $status; + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/AllianceModel.php b/app/Model/Pathfinder/AllianceModel.php new file mode 100644 index 000000000..7d9e408dc --- /dev/null +++ b/app/Model/Pathfinder/AllianceModel.php @@ -0,0 +1,169 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'ticker' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'shared' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 0 + ], + 'allianceCharacters' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\CharacterModel', 'allianceId'] + ], + 'mapAlliances' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\AllianceMapModel', 'allianceId'] + ] + ]; + + /** + * get all alliance data + * @return \stdClass + */ + public function getData(){ + $allianceData = (object) []; + + $allianceData->id = $this->id; + $allianceData->name = $this->name; + $allianceData->shared = $this->shared; + + return $allianceData; + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + * @return bool + */ + public function beforeUpdateEvent($self, $pkeys) : bool { + // if model changed, 'update' col needs to be updated as well + // -> data no longer "outdated" + $this->touch('updated'); + + return parent::beforeUpdateEvent($self, $pkeys); + } + + /** + * get all maps for this alliance + * @return array|mixed + */ + public function getMaps(){ + $maps = []; + $this->filterRel(); + + if($this->mapAlliances){ + $mapCount = 0; + foreach($this->mapAlliances as $mapAlliance){ + if( + $mapAlliance->mapId->isActive() && + $mapCount < Config::getMapsDefaultConfig('alliance')['max_count'] + ){ + $maps[] = $mapAlliance->mapId; + $mapCount++; + } + } + } + + return $maps; + } + + /** + * get all characters in this alliance + * @param array $characterIds + * @param array $options + * @return CharacterModel[] + */ + public function getCharacters($characterIds = [], $options = []) : array { + $characters = []; + $filter = ['active = ?', 1]; + + if( !empty($characterIds) ){ + $filter[0] .= ' AND id IN (?)'; + $filter[] = $characterIds; + } + + $this->filter('allianceCharacters', $filter); + + if($options['hasLog']){ + // just characters with active log data + $this->has('allianceCharacters.characterLog', ['active = ?', 1]); + } + + + if($this->allianceCharacters){ + foreach($this->allianceCharacters as $character){ + $characters[] = $character; + } + } + + return $characters; + } + + /** + * load alliance by Id either from DB or load data from API + * @param int $id + * @param int $ttl + * @param bool $isActive + * @return bool + */ + public function getById(int $id, int $ttl = self::DEFAULT_SQL_TTL, bool $isActive = true) : bool { + /** + * @var AllianceModel $alliance + */ + $loaded = parent::getById($id, $ttl, $isActive); + if($this->isOutdated()){ + // request alliance data + $allianceData = self::getF3()->ccpClient()->send('getAlliance', $id); + if(!empty($allianceData) && !isset($allianceData['error'])){ + $this->copyfrom($allianceData, ['id', 'name', 'ticker']); + $this->save(); + } + } + + return $loaded; + } + + /** + * @see parent + */ + public function filterRel() : void { + $this->filter('mapAlliances', self::getFilter('active', true), ['order' => 'created']); + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/CharacterAuthenticationModel.php b/app/Model/Pathfinder/CharacterAuthenticationModel.php new file mode 100644 index 000000000..f73101c50 --- /dev/null +++ b/app/Model/Pathfinder/CharacterAuthenticationModel.php @@ -0,0 +1,78 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'characterId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\CharacterModel', + 'constraint' => [ + [ + 'table' => 'character', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'selector' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'index' => true, + 'unique' => true + ], + 'token' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'index' => true + ], + 'expires' => [ + 'type' => Schema::DT_TIMESTAMP, + 'default' => Schema::DF_CURRENT_TIMESTAMP, + 'index' => true + ] + ]; + + + /** + * Event "Hook" function + * can be overwritten + * @param CharacterAuthenticationModel $self + * @param $pkeys + * @return bool + */ + public function beforeEraseEvent($self, $pkeys) : bool { + // clear existing client Cookies as well + $cookieName = Controller\Controller::COOKIE_PREFIX_CHARACTER; + $cookieName .= '_' . $this->characterId->getCookieName(); + $self::getF3()->clear('COOKIE.' . $cookieName); + return true; + } + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/CharacterLogModel.php b/app/Model/Pathfinder/CharacterLogModel.php new file mode 100644 index 000000000..82afa5d0e --- /dev/null +++ b/app/Model/Pathfinder/CharacterLogModel.php @@ -0,0 +1,277 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'characterId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'unique' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\CharacterModel', + 'constraint' => [ + [ + 'table' => 'character', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'activity-log' => true, + 'validate' => 'notEmpty' + ], + 'systemName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true, + 'validate' => 'notEmpty' + ], + 'shipTypeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'activity-log' => true + ], + 'shipTypeName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ], + 'shipId' => [ + 'type' => Schema::DT_BIGINT, + 'index' => true, + 'activity-log' => true + ], + 'shipMass' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0, + 'activity-log' => true + ], + 'shipName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'stationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'activity-log' => true + ], + 'stationName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ], + 'structureTypeId' => [ + 'type' => Schema::DT_INT, + 'index' => true + ], + 'structureTypeName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'structureId' => [ + 'type' => Schema::DT_BIGINT, + 'index' => true, + 'activity-log' => true + ], + 'structureName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ] + ]; + + /** + * set log data by associative array + * @param array $logData + */ + public function setData($logData){ + + if( isset($logData['system']) ){ + $this->systemId = (int)$logData['system']['id']; + $this->systemName = $logData['system']['name']; + }else{ + $this->systemId = null; + $this->systemName = ''; + } + + if( isset($logData['ship']) ){ + $this->shipTypeId = (int)$logData['ship']['typeId']; + $this->shipTypeName = $logData['ship']['typeName']; + $this->shipId = (int)$logData['ship']['id']; + $this->shipName = $logData['ship']['name']; + $this->shipMass = (float)$logData['ship']['mass']; + }else{ + $this->shipTypeId = null; + $this->shipTypeName = ''; + $this->shipId = null; + $this->shipName = ''; + $this->shipMass = 0; + } + + if( isset($logData['station']) ){ + $this->stationId = (int)$logData['station']['id']; + $this->stationName = $logData['station']['name']; + }else{ + $this->stationId = null; + $this->stationName = ''; + } + + if( isset($logData['structure']) ){ + $this->structureTypeId = (int)$logData['structure']['type']['id']; + $this->structureTypeName = $logData['structure']['type']['name']; + $this->structureId = (int)$logData['structure']['id']; + $this->structureName = $logData['structure']['name']; + }else{ + $this->structureTypeId = null; + $this->structureTypeName = ''; + $this->structureId = null; + $this->structureName = ''; + } + + } + + /** + * get character log data + * @return \stdClass + */ + public function getData() : \stdClass { + + $logData = (object) []; + $logData->system = (object) []; + $logData->system->id = (int)$this->systemId; + $logData->system->name = $this->systemName; + + $logData->ship = (object) []; + $logData->ship->typeId = (int)$this->shipTypeId; + $logData->ship->typeName = $this->shipTypeName; + $logData->ship->id = $this->shipId; + $logData->ship->name = $this->shipName; + $logData->ship->mass = $this->shipMass; + + $logData->station = (object) []; + $logData->station->id = (int)$this->stationId; + $logData->station->name = $this->stationName; + + $logData->structure = (object) []; + $logData->structure->type = (object) []; + $logData->structure->type->id = $this->structureTypeId; + $logData->structure->type->name = $this->structureTypeName; + $logData->structure->id = (int)$this->structureId; + $logData->structure->name = $this->structureName; + + return $logData; + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterInsertEvent($self, $pkeys){ + $self->clearCacheData(); + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterUpdateEvent($self, $pkeys){ + $self->updateLogsHistory('update'); + + // check if any "relevant" column has changed + if(!empty($this->fieldChanges)){ + $self->clearCacheData(); + } + } + + /** + * Event "Hook" function + * can be overwritten + * @param self $self + * @param $pkeys + */ + public function afterEraseEvent($self, $pkeys){ + $self->deleteLogsHistory(); + $self->clearCacheData(); + } + + /** + * see parent + */ + public function clearCacheData(){ + // clear character "LOG" cache + // -> character data without "LOG" has not changed! + if(is_object($this->characterId)){ + // characterId relation could be deleted by cron therefore check again first... + $this->characterId->clearCacheDataWithPrefix(CharacterModel::DATA_CACHE_KEY_LOG); + + // broadcast updated character data (with changed log data) + $this->characterId->broadcastCharacterUpdate(); + } + } + + /** + * update 'character log' history data + * -> checks $this->fieldChanges + * @param string $action + */ + protected function updateLogsHistory(string $action){ + if( + $this->valid() && + is_object($this->characterId) + ){ + $this->characterId->updateLogsHistory($this, $action); + } + } + + /** + * delete 'character log' history data + */ + protected function deleteLogsHistory(){ + if(is_object($this->characterId)){ + $this->characterId->clearCacheDataWithPrefix(CharacterModel::DATA_CACHE_KEY_LOG_HISTORY); + } + } + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/CharacterMapModel.php b/app/Model/Pathfinder/CharacterMapModel.php new file mode 100644 index 000000000..834282640 --- /dev/null +++ b/app/Model/Pathfinder/CharacterMapModel.php @@ -0,0 +1,77 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'characterId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\CharacterModel', + 'constraint' => [ + [ + 'table' => 'character', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'mapId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\MapModel', + 'constraint' => [ + [ + 'table' => 'map', + 'on-delete' => 'CASCADE' + ] + ] + ] + ]; + + /** + * see parent + */ + public function clearCacheData(){ + // clear map cache + $this->mapId->clearCacheData(); + } + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['characterId', 'mapId'], true); + } + return $status; + } + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/CharacterModel.php b/app/Model/Pathfinder/CharacterModel.php new file mode 100644 index 000000000..ac6af0df8 --- /dev/null +++ b/app/Model/Pathfinder/CharacterModel.php @@ -0,0 +1,1427 @@ + this includes logs where just e.g. shipTypeId has changed but no systemId change! + */ + const MAX_LOG_HISTORY_DATA = 10; + + /** + * TTL for historic character logs + */ + const TTL_LOG_HISTORY = 60 * 60 * 22; + + /** + * cache key prefix historic character logs + */ + const DATA_CACHE_KEY_LOG_HISTORY = 'LOG_HISTORY'; + + /** + * character authorization status + * @var array + */ + const AUTHORIZATION_STATUS = [ + 'OK' => true, // success + 'UNKNOWN' => 'error', // general authorization error + 'CHARACTER' => 'failed to match character whitelist', + 'CORPORATION' => 'failed to match corporation whitelist', + 'ALLIANCE' => 'failed to match alliance whitelist', + 'KICKED' => 'character is kicked', + 'BANNED' => 'character is banned' + ]; + + /** + * enables change for "kicked" column + * -> see kick(); + * @var bool + */ + private $allowKickChange = false; + + /** + * enables change for "banned" column + * -> see ban(); + * @var bool + */ + private $allowBanChange = false; + + /** + * @var array + */ + protected $fieldConf = [ + 'lastLogin' => [ + 'type' => Schema::DT_TIMESTAMP, + 'index' => true + ], + 'active' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'ownerHash' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'esiAccessToken' => [ + 'type' => Schema::DT_VARCHAR256 + ], + 'esiAccessTokenExpires' => [ + 'type' => Schema::DT_TIMESTAMP, + 'default' => Schema::DF_CURRENT_TIMESTAMP, + 'index' => true + ], + 'esiRefreshToken' => [ + 'type' => Schema::DT_VARCHAR256 + ], + 'esiScopes' => [ + 'type' => self::DT_JSON + ], + 'corporationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\CorporationModel', + 'constraint' => [ + [ + 'table' => 'corporation', + 'on-delete' => 'SET NULL' + ] + ] + ], + 'allianceId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\AllianceModel', + 'constraint' => [ + [ + 'table' => 'alliance', + 'on-delete' => 'SET NULL' + ] + ] + ], + 'roleId' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 1, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\RoleModel', + 'constraint' => [ + [ + 'table' => 'role', + 'on-delete' => 'CASCADE' + ] + ], + ], + 'cloneLocationId' => [ + 'type' => Schema::DT_BIGINT, + 'index' => true, + 'activity-log' => true + ], + 'cloneLocationType' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'kicked' => [ + 'type' => Schema::DT_TIMESTAMP, + 'index' => true + ], + 'banned' => [ + 'type' => Schema::DT_TIMESTAMP, + 'index' => true + ], + 'shared' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 0 + ], + 'logLocation' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1 + ], + 'selectLocation' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 0 + ], + 'securityStatus' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ], + 'userCharacter' => [ + 'has-one' => ['Exodus4D\Pathfinder\Model\Pathfinder\UserCharacterModel', 'characterId'] + ], + 'characterLog' => [ + 'has-one' => ['Exodus4D\Pathfinder\Model\Pathfinder\CharacterLogModel', 'characterId'] + ], + 'characterMaps' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\CharacterMapModel', 'characterId'] + ], + 'characterAuthentications' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\CharacterAuthenticationModel', 'characterId'] + ] + ]; + + /** + * get character data + * @param bool $addLogData + * @param bool $addLogHistoryData + * @return mixed|object|null + * @throws \Exception + */ + public function getData($addLogData = false, $addLogHistoryData = false){ + // check for cached data + if(is_null($characterData = $this->getCacheData())){ + // no cached character data found + + $characterData = (object) []; + $characterData->id = $this->_id; + $characterData->name = $this->name; + $characterData->role = $this->roleId->getData(); + $characterData->shared = $this->shared; + $characterData->logLocation = $this->logLocation; + $characterData->selectLocation = $this->selectLocation; + + // check for corporation + if($corporation = $this->getCorporation()){ + $characterData->corporation = $corporation->getData(); + } + + // check for alliance + if($alliance = $this->getAlliance()){ + $characterData->alliance = $alliance->getData(); + } + + // max caching time for a system + // cached date has to be cleared manually on any change + // this applies to system, connection,... changes (+ all other dependencies) + $this->updateCacheData($characterData); + } + + if($addLogData){ + if(is_null($logData = $this->getCacheData(self::DATA_CACHE_KEY_LOG))){ + if($logModel = $this->getLog()){ + $logData = $logModel->getData(); + $this->updateCacheData($logData, self::DATA_CACHE_KEY_LOG); + } + } + + if($logData){ + $characterData->log = $logData; + } + } + + if($addLogHistoryData && $characterData->log){ + $characterData->logHistory = $this->getLogHistoryJumps($characterData->log->system->id); + } + + // temp "authStatus" should not be cached + if($this->authStatus){ + $characterData->authStatus = $this->authStatus; + } + + return $characterData; + } + + /** + * get "basic" character data + * @return \stdClass + * @throws \Exception + */ + public function getBasicData() : \stdClass { + $characterData = (object) []; + $characterData->id = $this->_id; + $characterData->name = $this->name; + + // check for corporation + if($corporation = $this->getCorporation()){ + $characterData->corporation = $corporation->getData(false); + } + + // check for alliance + if($alliance = $this->getAlliance()){ + $characterData->alliance = $alliance->getData(); + } + + return $characterData; + } + + /** + * set corporation for this character + * -> corp change resets admin actions (e.g. kick/ban) + * @param $corporationId + * @return mixed + */ + public function set_corporationId($corporationId){ + $currentCorporationId = (int)$this->get('corporationId', true); + + if($currentCorporationId !== $corporationId){ + $this->resetAdminColumns(); + } + + return $corporationId; + } + + /** + * set unique "ownerHash" for this character + * -> Hash will change when character is transferred (sold) + * @param string $ownerHash + * @return string + */ + public function set_ownerHash($ownerHash){ + if( $this->ownerHash !== $ownerHash ){ + if( $this->hasUserCharacter() ){ + // reset admin actions (e.g. kick/ban) + $this->resetAdminColumns(); + + // new ownerHash -> new user (reset) + $this->userCharacter->erase(); + } + + // delete all existing login-cookie data + $this->logout(); + } + + return $ownerHash; + } + + /** + * setter for "kicked" until time + * @param $minutes + * @return mixed|null|string + * @throws \Exception + */ + public function set_kicked($minutes){ + if($this->allowKickChange){ + // allowed to set/change -> reset "allowed" property + $this->allowKickChange = false; + $kicked = null; + + if($minutes){ + $seconds = $minutes * 60; + $timezone = self::getF3()->get('getTimeZone')(); + $kickedUntil = new \DateTime('now', $timezone); + + // add cookie expire time + $kickedUntil->add(new \DateInterval('PT' . $seconds . 'S')); + $kicked = $kickedUntil->format('Y-m-d H:i:s'); + } + }else{ + // not allowed to set/change -> keep current status + $kicked = $this->kicked; + } + + return $kicked; + } + + /** + * setter for "banned" status + * @param $status + * @return mixed|string|null + * @throws \Exception + */ + public function set_banned($status){ + if($this->allowBanChange){ + // allowed to set/change -> reset "allowed" property + $this->allowBanChange = false; + $banned = null; + + if($status){ + $timezone = self::getF3()->get('getTimeZone')(); + $bannedSince = new \DateTime('now', $timezone); + $banned = $bannedSince->format('Y-m-d H:i:s'); + } + }else{ + // not allowed to set/change -> keep current status + $banned = $this->banned; + } + + return $banned; + } + + /** + * logLocation specifies whether the current system should be tracked or not + * @param $logLocation + * @return bool + */ + public function set_logLocation($logLocation){ + $logLocation = (bool)$logLocation; + if( + !$logLocation && + $logLocation !== $this->logLocation + ){ + $this->deleteLog(); + } + + return $logLocation; + } + + /** + * kick character for $minutes + * -> do NOT use $this->kicked! + * -> this will not work (prevent abuse) + * @param bool|int $minutes + */ + public function kick($minutes = false){ + // enables "kicked" change for this model + $this->allowKickChange = true; + $this->kicked = $minutes; + } + + /** + * ban character + * -> do NOT use $this->banned! + * -> this will not work (prevent abuse) + * @param bool|int $status + */ + public function ban($status = false){ + // enables "banned" change for this model + $this->allowBanChange = true; + $this->banned = $status; + } + + /** + * Event "Hook" function + * @param self $self + * @param $pkeys + */ + public function afterInsertEvent($self, $pkeys){ + $self->clearCacheData(); + } + + /** + * Event "Hook" function + * @param self $self + * @param $pkeys + */ + public function afterUpdateEvent($self, $pkeys){ + $self->clearCacheData(); + } + + /** + * Event "Hook" function + * @param self $self + * @param $pkeys + */ + public function afterEraseEvent($self, $pkeys){ + $self->clearCacheData(); + } + + /** + * see parent + */ + public function clearCacheData(){ + parent::clearCacheData(); + + // clear data with "log" as well! + parent::clearCacheDataWithPrefix(self::DATA_CACHE_KEY_LOG); + } + + /** + * resets some columns that could have changed by admins (e.g. kick/ban) + */ + private function resetAdminColumns(){ + $this->kick(); + $this->ban(); + } + + /** + * check whether this character has already a user assigned to it + * @return bool + */ + public function hasUserCharacter() : bool { + return is_object($this->userCharacter); + } + + /** + * check whether this character has an active location log + * @return bool + */ + public function hasLog() : bool { + return is_object($this->characterLog); + } + + /** + * check whether this character has a corporation + * @return bool + */ + public function hasCorporation() : bool { + return is_object($this->corporationId); + } + + /** + * check whether this character has an alliance + * @return bool + */ + public function hasAlliance() : bool { + return is_object($this->allianceId); + } + + /** + * @return UserModel|null + */ + public function getUser() : ?UserModel { + return $this->hasUserCharacter() ? $this->userCharacter->userId : null; + } + + /** + * get the corporation from character + * @return CorporationModel|null + */ + public function getCorporation() : ?CorporationModel { + return $this->corporationId; + } + + /** + * get the alliance from character + * @return AllianceModel|null + */ + public function getAlliance() : ?AllianceModel { + return $this->allianceId; + } + + /** + * get ESI API "access_token" from OAuth + * @return bool|string + */ + public function getAccessToken(){ + $accessToken = false; + $refreshToken = true; + + try{ + $timezone = self::getF3()->get('getTimeZone')(); + $now = new \DateTime('now', $timezone); + + if( + !empty($this->esiAccessToken) && + !empty($this->esiAccessTokenExpires) + ){ + $expireTime = \DateTime::createFromFormat( + 'Y-m-d H:i:s', + $this->esiAccessTokenExpires, + $timezone + ); + + // check if token is not expired + if($expireTime->getTimestamp() > $now->getTimestamp()){ + // token still valid + $accessToken = $this->esiAccessToken; + + // check if token should be renewed (close to expire) + $timeBuffer = 2 * 60; + $expireTime->sub(new \DateInterval('PT' . $timeBuffer . 'S')); + + if($expireTime->getTimestamp() > $now->getTimestamp()){ + // token NOT close to expire + $refreshToken = false; + } + } + } + }catch(\Exception $e){ + self::getF3()->error(500, $e->getMessage(), $e->getTrace()); + } + + // no valid "accessToken" found OR + // existing token is close to expire + // -> get a fresh one by an existing "refreshToken" + // -> in case request for new token fails (e.g. timeout) and old token is still valid -> keep old token + if( + $refreshToken && + !empty($this->esiRefreshToken) + ){ + $ssoController = new Sso(); + $accessData = $ssoController->refreshAccessToken($this->esiRefreshToken); + + if(isset($accessData->accessToken, $accessData->esiAccessTokenExpires, $accessData->refreshToken)){ + $this->esiAccessToken = $accessData->accessToken; + $this->esiAccessTokenExpires = $accessData->esiAccessTokenExpires; + $this->save(); + + $accessToken = $this->esiAccessToken; + } + } + + return $accessToken; + } + + /** + * check if character is currently kicked + * @return bool + */ + public function isKicked() : bool { + $kicked = false; + if( !is_null($this->kicked) ){ + try{ + $kickedUntil = new \DateTime(); + $kickedUntil->setTimestamp( (int)strtotime($this->kicked) ); + $now = new \DateTime(); + $kicked = ($kickedUntil > $now); + }catch(\Exception $e){ + self::getF3()->error(500, $e->getMessage(), $e->getTrace()); + } + } + + return $kicked; + } + + /** + * checks whether this character is currently logged in + * @return bool + */ + public function checkLoginTimer() : bool { + $loginCheck = false; + + if( !$this->dry() && $this->lastLogin ){ + // get max login time (minutes) from config + $maxLoginMinutes = (int)Config::getPathfinderData('timer.logged'); + if($maxLoginMinutes){ + $timezone = self::getF3()->get('getTimeZone')(); + try{ + $now = new \DateTime('now', $timezone); + $logoutTime = new \DateTime($this->lastLogin, $timezone); + $logoutTime->add(new \DateInterval('PT' . $maxLoginMinutes . 'M')); + if($logoutTime->getTimestamp() > $now->getTimestamp()){ + $loginCheck = true; + } + }catch(\Exception $e){ + self::getF3()->error(500, $e->getMessage(), $e->getTrace()); + } + }else{ + // no "max login" timer configured -> character still logged in + $loginCheck = true; + } + } + + return $loginCheck; + } + + /** + * checks whether this character is authorized to log in + * -> check corp/ally whitelist config (pathfinder.ini) + * @return string + */ + public function isAuthorized() : string { + $authStatus = 'UNKNOWN'; + + // check whether character is banned or temp kicked + if(is_null($this->banned)){ + if( !$this->isKicked() ){ + $whitelistCharacter = array_filter( array_map('trim', (array)Config::getPathfinderData('login.character') ) ); + $whitelistCorporations = array_filter( array_map('trim', (array)Config::getPathfinderData('login.corporation') ) ); + $whitelistAlliance = array_filter( array_map('trim', (array)Config::getPathfinderData('login.alliance') ) ); + + if( + empty($whitelistCharacter) && + empty($whitelistCorporations) && + empty($whitelistAlliance) + ){ + // no corp/ally restrictions set -> any character is allowed to login + $authStatus = 'OK'; + }else{ + // check if character is set in whitelist + if( + !empty($whitelistCharacter) && + in_array((int)$this->_id, $whitelistCharacter) + ){ + $authStatus = 'OK'; + }else{ + $authStatus = 'CHARACTER'; + } + + // check if character corporation is set in whitelist + if( + $authStatus != 'OK' && + !empty($whitelistCorporations) && + $this->hasCorporation() + ){ + if( in_array((int)$this->get('corporationId', true), $whitelistCorporations) ){ + $authStatus = 'OK'; + }else{ + $authStatus = 'CORPORATION'; + } + } + + // check if character alliance is set in whitelist + if( + $authStatus != 'OK' && + !empty($whitelistAlliance) && + $this->hasAlliance() + ){ + if( in_array((int)$this->get('allianceId', true), $whitelistAlliance) ){ + $authStatus = 'OK'; + }else{ + $authStatus = 'ALLIANCE'; + } + } + } + }else{ + $authStatus = 'KICKED'; + } + }else{ + $authStatus = 'BANNED'; + } + + return $authStatus; + } + + /** + * get Pathfinder role for character + * @return RoleModel + * @throws \Exception + */ + protected function getRole() : RoleModel { + $role = null; + + // check config files for hardcoded character roles + if(self::getF3()->exists('PATHFINDER.ROLES.CHARACTER', $globalAdminData)){ + foreach((array)$globalAdminData as $adminData){ + if($adminData['ID'] === $this->_id){ + switch($adminData['ROLE']){ + case 'SUPER': + $role = RoleModel::getAdminRole(); + break; + case 'CORPORATION': + $role = RoleModel::getCorporationManagerRole(); + break; + } + break; + } + } + } + + // check in-game roles + if( + is_null($role) && + !empty($rolesData = $this->requestRoles()) && + !empty($roles = $rolesData['roles']) + ){ + // roles that grant admin access for this character + $adminRoles = array_intersect(CorporationModel::ADMIN_ROLES, $roles); + if(!empty($adminRoles)){ + $role = RoleModel::getCorporationManagerRole(); + } + } + + // default role + if(is_null($role)){ + $role = RoleModel::getDefaultRole(); + } + + return $role; + } + + /** + * get all character roles grouped by 'role type' + * -> 'role types' are 'roles', 'rolesAtBase', 'rolesAtHq', 'rolesAtOther' + * @return array + */ + protected function requestRoles() : array { + $rolesData = []; + $response = self::getF3()->ccpClient()->send('getCharacterRoles', $this->_id, $this->getAccessToken()); + if(!empty($response) && !isset($response['error'])){ + $rolesData = $response; + } + return $rolesData; + } + + /** + * check whether this char has accepted all "basic" api scopes + * @return bool + */ + public function hasBasicScopes() : bool { + return empty(array_diff(Sso::getScopesByAuthType(), $this->esiScopes)); + } + + /** + * check whether this char has accepted all admin api scopes + * @return bool + */ + public function hasAdminScopes() : bool { + return empty(array_diff(Sso::getScopesByAuthType('admin'), $this->esiScopes)); + } + + /** + * update clone data + */ + public function updateCloneData(){ + if($accessToken = $this->getAccessToken()){ + $clonesData = self::getF3()->ccpClient()->send('getCharacterClones', $this->_id, $accessToken); + if(!isset($clonesData['error'])){ + if(!empty($homeLocationData = $clonesData['home']['location'])){ + // clone home location data + $this->cloneLocationId = (int)$homeLocationData['id']; + $this->cloneLocationType = (string)$homeLocationData['type']; + } + } + } + } + + /** + * @throws \Exception + */ + public function updateRoleData(){ + $this->roleId = $this->getRole(); + } + + /** + * get online status data from ESI + * @param string $accessToken + * @return array + */ + protected function getOnlineData(string $accessToken) : array { + return self::getF3()->ccpClient()->send('getCharacterOnline', $this->_id, $accessToken); + } + + /** + * check online state from ESI + * @param string $accessToken + * @return bool + */ + public function isOnline(string $accessToken) : bool { + $isOnline = false; + $onlineData = $this->getOnlineData($accessToken); + + if($onlineData['online'] === true){ + $isOnline = true; + } + + return $isOnline; + } + + /** + * update character log (active system, ...) + * -> API request for character log data + * @param array $additionalOptions (optional) request options for cURL request + * @return CharacterModel + * @throws \Exception + */ + public function updateLog($additionalOptions = []) : self { + $deleteLog = false; + $invalidResponse = false; + + //check if log update is enabled for this character + // check if character has accepted all scopes. (This fkt is called by cron as well) + if( + $this->logLocation && + $this->hasBasicScopes() + ){ + // Try to pull data from API + if($accessToken = $this->getAccessToken()){ + if($this->isOnline($accessToken)){ + $locationData = self::getF3()->ccpClient()->send('getCharacterLocation', $this->_id, $accessToken); + + if(!empty($locationData['system']['id'])){ + // character is currently in-game + + // get current $characterLog or get new ------------------------------------------------------- + if(!$characterLog = $this->getLog()){ + // create new log + $characterLog = $this->rel('characterLog'); + } + + // get current log data and modify on change + $logData = $characterLog::toArray($characterLog->getData()); + + // check system and station data for changes -------------------------------------------------- + + // IDs for "systemId", "stationId" that require more data + $lookupUniverseIds = []; + if( + empty($logData['system']['name']) || + $logData['system']['id'] !== $locationData['system']['id'] + ){ + // system changed -> request "system name" for current system + $lookupUniverseIds[] = $locationData['system']['id']; + } + + $logData = array_replace_recursive($logData, $locationData); + + // get "more" data for systemId --------------------------------------------------------------- + if(!empty($lookupUniverseIds)){ + // get "more" information for some Ids (e.g. name) + $universeData = self::getF3()->ccpClient()->send('getUniverseNames', $lookupUniverseIds); + + if(!empty($universeData) && !isset($universeData['error'])){ + // We expect max ONE system AND/OR station data, not an array of e.g. systems + if(!empty($universeData['system'])){ + $universeData['system'] = reset($universeData['system']); + } + + $logData = array_replace_recursive($logData, $universeData); + }else{ + // this is important! universe data is a MUST HAVE! + $deleteLog = true; + } + } + + // check station data for changes ------------------------------------------------------------- + if(!$deleteLog){ + // IDs for "stationId" that require more data + $lookupStationId = 0; + if(!empty($locationData['station']['id'])){ + if( + empty($logData['station']['name']) || + $logData['station']['id'] !== $locationData['station']['id'] + ){ + // station changed -> request station data + $lookupStationId = $locationData['station']['id']; + } + }else{ + unset($logData['station']); + } + + // get "more" data for stationId + if($lookupStationId > 0){ + /** + * @var $stationModel Universe\StationModel + */ + $stationModel = Universe\AbstractUniverseModel::getNew('StationModel'); + $stationModel->loadById($lookupStationId, $accessToken, $additionalOptions); + if($stationModel->valid()){ + $stationData['station'] = $stationModel::toArray($stationModel->getData()); + $logData = array_replace_recursive($logData, $stationData); + }else{ + unset($logData['station']); + } + } + } + + // check structure data for changes ----------------------------------------------------------- + if(!$deleteLog){ + // IDs for "structureId" that require more data + $lookupStructureId = 0; + if(!empty($locationData['structure']['id'])){ + if( + empty($logData['structure']['name']) || + $logData['structure']['id'] !== $locationData['structure']['id'] + ){ + // structure changed -> request structure data + $lookupStructureId = $locationData['structure']['id']; + } + }else{ + unset($logData['structure']); + } + + // get "more" data for structureId + if($lookupStructureId > 0){ + /** + * @var $structureModel Universe\StructureModel + */ + $structureModel = Universe\AbstractUniverseModel::getNew('StructureModel'); + $structureModel->loadById($lookupStructureId, $accessToken, $additionalOptions); + if($structureModel->valid()){ + $structureData['structure'] = $structureModel::toArray($structureModel->getData()); + $logData = array_replace_recursive($logData, $structureData); + }else{ + unset($logData['structure']); + } + } + } + + // check ship data for changes ---------------------------------------------------------------- + if(!$deleteLog){ + $shipData = self::getF3()->ccpClient()->send('getCharacterShip', $this->_id, $accessToken); + + // IDs for "shipTypeId" that require more data + $lookupShipTypeId = 0; + if(!empty($shipData['ship']['typeId'])){ + if( + empty($logData['ship']['typeName']) || + $logData['ship']['typeId'] !== $shipData['ship']['typeId'] + ){ + // ship changed -> request "station name" for current station + $lookupShipTypeId = $shipData['ship']['typeId']; + } + + // "shipName"/"shipId" could have changed... + $logData = array_replace_recursive($logData, $shipData); + }else{ + // ship data should never be empty -> keep current one + //unset($logData['ship']); + $invalidResponse = true; + } + + // get "more" data for shipTypeId + if($lookupShipTypeId > 0){ + /** + * @var $typeModel Universe\TypeModel + */ + $typeModel = Universe\AbstractUniverseModel::getNew('TypeModel'); + $typeModel->loadById($lookupShipTypeId, '', $additionalOptions); + if(!$typeModel->dry()){ + $shipData['ship'] = (array)$typeModel->getShipData(); + $logData = array_replace_recursive($logData, $shipData); + }else{ + // this is important! ship data is a MUST HAVE! + $deleteLog = true; + } + } + } + + if(!$deleteLog){ + // mark log as "updated" even if no changes were made + if($additionalOptions['markUpdated'] === true){ + $characterLog->touch('updated'); + } + + $characterLog->setData($logData); + $characterLog->characterId = $this->_id; + $characterLog->save(); + + $this->characterLog = $characterLog; + } + }else{ + // systemId should always exists + $invalidResponse = true; + } + }else{ + // user is in-game offline + $deleteLog = true; + } + }else{ + // access token request failed + $deleteLog = true; + } + }else{ + // character deactivated location logging + $deleteLog = true; + } + + if($deleteLog){ + $this->deleteLog(); + } + + return $this; + } + + /** + * get 'character log' history data. Filter all data that does not represent a 'jump' (systemId change) + * -> e.g. If just 'shipTypeId' has changed, this entry is filtered + * @param int $systemIdPrev + * @return array + */ + protected function getLogHistoryJumps(int $systemIdPrev = 0) : array { + return $this->filterLogsHistory(function(array $historyEntry) use (&$systemIdPrev) : bool { + $addEntry = false; + if( + !empty($historySystemId = (int)$historyEntry['log']['system']['id']) && + $historySystemId !== $systemIdPrev + ){ + $addEntry = true; + $systemIdPrev = $historySystemId; + } + + return $addEntry; + }); + } + + /** + * filter 'character log' history data by $callback + * -> reindex array keys! Otherwise json_encode() on result would return object! + * @param \Closure $callback + * @return array + */ + protected function filterLogsHistory(\Closure $callback) : array { + return array_values(array_filter($this->getLogsHistory() , $callback)); + } + + /** + * @return array + */ + public function getLogsHistory() : array { + if(!is_array($logHistoryData = $this->getCacheData(self::DATA_CACHE_KEY_LOG_HISTORY))){ + $logHistoryData = []; + } + return $logHistoryData; + } + + /** + * add new 'character log' history entry + * @param CharacterLogModel $characterLog + * @param string $action + */ + public function updateLogsHistory(CharacterLogModel $characterLog, string $action = 'update') : void { + if( + $this->valid() && + $this->_id === $characterLog->get('characterId', true) + ){ + $task = 'add'; + $mapIds = []; + $historyLog = $characterLog::toArray($characterLog->getData()); + + if($logHistoryData = $this->getLogsHistory()){ + // skip logging if no relevant fields changed + [$historyEntryPrev] = $logHistoryData; + if($historyLogPrev = $historyEntryPrev['log']){ + if( + $historyLog['system']['id'] === $historyLogPrev['system']['id'] && + $historyLog['ship']['typeId'] === $historyLogPrev['ship']['typeId'] && + $historyLog['station']['id'] === $historyLogPrev['station']['id'] && + $historyLog['structure']['id'] === $historyLogPrev['structure']['id'] + ){ + // no changes in 'relevant' fields -> just update timestamp + $task = 'update'; + $mapIds = (array)$historyEntryPrev['mapIds']; + } + } + } + + $historyEntry = [ + 'stamp' => strtotime($characterLog->updated), + 'action' => $action, + 'mapIds' => $mapIds, + 'log' => $historyLog + ]; + + if($task == 'update'){ + $logHistoryData[0] = $historyEntry; + }else{ + array_unshift($logHistoryData, $historyEntry); + + // limit max history data + array_splice($logHistoryData, self::MAX_LOG_HISTORY_DATA); + } + + $this->updateCacheData($logHistoryData, self::DATA_CACHE_KEY_LOG_HISTORY, self::TTL_LOG_HISTORY); + } + } + + /** + * try to update existing 'character log' history entry (replace data) + * -> matched by 'stamp' timestamp + * @param array $historyEntry + * @return bool + */ + protected function updateLogHistoryEntry(array $historyEntry) : bool { + $updated = false; + + if( + $this->valid() && + ($logHistoryData = $this->getLogsHistory()) + ){ + $map = function(array $entry) use ($historyEntry, &$updated) : array { + if($entry['stamp'] === $historyEntry['stamp']){ + $updated = true; + $entry = $historyEntry; + } + return $entry; + }; + + $logHistoryData = array_map($map, $logHistoryData); + + if($updated){ + $this->updateCacheData($logHistoryData, self::DATA_CACHE_KEY_LOG_HISTORY, self::TTL_LOG_HISTORY); + } + } + + return $updated; + } + + /** + * broadcast characterData + */ + public function broadcastCharacterUpdate(){ + $characterData = $this->getData(true); + + self::getF3()->webSocket()->write('characterUpdate', $characterData); + } + + /** + * update character data from CCPs ESI API + * @return array (some status messages) + * @throws \Exception + */ + public function updateFromESI() : array { + $status = []; + + if( $accessToken = $this->getAccessToken() ){ + // et basic character data + // -> this is required for "ownerHash" hash check (e.g. character was sold,..) + // -> the "id" check is just for security and should NEVER fail! + $ssoController = new Sso(); + if( + !empty( $verificationCharacterData = $ssoController->verifyCharacterData($accessToken) ) && + $verificationCharacterData['characterId'] === $this->_id + ){ + // get character data from API + $characterData = $ssoController->getCharacterData($this->_id); + if( !empty($characterData->character) ){ + $characterData->character['ownerHash'] = $verificationCharacterData['characterOwnerHash']; + $characterData->character['esiScopes'] = $verificationCharacterData['scopes']; + + $this->copyfrom($characterData->character, ['ownerHash', 'esiScopes', 'securityStatus']); + $this->corporationId = $characterData->corporation; + $this->allianceId = $characterData->alliance; + $this->save(); + } + }else{ + $status[] = sprintf(Sso::ERROR_VERIFY_CHARACTER, $this->name); + } + }else{ + $status[] = sprintf(Sso::ERROR_ACCESS_TOKEN, $this->name); + } + + return $status; + } + + /** + * get a unique cookie name for this character + * -> cookie name does not have to be "secure" + * -> but is should be unique + * @return string + */ + public function getCookieName() : string { + return md5($this->name); + } + + /** + * get the character log entry for this character + * @return CharacterLogModel|null + */ + public function getLog() : ?CharacterLogModel { + return ($this->hasLog() && !$this->characterLog->dry()) ? $this->characterLog : null; + } + + /** + * get the first matched (most recent) log entry before $systemId. + * -> The returned log entry *might* be previous system for this character + * @param int $mapId + * @param int $systemId + * @return CharacterLogModel|null + */ + public function getLogPrevSystem(int $mapId, int $systemId) : ?CharacterLogModel { + $characterLog = null; + + if($mapId && $systemId){ + $skipRest = false; + $logHistoryData = $this->filterLogsHistory(function(array $historyEntry) use ($mapId, $systemId, &$skipRest) : bool { + $addEntry = false; + //if(in_array($mapId, (array)$historyEntry['mapIds'], true)){ // $historyEntry is checked by EACH map -> would auto add system on map switch! #827 + if(!empty((array)$historyEntry['mapIds'])){ // if $historyEntry was already checked by ANY other map -> no further checks + $skipRest = true; + } + + if( + !$skipRest && + !empty($historySystemId = (int)$historyEntry['log']['system']['id']) && + $historySystemId !== $systemId + ){ + $addEntry = true; + $skipRest = true; + } + + return $addEntry; + }); + + if( + !empty($historyEntry = reset($logHistoryData)) && + is_array($historyEntry['mapIds']) + ){ + /** + * @var $characterLog CharacterLogModel + */ + $characterLog = $this->rel('characterLog'); + $characterLog->setData($historyEntry['log']); + + // mark $historyEntry data as "checked" for $mapId + array_push($historyEntry['mapIds'], $mapId); + + $this->updateLogHistoryEntry($historyEntry); + } + } + + return $characterLog; + } + + /** + * get mapModel by id and check if user has access + * @param $mapId + * @return MapModel|null + * @throws \Exception + */ + public function getMap(int $mapId) : ?MapModel { + /** + * @var $map MapModel + */ + $map = self::getNew('MapModel'); + $map->getById($mapId); + + return $map->hasAccess($this) ? $map : null; + } + + /** + * get all accessible map models for this character + * @return MapModel[] + */ + public function getMaps() : array { + $maps = []; + + if($alliance = $this->getAlliance()){ + $maps = array_merge($maps, $alliance->getMaps()); + } + + if($corporation = $this->getCorporation()){ + $maps = array_merge($maps, $corporation->getMaps()); + } + + if(is_object($this->characterMaps)){ + $mapCountPrivate = 0; + foreach($this->characterMaps as $characterMap){ + if( + $mapCountPrivate < Config::getMapsDefaultConfig('private')['max_count'] && + $characterMap->mapId->isActive() + ){ + $maps[] = $characterMap->mapId; + $mapCountPrivate++; + } + } + } + + return $maps; + } + + /** + * delete current location + */ + protected function deleteLog(){ + if($characterLog = $this->getLog()){ + $characterLog->erase(); + } + } + + /** + * delete authentications data + */ + protected function deleteAuthentications(){ + if(is_object($this->characterAuthentications)){ + foreach($this->characterAuthentications as $characterAuthentication){ + /** + * @var $characterAuthentication CharacterAuthenticationModel + */ + $characterAuthentication->erase(); + } + } + } + /** + * character logout + * @param bool $deleteLog + * @param bool $deleteSession + * @param bool $deleteCookie + */ + public function logout(bool $deleteSession = true, bool $deleteLog = true, bool $deleteCookie = false){ + // delete current session data -------------------------------------------------------------------------------- + if($deleteSession){ + $sessionCharacterData = (array)$this->getF3()->get(User::SESSION_KEY_CHARACTERS); + $sessionCharacterData = array_filter($sessionCharacterData, function($data){ + return ($data['ID'] != $this->_id); + }); + + if(empty($sessionCharacterData)){ + // no active characters logged in -> log user out + $this->getF3()->clear(User::SESSION_KEY_USER); + $this->getF3()->clear(User::SESSION_KEY_CHARACTERS); + }else{ + // update remaining active characters + $this->getF3()->set(User::SESSION_KEY_CHARACTERS, $sessionCharacterData); + } + } + + // delete current location data ------------------------------------------------------------------------------- + if($deleteLog){ + $this->deleteLog(); + } + + // delete auth cookie data ------------------------------------------------------------------------------------ + if($deleteCookie){ + $this->deleteAuthentications(); + } + } + + /** + * @see parent + */ + public function filterRel() : void { + $this->filter('userCharacter', self::getFilter('active', true)); + $this->filter('corporationId', self::getFilter('active', true)); + $this->filter('allianceId', self::getFilter('active', true)); + $this->filter('characterMaps', self::getFilter('active', true), ['order' => 'created']); + } + + /** + * merges two multidimensional characterSession arrays by checking characterID + * @param array $characterDataBase + * @return array + */ + public static function mergeSessionCharacterData(array $characterDataBase = []) : array { + $addData = []; + // get current session characters to be merged with + $characterData = (array)self::getF3()->get(User::SESSION_KEY_CHARACTERS); + + foreach($characterDataBase as $i => $baseData){ + foreach($characterData as $data){ + if((int)$baseData['ID'] === (int)$data['ID']){ + // overwrite static data -> should NEVER change on merge! + $characterDataBase[$i]['NAME'] = $data['NAME']; + $characterDataBase[$i]['TIME'] = $data['TIME']; + }else{ + $addData[] = $data; + } + } + } + + return array_merge($characterDataBase, $addData); + } + + /** + * get all characters + * @param array $characterIds + * @return \DB\CortexCollection + */ + public static function getAll($characterIds = []){ + $query = [ + 'active = :active AND id IN :characterIds', + ':active' => 1, + ':characterIds' => $characterIds + ]; + + return (new self())->find($query); + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/CharacterStatusModel.php b/app/Model/Pathfinder/CharacterStatusModel.php new file mode 100644 index 000000000..ba98c2e39 --- /dev/null +++ b/app/Model/Pathfinder/CharacterStatusModel.php @@ -0,0 +1,62 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'class' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ] + ]; + + /** + * @var array + */ + protected static $tableData = [ + [ + 'id' => 1, + 'name' => 'corporation', + 'class' => 'pf-user-status-corp' + ], + [ + 'id' => 2, + 'name' => 'alliance', + 'class' => 'pf-user-status-ally' + ], + [ + 'id' => 3, + 'name' => 'own', + 'class' => 'pf-user-status-own' + ] + ]; +} \ No newline at end of file diff --git a/app/Model/Pathfinder/ConnectionLogModel.php b/app/Model/Pathfinder/ConnectionLogModel.php new file mode 100644 index 000000000..96ffa6cfb --- /dev/null +++ b/app/Model/Pathfinder/ConnectionLogModel.php @@ -0,0 +1,144 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'connectionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\ConnectionModel', + 'constraint' => [ + [ + 'table' => 'connection', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'record' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'shipTypeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'validate' => 'notEmpty' + ], + 'shipTypeName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'shipMass' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0, + 'validate' => 'notEmpty' + ], + 'characterId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'validate' => 'notEmpty' + ], + 'characterName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ] + ]; + + /** + * set data by associative array + * @param array $data + */ + public function setData(array $data){ + $this->copyfrom($data, ['shipTypeId', 'shipTypeName', 'shipMass', 'characterId', 'characterName']); + } + + /** + * get connection log data + * @return \stdClass + */ + public function getData() : \stdClass { + $logData = (object) []; + $logData->id = $this->id; + $logData->active = $this->active; + $logData->record = $this->record; + + $logData->connection = (object) []; + $logData->connection->id = $this->get('connectionId', true); + + $logData->ship = (object) []; + $logData->ship->typeId = (int)$this->shipTypeId; + $logData->ship->typeName = $this->shipTypeName; + $logData->ship->mass = $this->shipMass; + + $logData->character = (object) []; + $logData->character->id = $this->characterId; + $logData->character->name = $this->characterName; + + $logData->created = (object) []; + $logData->created->created = strtotime($this->created); + + $logData->updated = (object) []; + $logData->updated->updated = strtotime($this->updated); + + return $logData; + } + + /** + * validate shipTypeId + * @param string $key + * @param string $val + * @return bool + */ + protected function validate_shipTypeId(string $key, string $val): bool { + return !empty((int)$val); + } + + /** + * @return ConnectionModel + */ + public function getConnection() : ConnectionModel { + return $this->get('connectionId'); + } + + /** + * check object for model access + * @param CharacterModel $characterModel + * @return bool + */ + public function hasAccess(CharacterModel $characterModel) : bool { + $access = false; + if( !$this->dry() ){ + $access = $this->getConnection()->hasAccess($characterModel); + } + return $access; + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/ConnectionModel.php b/app/Model/Pathfinder/ConnectionModel.php new file mode 100644 index 000000000..11919d7df --- /dev/null +++ b/app/Model/Pathfinder/ConnectionModel.php @@ -0,0 +1,522 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'mapId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\MapModel', + 'constraint' => [ + [ + 'table' => 'map', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'source' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'activity-log' => true + ], + 'target' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'activity-log' => true + ], + 'scope' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ], + 'type' => [ + 'type' => self::DT_JSON, + 'activity-log' => true + ], + 'sourceEndpointType' => [ + 'type' => self::DT_JSON, + 'activity-log' => true + ], + 'targetEndpointType' => [ + 'type' => self::DT_JSON, + 'activity-log' => true + ], + 'eolUpdated' => [ + 'type' => Schema::DT_TIMESTAMP, + 'default' => null + ], + 'signatures' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\SystemSignatureModel', 'connectionId'] + ], + 'connectionLog' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\ConnectionLogModel', 'connectionId'] + ] + ]; + + /** + * allowed connection types + * @var array + */ + protected static $connectionTypeWhitelist = [ + // base type for scopes + 'abyssal', + 'jumpbridge', + 'stargate', + // wh mass reduction types + 'wh_fresh', + 'wh_reduced', + 'wh_critical', + // wh jump mass types + 'wh_jump_mass_s', + 'wh_jump_mass_m', + 'wh_jump_mass_l', + 'wh_jump_mass_xl', + // other types + 'wh_eol', + 'preserve_mass' + ]; + + /** + * get connection data + * @param bool $addSignatureData + * @param bool $addLogData + * @return \stdClass + */ + public function getData($addSignatureData = false, $addLogData = false){ + $connectionData = (object) []; + $connectionData->id = $this->id; + $connectionData->source = $this->source->id; + $connectionData->target = $this->target->id; + $connectionData->scope = $this->scope; + $connectionData->type = (array)json_decode($this->get('type', true)); + $connectionData->updated = strtotime($this->updated); + $connectionData->created = strtotime($this->created); + $connectionData->eolUpdated = strtotime($this->eolUpdated); + + if( !empty($endpointsData = $this->getEndpointsData()) ){ + $connectionData->endpoints = $endpointsData; + } + + if($addSignatureData){ + if( !empty($signaturesData = $this->getSignaturesData()) ){ + $connectionData->signatures = $signaturesData; + } + } + + if($addLogData){ + if( !empty($logsData = $this->getLogsData()) ){ + $connectionData->logs = $logsData; + } + } + + return $connectionData; + } + + /** + * setter for connection type + * @param $type + * @return array + */ + public function set_type($type){ + // remove unwanted types -> they should not be send from client + // -> reset keys! otherwise JSON format results in object and not in array + $type = array_values(array_intersect(array_unique((array)$type), self::$connectionTypeWhitelist)); + + // set EOL timestamp + if( !in_array('wh_eol', $type) ){ + $this->eolUpdated = null; + }elseif( + in_array('wh_eol', $type) && + !in_array('wh_eol', (array)$this->type) // $this->type == null for new connection! (e.g. map import) + ){ + // connection EOL status change + $this->touch('eolUpdated'); + } + + return $type; + } + + /** + * setter for endpoints data (data for source/target endpoint) + * @param $endpointsData + */ + public function set_endpoints($endpointsData){ + if(!empty($endpointData = (array)$endpointsData['source'])){ + $this->setEndpointData('source', $endpointData); + } + if(!empty($endpointData = (array)$endpointsData['target'])){ + $this->setEndpointData('target', $endpointData); + } + } + + /** + * set connection endpoint related data + * @param string $label (source||target) + * @param array $endpointData + */ + public function setEndpointData(string $label, array $endpointData = []){ + if($this->exists($field = $label . 'EndpointType')){ + $types = empty($types = (array)$endpointData['types']) ? null : $types; + if($this->$field != $types){ + $this->$field = $types; + } + } + } + + /** + * check object for model access + * @param CharacterModel $characterModel + * @return bool + */ + public function hasAccess(CharacterModel $characterModel) : bool { + $access = false; + if( !$this->dry() ){ + $access = $this->mapId->hasAccess($characterModel); + } + return $access; + } + + /** + * set default connection scope + type by search route between endpoints + * @throws \Exception + */ + public function setAutoScopeAndType(){ + if( + is_object($this->source) && + is_object($this->target) + ){ + if( + $this->source->isAbyss() || + $this->target->isAbyss() + ){ + $this->scope = 'abyssal'; + $this->type = ['abyssal']; + }elseif( + $this->source->isKspace() && + $this->target->isKspace() && + (new Route())->searchRoute($this->source->systemId, $this->target->systemId, 1)['routePossible'] + ){ + $this->scope = 'stargate'; + $this->type = ['stargate']; + }else{ + $this->scope = 'wh'; + $this->type = ['wh_fresh']; + } + } + } + + /** + * check whether this connection is a wormhole or not + * @return bool + */ + public function isWormhole() : bool { + return ($this->scope === 'wh'); + } + + /** + * check whether this model is valid or not + * @return bool + * @throws Exception\DatabaseException + */ + public function isValid() : bool { + if($valid = parent::isValid()){ + // check if source/target system are not equal + // check if source/target belong to same map + if( + is_object($this->source) && + is_object($this->target) && + $this->get('source', true) === $this->get('target', true) || + $this->source->get('mapId', true) !== $this->target->get('mapId', true) + ){ + $valid = false; + } + } + + return $valid; + } + + /** + * Event "Hook" function + * can be overwritten + * return false will stop any further action + * @param \Exodus4D\Pathfinder\Model\AbstractModel $self + * @param $pkeys + * @return bool + * @throws Exception\DatabaseException + * @throws \Exception + */ + public function beforeInsertEvent($self, $pkeys) : bool { + // check for "default" connection type and add them if missing + // -> get() with "true" returns RAW data! important for JSON table column check! + $types = (array)json_decode($this->get('type', true)); + if( + !$this->scope || + empty($types) + ){ + $this->setAutoScopeAndType(); + } + + return $this->isValid() ? parent::beforeInsertEvent($self, $pkeys) : false; + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterInsertEvent($self, $pkeys){ + $self->clearCacheData(); + $self->logActivity('connectionCreate'); + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterUpdateEvent($self, $pkeys){ + $self->clearCacheData(); + $self->logActivity('connectionUpdate'); + } + + /** + * Event "Hook" function + * can be overwritten + * @param self $self + * @param $pkeys + */ + public function afterEraseEvent($self, $pkeys){ + $self->clearCacheData(); + $self->logActivity('connectionDelete'); + } + + /** + * @param string $action + * @return Logging\LogInterface + * @throws Exception\ConfigException + */ + public function newLog(string $action = '') : Logging\LogInterface { + return $this->getMap()->newLog($action)->setTempData($this->getLogObjectData()); + } + + /** + * @return MapModel + */ + public function getMap() : MapModel { + return $this->get('mapId'); + } + + /** + * delete a connection + * @param CharacterModel $characterModel + * @return bool + */ + public function delete(CharacterModel $characterModel) : bool { + return ($this->valid() && $this->hasAccess($characterModel)) ? $this->erase() : false; + } + + /** + * get object relevant data for model log + * @return array + */ + public function getLogObjectData() : array { + return [ + 'objId' => $this->_id, + 'objName' => $this->scope + ]; + } + + /** + * see parent + */ + public function clearCacheData(){ + $this->mapId->clearCacheData(); + } + + /** + * get all signatures that are connected with this connection + * @return array|mixed + */ + public function getSignatures(){ + $signatures = []; + $this->filter('signatures', [ + 'active = :active', + ':active' => 1 + ]); + + if($this->signatures){ + $signatures = $this->signatures; + } + + return $signatures; + } + + /** + * get all jump logs that are connected with this connection + * @return array|mixed + */ + public function getLogs(){ + $logs = []; + + if($this->connectionLog){ + $logs = $this->connectionLog; + } + + return $logs; + } + + /** + * get endpoint data for $type (source || target) + * @param string $type + * @return array + */ + protected function getEndpointData(string $type) : array { + $endpointData = []; + + if($this->exists($field = $type . 'EndpointType') && !empty($types = (array)$this->$field)){ + $endpointData['types'] = $types; + } + + return $endpointData; + } + + /** + * get all endpoint data for this connection + * @return array + */ + protected function getEndpointsData() : array { + $endpointsData = []; + + if(!empty($endpointData = $this->getEndpointData('source'))){ + $endpointsData['source'] = $endpointData; + } + if(!empty($endpointData = $this->getEndpointData('target'))){ + $endpointsData['target'] = $endpointData; + } + + return $endpointsData; + } + + /** + * get all signature data linked to this connection + * @return array + */ + public function getSignaturesData() : array { + $signaturesData = []; + $signatures = $this->getSignatures(); + + foreach($signatures as $signature){ + $signaturesData[] = $signature->getData(); + } + + return $signaturesData; + } + + /** + * get all connection log data linked to this connection + * @return array + */ + public function getLogsData() : array { + $logsData = []; + $logs = $this->getLogs(); + + foreach($logs as $log){ + $logsData[] = $log->getData(); + } + + return $logsData; + } + + /** + * get blank connectionLog model + * @return ConnectionLogModel + * @throws \Exception + */ + public function getNewLog() : ConnectionLogModel { + /** + * @var $log ConnectionLogModel + */ + $log = self::getNew('ConnectionLogModel'); + $log->connectionId = $this; + return $log; + } + + /** + * log new mass for this connection + * @param CharacterLogModel $characterLog + * @return ConnectionModel + * @throws \Exception + */ + public function logMass(CharacterLogModel $characterLog) : self { + if( !$characterLog->dry() ){ + $log = $this->getNewLog(); + $log->shipTypeId = $characterLog->shipTypeId; + $log->shipTypeName = $characterLog->shipTypeName; + $log->shipMass = $characterLog->shipMass; + $log->characterId = $characterLog->characterId->_id; + $log->characterName = $characterLog->characterId->name; + $log->save(); + } + + return $this; + } + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['source', 'target', 'scope']); + } + return $status; + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/ConnectionScopeModel.php b/app/Model/Pathfinder/ConnectionScopeModel.php new file mode 100644 index 000000000..5d07fea91 --- /dev/null +++ b/app/Model/Pathfinder/ConnectionScopeModel.php @@ -0,0 +1,77 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'label' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'connectorDefinition' => [ + 'type' => Schema::DT_VARCHAR256, + 'nullable' => false, + 'default' => '' + ] + ]; + + /** + * @var array + */ + protected static $tableData = [ + [ + 'id' => 1, + 'name' => 'wh', + 'label' => 'wormhole', + 'connectorDefinition' => '[ "Bezier", { "curviness": 40 } ]' + ], + [ + 'id' => 2, + 'name' => 'stargate', + 'label' => 'stargate', + 'connectorDefinition' => '[ "Flowchart", { "stub": [20, 20], "gap": 0, "cornerRadius": 5, "alwaysRespectStubs": false } ]' + ], + [ + 'id' => 3, + 'name' => 'jumpbridge', + 'label' => 'jumpbridge', + 'connectorDefinition' => '[ "Straight", { "stub": [5, 5], "gap": 0 } ]' + ], + [ + 'id' => 4, + 'name' => 'abyssal', + 'label' => 'abyssal', + 'connectorDefinition' => '[ "Straight", { "stub": [5, 5], "gap": 0 } ]' + ] + ]; + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/CorporationMapModel.php b/app/Model/Pathfinder/CorporationMapModel.php new file mode 100644 index 000000000..24ada8c99 --- /dev/null +++ b/app/Model/Pathfinder/CorporationMapModel.php @@ -0,0 +1,76 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'corporationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\CorporationModel', + 'constraint' => [ + [ + 'table' => 'corporation', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'mapId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\MapModel', + 'constraint' => [ + [ + 'table' => 'map', + 'on-delete' => 'CASCADE' + ] + ] + ] + ]; + + /** + * see parent + */ + public function clearCacheData(){ + // clear map cache + $this->mapId->clearCacheData(); + } + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['corporationId', 'mapId'], true); + } + return $status; + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/CorporationModel.php b/app/Model/Pathfinder/CorporationModel.php new file mode 100644 index 000000000..17e39b26f --- /dev/null +++ b/app/Model/Pathfinder/CorporationModel.php @@ -0,0 +1,413 @@ + a corp member has granted roles 0 up to all roles + */ + const CCP_ROLES = [ + 'director', + 'personnel_manager', + 'accountant', + 'security_officer', + 'factory_manager', + 'station_manager', + 'auditor', + 'hangar_take_1', + 'hangar_take_2', + 'hangar_take_3', + 'hangar_take_4', + 'hangar_take_5', + 'hangar_take_6', + 'hangar_take_7', + 'hangar_query_1', + 'hangar_query_2', + 'hangar_query_3', + 'hangar_query_4', + 'hangar_query_5', + 'hangar_query_6', + 'hangar_query_7', + 'account_take_1', + 'account_take_2', + 'account_take_3', + 'account_take_4', + 'account_take_5', + 'account_take_6', + 'account_take_7', + 'diplomat', + 'config_equipment', + 'container_take_1', + 'container_take_2', + 'container_take_3', + 'container_take_4', + 'container_take_5', + 'container_take_6', + 'container_take_7', + 'rent_office', + 'rent_factory_facility', + 'rent_research_facility', + 'junior_accountant', + 'config_starbase_equipment', + 'trader', + 'communications_officer', + 'contract_manager', + 'starbase_defense_operator', + 'starbase_fuel_technician', + 'fitting_manager', + 'terrestrial_combat_officer', + 'terrestrial_logistics_officer' + ]; + + /** + * corp roles that give admin access for a corp + */ + const ADMIN_ROLES = [ + 'director', + 'personnel_manager', + 'security_officer' + ]; + + /** + * corp rights that can be stored to a corp + */ + const RIGHTS = [ + 'map_create', + 'map_update', + 'map_delete', + 'map_import', + 'map_export', + 'map_share' + ]; + + /** + * @var array + */ + protected $fieldConf = [ + 'active' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'ticker' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'memberCount' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ], + 'shared' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 0 + ], + 'isNPC' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 0 + ], + 'corporationCharacters' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\CharacterModel', 'corporationId'] + ], + 'mapCorporations' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\CorporationMapModel', 'corporationId'] + ], + 'corporationRights' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\CorporationRightModel', 'corporationId'] + ], + 'corporationStructures' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\CorporationStructureModel', 'corporationId'] + ], + 'structures' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\StructureModel', 'corporationId'] + ] + ]; + + /** + * get cooperation data + * @param bool $addRights + * @return \stdClass + * @throws \Exception + */ + public function getData(bool $addRights = true) : \stdClass { + $cooperationData = (object) []; + + $cooperationData->id = $this->id; + $cooperationData->name = $this->name; + $cooperationData->shared = $this->shared; + + if($addRights){ + if($corporationRights = $this->getRights()){ + foreach($corporationRights as $corporationRight){ + $cooperationData->rights[] = $corporationRight->getData(); + } + } + } + + return $cooperationData; + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + * @return bool + */ + public function beforeUpdateEvent($self, $pkeys) : bool { + // if model changed, 'update' col needs to be updated as well + // -> data no longer "outdated" + $this->touch('updated'); + + return parent::beforeUpdateEvent($self, $pkeys); + } + + /** + * get all maps for this corporation + * @param int|null $mapId + * @param array $options + * @return array + */ + public function getMaps(?int $mapId = null, $options = []) : array { + $maps = []; + $this->filterRel(); + + if($mapId){ + $filters = [ + self::getFilter('mapId', $mapId) + ]; + + $this->filter('mapCorporations', $this->mergeWithRelFilter('mapCorporations', $this->mergeFilter($filters)), $this->getRelFilterOption('mapCorporations')); + } + + if($this->mapCorporations){ + $mapCount = 0; + foreach($this->mapCorporations as $mapCorporation){ + $validActive = !$options['addInactive'] ? $mapCorporation->mapId->isActive() : true; + $validMapCount = !$options['ignoreMapCount'] ? $mapCount < Config::getMapsDefaultConfig('corporation')['max_count'] : true; + + if($validActive && $validMapCount){ + $maps[] = $mapCorporation->mapId; + $mapCount++; + } + } + } + + return $maps; + } + + /** + * get all characters in this corporation + * @param array $characterIds + * @param array $options + * @return CharacterModel[] + */ + public function getCharacters($characterIds = [], $options = []) : array { + $characters = []; + $filter = ['active = ?', 1]; + + if( !empty($characterIds) ){ + $filter[0] .= ' AND id IN (?)'; + $filter[] = $characterIds; + } + + $this->filter('corporationCharacters', $filter); + + if($options['hasLog']){ + // just characters with active log data + $this->has('corporationCharacters.characterLog', ['active = ?', 1]); + } + + if($this->corporationCharacters){ + foreach($this->corporationCharacters as $character){ + $characters[] = $character; + } + } + + return $characters; + } + + /** + * get all structure data for this corporation + * @param int $systemId + * @return array + */ + public function getStructuresData(int $systemId) : array { + $structuresData = []; + $structure = $this->rel('structures'); + + $filters = [ + self::getFilter('corporationId', $this->id), + self::getFilter('active', true) + ]; + + $structure->has('structureCorporations', $this->mergeFilter($filters)); + + $filters = [ + self::getFilter('systemId', $systemId), + self::getFilter('active', true) + ]; + + if($structures = $structure->find($this->mergeFilter($filters))){ + foreach($structures as $structure){ + $structuresData[] = $structure->getData(); + } + } + + return $structuresData; + } + + /** + * get roles for each character in this corp + * -> CCP API call + * @param string $accessToken + * @return array + */ + public function getCharactersRoles($accessToken){ + $characterRolesData = []; + if( + !empty($accessToken) && + !$this->isNPC + ){ + $response = self::getF3()->ccpClient()->send('getCorporationRoles', $this->_id, $accessToken); + if( !empty($response['roles']) ){ + $characterRolesData = (array)$response['roles']; + } + } + + return $characterRolesData; + } + + /** + * get all corporation rights + * @param array $names + * @param array $options + * @return CorporationRightModel[] + * @throws \Exception + */ + public function getRights($names = self::RIGHTS, $options = []) : array { + $corporationRights = []; + // get available rights + $right = self::getNew('RightModel'); + if($rights = $right->find(['active = ? AND name IN (?)', 1, $names])){ + // get already stored rights + if( !$options['addInactive'] ){ + $this->filter('corporationRights', ['active = ?', 1]); + } + + foreach($rights as $i => $tempRight){ + $corporationRight = false; + if($this->corporationRights){ + foreach($this->corporationRights as $tempCorporationRight){ + /** + * @var $tempCorporationRight CorporationRightModel + */ + if($tempCorporationRight->get('rightId', true) === $tempRight->_id){ + $corporationRight = $tempCorporationRight; + break; + } + } + } + + if(!$corporationRight){ + $corporationRight = self::getNew('CorporationRightModel'); + $corporationRight->corporationId = $this; + $corporationRight->rightId = $tempRight; + $corporationRight->roleId = RoleModel::getDefaultRole(); + } + + $corporationRights[] = $corporationRight; + } + } + + return $corporationRights; + } + + /** + * load corporation by Id either from DB or load data from API + * @param int $id + * @param int $ttl + * @param bool $isActive + * @return bool + */ + public function getById(int $id, int $ttl = self::DEFAULT_SQL_TTL, bool $isActive = true) : bool { + $loaded = parent::getById($id, $ttl, $isActive); + if($this->isOutdated()){ + // request corporation data + $corporationData = self::getF3()->ccpClient()->send('getCorporation', $id); + if(!empty($corporationData) && !isset($corporationData['error'])){ + // check for NPC corporation + $corporationData['isNPC'] = in_array($id, self::getF3()->ccpClient()->send('getNpcCorporations')); + + $this->copyfrom($corporationData, ['id', 'name', 'ticker', 'memberCount', 'isNPC']); + $this->save(); + } + } + + return $loaded; + } + + /** + * add new structure for this corporation + * @param StructureModel $structure + */ + public function saveStructure(StructureModel $structure){ + if( !$structure->dry() ){ + $corporationStructure = $this->rel('corporationStructures'); + $corporationStructure->corporationId = $this; + $corporationStructure->structureId = $structure; + $corporationStructure->save(); + } + } + + /** + * @see parent + */ + public function filterRel() : void { + $this->filter('mapCorporations', self::getFilter('active', true), ['order' => 'created']); + } + + /** + * get all corporations + * @param array $options + * @return \DB\CortexCollection + */ + public static function getAll($options = []){ + $query = [ + 'active = :active', + ':active' => 1 + ]; + + if( !$options['addNPC'] ){ + $query[0] .= ' AND isNPC = :isNPC'; + $query[':isNPC'] = 1; + } + + return (new self())->find($query); + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/CorporationRightModel.php b/app/Model/Pathfinder/CorporationRightModel.php new file mode 100644 index 000000000..b42b8c45d --- /dev/null +++ b/app/Model/Pathfinder/CorporationRightModel.php @@ -0,0 +1,110 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'corporationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\CorporationModel', + 'constraint' => [ + [ + 'table' => 'corporation', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'rightId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\RightModel', + 'constraint' => [ + [ + 'table' => 'right', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'roleId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\RoleModel', + 'constraint' => [ + [ + 'table' => 'role', + 'on-delete' => 'CASCADE' + ] + ] + ] + ]; + + /** + * set data by associative array + * @param array $data + */ + public function setData($data){ + unset($data['id']); + unset($data['created']); + unset($data['updated']); + + foreach((array)$data as $key => $value){ + if(!is_array($value)){ + if($this->exists($key)){ + $this->$key = $value; + } + } + } + } + + /** + * get cooperation right data + * @return \stdClass + */ + public function getData(){ + $cooperationRightData = (object) []; + + $cooperationRightData->right = $this->rightId->getData(); + $cooperationRightData->role = $this->roleId->getData(); + + return $cooperationRightData; + } + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['corporationId', 'rightId'], true); + } + return $status; + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/CorporationStructureModel.php b/app/Model/Pathfinder/CorporationStructureModel.php new file mode 100644 index 000000000..5ee8f4a71 --- /dev/null +++ b/app/Model/Pathfinder/CorporationStructureModel.php @@ -0,0 +1,68 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'corporationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\CorporationModel', + 'constraint' => [ + [ + 'table' => 'corporation', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'structureId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\StructureModel', + 'constraint' => [ + [ + 'table' => 'structure', + 'on-delete' => 'CASCADE' + ] + ] + ] + ]; + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['corporationId', 'structureId'], true); + } + return $status; + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/CronModel.php b/app/Model/Pathfinder/CronModel.php new file mode 100644 index 000000000..973f9c118 --- /dev/null +++ b/app/Model/Pathfinder/CronModel.php @@ -0,0 +1,292 @@ + [ + 'type' => 'warning', + 'icon' => 'question', + 'msg' => 'No status information available' + ], + 'dbError' => [ + 'type' => 'warning', + 'icon' => 'fa-exclamation-triangle', + 'msg' => 'Failed to sync job data with DB' + ], + 'notExecuted' => [ + 'type' => 'hint', + 'icon' => 'fa-bolt', + 'msg' => 'Has not been executed' + ], + 'notFinished' => [ + 'type' => 'danger', + 'icon' => 'fa-clock', + 'msg' => 'Not finished within max exec. time' + ], + 'inProgress' => [ + 'type' => 'success', + 'icon' => 'fa-play', + 'msg' => 'Started. In execution…' + ], + 'isPaused' => [ + 'type' => 'warning', + 'icon' => 'fa-pause', + 'msg' => 'Paused. No execution on next time trigger (skip further execution)' + ], + 'onHold' => [ + 'type' => 'information', + 'icon' => 'fa-history fa-flip-horizontal', + 'msg' => 'Is active. Waiting for next trigger…' + ] + ]; + + /** + * @var array + */ + protected $fieldConf = [ + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'index' => true, + 'unique' => true, + 'validate' => 'notEmpty' + ], + 'handler' => [ + 'type' => Schema::DT_VARCHAR256, + 'nullable' => false, + 'default' => '', + 'index' => true, + 'unique' => true, + 'validate' => 'notEmpty' + ], + 'expr' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'validate' => 'notEmpty' + ], + 'isPaused' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 0 + ], + 'lastExecStart' => [ + 'type' => Schema::DT_DOUBLE, + 'nullable' => true, + 'default' => null + ], + 'lastExecEnd' => [ + 'type' => Schema::DT_DOUBLE, + 'nullable' => true, + 'default' => null + ], + 'lastExecMemPeak' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => true, + 'default' => null + ], + 'lastExecState' => [ + 'type' => self::DT_JSON + ], + 'history' => [ + 'type' => self::DT_JSON + ] + ]; + + /** + * set data by associative array + * @param array $data + */ + public function setData(array $data){ + $this->copyfrom($data, ['handler', 'expr', 'lastExecStart', 'lastExecEnd', 'lastExecMemPeak', 'lastExecState']); + } + + /** + * get data + * @return object + */ + public function getData(){ + $data = (object) []; + $data->id = $this->_id; + $data->name = $this->name; + $data->handler = $this->handler; + $data->expr = $this->expr; + $data->logFile = $this->logFileExists(); + + $data->lastExecStart = $this->lastExecStart; + $data->lastExecEnd = $this->lastExecEnd; + $data->lastExecMemPeak = $this->lastExecMemPeak; + $data->lastExecDuration = $this->getExecDuration(); + $data->lastExecState = $this->lastExecState; + + $data->isPaused = $this->isPaused; + $data->status = $this->getStatus(); + $data->history = $this->getHistory(true); + + return $data; + } + + /** + * setter for system alias + * @param string $lastExecStart + * @return string + */ + public function set_lastExecStart($lastExecStart){ + $this->logState(); + return $lastExecStart; + } + + /** + * log execution "state" for prev run in 'history' column + */ + protected function logState(){ + $this->history = $this->getHistory() ? : null; + // reset data from last run + $this->lastExecEnd = null; + $this->lastExecMemPeak = null; + } + + /** + * @param bool $addLastIfFinished + * @return array + * @throws \Exception + */ + protected function getHistory(bool $addLastIfFinished = false) : array { + $history = $this->history ? : []; + + if(!is_null($this->lastExecStart)){ + if(!$addLastIfFinished || !is_null($this->lastExecEnd)){ + array_unshift($history, [ + 'lastExecStart' => $this->lastExecStart, + 'lastExecMemPeak' => $this->lastExecMemPeak, + 'lastExecDuration' => (!$this->inExec() && !$this->isTimedOut()) ? $this->getExecDuration() : 0, + 'status' => array_intersect(array_keys($this->getStatus()), ['inProgress', 'notFinished']) + ]); + $history = array_slice($history, 0, 10); + } + + } + + return $history; + } + + /** + * get current job status based on its current data + * @return array + * @throws \Exception + */ + protected function getStatus() : array { + $status = []; + + if($this->isPaused){ + $status['isPaused'] = self::STATUS['isPaused']; + } + + if($this->inExec() && !$this->isTimedOut()){ + $status['inProgress'] = self::STATUS['inProgress']; + } + + if(empty($status)){ + $status['onHold'] = self::STATUS['onHold']; + } + + if($this->isTimedOut()){ + $status['notFinished'] = self::STATUS['notFinished']; + } + + if(!$this->lastExecStart){ + $status['notExecuted'] = self::STATUS['notExecuted']; + } + + return empty($status) ? ['unknown' => self::STATUS['unknown']] : array_reverse($status); + } + + /** + * based on the data on DB, job is marked at "in progress" + * @return bool + */ + protected function inExec() : bool { + return $this->lastExecStart && !$this->lastExecEnd; + } + + /** + * @return bool + * @throws \Exception + */ + protected function isTimedOut() : bool { + $timedOut = false; + if($this->lastExecStart){ + $timezone = self::getF3()->get('getTimeZone')(); + $startTime = \DateTime::createFromFormat( + 'U.u', + number_format($this->lastExecStart, 6, '.', ''), + $timezone + ); + + $timeBuffer = 60 * 60; + $startTime->add(new \DateInterval('PT' . $timeBuffer . 'S')); + + if($this->lastExecEnd){ + $endTime = \DateTime::createFromFormat( + 'U.u', + number_format($this->lastExecEnd, 6, '.', ''), + $timezone + ); + }else{ + $endTime = new \DateTime('now', $timezone); + } + + $timedOut = $startTime < $endTime; + } + + return $timedOut; + } + + /** + * @return float|null + */ + protected function getExecDuration() : ?float { + $duration = null; + if($this->lastExecStart && $this->lastExecEnd){ + $duration = (float)$this->lastExecEnd - (float)$this->lastExecStart; + } + + return $duration; + } + + /** + * extract function name from $this->handler + * -> it is used for the log file name + * @return string|null + */ + protected function getLogFileName() : ?string { + return ($this->handler && preg_match('/^.*->(\w+)$/', $this->handler,$m)) ? 'cron_' . $m[1] . '.log' : null; + } + + /** + * checks whether a log file exists for this cronjob + * -> will be created after job execution + * @return string + */ + protected function logFileExists() : ?string { + $filePath = null; + if($file = $this->getLogFileName()){ + $filePath = is_file($path = self::getF3()->get('LOGS') . $file) ? $path : null; + } + return $filePath; + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/LogModelInterface.php b/app/Model/Pathfinder/LogModelInterface.php new file mode 100644 index 000000000..b2e7245e4 --- /dev/null +++ b/app/Model/Pathfinder/LogModelInterface.php @@ -0,0 +1,19 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true, + 'activity-log' => true + ], + 'scopeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\MapScopeModel', + 'constraint' => [ + [ + 'table' => 'map_scope', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry', + 'activity-log' => true + ], + 'typeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\MapTypeModel', + 'constraint' => [ + [ + 'table' => 'map_type', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry', + 'activity-log' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true, + 'validate' => true + ], + 'icon' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ], + 'deleteExpiredConnections' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'activity-log' => true + ], + 'deleteEolConnections' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'activity-log' => true + ], + 'persistentAliases' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'activity-log' => true + ], + 'persistentSignatures' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'activity-log' => true + ], + 'trackAbyssalJumps' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'activity-log' => true + ], + 'logActivity' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'activity-log' => true + ], + 'logHistory' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 0, + 'activity-log' => true + ], + 'slackWebHookURL' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'validate' => true + ], + 'slackUsername' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ], + 'slackIcon' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ], + 'slackChannelHistory' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ], + 'slackChannelRally' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ], + 'discordUsername' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ], + 'discordWebHookURLRally' => [ + 'type' => Schema::DT_VARCHAR256, + 'nullable' => false, + 'default' => '', + 'validate' => true + ], + 'discordWebHookURLHistory' => [ + 'type' => Schema::DT_VARCHAR256, + 'nullable' => false, + 'default' => '', + 'validate' => true + ], + 'systems' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\SystemModel', 'mapId'] + ], + 'connections' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\ConnectionModel', 'mapId'] + ], + 'mapCharacters' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\CharacterMapModel', 'mapId'] + ], + 'mapCorporations' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\CorporationMapModel', 'mapId'] + ], + 'mapAlliances' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\AllianceMapModel', 'mapId'] + ] + ]; + + /** + * set data by associative array + * @param array $data + */ + public function setData($data){ + unset($data['id']); + unset($data['created']); + unset($data['updated']); + unset($data['createdCharacterId']); + unset($data['updatedCharacterId']); + + foreach((array)$data as $key => $value){ + if(!is_array($value)){ + if($this->exists($key)){ + $this->$key = $value; + } + }else{ + // special array data + if($key == 'scope'){ + $this->scopeId = (int)$value['id']; + }elseif($key == 'type'){ + $this->typeId = (int)$value['id']; + } + } + } + } + + /** + * get data + * -> this includes system and connection data as well + * @param bool $noCache + * @return mixed|object|null + * @throws Exception\ConfigException + */ + public function getData(bool $noCache = false){ + // check if there is cached data + if($noCache || is_null($mapDataAll = $this->getCacheData())){ + // no cached map data found + + $mapData = (object) []; + $mapData->id = $this->id; + $mapData->name = $this->name; + $mapData->icon = $this->icon; + $mapData->deleteExpiredConnections = $this->deleteExpiredConnections; + $mapData->deleteEolConnections = $this->deleteEolConnections; + $mapData->persistentAliases = $this->persistentAliases; + $mapData->persistentSignatures = $this->persistentSignatures; + $mapData->trackAbyssalJumps = $this->trackAbyssalJumps; + + // map scope + $mapData->scope = (object) []; + $mapData->scope->id = $this->scopeId->id; + $mapData->scope->name = $this->scopeId->name; + $mapData->scope->label = $this->scopeId->label; + + // map type + $mapData->type = (object) []; + $mapData->type->id = $this->typeId->id; + $mapData->type->name = $this->typeId->name; + $mapData->type->classTab = $this->typeId->classTab; + + // map logging + $mapData->logging = (object) []; + $mapData->logging->activity = $this->isActivityLogEnabled(); + $mapData->logging->history = $this->isHistoryLogEnabled(); + + // map Slack logging + $mapData->logging->slackHistory = $this->isSlackChannelEnabled('slackChannelHistory'); + $mapData->logging->slackRally = $this->isSlackChannelEnabled('slackChannelRally'); + $mapData->logging->slackWebHookURL = $this->slackWebHookURL; + $mapData->logging->slackUsername = $this->slackUsername; + $mapData->logging->slackIcon = $this->slackIcon; + $mapData->logging->slackChannelHistory = $this->slackChannelHistory; + $mapData->logging->slackChannelRally = $this->slackChannelRally; + + // map Discord logging + $mapData->logging->discordRally = $this->isDiscordChannelEnabled('discordWebHookURLRally'); + $mapData->logging->discordUsername = $this->discordUsername; + $mapData->logging->discordWebHookURLRally = $this->discordWebHookURLRally; + $mapData->logging->discordWebHookURLHistory = $this->discordWebHookURLHistory; + + // map mail logging + $mapData->logging->mailRally = $this->isMailSendEnabled('RALLY_SET'); + + // map access + $mapData->access = (object) []; + $mapData->access->character = []; + $mapData->access->corporation = []; + $mapData->access->alliance = []; + + $mapData->created = (object) []; + $mapData->created->created = strtotime($this->created); + if(is_object($this->createdCharacterId)){ + $mapData->created->character = $this->createdCharacterId->getData(); + } + + $mapData->updated = (object) []; + $mapData->updated->updated = strtotime($this->updated); + if(is_object($this->updatedCharacterId)){ + $mapData->updated->character = $this->updatedCharacterId->getData(); + } + + // get access object data --------------------------------------------------------------------------------- + if($this->isPrivate()){ + $characters = $this->getCharacters(); + $characterData = []; + foreach($characters as $character){ + $characterData[] = $character->getData(); + } + $mapData->access->character = $characterData; + }elseif($this->isCorporation()){ + $corporations = $this->getCorporations(); + $corporationData = []; + + foreach($corporations as $corporation){ + $corporationData[] = $corporation->getData(); + } + $mapData->access->corporation = $corporationData; + }elseif($this->isAlliance()){ + $alliances = $this->getAlliances(); + $allianceData = []; + + foreach($alliances as $alliance){ + $allianceData[] = $alliance->getData(); + } + $mapData->access->alliance = $allianceData; + } + + // merge all data ----------------------------------------------------------------------------------------- + $mapDataAll = (object) []; + $mapDataAll->mapData = $mapData; + + // map system data ---------------------------------------------------------------------------------------- + $mapDataAll->systems = $this->getSystemsData(); + + // map connection data ------------------------------------------------------------------------------------ + $mapDataAll->connections = $this->getConnectionsData(); + + // max caching time for a map + // the cached date has to be cleared manually on any change + // this includes system, connection,... changes (all dependencies) + $this->updateCacheData($mapDataAll, '', self::DEFAULT_CACHE_TTL); + } + + return $mapDataAll; + } + + /** + * validate name column + * @param string $key + * @param string $val + * @return bool + * @throws Exception\ValidationException + */ + protected function validate_name(string $key, string $val) : bool { + $valid = true; + if(mb_strlen($val) < 3){ + $valid = false; + $this->throwValidationException($key); + } + return $valid; + } + + /** + * validate Slack WebHook URL + * @param string $key + * @param string $val + * @return bool + * @throws Exception\ValidationException + */ + protected function validate_slackWebHookURL(string $key, string $val) : bool { + return $this->validate_WebHookURL($key, $val, 'slack'); + } + + /** + * validate Discord History WebHook URL + * @param string $key + * @param string $val + * @return bool + * @throws Exception\ValidationException + */ + protected function validate_discordWebHookURLHistory(string $key, string $val) : bool { + return $this->validate_WebHookURL($key, $val, 'discord'); + } + + /** + * validate Discord Rally WebHook URL + * @param string $key + * @param string $val + * @return bool + * @throws Exception\ValidationException + */ + protected function validate_discordWebHookURLRally(string $key, string $val) : bool { + return $this->validate_WebHookURL($key, $val, 'discord'); + } + + /** + * validate Slack/Discord WebHook URL + * @param string $key + * @param string $val + * @param string $type + * @return bool + * @throws Exception\ValidationException + */ + protected function validate_WebHookURL(string $key, string $val, string $type) : bool { + $valid = true; + if( !empty($val) ){ + $hosts = [ + 'slack' => ['hooks.slack.com'], + 'discord' => ['discordapp.com', 'ptb.discordapp.com'] + ]; + + if( + !\Audit::instance()->url($val) || + !in_array(parse_url($val, PHP_URL_HOST), $hosts[$type]) + ){ + $valid = false; + $this->throwValidationException($key); + } + } + return $valid; + } + + /** + * @param $channel + * @return string + */ + protected function set_slackChannelHistory($channel){ + return $this->formatSlackChannelName($channel); + } + + /** + * @param $channel + * @return string + */ + protected function set_slackChannelRally($channel){ + return $this->formatSlackChannelName($channel); + } + + /** + * convert a Slack channel name into correct format + * @param $channel + * @return string + */ + private function formatSlackChannelName($channel){ + $channel = strtolower(str_replace(' ','', trim(trim((string)$channel), '#@'))); + if($channel){ + $channel = '#' . $channel; + } + return $channel; + } + + /** + * Event "Hook" function + * @param self $self + * @param $pkeys + */ + public function afterInsertEvent($self, $pkeys){ + $self->clearCacheData(); + $self->logActivity('mapCreate'); + } + + /** + * Event "Hook" function + * @param self $self + * @param $pkeys + */ + public function afterUpdateEvent($self, $pkeys){ + $self->clearCacheData(); + + $activity = ($self->isActive()) ? 'mapUpdate' : 'mapDelete'; + $self->logActivity($activity); + } + + /** + * Event "Hook" function + * @param self $self + * @param $pkeys + */ + public function afterEraseEvent($self, $pkeys){ + $self->clearCacheData(); + $self->deleteLogFile(); + } + + /** + * see parent + */ + public function clearCacheData(){ + parent::clearCacheData(); + + // clear character data with map access as well! + parent::clearCacheDataWithPrefix(self::DATA_CACHE_KEY_CHARACTER); + } + + /** + * get blank system model pre-filled with default SDE data + * -> check for "inactive" systems on this map first! + * @param int $systemId + * @return SystemModel + * @throws \Exception + */ + public function getNewSystem(int $systemId) : SystemModel { + // check for "inactive" system + $system = $this->getSystemByCCPId($systemId); + if(is_null($system)){ + /** + * NO ->rel() here! we work with unsaved models + * @var $system SystemModel + */ + $system = self::getNew('SystemModel'); + $system->systemId = $systemId; + $system->mapId = $this; + $system->setType(); + } + + $system->setActive(true); + + return $system; + } + + /** + * get blank connection model for given source/target systems + * @param SystemModel $sourceSystem + * @param SystemModel $targetSystem + * @return ConnectionModel + * @throws \Exception + */ + public function getNewConnection(SystemModel $sourceSystem, SystemModel $targetSystem) : ConnectionModel { + /** + * @var $connection ConnectionModel + */ + $connection = self::getNew('ConnectionModel'); + $connection->mapId = $this; + $connection->source = $sourceSystem; + $connection->target = $targetSystem; + return $connection; + } + + /** + * search for a system by id + * @param int $id + * @return SystemModel|null + */ + public function getSystemById(int $id) : ?SystemModel { + /** + * @var $system SystemModel + */ + $system = $this->rel('systems'); + $system->filterRel(); + + $filters = [ + self::getFilter('id', $id), + self::getFilter('mapId', $this->_id), + self::getFilter('active', true) + ]; + + return $system->findone($this->mergeFilter($filters)) ? : null; + } + + /** + * search for a system by CCPs systemId + * -> "active" column is NOT checked + * -> removed systems become "active" = 0 + * @param int $systemId + * @param array $addFilters + * @return SystemModel|null + */ + public function getSystemByCCPId(int $systemId, array $addFilters = []) : ?SystemModel { + /** + * @var $system SystemModel + */ + $system = $this->rel('systems'); + $system->filterRel(); + + $filters = [ + self::getFilter('systemId', $systemId), + self::getFilter('mapId', $this->_id) + ]; + + // add optional filter -> e.g. search for "active = 1" system + foreach($addFilters as $filter){ + $filters[] = $filter; + } + + return $system->findone($this->mergeFilter($filters)) ? : null; + } + + /** + * get systems in this map + * @return CortexCollection|array + */ + protected function getSystems(){ + $filters = [ + self::getFilter('active', true) + ]; + + return $this->relFind('systems', $this->mergeFilter($filters)) ? : []; + } + + /** + * get all system data for all systems in this map + * @return \stdClass[] + */ + public function getSystemsData() : array { + $systemsData = []; + + foreach($this->getSystems() as $system){ + /** + * @var $system SystemModel + */ + $systemsData[] = $system->getData(); + } + + // orderBy x-Coordinate for smoother frontend animation (left to right) + usort($systemsData, function($sysDataA, $sysDataB){ + return $sysDataA->position->x <=> $sysDataB->position->x; + }); + + return $systemsData; + } + + /** + * search for a connection by id + * @param int $id + * @return ConnectionModel|null + */ + public function getConnectionById(int $id) : ?ConnectionModel { + /** + * @var $connection ConnectionModel + */ + $connection = $this->rel('connections'); + $connection->filterRel(); + + $filters = [ + self::getFilter('id', $id), + self::getFilter('mapId', $this->_id), + self::getFilter('active', true) + ]; + + return $connection->findone($this->mergeFilter($filters)) ? : null; + } + + /** + * get connections in this map + * -> $connectionIds can be used for filter + * @param null $connectionIds + * @param string $scope + * @return CortexCollection|array + */ + public function getConnections($connectionIds = null, $scope = ''){ + $filters = [ + self::getFilter('source', 0, '>'), + self::getFilter('target', 0, '>') + ]; + + if(!empty($scope)){ + $filters[] = self::getFilter('scope', $scope); + } + + if(!empty($connectionIds)){ + $filters[] = self::getFilter('id', $connectionIds, 'IN'); + } + + return $this->relFind('connections', $this->mergeFilter($filters)) ? : []; + } + + /** + * get all connection data in this map + * @return \stdClass[] + */ + public function getConnectionsData() : array { + $connectionsData = []; + + foreach($this->getConnections() as $connection){ + /** + * @var $connection ConnectionModel + */ + $connectionsData[] = $connection->getData(true); + } + + return $connectionsData; + } + + /** + * get all structures data for this map + * @param int $systemId + * @return array + */ + public function getStructuresData(int $systemId) : array { + $structuresData = []; + $corporations = $this->getAllCorporations(); + + foreach($corporations as $corporation){ + // corporations should be unique + if( !isset($structuresData[$corporation->_id]) ){ + // get all structures for current corporation + $corporationStructuresData = $corporation->getStructuresData($systemId); + if( !empty($corporationStructuresData) ){ + // corporation has structures + $structuresData[$corporation->_id] = [ + 'id' => $corporation->_id, + 'name' => $corporation->name, + 'structures' => $corporationStructuresData + ]; + } + } + } + + return $structuresData; + } + + /** + * set map access for an object (character, corporation or alliance) + * @param $obj + * @return bool + * @throws \Exception + */ + public function setAccess($obj) : bool { + $newAccessGranted = false; + + if($obj instanceof CharacterModel){ + // check whether the user has already map access + $result = $this->relFindOne('mapCharacters', self::getFilter('characterId', $obj->_id)); + if(!$result){ + // grant access for the character + $characterMap = self::getNew('CharacterMapModel'); + $characterMap->characterId = $obj; + $characterMap->mapId = $this; + if($characterMap->save()){ + $newAccessGranted = true; + } + } + }elseif($obj instanceof CorporationModel){ + // check whether the corporation already has map access + $result = $this->relFindOne('mapCorporations', self::getFilter('corporationId', $obj->_id)); + if(!$result){ + // grant access for this corporation + $corporationMap = self::getNew('CorporationMapModel'); + $corporationMap->corporationId = $obj; + $corporationMap->mapId = $this; + if($corporationMap->save()){ + $newAccessGranted = true; + } + } + }elseif($obj instanceof AllianceModel){ + // check whether the alliance already has map access + $result = $this->relFindOne('mapAlliances', self::getFilter('allianceId', $obj->_id)); + if(!$result){ + $allianceMap = self::getNew('AllianceMapModel'); + $allianceMap->allianceId = $obj; + $allianceMap->mapId = $this; + if($allianceMap->save()){ + $newAccessGranted = true; + } + } + } + return $newAccessGranted; + } + + /** + * @param $stack + * @return array + */ + public function compareAccess($stack) : array { + $result = []; + if($this->valid()){ + if($this->isPrivate()){ + $result = $this->mapCharacters ? $this->mapCharacters->compare($stack, 'characterId') : ['new' => $stack]; + }elseif($this->isCorporation()){ + $result = $this->mapCorporations ? $this->mapCorporations->compare($stack, 'corporationId') : ['new' => $stack]; + }elseif($this->isAlliance()){ + $result = $this->mapAlliances ? $this->mapAlliances->compare($stack, 'allianceId') : ['new' => $stack]; + } + } + return $result; + } + + /** + * @param int $id + * @return int + */ + public function removeFromAccess(int $id) : int { + $count = 0; + if($id && $this->valid()){ + $result = null; + if($this->isPrivate()){ + $result = $this->relFindOne('mapCharacters', self::getFilter('characterId', $id)); + }elseif($this->isCorporation()){ + $result = $this->relFindOne('mapCorporations', self::getFilter('corporationId', $id)); + }elseif($this->isAlliance()){ + $result = $this->relFindOne('mapAlliances', self::getFilter('allianceId', $id)); + } + + if($result && $result->erase()){ + $count++; + } + } + return $count; + } + + /** + * clear map access for entities that do not match the map "mapType" + * @return int + */ + public function clearAccessByType() : int { + $count = 0; + if($this->valid()){ + if($this->isPrivate()){ + $count = $this->clearAccess(['corporation', 'alliance']); + }elseif($this->isCorporation()){ + $count = $this->clearAccess(['character', 'alliance']); + }elseif($this->isAlliance()){ + $count = $this->clearAccess(['character', 'corporation']); + } + } + return $count; + } + + /** + * clear access for a given type of objects + * @param array $clearKeys + * @return int + */ + public function clearAccess($clearKeys = ['character', 'corporation', 'alliance']) : int { + $count = 0; + foreach($clearKeys as $key){ + $field = null; + switch($key){ + case 'character': $field = 'mapCharacters'; break; + case 'corporation': $field = 'mapCorporations'; break; + case 'alliance': $field = 'mapAlliances'; break; + } + + if($this->$field){ + foreach((array)$this->$field as $model){ + if($model->erase()){ + $count++; + } + } + } + } + return $count; + } + + /** + * checks whether a character has access to this map or not + * @param CharacterModel $characterModel + * @return bool + */ + public function hasAccess(CharacterModel $characterModel) : bool { + $hasAccess = false; + + if($this->valid()){ + // get all maps the user has access to + // this includes corporation and alliance maps + foreach($characterModel->getMaps() as $map){ + if($map->_id === $this->_id){ + $hasAccess = true; + break; + } + } + } + + return $hasAccess; + } + + /** + * get all (private) characters for this map + * @return CharacterModel[] + */ + private function getCharacters(){ + $characters = []; + $filter = ['active = ?', 1]; + + $this->filter('mapCharacters', $filter); + + if($this->mapCharacters){ + foreach($this->mapCharacters as $characterMapModel){ + $characters[] = $characterMapModel->characterId; + } + } + + return $characters; + } + + /** + * get corporations that have access to this map + * @return CorporationModel[] + */ + private function getCorporations() : array { + $corporations = []; + + if($this->isCorporation()){ + $this->filter('mapCorporations', ['active = ?', 1]); + + if($this->mapCorporations){ + foreach($this->mapCorporations as $mapCorporation){ + $corporations[$mapCorporation->corporationId->_id] = $mapCorporation->corporationId; + } + } + } + + return $corporations; + } + + /** + * get alliances that have access to this map + * @return AllianceModel[] + */ + public function getAlliances() : array { + $alliances = []; + + if($this->isAlliance()){ + $this->filter('mapAlliances', ['active = ?', 1]); + + if($this->mapAlliances){ + foreach($this->mapAlliances as $mapAlliance){ + $alliances[] = $mapAlliance->allianceId; + } + } + } + + return $alliances; + } + + /** + * get all character models that are currently online "viewing" this map + * @param array $options filter options + * @return CharacterModel[] + */ + private function getAllCharacters($options = []) : array { + $characters = []; + + if($this->isPrivate()){ + // add active character for each user + foreach($this->getCharacters() as $character){ + $characters[] = $character; + } + }elseif($this->isCorporation()){ + $corporations = $this->getCorporations(); + + foreach($corporations as $corporation){ + $characters = array_merge($characters, $corporation->getCharacters([], $options)); + } + }elseif($this->isAlliance()){ + $alliances = $this->getAlliances(); + + foreach($alliances as $alliance){ + $characters = array_merge($characters, $alliance->getCharacters([], $options)); + } + } + + return $characters; + } + + /** + * get data for ALL characters with map access + * -> The result of this function is cached! + * @param array $options + * @return array|null|\stdClass + * @throws \Exception + */ + public function getCharactersData($options = []){ + // check if there is cached data + $charactersData = $this->getCacheData(self::DATA_CACHE_KEY_CHARACTER); + + if(is_null($charactersData)){ + $charactersData = []; + $characters = $this->getAllCharacters($options); + + foreach($characters as $character){ + $charactersData[] = $character->getData(true); + } + + // cache active characters (if found) + if(!empty($charactersData)){ + $this->updateCacheData($charactersData, self::DATA_CACHE_KEY_CHARACTER, 5); + } + } + + return $charactersData; + } + + /** + * get all corporations that have access + * -> for private maps -> get corporations from characters + * -> for corporation maps -> get corporations + * -> for alliance maps -> get corporations from alliances + * @return CorporationModel[] + */ + public function getAllCorporations() : array { + $corporations = []; + + if($this->isPrivate()){ + foreach($this->getCharacters() as $character){ + if( + $character->hasCorporation() && + !array_key_exists($character->get('corporationId', true), $corporations) + ){ + $corporations[$character->getCorporation()->_id] = $character->getCorporation(); + } + } + }elseif($this->isCorporation()){ + $corporations = $this->getCorporations(); + }elseif($this->isAlliance()){ + foreach($this->getAlliances() as $alliance){ + foreach($alliance->getCharacters() as $character){ + if( + $character->hasCorporation() && + !array_key_exists($character->get('corporationId', true), $corporations) + ){ + $corporations[$character->getCorporation()->_id] = $character->getCorporation(); + } + } + } + } + + return $corporations; + } + + /** + * @param string $action + * @return Logging\LogInterface + * @throws Exception\ConfigException + * @throws \Exception + */ + public function newLog(string $action = '') : Logging\LogInterface{ + $logChannelData = $this->getLogChannelData(); + $logObjectData = $this->getLogObjectData(); + $log = (new Logging\MapLog($action, $logChannelData))->setTempData($logObjectData); + + // update map history *.log files ----------------------------------------------------------------------------- + if($this->isHistoryLogEnabled()){ + // check socket config + if(Config::validSocketConnect(Config::getSocketUri())){ + $log->addHandler('socket', 'json', $this->getSocketConfig()); + }else{ + // update log file local (slow) + $log->addHandler('stream', 'json', $this->getStreamConfig()); + } + } + + // send map history to Slack channel -------------------------------------------------------------------------- + $slackChannelKey = 'slackChannelHistory'; + if($this->isSlackChannelEnabled($slackChannelKey)){ + $log->addHandler('slackMap', null, $this->getSlackWebHookConfig($slackChannelKey)); + $log->addHandlerGroup('slackMap'); + } + + // send map history to Discord channel ------------------------------------------------------------------------ + $discordChannelKey = 'discordWebHookURLHistory'; + if($this->isDiscordChannelEnabled($discordChannelKey)){ + $log->addHandler('discordMap', null, $this->getDiscordWebHookConfig($discordChannelKey)); + $log->addHandlerGroup('discordMap'); + } + + // update map activity ---------------------------------------------------------------------------------------- + $log->logActivity($this->isActivityLogEnabled()); + + return $log; + } + + /** + * @return MapModel + */ + public function getMap() : MapModel{ + return $this; + } + + /** + * get object relevant data for model log channel + * @return array + */ + public function getLogChannelData() : array { + return [ + 'channelId' => $this->_id, + 'channelName' => $this->name + ]; + } + /** + * get object relevant data for model log object + * @return array + */ + public function getLogObjectData() : array { + return [ + 'objId' => $this->_id, + 'objName' => $this->name + ]; + } + + /** + * map log formatter callback + * @return \Closure + */ + protected function getLogFormatter(){ + return function(&$rowDataObj){ + unset($rowDataObj['extra']); + }; + } + + /** + * check if "activity logging" is enabled for this map type + * @return bool + */ + public function isActivityLogEnabled() : bool { + return $this->logActivity && (bool) Config::getMapsDefaultConfig($this->typeId->name)['log_activity_enabled']; + } + + /** + * check if "history logging" is enabled for this map type + * @return bool + */ + public function isHistoryLogEnabled() : bool { + return $this->logHistory && (bool) Config::getMapsDefaultConfig($this->typeId->name)['log_history_enabled']; + } + + /** + * check if "Slack WebHook" is enabled for this map type + * @param string $channel + * @return bool + * @throws Exception\ConfigException + */ + public function isSlackChannelEnabled(string $channel) : bool { + $enabled = false; + // check global Slack status + if((bool)Config::getPathfinderData('slack.status')){ + // check global map default config for this channel + switch($channel){ + case 'slackChannelHistory': $defaultMapConfigKey = 'send_history_slack_enabled'; break; + case 'slackChannelRally': $defaultMapConfigKey = 'send_rally_slack_enabled'; break; + default: throw new Exception\ConfigException(sprintf(self::ERROR_SLACK_CHANNEL, $channel)); + } + + if((bool) Config::getMapsDefaultConfig($this->typeId->name)[$defaultMapConfigKey]){ + $config = $this->getSlackWebHookConfig($channel); + if($config->slackWebHookURL && $config->slackChannel){ + $enabled = true; + } + } + } + + return $enabled; + } + + /** + * check if "Discord WebHook" is enabled for this map type + * @param string $channel + * @return bool + * @throws Exception\ConfigException + */ + public function isDiscordChannelEnabled(string $channel) : bool { + $enabled = false; + // check global Slack status + if((bool)Config::getPathfinderData('discord.status')){ + // check global map default config for this channel + switch($channel){ + case 'discordWebHookURLHistory': $defaultMapConfigKey = 'send_history_discord_enabled'; break; + case 'discordWebHookURLRally': $defaultMapConfigKey = 'send_rally_discord_enabled'; break; + default: throw new Exception\ConfigException(sprintf(self::ERROR_DISCORD_CHANNEL, $channel)); + } + + if((bool) Config::getMapsDefaultConfig($this->typeId->name)[$defaultMapConfigKey]){ + $config = $this->getDiscordWebHookConfig($channel); + if($config->slackWebHookURL){ + $enabled = true; + } + } + } + + return $enabled; + } + + /** + * check if "E-Mail" Log is enabled for this map + * @param string $type + * @return bool + */ + public function isMailSendEnabled(string $type) : bool{ + $enabled = false; + if((bool) Config::getMapsDefaultConfig($this->typeId->name)['send_rally_mail_enabled']){ + $enabled = Config::isValidSMTPConfig($this->getSMTPConfig($type)); + } + + return $enabled; + } + + /** + * get config for stream logging + * @param bool $abs absolute path + * @return \stdClass + */ + public function getStreamConfig(bool $abs = false) : \stdClass{ + $config = (object) []; + $config->stream = ''; + if( $this->getF3()->exists('PATHFINDER.HISTORY.LOG', $dir) ){ + $config->stream .= $abs ? $this->getF3()->get('ROOT') . '/' : './'; + $config->stream .= $dir . 'map/map_' . $this->_id . '.log'; + $config->stream = $this->getF3()->fixslashes($config->stream); + } + return $config; + } + + /** + * get config for Socket connection (e.g. where to send log data) + * @return \stdClass + */ + public function getSocketConfig() : \stdClass{ + $config = (object) []; + $config->dsn = Config::getSocketUri(); + $config->streamConf = $this->getStreamConfig(true); + return $config; + } + + /** + * get Config for Slack WebHook cURL calls + * -> https://api.slack.com/incoming-webhooks + * @param string $channel + * @return \stdClass + */ + public function getSlackWebHookConfig(string $channel = '') : \stdClass{ + $config = (object) []; + $config->slackWebHookURL = $this->slackWebHookURL; + $config->slackUsername = $this->slackUsername; + $config->slackIcon = $this->slackIcon; + if($channel && $this->exists($channel) && !empty($this->$channel)){ + $config->slackChannel = $this->$channel; + } + return $config; + } + + /** + * get Config for Discord WebHook cURL calls + * @param string $channel + * @return \stdClass + */ + public function getDiscordWebHookConfig(string $channel = '') : \stdClass { + $config = (object) []; + $config->slackUsername = $this->discordUsername; + if($channel && $this->exists($channel) && !empty($this->$channel)){ + $config->slackWebHookURL = $this->$channel . '/slack'; + } + return $config; + } + + /** + * get Config for SMTP connection and recipient address + * @param string $type + * @param bool $addJson + * @return \stdClass + */ + public function getSMTPConfig(string $type, bool $addJson = true) : \stdClass { + $config = Config::getSMTPConfig(); + $config->to = Config::getNotificationMail($type); + $config->addJson = $addJson; + return $config; + } + + /** + * checks whether this map is private map + * @return bool + */ + public function isPrivate() : bool { + return ($this->typeId->id === 2); + } + + /** + * checks whether this map is corporation map + * @return bool + */ + public function isCorporation() : bool { + return ($this->typeId->id === 3); + } + + /** + * checks whether this map is alliance map + * @return bool + */ + public function isAlliance() : bool { + return ($this->typeId->id === 4); + } + + /** + * + * @return mixed|null + */ + public function getScope(){ + $scope = null; + if( $this->scopeId->isActive() ){ + $scope = $this->scopeId; + } + return $scope; + } + + /** + * get deeplink url for map + * -> optional return link for map + system + * @param int $systemId + * @return string + */ + public function getDeeplinkUrl(int $systemId = 0) : string { + $url = ''; + if( !$this->dry() ){ + $param = rawurlencode(base64_encode($this->_id)); + $param .= $systemId ? '_' . rawurlencode(base64_encode($systemId)) : ''; + $url = $this->getF3()->get('SCHEME') . '://' . $this->getF3()->get('HOST') . $this->getF3()->alias('map', ['*' => '/' . $param]); + } + return $url; + } + + /** + * get log file data + * @param int $offset + * @param int $limit + * @return array + */ + public function getLogData(int $offset = FileHandler::LOG_FILE_OFFSET, int $limit = FileHandler::LOG_FILE_LIMIT) : array { + $streamConf = $this->getStreamConfig(); + + $rowFormatter = $this->getLogFormatter(); + $rowParser = function(string &$rowData, array &$data) use ($rowFormatter){ + if( !empty($rowDataObj = (array)json_decode($rowData, true)) ){ + $rowFormatter($rowDataObj); + $data[] = $rowDataObj; + } + }; + + return FileHandler::instance()->readFileReverse($streamConf->stream, $offset, $limit, $rowParser); + } + + /** + * save a system to this map + * @param SystemModel $system + * @param CharacterModel $character + * @param int $posX + * @param int $posY + * @return false|ConnectionModel + */ + public function saveSystem(SystemModel $system, CharacterModel $character, $posX = 10, $posY = 0){ + $system->setActive(true); + $system->mapId = $this->id; + $system->posX = $posX; + $system->posY = $posY; + return $system->save($character); + } + + /** + * search for a connection by (source -> target) system ids + * -> this also searches the revers way (target -> source) + * @param SystemModel $sourceSystem + * @param SystemModel $targetSystem + * @return ConnectionModel|null + */ + public function searchConnection(SystemModel $sourceSystem, SystemModel $targetSystem) : ?ConnectionModel { + $connection = null; + + // check if both systems belong to this map + if( + $sourceSystem->get('mapId', true) === $this->_id && + $targetSystem->get('mapId', true) === $this->_id + ){ + $filter = $this->mergeFilter([ + $this->mergeFilter([self::getFilter('source', $sourceSystem->id, '=', 'A'), self::getFilter('target', $targetSystem->id, '=', 'A')]), + $this->mergeFilter([self::getFilter('source', $targetSystem->id, '=', 'B'), self::getFilter('target', $sourceSystem->id, '=', 'B')]) + ], 'or'); + + $connection = $this->relFindOne('connections', $filter); + } + + return $connection; + } + + /** + * @see parent + */ + public function filterRel() : void { + $this->filter('connections', self::getFilter('active', true)); + } + + /** + * save new connection + * -> connection scope/type is automatically added + * @param ConnectionModel $connection + * @param CharacterModel $character + * @return false|ConnectionModel + */ + public function saveConnection(ConnectionModel $connection, CharacterModel $character){ + $connection->mapId = $this; + return $connection->save($character); + } + + /** + * delete existing log file + */ + protected function deleteLogFile(){ + $config = $this->getStreamConfig(); + if(is_file($config->stream)){ + // try to set write access + if(!is_writable($config->stream)){ + chmod($config->stream, 0666); + } + @unlink($config->stream); + } + } + + /** + * get all active characters (with active log) + * grouped by systems + * @return \stdClass + * @throws \Exception + */ + public function getUserData(){ + + // get systems for this map + // the getData() function is cached. So this is more suitable than getSystems(); + $mapDataAll = $this->getData(); + + // get data of characters which have with map access + $activeUserCharactersData = $this->getCharactersData(['hasLog' => true]); + + // sort characters by "active" status + $sortByActiveLog = function($a, $b){ + if($a->log->active == $b->log->active){ + return 0; + }else{ + return ($a->log->active && !$b->log->active) ? 0 : 1; + } + }; + + $mapUserData = (object)[]; + $mapUserData->config = (object)[]; + $mapUserData->config->id = $this->id; + $mapUserData->data = (object)[]; + $mapUserData->data->systems = []; + foreach($mapDataAll->systems as $systemData){ + $systemUserData = (object)[]; + $systemUserData->id = $systemData->systemId; + $systemUserData->user = []; + + // check if a system has active characters + foreach($activeUserCharactersData as $key => $activeUserCharacterData){ + if(isset($activeUserCharacterData->log)){ + // user as log data + if($activeUserCharacterData->log->system->id == $systemData->systemId){ + $systemUserData->user[] = $activeUserCharacterData; + + // remove user from array -> speed up looping over characters. + // each userCharacter can only be active in a SINGLE system + unset($activeUserCharactersData[$key]); + } + }else{ + // character has NO log data. If its an corp/ally map not each member is active + // -> character is not relevant for this function! + unset($activeUserCharactersData[$key]); + } + } + + // add system if active users were found + if(count($systemUserData->user) > 0){ + usort($systemUserData->user, $sortByActiveLog); + $mapUserData->data->systems[] = $systemUserData; + } + } + + return $mapUserData; + } + + /** + * @param CharacterModel|null $characterModel + * @return false|ConnectionModel|MapModel + */ + public function save(CharacterModel $characterModel = null){ + /** + * @var $mapModel MapModel + */ + $mapModel = parent::save($characterModel); + + return $mapModel; + } + + /** + * get all maps + * @param array $mapIds + * @param array $options + * @return CortexCollection + */ + public static function getAll($mapIds = [], $options = []){ + $query = [ + 'id IN :mapIds', + ':mapIds' => $mapIds + ]; + if( !$options['addInactive'] ){ + $query[0] .= ' AND active = :active'; + $query[':active'] = 1; + } + + return (new self())->find($query); + } +} diff --git a/app/Model/Pathfinder/MapScopeModel.php b/app/Model/Pathfinder/MapScopeModel.php new file mode 100644 index 000000000..e3d3d9c39 --- /dev/null +++ b/app/Model/Pathfinder/MapScopeModel.php @@ -0,0 +1,65 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'label' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ] + ]; + + /** + * @var array + */ + protected static $tableData = [ + [ + 'id' => 1, + 'name' => 'wh', + 'label' => 'wormholes' + ],[ + 'id' => 2, + 'name' => 'k-space', + 'label' => 'stargates' + ],[ + 'id' => 3, + 'name' => 'none', + 'label' => 'none' + ],[ + 'id' => 4, + 'name' => 'all', + 'label' => 'all' + ] + ]; + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/MapTypeModel.php b/app/Model/Pathfinder/MapTypeModel.php new file mode 100644 index 000000000..35fee5147 --- /dev/null +++ b/app/Model/Pathfinder/MapTypeModel.php @@ -0,0 +1,93 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'label' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'class' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'classTab' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ] + ]; + + /** + * @var array + */ + protected static $tableData = [ + [ + 'id' => 1, + 'name' => 'standard', + 'label' => '', + 'class' => '', + 'classTab' => 'pf-map-type-tab-default' + ], + [ + 'id' => 2, + 'name' => 'private', + 'label' => 'private', + 'class' => 'pf-map-type-private', + 'classTab' => 'pf-map-type-tab-private' + ], + [ + 'id' => 3, + 'name' => 'corporation', + 'label' => 'corporation', + 'class' => 'pf-map-type-corporation', + 'classTab' => 'pf-map-type-tab-corporation' + ], + [ + 'id' => 4, + 'name' => 'alliance', + 'label' => 'alliance', + 'class' => 'pf-map-type-alliance', + 'classTab' => 'pf-map-type-tab-alliance' + ], + [ + 'id' => 5, + 'name' => 'global', + 'label' => 'global', + 'class' => 'pf-map-type-global', + 'classTab' => 'pf-map-type-tab-global' + ] + ]; + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/RightModel.php b/app/Model/Pathfinder/RightModel.php new file mode 100644 index 000000000..13cc9c436 --- /dev/null +++ b/app/Model/Pathfinder/RightModel.php @@ -0,0 +1,105 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'index' => true, + 'unique' => true + ], + 'label' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'description' => [ + 'type' => Schema::DT_VARCHAR512, + 'nullable' => false, + 'default' => '' + ], + 'corporationRights' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\CorporationRightModel', 'rightId'] + ] + ]; + + /** + * @var array + */ + protected static $tableData = [ + [ + 'id' => 1, + 'name' => 'map_update', + 'label' => 'update', + 'description' => 'Map settings update right' + ], + [ + 'id' => 2, + 'name' => 'map_delete', + 'label' => 'delete', + 'description' => 'Map delete right' + ], + [ + 'id' => 3, + 'name' => 'map_import', + 'label' => 'import', + 'description' => 'Map import right' + ], + [ + 'id' => 4, + 'name' => 'map_export', + 'label' => 'export', + 'description' => 'Map export right' + ], + [ + 'id' => 5, + 'name' => 'map_share', + 'label' => 'share', + 'description' => 'Map share right' + ], + [ + 'id' => 6, + 'name' => 'map_create', + 'label' => 'create', + 'description' => 'Map create right' + ] + ]; + + /** + * get right data + * @return \stdClass + */ + public function getData(){ + $rightData = (object) []; + + $rightData->name = $this->name; + + return $rightData; + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/RoleModel.php b/app/Model/Pathfinder/RoleModel.php new file mode 100644 index 000000000..e0d1463c8 --- /dev/null +++ b/app/Model/Pathfinder/RoleModel.php @@ -0,0 +1,147 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'label' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'level' => [ + 'type' => Schema::DT_INT, + 'index' => true + ], + 'style' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'corporationRights' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\CorporationRightModel', 'roleId'] + ] + ]; + + /** + * @var array + */ + protected static $tableData = [ + [ + 'id' => 1, + 'name' => 'MEMBER', + 'label' => 'member', + 'level' => 2, + 'style' => 'default' + ], + [ + 'id' => 2, + 'name' => 'SUPER', + 'label' => 'admin', + 'level' => 10, + 'style' => 'danger' + ], + [ + 'id' => 3, + 'name' => 'CORPORATION', + 'label' => 'manager', + 'level' => 4, + 'style' => 'info' + ] + ]; + + /** + * get role data + * @return \stdClass + */ + public function getData(){ + $roleData = (object) []; + + $roleData->name = $this->name; + $roleData->label = $this->label; + $roleData->style = $this->style; + + return $roleData; + } + + /** + * get default role + * @return self|null + */ + public static function getDefaultRole(){ + return self::getRoleById(1); + } + + /** + * get admin role + * @return self|null + */ + public static function getAdminRole(){ + return self::getRoleById(2); + } + + /** + * get corporation admin role + * @return self|null + */ + public static function getCorporationManagerRole(){ + return self::getRoleById(3); + } + + /** + * get role by id + * @param int $roleId + * @return self|null + */ + public static function getRoleById(int $roleId = 1){ + $role = new self(); + $role->getById($roleId); + return $role->dry() ? null : $role; + } + + /** + * get all corporations + * @return \DB\CortexCollection + */ + public static function getAll(){ + $query = [ + 'active = :active', + ':active' => 1 + ]; + + $options = [ + 'order' => 'level' + ]; + + return (new self())->find($query, $options); + } + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/StructureModel.php b/app/Model/Pathfinder/StructureModel.php new file mode 100644 index 000000000..25533f68a --- /dev/null +++ b/app/Model/Pathfinder/StructureModel.php @@ -0,0 +1,262 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'structureId' => [ + 'type' => Schema::DT_INT, + 'index' => true + ], + 'corporationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\CorporationModel', + 'constraint' => [ + [ + 'table' => 'corporation', + 'on-delete' => 'SET NULL' + ] + ] + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'validate' => true + ], + 'statusId' => [ + 'type' => Schema::DT_INT, + 'default' => 1, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\StructureStatusModel', + 'constraint' => [ + [ + 'table' => 'structure_status', + 'on-delete' => 'SET NULL' + ] + ] + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'description' => [ + 'type' => Schema::DT_VARCHAR512, + 'nullable' => false, + 'default' => '' + ], + 'structureCorporations' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\CorporationStructureModel', 'structureId'] + ] + ]; + + /** + * set data by associative array + * @param array $data + */ + public function setData(array $data){ + $this->copyfrom($data, ['structureId', 'corporationId', 'systemId', 'statusId', 'name', 'description']); + } + /** + * get structure data + * @return \stdClass + * @throws \Exception + */ + public function getData() : \stdClass { + $structureData = (object) []; + $structureData->id = $this->_id; + $structureData->systemId = $this->systemId; + $structureData->status = $this->statusId->getData(); + $structureData->name = $this->name; + $structureData->description = $this->description; + + if($this->structureId){ + $structureData->structure = $this->getUniverseTypeData($this->structureId); + } + + if($this->corporationId){ + $structureData->owner = (object) []; + $structureData->owner->id = $this->corporationId->_id; + $structureData->owner->name = $this->corporationId->name; + } + + $structureData->updated = (object) []; + $structureData->updated->updated = strtotime($this->updated); + + return $structureData; + } + + /** + * set structureId (universeType) for this structure + * @param $structureId + * @return int|null + */ + public function set_structureId($structureId) : ?int { + $structureId = (int)$structureId; + return $structureId ? : null; + } + + /** + * set corporationId (owner) for this structure + * -> if corporation does not exists in DB -> load from API + * @param int|null $corporationId + * @return int|null + */ + public function set_corporationId(?int $corporationId) : ?int { + $oldCorporationId = $this->get('corporationId', true) ? : 0; + + if($corporationId){ + if($corporationId !== $oldCorporationId){ + // make sure there is already corporation data available for new corporationId + // -> $ttl = 0 is important! Otherwise "bulk" update for structures could fail + /** + * @var CorporationModel $corporation + */ + $corporation = $this->rel('corporationId'); + $corporation->getById($corporationId, 0); + if($corporation->dry()){ + $corporationId = null; + } + } + }else{ + $corporationId = null; + } + + return $corporationId; + } + /** + * validates systemId + * -> a structure always belongs to the same system + * @param string $key + * @param string $val + * @return bool + */ + protected function validate_systemId(string $key, string $val) : bool { + return !($this->valid() && $this->systemId !== (int)$val); + } + + /** + * check whether this model is valid or not + * @return bool + */ + public function isValid() : bool { + if($valid = parent::isValid()){ + // structure always belongs to a systemId + if(!(int)$this->systemId){ + $valid = false; + } + } + + return $valid; + } + + /** + * Event "Hook" function + * can be overwritten + * return false will stop any further action + * @param self $self + * @param $pkeys + * @return bool + */ + public function beforeInsertEvent($self, $pkeys) : bool { + return $this->isValid() ? parent::beforeInsertEvent($self, $pkeys) : false; + } + + /** + * check access by chraacter + * @param CharacterModel $characterModel + * @return bool + */ + public function hasAccess(CharacterModel $characterModel) : bool { + $access = false; + if($characterModel->hasCorporation()){ + $this->filter('structureCorporations', ['active = ?', 1]); + $this->has('structureCorporations.corporationId', ['active = ?', 1]); + $this->has('structureCorporations.corporationId', ['id = ?', $characterModel->get('corporationId', true)]); + + if($this->structureCorporations){ + $access = true; + } + } + + return $access; + } + + /** + * get structure data grouped by corporations + * @return array + * @throws \Exception + */ + public function getDataByCorporations() : array { + $structuresData = []; + foreach((array)$this->structureCorporations as $structureCorporation){ + if($structureCorporation->isActive() && $structureCorporation->corporationId->isActive()){ + $structuresData[$structureCorporation->corporationId->_id] = [ + 'id' => $structureCorporation->corporationId->_id, + 'name' => $structureCorporation->corporationId->name, + 'structures' => [$this->getData()] + ]; + } + } + + return $structuresData; + } + + /** + * load structure by $corporation, $name and $systemId + * @param CorporationModel $corporation + * @param string $name + * @param int $systemId + */ + public function getByName(CorporationModel $corporation, string $name, int $systemId){ + if($corporation->valid() && $name){ + $this->has('structureCorporations', ['corporationId = :corporationId', ':corporationId' => $corporation->_id]); + $this->load(['name = :name AND systemId = :systemId AND active = :active', + ':name' => $name, + ':systemId' => $systemId, + ':active' => 1 + ]); + } + } + + /** + * get universe type data for structureId + * @param int $structureId + * @return \stdClass + * @throws \Exception + */ + protected function getUniverseTypeData(int $structureId) : \stdClass { + /** + * @var $type Universe\TypeModel + */ + $type = Universe\AbstractUniverseModel::getNew('TypeModel'); + $type->getById($structureId); + return $type->dry() ? (object)[] : $type->getData(); + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/StructureStatusModel.php b/app/Model/Pathfinder/StructureStatusModel.php new file mode 100644 index 000000000..cb814b683 --- /dev/null +++ b/app/Model/Pathfinder/StructureStatusModel.php @@ -0,0 +1,101 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'label' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'class' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'structures' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\StructureModel', 'statusId'] + ] + ]; + + /** + * @var array + */ + protected static $tableData = [ + [ + 'id' => 1, + 'name' => 'unknown', + 'label' => '', + 'class' => 'pf-structure-status-unknown' + ], + [ + 'id' => 2, + 'name' => 'online', + 'label' => 'online', + 'class' => 'pf-structure-status-online' + ], + [ + 'id' => 3, + 'name' => 'offline', + 'label' => 'offline', + 'class' => 'pf-structure-status-offline' + ] + ]; + + /** + * get structure status data + * @return \stdClass + */ + public function getData() : \stdClass { + $statusData = (object) []; + $statusData->id = $this->_id; + $statusData->name = $this->name; + $statusData->label = $this->label; + $statusData->class = $this->class; + + return $statusData; + } + + /** + * get all status options + * @return \DB\CortexCollection + */ + public static function getAll(){ + $query = [ + 'active = :active', + ':active' => 1 + ]; + + return (new self())->find($query); + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/SystemFactionKillModel.php b/app/Model/Pathfinder/SystemFactionKillModel.php new file mode 100644 index 000000000..8bd773fb9 --- /dev/null +++ b/app/Model/Pathfinder/SystemFactionKillModel.php @@ -0,0 +1,36 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'unique' => true + ] + ]; +} \ No newline at end of file diff --git a/app/Model/Pathfinder/SystemJumpModel.php b/app/Model/Pathfinder/SystemJumpModel.php new file mode 100644 index 000000000..9fd6e2e57 --- /dev/null +++ b/app/Model/Pathfinder/SystemJumpModel.php @@ -0,0 +1,36 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'unique' => true + ] + ]; +} \ No newline at end of file diff --git a/app/Model/Pathfinder/SystemModel.php b/app/Model/Pathfinder/SystemModel.php new file mode 100644 index 000000000..a038b2b83 --- /dev/null +++ b/app/Model/Pathfinder/SystemModel.php @@ -0,0 +1,921 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true, + 'activity-log' => true + ], + 'mapId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\MapModel', + 'constraint' => [ + [ + 'table' => 'map', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'validate' => true + ], + 'alias' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ], + 'typeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\SystemTypeModel', + 'constraint' => [ + [ + 'table' => 'system_type', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'statusId' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 1, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\SystemStatusModel', + 'constraint' => [ + [ + 'table' => 'system_status', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => true, + 'activity-log' => true + ], + 'locked' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 0, + 'activity-log' => true + ], + 'rallyUpdated' => [ + 'type' => Schema::DT_TIMESTAMP, + 'default' => null + ], + 'rallyPoke' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 0, + 'activity-log' => true + ], + 'description' => [ + 'type' => Schema::DT_TEXT, + 'activity-log' => true, + 'validate' => true + ], + 'posX' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ], + 'posY' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ], + 'signatures' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\SystemSignatureModel', 'systemId'] + ], + 'connectionsSource' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\ConnectionModel', 'source'] + ], + 'connectionsTarget' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\ConnectionModel', 'target'] + ] + ]; + + /** + * set data by associative array + * @param array $data + */ + public function setData(array $data){ + $this->copyfrom($data, ['statusId', 'locked', 'rallyUpdated', 'position', 'description']); + } + + /** + * get map data as object + * @return \stdClass + */ + public function getData(){ + // check if there is cached data + if(is_null($data = $this->getCacheData())){ + $data = (object) []; + $data->id = $this->_id; + $data->mapId = is_object($this->mapId) ? $this->get('mapId', true) : 0; + $data->systemId = $this->systemId; + $data->alias = $this->alias; + + if(is_object($this->typeId)){ + $data->type = $this->typeId->getData(); + } + + if(is_object($this->statusId)){ + $data->status = $this->statusId->getData(); + } + + $data->locked = $this->locked; + $data->drifter = $this->isDrifter(); + $data->rallyUpdated = strtotime($this->rallyUpdated); + $data->rallyPoke = $this->rallyPoke; + $data->description = $this->description ? : ''; + + $data->position = (object) []; + $data->position->x = $this->posX; + $data->position->y = $this->posY; + + $data->created = (object) []; + $data->created->created = strtotime($this->created); + if(is_object($this->createdCharacterId)){ + $data->created->character = $this->createdCharacterId->getData(); + } + + $data->updated = (object) []; + $data->updated->updated = strtotime($this->updated); + if(is_object($this->updatedCharacterId)){ + $data->updated->character = $this->updatedCharacterId->getData(); + } + + // static system data ------------------------------------------------------------------------------------- + $data->name = $this->name; + $data->security = $this->security; + $data->trueSec = $this->trueSec; + $data->effect = $this->effect; + $data->shattered = $this->shattered; + + $data->constellation = (object) []; + $data->constellation->id = $this->constellationId; + $data->constellation->name = $this->constellation; + + $data->region = (object) []; + $data->region->id = $this->regionId; + $data->region->name = $this->region; + + $data->planets = $this->planets ? : []; + $data->statics = $this->statics ? : []; + + if(is_object($sovereignty = $this->sovereignty)){ + $data->sovereignty = $sovereignty; + } + + if(is_object($factionWar = $this->factionWar)){ + $data->factionWar = $factionWar; + } + + // max caching time for a system + // the cached date has to be cleared manually on any change + // this includes system, connection,... changes (all dependencies) + $this->updateCacheData($data); + } + + return $data; + } + + /** + * get all static data + * @return mixed|null|\stdClass + * @throws \Exception + */ + private function getStaticSystemData(){ + $staticData = null; + if(!is_object(self::$priorityCacheStore)){ + self::$priorityCacheStore = new PriorityCacheStore(); + } + + if(self::$priorityCacheStore->exists($this->systemId)){ + $staticData = self::$priorityCacheStore->get($this->systemId); + }else{ + $staticData = (new Universe())->getSystemData($this->systemId); + if($staticData){ + self::$priorityCacheStore->set($this->systemId, $staticData); + } + } + + return $staticData; + } + + /** + * get static system data by key + * @param string $key + * @return mixed|null + * @throws \Exception + */ + private function getStaticSystemValue(string $key){ + $value = null; + if($staticData = $this->getStaticSystemData()){ + if(isset($staticData->$key)){ + $value = $staticData->$key; + } + } + return $value; + } + + /** + * @param string $key + * @param int $val + * @return bool + * @throws \Exception + */ + protected function validate_systemId(string $key, int $val) : bool { + $valid = true; + // check if static system data exists for systemId = $val + if( !(bool)(new Universe())->getSystemData($val) ){ + $valid = false; + $this->throwValidationException($key, 'Validation failed: "' . $key . '" = "' . $val . '"'); + } + + return $valid; + } + + /** + * @param string $key + * @param int $val + * @return bool + * @throws Exception\ValidationException + */ + protected function validate_statusId(string $key, int $val) : bool { + $valid = true; + if( !$this->rel('statusId')::getStatusById($val) ){ + $valid = false; + $this->throwValidationException($key, 'Validation failed: "' . $key . '" = "' . $val . '"'); + } + + return $valid; + } + + /** + * @param string $key + * @param string $val + * @return bool + * @throws Exception\ValidationException + */ + protected function validate_description(string $key, string $val) : bool { + $valid = true; + if(mb_strlen($val) > 9000){ + $valid = false; + $this->throwValidationException($key, 'Validation failed: "' . $key . '" too long'); + } + return $valid; + } + + /** + * setter for system alias + * @param string $alias + * @return string + */ + public function set_alias($alias){ + $alias = trim($alias); + + // we don´t need redundant data. "name" is always preferred if "alias" is empty + if($alias === $this->name){ + $alias = ''; + } + + return $alias; + } + + /** + * setter for statusId + * @param $status + */ + public function set_status($status){ + if($statusId = (int)$status['id']){ + $this->statusId = $statusId; + } + } + + /** + * setter for position array + * @param $position + * @return null + */ + public function set_position($position){ + $position = (array)$position; + if(count($position) === 2){ + $this->posX = $position['x']; + $this->posY = $position['y']; + } + return null; + } + + /** + * setter for x coordinate + * @param int $posX + * @return int + */ + public function set_posX(int $posX) : int { + $posX = abs($posX); + if($posX > self::MAX_POS_X){ + $posX = self::MAX_POS_X; + } + + return $posX; + } + + /** + * setter for y coordinate + * @param int $posY + * @return int + */ + public function set_posY(int $posY) : int{ + $posY = abs($posY); + if($posY > self::MAX_POS_Y){ + $posY = self::MAX_POS_Y; + } + + return $posY; + } + + /** + * setter for system rally timestamp + * @param int $rally + * @return null|string + */ + public function set_rallyUpdated($rally){ + $rally = (int)$rally; + + switch($rally){ + case 0: + $rally = null; + break; + case 1: + // new rally point set + $rally = date('Y-m-d H:i:s', time()); + break; + default: + $rally = date('Y-m-d H:i:s', $rally); + break; + } + + return $rally; + } + + public function get_name(){ + return $this->getStaticSystemValue('name'); + } + + public function get_constellationId(){ + $constellationData = $this->getStaticSystemValue('constellation'); + return $constellationData ? $constellationData->id : null; + } + + public function get_constellation(){ + $constellationData = $this->getStaticSystemValue('constellation'); + return $constellationData ? $constellationData->name : null; + } + + public function get_regionId(){ + $constellationData = $this->getStaticSystemValue('constellation'); + return ($constellationData && $constellationData->region) ? $constellationData->region->id : null; + } + + public function get_region(){ + $constellationData = $this->getStaticSystemValue('constellation'); + return ($constellationData && $constellationData->region) ? $constellationData->region->name : null; + } + + public function get_security(){ + return $this->getStaticSystemValue('security'); + } + + public function get_trueSec(){ + return $this->getStaticSystemValue('trueSec'); + } + + public function get_effect(){ + return $this->getStaticSystemValue('effect'); + } + + public function get_shattered(){ + return $this->getStaticSystemValue('shattered'); + } + + public function get_statics(){ + return $this->getStaticSystemValue('statics'); + } + + public function get_planets(){ + return $this->getStaticSystemValue('planets'); + } + + public function get_stations(){ + return $this->getStaticSystemValue('stations'); + } + + public function get_sovereignty(){ + return $this->getStaticSystemValue('sovereignty'); + } + + public function get_factionWar(){ + return $this->getStaticSystemValue('factionWar'); + } + + /** + * Event "Hook" function + * @param self $self + * @param $pkeys + */ + public function afterInsertEvent($self, $pkeys){ + $self->clearCacheData(); + $self->logActivity('systemCreate'); + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + * @return bool + */ + public function beforeUpdateEvent($self, $pkeys) : bool { + $status = parent::beforeUpdateEvent($self, $pkeys); + + if($status && !$self->isActive()){ + // reset "rally point" fields + $self->rallyUpdated = 0; + $self->rallyPoke = false; + + // delete connections + foreach($self->getConnections() as $connection){ + $connection->erase(); + } + + // delete signatures + if(!$self->getMap()->persistentSignatures){ + foreach($self->getSignatures() as $signature){ + $signature->erase(); + } + } + } + + return $status; + } + + /** + * Event "Hook" function + * @param self $self + * @param $pkeys + */ + public function afterUpdateEvent($self, $pkeys){ + $self->clearCacheData(); + $activity = ($self->isActive()) ? 'systemUpdate' : 'systemDelete'; + $self->logActivity($activity); + } + + /** + * Event "Hook" function + * @param self $self + * @param $pkeys + */ + public function afterEraseEvent($self, $pkeys){ + $self->clearCacheData(); + $self->logActivity('systemDelete'); + } + + /** + * get blank signature model + * @return SystemSignatureModel + * @throws \Exception + */ + public function getNewSignature() : SystemSignatureModel { + /** + * @var $signature SystemSignatureModel + */ + $signature = self::getNew('SystemSignatureModel'); + $signature->systemId = $this; + return $signature; + } + + /** + * @param string $action + * @return Logging\LogInterface + * @throws Exception\ConfigException + */ + public function newLog(string $action = '') : Logging\LogInterface{ + return $this->getMap()->newLog($action)->setTempData($this->getLogObjectData()); + } + + /** + * @return MapModel + */ + public function getMap() : MapModel { + return $this->get('mapId'); + } + + /** + * check object for model access + * @param CharacterModel $characterModel + * @return bool + */ + public function hasAccess(CharacterModel $characterModel) : bool { + return $this->mapId ? $this->mapId->hasAccess($characterModel) : false; + } + + /** + * delete a system from a map + * hint: signatures and connections will be deleted on cascade + * @param CharacterModel $characterModel + * @return bool + */ + public function delete(CharacterModel $characterModel){ + return ($this->valid() && $this->hasAccess($characterModel)) ? $this->erase() : false; + } + + /** + * get all connections of this system + * @return ConnectionModel[] + */ + public function getConnections(){ + $connections = []; + + $this->filter('connectionsTarget', [ + 'active = :active AND target = :targetId', + ':active' => 1, + ':targetId' => $this->_id + ]); + if($this->connectionsTarget){ + foreach($this->connectionsTarget as $connection){ + $connections[$connection->_id] = $connection; + } + } + + $this->filter('connectionsSource', [ + 'active = :active AND source = :sourceId', + ':active' => 1, + ':sourceId' => $this->_id + ]); + if($this->connectionsSource){ + foreach($this->connectionsSource as $connection){ + $connections[$connection->_id] = $connection; + } + } + + return $connections; + } + + /** + * get all signatures of this system + * -> might be filtered by active has() filter + * @return SystemSignatureModel[] + */ + public function getSignatures(){ + return $this->signatures ? : []; + } + + /** + * get data for all Signatures in this system + * @return \stdClass[] + */ + public function getSignaturesData() : array { + $signaturesData = []; + $signatures = $this->getSignatures(); + foreach($signatures as $signature){ + $signaturesData[] = $signature->getData(); + } + + return $signaturesData; + } + + /** + * get Signature by id and check for access + * @param int $id + * @return SystemSignatureModel|null + */ + public function getSignatureById(int $id) : ?SystemSignatureModel { + return $this->relFindOne('signatures', self::getFilter('id', $id)); + } + + /** + * get a signature by its "unique" 3-digit name + * @param string $name + * @return SystemSignatureModel|null + */ + public function getSignatureByName(string $name) : ?SystemSignatureModel { + return $this->relFindOne('signatures', self::getFilter('name', $name)); + } + + /** + * get data for all structures in this system + * @return \stdClass[] + */ + public function getStructuresData() : array { + return $this->getMap()->getStructuresData($this->systemId); + } + + /** + * get data for all stations in this system + * @return array + */ + public function getStationsData() : array { + return $this->stations ? : []; + } + + /** + * check whether this system is in w-space + * @return bool + */ + public function isWormhole() : bool { + return ($this->typeId->id === 1); + } + + /** + * check whether this system is in k-space + * @return bool + */ + public function isKspace() : bool { + return ($this->typeId->id === 2); + } + + /** + * check whether this system is in a-space + * @return bool + */ + public function isAbyss() : bool { + return ($this->typeId->id === 3 && $this->security === 'A'); + } + + /** + * check whether this system is in drifter-space + * @return bool + */ + public function isDrifter() : bool { + return in_array($this->security, ['C14', 'C15', 'C16', 'C17', 'C18']); + } + + /** + * send rally point poke to various "APIs" + * -> send to a Slack channel + * -> send to a Discord channel + * -> send to an Email + * @param array $rallyData + * @param CharacterModel $characterModel + * @throws Exception\ConfigException + * @throws \Exception + */ + public function sendRallyPoke(array $rallyData, CharacterModel $characterModel){ + // rally log needs at least one handler to be valid + $isValidLog = false; + $log = new Logging\RallyLog('rallySet', $this->getMap()->getLogChannelData()); + + // Slack poke ----------------------------------------------------------------------------- + $slackChannelKey = 'slackChannelRally'; + if( + $rallyData['pokeSlack'] === true && + $this->getMap()->isSlackChannelEnabled($slackChannelKey) + ){ + $isValidLog = true; + $log->addHandler('slackRally', null, $this->getMap()->getSlackWebHookConfig($slackChannelKey)); + } + + // Discord poke --------------------------------------------------------------------------- + $discordChannelKey = 'discordWebHookURLRally'; + if( + $rallyData['pokeDiscord'] === true && + $this->getMap()->isDiscordChannelEnabled($discordChannelKey) + ){ + $isValidLog = true; + + $log->addHandler('discordRally', null, $this->getMap()->getDiscordWebHookConfig($discordChannelKey)); + } + + // Mail poke ------------------------------------------------------------------------------ + $mailAddressKey = 'RALLY_SET'; + if( + $rallyData['pokeMail'] === true && + $this->getMap()->isMailSendEnabled('RALLY_SET') + ){ + $isValidLog = true; + $mailConf = $this->getMap()->getSMTPConfig($mailAddressKey, false); + $log->addHandler('mail', 'mail', $mailConf); + } + + // Buffer log ----------------------------------------------------------------------------- + if($isValidLog){ + $log->setTempData($this->getLogObjectData(true)); + $log->setCharacter($characterModel); + if( !empty($rallyData['message']) ){ + $log->setData([ + 'message' => $rallyData['message'] + ]); + } + $log->buffer(); + } + } + + /** + * set system type based on security + */ + public function setType(){ + switch($this->security){ + case 'H': + case 'L': + case '0.0': + $typeId = 2; // k-space + break; + case 'A': + $typeId = 3; // a-space + break; + default: + $typeId = 1; // w-space + } + + /** + * @var $type MapTypeModel + */ + $type = $this->rel('typeId'); + $type->getById($typeId); + $this->typeId = $type; + } + + /** + * save signature for this system + * @param SystemSignatureModel $signature + * @param CharacterModel $character + * @return false|ConnectionModel + */ + public function saveSignature(SystemSignatureModel $signature, CharacterModel $character){ + $signature->systemId = $this; + return $signature->save($character); + } + + /** + * get object relevant data for model log + * @param bool $fullData + * @return array + */ + public function getLogObjectData($fullData = false) : array{ + $objectData = [ + 'objId' => $this->_id, + 'objName' => $this->name + ]; + + if($fullData){ + $objectData['objUrl'] = $this->getMap()->getDeeplinkUrl($this->_id); + $objectData['objAlias'] = $this->alias; + $objectData['objRegion'] = $this->region; + $objectData['objIsWormhole'] = $this->isWormhole(); + $objectData['objEffect'] = $this->effect; + $objectData['objSecurity'] = $this->security; + $objectData['objTrueSec'] = $this->trueSec; + $objectData['objCountPlanets'] = count((array)$this->planets); + $objectData['objDescription'] = $this->description; + } + + return $objectData; + } + + /** + * @param string $stamp + * @return array|null + */ + public function getSignatureHistoryEntry(string $stamp) : ?array { + $signatureHistoryData = array_filter($this->getSignaturesHistory(), function($historyEntry) use ($stamp){ + return md5($historyEntry['stamp']) == $stamp; + }); + return empty($signatureHistoryData) ? null : reset($signatureHistoryData); + } + + /** + * @return array + */ + public function getSignaturesHistory() : array { + if(!is_array($signaturesHistoryData = $this->getCacheData(self::DATA_CACHE_KEY_SIGNATURES_HISTORY))){ + $signaturesHistoryData = []; + } + return $signaturesHistoryData; + } + + /** + * Updates the signature history cache + * -> each (bulk) change to signatures of this system must result in a new signature history cache entry + * -> This method also clears the cache of this system, so that new signature data gets returned for in getData() + * @param CharacterModel $character + * @param string $action + * @throws \Exception + */ + public function updateSignaturesHistory(CharacterModel $character, string $action = 'edit') : void { + if(!$this->dry()){ + $signaturesHistoryData = $this->getSignaturesHistory(); + $historyEntry = [ + 'stamp' => microtime(true), + 'character' => $character->getBasicData(), + 'action' => $action, + 'signatures' => $this->getSignaturesData() + ]; + + array_unshift($signaturesHistoryData, $historyEntry); + + // limit max history data + array_splice($signaturesHistoryData, self::MAX_SIGNATURES_HISTORY_DATA); + + $this->updateCacheData($signaturesHistoryData, self::DATA_CACHE_KEY_SIGNATURES_HISTORY, self::TTL_SIGNATURES_HISTORY); + + // clear system cache here + // -> Signature model updates should NOT update the system cache on change + // because a "bulk" change to signatures would clear the system cache multiple times + $this->clearCacheData(); + } + } + + /** + * @see parent + */ + public function clearCacheData(){ + parent::clearCacheData(); + + // clear map cache as well + $this->mapId->clearCacheData(); + } + + /** + * @see parent + */ + public function filterRel() : void { + $this->filter('signatures', self::getFilter('active', true), ['order' => 'name']); + $this->filter('connectionsTarget', self::getFilter('active', true)); + $this->filter('connectionsSource', self::getFilter('active', true)); + } + + /** + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['mapId', 'systemId'], true); + } + + return $status; + } + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/SystemPodKillModel.php b/app/Model/Pathfinder/SystemPodKillModel.php new file mode 100644 index 000000000..99b5d51da --- /dev/null +++ b/app/Model/Pathfinder/SystemPodKillModel.php @@ -0,0 +1,36 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'unique' => true + ] + ]; +} \ No newline at end of file diff --git a/app/Model/Pathfinder/SystemShipKillModel.php b/app/Model/Pathfinder/SystemShipKillModel.php new file mode 100644 index 000000000..9c859d0b2 --- /dev/null +++ b/app/Model/Pathfinder/SystemShipKillModel.php @@ -0,0 +1,37 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'unique' => true + ] + ]; + +} diff --git a/app/Model/Pathfinder/SystemSignatureModel.php b/app/Model/Pathfinder/SystemSignatureModel.php new file mode 100644 index 000000000..8a95f78af --- /dev/null +++ b/app/Model/Pathfinder/SystemSignatureModel.php @@ -0,0 +1,322 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'groupId' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0, + 'index' => true, + 'activity-log' => true + ], + 'typeId' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0, + 'index' => true, + 'activity-log' => true + ], + 'connectionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\ConnectionModel', + 'constraint' => [ + [ + 'table' => 'connection', + 'on-delete' => 'CASCADE' + ] + ], + 'activity-log' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'activity-log' => true, + 'validate' => true + ], + 'description' => [ + 'type' => Schema::DT_VARCHAR512, + 'nullable' => false, + 'default' => '', + 'activity-log' => true + ] + ]; + + /** + * set data by associative array + * @param array $data + */ + public function setData(array $data){ + $this->copyfrom($data, ['name', 'groupId', 'typeId', 'description', 'connectionId']); + } + + /** + * get signature data + * @return \stdClass + */ + public function getData(){ + $signatureData = (object) []; + $signatureData->id = $this->id; + + $signatureData->system = (object) []; + $signatureData->system->id = $this->get('systemId', true); + + $signatureData->groupId = $this->groupId; + $signatureData->typeId = $this->typeId; + $signatureData->name = $this->name; + $signatureData->description = $this->description; + + if($connection = $this->getConnection()){ + $signatureData->connection = (object) []; + $signatureData->connection->id = $connection->_id; + } + + $signatureData->created = (object) []; + $signatureData->created->created = strtotime($this->created); + if( is_object($this->createdCharacterId) ){ + $signatureData->created->character = $this->createdCharacterId->getBasicData(); + } + + $signatureData->updated = (object) []; + $signatureData->updated->updated = strtotime($this->updated); + if( is_object($this->updatedCharacterId) ){ + $signatureData->updated->character = $this->updatedCharacterId->getBasicData(); + } + + return $signatureData; + } + + /** + * setter for connectionId + * @param $connectionId + * @return int|null + */ + public function set_connectionId($connectionId){ + $connectionId = (int)$connectionId; + $validConnectionId = null; + + if($connectionId > 0){ + // check if connectionId is valid + $systemId = (int) $this->get('systemId', true); + + /** + * @var $connection ConnectionModel + */ + $connection = $this->rel('connectionId'); + $connection->getById($connectionId); + + if( + !$connection->dry() && + ( + $connection->get('source', true) === $systemId|| + $connection->get('target', true) === $systemId + ) + ){ + // connectionId belongs to same system as $this signature -> is valid + $validConnectionId = $connectionId; + } + } + + return $validConnectionId; + } + + /** + * validate name column + * @param string $key + * @param string $val + * @return bool + * @throws Exception\ValidationException + */ + protected function validate_name(string $key, string $val): bool { + $valid = true; + if(!mb_ereg('^[a-zA-Z]{3}-\d{3}$', $val)){ + $valid = false; + $this->throwValidationException($key); + } + return $valid; + } + + /** + * @param string $action + * @return Logging\LogInterface + * @throws Exception\ConfigException + */ + public function newLog(string $action = ''): Logging\LogInterface{ + return $this->getMap()->newLog($action)->setTempData($this->getLogObjectData()); + } + + /** + * @return MapModel + */ + public function getMap(): MapModel{ + return $this->get('systemId')->getMap(); + } + + /** + * get the connection (if attached) + * @return ConnectionModel|null + */ + public function getConnection(){ + return $this->connectionId; + } + + /** + * compares a new data set (array) with the current values + * and checks if something has changed + * @param array $signatureData + * @return bool + */ + public function hasChanged(array $signatureData) : bool { + $hasChanged = false; + + foreach((array)$signatureData as $key => $value){ + if($this->exists($key)){ + if($this->$key instanceof ConnectionModel){ + $currentValue = $this->get($key, true); + }else{ + $currentValue = $this->$key; + } + + $hasChanged = $currentValue !== $value; + break; + } + } + + return $hasChanged; + } + + /** + * check object for model access + * @param CharacterModel $characterModel + * @return bool + */ + public function hasAccess(CharacterModel $characterModel) : bool { + return $this->systemId ? $this->systemId->hasAccess($characterModel) : false; + } + + /** + * delete signature + * @return bool + */ + public function delete() : bool { + return $this->valid() ? $this->erase() : false; + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterInsertEvent($self, $pkeys){ + $self->logActivity('signatureCreate'); + } + + /** + * Event "Hook" function + * can be overwritten + * return false will stop any further action + * @param self $self + * @param $pkeys + * @return bool + */ + public function beforeUpdateEvent($self, $pkeys) : bool { + // "updated" column should always be updated if no changes made this signature + // -> makes it easier to see what signatures have not been updated + $this->touch('updated'); + + return parent::beforeUpdateEvent($self, $pkeys); + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterUpdateEvent($self, $pkeys){ + $self->logActivity('signatureUpdate'); + } + + /** + * Event "Hook" function + * can be overwritten + * @param self $self + * @param $pkeys + */ + public function afterEraseEvent($self, $pkeys){ + $self->logActivity('signatureDelete'); + + if( + $self->connectionIdDeleteCascade === true && + ($connection = $self->getConnection()) + ){ + $connection->erase(); + } + } + + /** + * get object relevant data for model log + * @return array + */ + public function getLogObjectData() : array{ + return [ + 'objId' => $this->_id, + 'objName' => $this->name + ]; + } + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['systemId', 'typeId', 'groupId']); + } + return $status; + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/SystemStatusModel.php b/app/Model/Pathfinder/SystemStatusModel.php new file mode 100644 index 000000000..74e0d71de --- /dev/null +++ b/app/Model/Pathfinder/SystemStatusModel.php @@ -0,0 +1,112 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'label' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'class' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ] + ]; + + /** + * @var array + */ + protected static $tableData = [ + [ + 'id' => 1, + 'name' => 'unknown', + 'label' => 'unknown', + 'class' => 'pf-system-status-unknown' + ], + [ + 'id' => 2, + 'name' => 'friendly', + 'label' => 'friendly', + 'class' => 'pf-system-status-friendly' + ], + [ + 'id' => 3, + 'name' => 'occupied', + 'label' => 'occupied', + 'class' => 'pf-system-status-occupied' + ], + [ + 'id' => 4, + 'name' => 'hostile', + 'label' => 'hostile', + 'class' => 'pf-system-status-hostile' + ], + [ + 'id' => 5, + 'name' => 'empty', + 'label' => 'empty', + 'class' => 'pf-system-status-empty' + ], + [ + 'id' => 6, + 'name' => 'unscanned', + 'label' => 'unscanned', + 'class' => 'pf-system-status-unscanned' + ] + ]; + + /** + * get system status data + * @return \stdClass + */ + public function getData(){ + + $statusData = (object)[]; + $statusData->id = $this->_id; + $statusData->name = $this->name; + + return $statusData; + } + + /** + * get status by id + * @param int $statusId + * @return self|null + */ + public static function getStatusById(int $statusId = 1){ + $status = new self(); + $status->getById($statusId); + return $status->dry() ? null : $status; + } +} \ No newline at end of file diff --git a/app/Model/Pathfinder/SystemTypeModel.php b/app/Model/Pathfinder/SystemTypeModel.php new file mode 100644 index 000000000..8e026e36c --- /dev/null +++ b/app/Model/Pathfinder/SystemTypeModel.php @@ -0,0 +1,68 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ] + ]; + + /** + * @var array + */ + protected static $tableData = [ + [ + 'id' => 1, + 'name' => 'w-space' // Wormhole Space + ], + [ + 'id' => 2, + 'name' => 'k-space' // Known Space + ], + [ + 'id' => 3, + 'name' => 'a-space' // Abyss Space + ] + ]; + + /** + * get system type data + * @return \stdClass + */ + public function getData(){ + + $typeData = (object)[]; + $typeData->id = $this->_id; + $typeData->name = $this->name; + + return $typeData; + } + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/UserCharacterModel.php b/app/Model/Pathfinder/UserCharacterModel.php new file mode 100644 index 000000000..492d79935 --- /dev/null +++ b/app/Model/Pathfinder/UserCharacterModel.php @@ -0,0 +1,93 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'userId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\UserModel', + 'constraint' => [ + [ + 'table' => 'user', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'characterId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'unique' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Pathfinder\CharacterModel', + 'constraint' => [ + [ + 'table' => 'character', + 'on-delete' => 'CASCADE' + ] + ] + ] + ]; + + /** + * event "Hook" + * -> remove user if there are no other characters bound to this user + * @param UserCharacterModel $self + * @param $pkeys + */ + public function afterEraseEvent($self, $pkeys){ + if( + is_object($self->userId) && + is_null($self->userId->userCharacters) + ){ + $self->userId->erase(); + } + } + + /** + * get the character model of this character + * @return mixed + */ + public function getCharacter(){ + return $this->characterId; + } + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['userId', 'characterId'], true); + } + return $status; + } + +} \ No newline at end of file diff --git a/app/Model/Pathfinder/UserModel.php b/app/Model/Pathfinder/UserModel.php new file mode 100644 index 000000000..51bce730f --- /dev/null +++ b/app/Model/Pathfinder/UserModel.php @@ -0,0 +1,365 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'index' => true, + 'validate' => true + ], + 'email' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '', + 'validate' => true + ], + 'userCharacters' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Pathfinder\UserCharacterModel', 'userId'] + ] + ]; + + /** + * get all data for this user + * -> ! caution ! this function returns sensitive data! (e.g. email,..) + * -> user getSimpleData() for faster performance and public user data + * @return \stdClass + * @throws \Exception + */ + public function getData() : \stdClass { + + // get public user data for this user + $userData = $this->getSimpleData(); + + // add sensitive user data + $userData->email = $this->email; + + // all chars + $userData->characters = []; + $characters = $this->getCharacters(); + foreach($characters as $character){ + /** + * @var $character CharacterModel + */ + $userData->characters[] = $character->getData(); + } + + // get active character with log data + $activeCharacter = $this->getActiveCharacter(); + $userData->character = $activeCharacter->getData(true, true); + + return $userData; + } + + /** + * get public user data + * - check out getData() for all user data + * @return \stdClass + */ + public function getSimpleData() : \stdClass{ + $userData = (object) []; + $userData->id = $this->id; + $userData->name = $this->name; + + return $userData; + } + + /** + * check if new user registration is allowed + * @param UserModel $self + * @param $pkeys + * @return bool + * @throws Exception\RegistrationException + */ + public function beforeInsertEvent($self, $pkeys) : bool { + $registrationStatus = Controller\Controller::getRegistrationStatus(); + switch($registrationStatus){ + case 0: + throw new Exception\RegistrationException('User registration is currently not allowed'); + break; + case 1: + return true; + break; + default: + return false; + } + } + + /** + * @param self $self + * @param $pkeys + */ + public function afterEraseEvent($self, $pkeys){ + $this->sendDeleteMail(); + } + + /** + * send delete confirm mail to this user + */ + protected function sendDeleteMail(){ + if($this->isMailSendEnabled()){ + $log = new Logging\UserLog('userDelete', $this->getLogChannelData()); + $log->addHandler('mail', 'mail', $this->getSMTPConfig()); + $log->setMessage('Delete Account - {channelName}'); + $log->setData([ + 'message' =>'Your account was successfully deleted.' + ]); + $log->buffer(); + } + } + + /** + * checks whether user has a valid email address and pathfinder has a valid SMTP config + * @return bool + */ + protected function isMailSendEnabled() : bool { + return Config::isValidSMTPConfig($this->getSMTPConfig()); + } + + /** + * get SMTP config for this user + * @return \stdClass + */ + protected function getSMTPConfig() : \stdClass { + $config = Config::getSMTPConfig(); + $config->to = $this->email; + return $config; + } + + /** + * validate name column + * @param string $key + * @param string $val + * @return bool + * @throws Exception\ValidationException + */ + protected function validate_name(string $key, string $val) : bool { + $valid = true; + if( + mb_strlen($val) < 3 || + mb_strlen($val) > 80 + ){ + $valid = false; + $this->throwValidationException($key); + } + return $valid; + } + + /** + * validate email column + * @param string $key + * @param string $val + * @return bool + * @throws Exception\ValidationException + */ + protected function validate_email(string $key, string $val) : bool { + $valid = true; + if ( !empty($val) && \Audit::instance()->email($val) == false ){ + $valid = false; + $this->throwValidationException($key); + } + return $valid; + } + + /** + * check whether this character has already a user assigned to it + * @return bool + */ + public function hasUserCharacters() : bool { + $this->filter('userCharacters', ['active = ?', 1]); + return is_object($this->userCharacters); + } + + /** + * get current character from session data + * -> if $characterId == 0 -> get first character data (random) + * @param int $characterId + * @param int $ttl + * @return CharacterModel|null + * @throws \Exception + */ + public function getSessionCharacter(int $characterId = 0, int $ttl = self::DEFAULT_SQL_TTL) : ?CharacterModel { + $data = []; + $currentSessionUser = (array)$this->getF3()->get(User::SESSION_KEY_USER); + + if($this->_id === $currentSessionUser['ID']){ + // user matches session data + if($characterId > 0){ + $data = $this->findSessionCharacterData($characterId); + }elseif( + is_array($sessionCharacters = $this->getF3()->get(User::SESSION_KEY_CHARACTERS)) && // check for null + !empty($sessionCharacters) + ){ + // no character was requested ($requestedCharacterId = 0) AND session characters were found + // -> get first matched character (e.g. user open /login browser tab) + $data = $sessionCharacters[0]; + } + } + + if($characterId = (int)$data['ID']){ + // check if character still exists on DB (e.g. was manually removed in the meantime) + // -> This should NEVER happen just for security and "local development" + /** + * @var $character CharacterModel + */ + $character = AbstractPathfinderModel::getNew('CharacterModel'); + $character->getById($characterId, $ttl); + + if($character->valid() && $character->hasUserCharacter()){ + // character data is valid! + return $character; + } + } + + return null; + } + + /** + * search in session data for $characterId + * @param int $characterId + * @return array + */ + public function findSessionCharacterData(int $characterId) : array { + $data = []; + if($characterId && $this->getF3()->exists(User::SESSION_KEY_CHARACTERS, $sessionCharacters)){ + // search for specific characterData + foreach((array)$sessionCharacters as $characterData){ + if($characterId === (int)$characterData['ID']){ + $data = $characterData; + break; + } + } + } + return $data; + } + + /** + * get all userCharacters models for a user + * characters will be checked/updated on login by CCP API call + * @return UserCharacterModel[] + */ + public function getUserCharacters(){ + $this->filter('userCharacters', ['active = ?', 1]); + + $userCharacters = []; + if($this->userCharacters){ + $userCharacters = $this->userCharacters; + } + + return $userCharacters; + } + + /** + * get the current active character for this user + * -> EITHER - the current active one for the current user + * -> OR - get the first active one + * @return null|CharacterModel + * @throws \Exception + */ + public function getActiveCharacter() : ?CharacterModel { + $activeCharacter = null; + $controller = new Controller\Controller(); + $currentActiveCharacter = $controller->getCharacter(); + + if( + !is_null($currentActiveCharacter) && + $currentActiveCharacter->getUser()->_id === $this->id + ){ + $activeCharacter = &$currentActiveCharacter; + }else{ + // set "first" found as active for this user + if($activeCharacters = $this->getActiveCharacters()){ + $activeCharacter = $activeCharacters[0]; + } + } + + return $activeCharacter; + } + + /** + * get all characters for this user + * @return CharacterModel[] + */ + public function getCharacters() : array { + $characters = []; + $userCharacters = $this->getUserCharacters(); + + foreach($userCharacters as $userCharacter){ + /** + * @var $userCharacter UserCharacterModel + */ + if( $currentCharacter = $userCharacter->getCharacter() ){ + // check if userCharacter has a valid character + // -> this should never fail! + $characters[] = $currentCharacter; + } + } + + return $characters; + } + + /** + * get all active characters (with log entry) + * hint: a user can have multiple active characters + * @return CharacterModel[] + */ + public function getActiveCharacters() : array { + $activeCharacters = []; + + foreach($this->getUserCharacters() as $userCharacter){ + /** + * @var $userCharacter UserCharacterModel + */ + $characterModel = $userCharacter->getCharacter(); + if($characterLog = $characterModel->getLog()){ + $activeCharacters[] = $characterModel; + } + } + + return $activeCharacters; + } + + /** + * get object relevant data for model log channel + * @return array + */ + public function getLogChannelData() : array{ + return [ + 'channelId' => $this->_id, + 'channelName' => $this->name + ]; + } + + +} \ No newline at end of file diff --git a/app/Model/Universe/AllianceModel.php b/app/Model/Universe/AllianceModel.php new file mode 100644 index 000000000..325f47454 --- /dev/null +++ b/app/Model/Universe/AllianceModel.php @@ -0,0 +1,103 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'ticker' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'dateFounded' => [ + 'type' => Schema::DT_DATETIME, + 'default' => null + ], + 'factionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\FactionModel', + 'constraint' => [ + [ + 'table' => 'faction', + 'on-delete' => 'SET NULL' + ] + ] + ], + 'corporations' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\CorporationModel', 'allianceId'] + ], + 'sovereigntySystems' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\SovereigntyMapModel', 'allianceId'] + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + $data->id = $this->_id; + $data->name = $this->name; + $data->ticker = $this->ticker; + + return $data; + } + + /** + * @param $date + * @return string|null + */ + public function set_dateFounded($date){ + if(is_string($date) && !empty($date)){ + try{ + $dateTime = new \DateTime($date); + $date = $dateTime->format('Y-m-d H:i:s'); + }catch(\Exception $e){ + $date = null; + } + } + return $date; + } + + /** + * load alliance by Id either from DB or load data from API + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getAlliance', $id); + if(!empty($data) && !isset($data['error'])){ + if($data['factionId']){ + /** + * @var $faction FactionModel + */ + $faction = $this->rel('factionId'); + $faction->loadById($data['factionId'], $accessToken, $additionalOptions); + $data['factionId'] = $faction; + } + + $this->copyfrom($data, ['id', 'name', 'ticker', 'dateFounded', 'factionId']); + $this->save(); + } + } +} \ No newline at end of file diff --git a/app/Model/Universe/CategoryModel.php b/app/Model/Universe/CategoryModel.php new file mode 100644 index 000000000..df0b7f3ee --- /dev/null +++ b/app/Model/Universe/CategoryModel.php @@ -0,0 +1,210 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'published' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'groups' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\GroupModel', 'categoryId'] + ] + ]; + + /** + * get category data + * @param array $additionalData + * @return null|object + */ + public function getData(array $additionalData = []){ + $categoryData = (object) []; + $categoryData->id = $this->_id; + $categoryData->name = $this->name; + + if($groupsData = $this->getGroupsData($additionalData)){ + $categoryData->groups = $groupsData; + } + + return $categoryData; + } + + /** + * get all groups for this category + * @param bool $published + * @return array|mixed + */ + protected function getGroups(bool $published = true){ + $groups = []; + if($published){ + $this->filter('groups', [ + 'published = :published', + ':published' => 1 + ]); + } + + if($this->groups){ + $groups = $this->groups; + } + + return $groups; + } + + /** + * @param array $additionalData + * @return array + */ + protected function getGroupsData(array $additionalData = []) : array { + $groupsData = []; + $groups = $this->getGroups(); + + /** + * @var $group GroupModel + */ + foreach($groups as $group){ + $groupsData[] = $group->getData($additionalData); + } + + return $groupsData; + } + + /** + * get groups count + * @param bool $published + * @return int + */ + public function getGroupsCount(bool $published = true) : int { + return $this->valid() ? count($this->getGroups($published)) : 0; + } + + /** + * count all types that belong to groups in this category + * @param bool $published + * @return int + */ + public function getTypesCount(bool $published = true) : int { + $count = 0; + if($this->valid()){ + /** + * @var $group GroupModel + */ + foreach($groups = $this->getGroups($published) as $group){ + $count += $group->getTypesCount($published); + } + } + return $count; + } + + /** + * load data from API into $this and save $this + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + if(!empty($data = self::getUniverseCategoryData($id))){ + $this->copyfrom($data, ['id', 'name', 'published']); + $this->save(); + } + } + + /** + * load groups data for this category + * @param int $offset + * @param int $length 0 -> all groups + * @return array + */ + public function loadGroupsData(int $offset = 0, int $length = 0) : array { + $info = ['countAll' => 0, 'countChunk' => 0, 'count' => 0, 'offset' => $offset, 'groupTypes' => []]; + + if( + $this->valid() && + !empty($data = self::getUniverseCategoryData($this->_id)) + ){ + $info['countAll'] = count($data['groups']); + + array_multisort($data['groups'], SORT_ASC, SORT_NUMERIC); + if($length){ + $data['groups'] = array_slice($data['groups'], $offset, $length); + } + + $info['countChunk'] = count($data['groups']); + foreach($data['groups'] as $groupId){ + /** + * @var $group GroupModel + */ + $group = $this->rel('groups'); + $group->loadById($groupId); + + $info['groupTypes'][$groupId] = $group->loadTypesData(); + + $group->reset(); + + $info['count']++; + $info['offset']++; + } + } + + return $info; + } + + /** + * @param int $id + * @return array + */ + public static function getUniverseCategoryData(int $id) : array { + return self::getF3()->ccpClient()->send('getUniverseCategory', $id); + } + + /** + * @return array + */ + public static function getUniverseCategories() : array { + return self::getF3()->ccpClient()->send('getUniverseCategories'); + } + + /** + * @param int $id + * @return array + */ + public static function getUniverseCategoryGroups(int $id) : array { + return empty($data = self::getUniverseCategoryData($id)) ? [] : $data['groups']; + } + + /** + * @param int $id + * @return array + */ + public static function getUniverseCategoryTypes(int $id) : array { + $types = []; + foreach($groupIds = self::getUniverseCategoryGroups($id) as $groupId){ + $types[$groupId] = GroupModel::getUniverseGroupTypes($groupId); + } + return $types; + } +} \ No newline at end of file diff --git a/app/Model/Universe/ConstellationModel.php b/app/Model/Universe/ConstellationModel.php new file mode 100644 index 000000000..9abd2af58 --- /dev/null +++ b/app/Model/Universe/ConstellationModel.php @@ -0,0 +1,116 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'regionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\RegionModel', + 'constraint' => [ + [ + 'table' => 'region', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'x' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'y' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'z' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'systems' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\SystemModel', 'constellationId'] + ], + 'systemNeighbours' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\SystemNeighbourModel', 'constellationId'] + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $constellationData = (object) []; + $constellationData->id = $this->_id; + $constellationData->name = $this->name; + $constellationData->region = $this->regionId->getData(); + + return $constellationData; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getUniverseConstellation', $id); + if(!empty($data)){ + /** + * @var $region RegionModel + */ + $region = $this->rel('regionId'); + $region->loadById($data['regionId'], $accessToken, $additionalOptions); + $data['regionId'] = $region; + + $this->copyfrom($data, ['id', 'name', 'regionId', 'position']); + $this->save(); + } + } + + /** + * load systems data for this constellation + */ + public function loadSystemsData(){ + if( !$this->dry() ){ + $data = self::getF3()->ccpClient()->send('getUniverseConstellation', $this->_id); + if(!empty($data)){ + foreach((array)$data['systems'] as $systemId){ + /** + * @var $system SystemModel + */ + $system = $this->rel('systems'); + $system->loadById($systemId); + $system->reset(); + } + } + } + } + +} \ No newline at end of file diff --git a/app/Model/Universe/CorporationModel.php b/app/Model/Universe/CorporationModel.php new file mode 100644 index 000000000..447f06f91 --- /dev/null +++ b/app/Model/Universe/CorporationModel.php @@ -0,0 +1,135 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'ticker' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'dateFounded' => [ + 'type' => Schema::DT_DATETIME, + 'default' => null + ], + 'memberCount' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ], + 'isNPC' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 0 + ], + 'factionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\FactionModel', + 'constraint' => [ + [ + 'table' => 'faction', + 'on-delete' => 'SET NULL' + ] + ] + ], + 'allianceId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\AllianceModel', + 'constraint' => [ + [ + 'table' => 'alliance', + 'on-delete' => 'SET NULL' + ] + ] + ], + 'sovereigntySystems' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\SovereigntyMapModel', 'corporationId'] + ], + 'stations' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\StationModel', 'corporationId'] + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + $data->id = $this->_id; + $data->name = $this->name; + + return $data; + } + + /** + * @param $date + * @return string|null + */ + public function set_dateFounded($date){ + if(is_string($date) && !empty($date)){ + try{ + $dateTime = new \DateTime($date); + $date = $dateTime->format('Y-m-d H:i:s'); + }catch(\Exception $e){ + $date = null; + } + } + return $date; + } + + /** + * load corporation by Id either from DB or load data from API + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getCorporation', $id); + if(!empty($data) && !isset($data['error'])){ + // check for NPC corporation + $data['isNPC'] = in_array($id, self::getF3()->ccpClient()->send('getNpcCorporations')); + + if($data['factionId']){ + /** + * @var $faction FactionModel + */ + $faction = $this->rel('factionId'); + $faction->loadById($data['factionId'], $accessToken, $additionalOptions); + $data['factionId'] = $faction; + } + + if($data['allianceId']){ + /** + * @var $alliance AllianceModel + */ + $alliance = $this->rel('allianceId'); + $alliance->loadById($data['allianceId'], $accessToken, $additionalOptions); + $data['allianceId'] = $alliance; + } + + $this->copyfrom($data, ['id', 'name', 'ticker', 'dateFounded', 'memberCount', 'isNPC', 'factionId', 'allianceId']); + $this->save(); + } + } +} \ No newline at end of file diff --git a/app/Model/Universe/DogmaAttributeModel.php b/app/Model/Universe/DogmaAttributeModel.php new file mode 100644 index 000000000..e06c2851f --- /dev/null +++ b/app/Model/Universe/DogmaAttributeModel.php @@ -0,0 +1,97 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'displayName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => true, + 'default' => null + ], + 'description' => [ + 'type' => Schema::DT_TEXT + ], + 'published' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => true, + 'default' => null + ], + 'stackable' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => true, + 'default' => null + ], + 'highIsGood' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => true, + 'default' => null + ], + 'defaultValue' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ], + 'iconId' => [ + 'type' => Schema::DT_INT, + 'nullable' => true, + 'default' => null + ], + 'unitId' => [ + 'type' => Schema::DT_INT, + 'nullable' => true, + 'default' => null + ], + 'attributeTypes' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\TypeAttributeModel', 'attributeId'] + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $attributeData = (object) []; + $attributeData->id = $this->_id; + $attributeData->name = $this->name; + $attributeData->description = $this->description; + + return $attributeData; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getDogmaAttribute', $id); + if(!empty($data) && !isset($data['error'])){ + $this->copyfrom($data, ['id', 'name', 'displayName', 'description', 'published', 'stackable', 'highIsGood', 'defaultValue', 'iconId', 'unitId']); + $this->save(); + } + } +} \ No newline at end of file diff --git a/app/Model/Universe/FactionModel.php b/app/Model/Universe/FactionModel.php new file mode 100644 index 000000000..a4b46661e --- /dev/null +++ b/app/Model/Universe/FactionModel.php @@ -0,0 +1,93 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'description' => [ + 'type' => Schema::DT_TEXT + ], + 'sizeFactor' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ], + 'stationCount' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ], + 'stationSystemCount' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ], + 'race' => [ // faction API endpoint dont have "raceId" data, but race API endpoint has + 'has-one' => ['Exodus4D\Pathfinder\Model\Universe\RaceModel', 'factionId'] + ], + 'alliances' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\AllianceModel', 'factionId'] + ], + 'corporations' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\CorporationModel', 'factionId'] + ], + 'sovereigntySystems' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\SovereigntyMapModel', 'factionId'] + ], + 'factionWarSystemOwners' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\FactionWarSystemModel', 'ownerFactionId'] + ], + 'factionWarSystemOccupiers' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\FactionWarSystemModel', 'occupierFactionId'] + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $factionData = (object) []; + $factionData->id = $this->_id; + $factionData->name = $this->name; + + return $factionData; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getUniverseFaction', $id); + if(!empty($data) && !isset($data['error'])){ + $this->copyfrom($data, ['id', 'name', 'description', 'sizeFactor', 'stationCount', 'stationSystemCount']); + $this->save(); + } + } +} \ No newline at end of file diff --git a/app/Model/Universe/FactionWarSystemModel.php b/app/Model/Universe/FactionWarSystemModel.php new file mode 100644 index 000000000..807010a39 --- /dev/null +++ b/app/Model/Universe/FactionWarSystemModel.php @@ -0,0 +1,120 @@ + [ + 'type' => Schema::DT_INT, + 'index' => true, + 'unique' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'ownerFactionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\FactionModel', + 'constraint' => [ + [ + 'table' => 'faction', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'occupierFactionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\FactionModel', + 'constraint' => [ + [ + 'table' => 'faction', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'contested' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'victoryPoints' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ], + 'victoryPointsThreshold' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ] + ]; + + /** + * No static columns added + * @var bool + */ + protected $addStaticFields = false; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + $data->contested = $this->contested; + + if($this->ownerFactionId){ + $data->ownerFaction = $this->ownerFactionId->getData(); + $data->victoryPercentage = $this->getVictoryPercentage(); + + if( + $this->occupierFactionId && + $this->get('occupierFactionId', true) !== $this->get('ownerFactionId', true) + ){ + $data->occupierFaction = $this->occupierFactionId->getData(); + } + } + + return $data; + } + + /** + * calculate victory progress in percent + * @return int + */ + protected function getVictoryPercentage() : int { + $percent = 0; + + if($this->victoryPoints && $this->victoryPointsThreshold){ + $percent = floor((100 / $this->victoryPointsThreshold) * $this->victoryPoints); + } + + return $percent; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){} +} \ No newline at end of file diff --git a/app/Model/Universe/GroupModel.php b/app/Model/Universe/GroupModel.php new file mode 100644 index 000000000..69010a016 --- /dev/null +++ b/app/Model/Universe/GroupModel.php @@ -0,0 +1,198 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'published' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'categoryId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\CategoryModel', + 'constraint' => [ + [ + 'table' => 'category', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'types' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\TypeModel', 'groupId'] + ] + ]; + + /** + * get group data + * @param array $additionalData + * @return null|object + */ + public function getData(array $additionalData = []){ + $groupData = (object) []; + $groupData->id = $this->_id; + $groupData->name = $this->name; + + if($typesData = $this->getTypesData($additionalData)){ + $groupData->types = $typesData; + } + + return $groupData; + } + + /** + * get all types for this group + * @param bool $published + * @return array|mixed + */ + public function getTypes(bool $published = true){ + $types = []; + if($published){ + $this->filter('types', [ + 'published = :published', + ':published' => 1 + ]); + } + + if($this->types){ + $types = $this->types; + } + + return $types; + } + + /** + * @param array $additionalData + * @return array + */ + protected function getTypesData(array $additionalData = []) : array { + $typesData = []; + $types = $this->getTypes(); + + foreach($types as $type){ + $typesData[] = $type->getData($additionalData); + } + + return $typesData; + } + + /** + * count all types in this group + * @param bool $published + * @return int + */ + public function getTypesCount(bool $published = true) : int { + return $this->valid() ? count($this->getTypes($published)) : 0; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + if(!empty($data = self::getUniverseGroupData($id))){ + /** + * @var $category CategoryModel + */ + $category = $this->rel('categoryId'); + $category->loadById($data['categoryId'], $accessToken, $additionalOptions); + $data['categoryId'] = $category; + + $this->copyfrom($data, ['id', 'name', 'published', 'categoryId']); + $this->save(); + } + } + + /** + * load types data for this group + * @param int $offset + * @param int $length 0 -> all types + * @return array + */ + public function loadTypesData(int $offset = 0, int $length = 0) : array { + $info = ['countAll' => 0, 'countChunk' => 0, 'count' => 0, 'offset' => $offset]; + + if( + $this->valid() && + !empty($data = self::getUniverseGroupData($this->_id)) + ){ + $info['countAll'] = count($data['types']); + + array_multisort($data['types'], SORT_ASC, SORT_NUMERIC); + if($length){ + $data['types'] = array_slice($data['types'], $offset, $length); + } + + $info['countChunk'] = count($data['types']); + foreach($data['types'] as $typeId){ + /** + * @var $type TypeModel + */ + $type = $this->rel('types'); + $type->storeDogmaAttributes = $this->storeDogmaAttributes; + $type->loadById($typeId); + $type->reset(); + + $info['count']++; + $info['offset']++; + } + } + + return $info; + } + + /** + * @param int $id + * @return array + */ + public static function getUniverseGroupData(int $id) : array { + return self::getF3()->ccpClient()->send('getUniverseGroup', $id); + } + + /** + * @return array + */ + public static function getUniverseGroups() : array { + return self::getF3()->ccpClient()->send('getUniverseGroups'); + } + + /** + * @param int $id + * @return array + */ + public static function getUniverseGroupTypes(int $id) : array { + return empty($data = self::getUniverseGroupData($id)) ? [] : $data['types']; + } +} \ No newline at end of file diff --git a/app/Model/Universe/PlanetModel.php b/app/Model/Universe/PlanetModel.php new file mode 100644 index 000000000..3d07ab83c --- /dev/null +++ b/app/Model/Universe/PlanetModel.php @@ -0,0 +1,111 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'typeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\TypeModel', + 'constraint' => [ + [ + 'table' => 'type', + 'on-delete' => 'SET NULL' + ] + ], + 'validate' => 'notDry' + ], + 'x' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'y' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'z' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + $data->name = $this->name; + + $data->type = (object) []; + $data->type->name = $this->typeId->name; + + return $data; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getUniversePlanet', $id); + if(!empty($data)){ + /** + * @var $system SystemModel + */ + $system = $this->rel('systemId'); + $system->loadById($data['systemId'], $accessToken, $additionalOptions); + $data['systemId'] = $system; + + /** + * @var $type TypeModel + */ + $type = $this->rel('typeId'); + $type->loadById($data['typeId'], $accessToken, $additionalOptions); + $data['typeId'] = $type; + + $this->copyfrom($data, ['id', 'name', 'systemId', 'typeId', 'position']); + $this->save(); + } + } + +} \ No newline at end of file diff --git a/app/Model/Universe/RaceModel.php b/app/Model/Universe/RaceModel.php new file mode 100644 index 000000000..45c265ff8 --- /dev/null +++ b/app/Model/Universe/RaceModel.php @@ -0,0 +1,82 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'description' => [ + 'type' => Schema::DT_TEXT + ], + 'factionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\FactionModel', + 'constraint' => [ + [ + 'table' => 'faction', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'stations' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\StationModel', 'raceId'] + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + $data->id = $this->_id; + $data->name = $this->name; + $data->faction = $this->factionId->getData(); + + return $data; + } + + /** + * load data from API into $this and save $this + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getUniverseRace', $id); + if(!empty($data) && !isset($data['error'])){ + /** + * @var $faction FactionModel + */ + $faction = $this->rel('factionId'); + $faction->loadById($data['factionId'], $accessToken, $additionalOptions); + $data['factionId'] = $faction; + + $this->copyfrom($data, ['id', 'name', 'description', 'factionId']); + $this->save(); + } + } +} \ No newline at end of file diff --git a/app/Model/Universe/RegionModel.php b/app/Model/Universe/RegionModel.php new file mode 100644 index 000000000..a5b72f678 --- /dev/null +++ b/app/Model/Universe/RegionModel.php @@ -0,0 +1,84 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'description' => [ + 'type' => Schema::DT_TEXT + ], + 'constellations' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\ConstellationModel', 'regionId'] + ], + 'systemNeighbours' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\SystemNeighbourModel', 'regionId'] + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $regionData = (object) []; + $regionData->id = $this->_id; + $regionData->name = $this->name; + + return $regionData; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getUniverseRegion', $id); + if(!empty($data)){ + $this->copyfrom($data, ['id', 'name', 'description']); + $this->save(); + } + } + + /** + * load constellations data for this region + */ + public function loadConstellationsData(){ + if( !$this->dry() ){ + $data = self::getF3()->ccpClient()->send('getUniverseRegion', $this->_id); + if(!empty($data)){ + foreach((array)$data['constellations'] as $constellationsId){ + /** + * @var $constellation ConstellationModel + */ + $constellation = $this->rel('constellations'); + $constellation->loadById($constellationsId); + $constellation->reset(); + } + } + } + } +} \ No newline at end of file diff --git a/app/Model/Universe/SovereigntyMapModel.php b/app/Model/Universe/SovereigntyMapModel.php new file mode 100644 index 000000000..6ac02c298 --- /dev/null +++ b/app/Model/Universe/SovereigntyMapModel.php @@ -0,0 +1,101 @@ + [ + 'type' => Schema::DT_INT, + 'index' => true, + 'unique' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'factionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\FactionModel', + 'constraint' => [ + [ + 'table' => 'faction', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'allianceId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\AllianceModel', + 'constraint' => [ + [ + 'table' => 'alliance', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'corporationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\CorporationModel', + 'constraint' => [ + [ + 'table' => 'corporation', + 'on-delete' => 'CASCADE' + ] + ] + ] + ]; + + /** + * No static columns added + * @var bool + */ + protected $addStaticFields = false; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + + if($this->factionId){ + $data->faction = $this->factionId->getData(); + }else{ + if($this->allianceId){ + $data->alliance = $this->allianceId->getData(); + } + + if($this->corporationId){ + $data->corporation = $this->corporationId->getData(); + } + } + + return $data; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){} +} \ No newline at end of file diff --git a/app/Model/Universe/StarModel.php b/app/Model/Universe/StarModel.php new file mode 100644 index 000000000..197273e5a --- /dev/null +++ b/app/Model/Universe/StarModel.php @@ -0,0 +1,102 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'typeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\TypeModel', + 'constraint' => [ + [ + 'table' => 'type', + 'on-delete' => 'SET NULL' + ] + ], + 'validate' => 'notDry' + ], + 'age' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => true, + 'default' => null + ], + 'radius' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => true, + 'default' => null + ], + 'temperature' => [ + 'type' => Schema::DT_INT, + 'nullable' => true, + 'default' => null + ], + 'luminosity' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => true, + 'default' => null + ], + 'spectralClass' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => true, + 'default' => null + ], + 'system' => [ + 'has-one' => ['Exodus4D\Pathfinder\Model\Universe\SystemModel', 'starId'] + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $starData = (object) []; + $starData->id = $this->_id; + $starData->name = $this->typeId->name; + + return $starData; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getUniverseStar', $id); + if(!empty($data)){ + /** + * @var $type TypeModel + */ + $type = $this->rel('typeId'); + $type->loadById($data['typeId'], $accessToken, $additionalOptions); + $data['typeId'] = $type; + + $this->copyfrom($data, ['id', 'name', 'typeId', 'age', 'radius', 'temperature', 'luminosity', 'spectralClass']); + $this->save(); + } + } +} \ No newline at end of file diff --git a/app/Model/Universe/StargateModel.php b/app/Model/Universe/StargateModel.php new file mode 100644 index 000000000..f8915d7bf --- /dev/null +++ b/app/Model/Universe/StargateModel.php @@ -0,0 +1,151 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'typeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\TypeModel', + 'constraint' => [ + [ + 'table' => 'type', + 'on-delete' => 'SET NULL' + ] + ], + 'validate' => 'notDry' + ], + 'destinationSystemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'x' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'y' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'z' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ] + ]; + + public function getData(){ + $stargateData = (object) []; + $stargateData->id = $this->_id; + $stargateData->type = $this->typeId->name; + $stargateData->destination = $this->destinationSystemId->name; + + return $stargateData; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getUniverseStargate', $id); + + if(!empty($data)){ + + if($this->get('systemId', true) !== $data['systemId']){ + // new stargate or system changed + /** + * @var $system SystemModel + */ + $system = $this->rel('systemId'); + $system->loadById($data['systemId'], $accessToken, $additionalOptions); + $data['systemId'] = $system; + } + + if($this->get('typeId', true) !== $data['typeId']){ + /** + * @var $type TypeModel + */ + $type = $this->rel('typeId'); + $type->loadById($data['typeId'], $accessToken, $additionalOptions); + $data['typeId'] = $type; + } + + if($this->get('destinationSystemId', true) !== $data['destination']->system_id){ + // new stargate or destinationSystem changed + /** + * @var $destinationSystem SystemModel + */ + $destinationSystem = $this->rel('destinationSystemId'); + // no loadById() here! we don´t want to insert/update systems that do not exist yet + $destinationSystem->getById($data['destination']->system_id, 0); + + if( !$destinationSystem->dry() ){ + $data['destinationSystemId'] = $destinationSystem; + $this->copyfrom($data, ['id', 'name', 'position', 'systemId', 'typeId', 'destinationSystemId']); + $this->save(); + } + } + } + } + + /** + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['systemId', 'destinationSystemId'], true); + } + return $status; + } +} \ No newline at end of file diff --git a/app/Model/Universe/StationModel.php b/app/Model/Universe/StationModel.php new file mode 100644 index 000000000..daafb4a39 --- /dev/null +++ b/app/Model/Universe/StationModel.php @@ -0,0 +1,165 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'typeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\TypeModel', + 'constraint' => [ + [ + 'table' => 'type', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'corporationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\CorporationModel', + 'constraint' => [ + [ + 'table' => 'corporation', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'raceId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\RaceModel', + 'constraint' => [ + [ + 'table' => 'race', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'services' => [ + 'type' => self::DT_JSON + ], + 'x' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'y' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'z' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + $data->id = $this->_id; + $data->name = $this->name; + $data->type = $this->typeId->getData(); + $data->services = $this->services ? : []; + + // according to ESIs Swagger conf, "raceId" AND "corporationId"(= "owner") are optional + // -> I haven´t seen any imported NPC station data where "raceId" OR "corporationId" not exist + if($this->corporationId){ + $data->corporation = $this->corporationId->getData(); + } + + if($this->raceId){ + $data->race = $this->raceId->getData(); + } + + return $data; + } + + /** + * load data from API into $this and save $this + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getUniverseStation', $id); + if(!empty($data) && !isset($data['error'])){ + /** + * @var $system SystemModel + */ + $system = $this->rel('systemId'); + $system->loadById($data['systemId'], $accessToken, $additionalOptions); + $data['systemId'] = $system; + + /** + * @var $type TypeModel + */ + $type = $this->rel('typeId'); + $type->loadById($data['typeId'], $accessToken, $additionalOptions); + $data['typeId'] = $type; + + if($data['corporationId']){ + /** + * @var $faction CorporationModel + */ + $corporation = $this->rel('corporationId'); + $corporation->loadById($data['corporationId'], $accessToken, $additionalOptions); + $data['corporationId'] = $corporation; + } + + if($data['raceId']){ + /** + * @var $race RaceModel + */ + $race = $this->rel('raceId'); + $race->loadById($data['raceId'], $accessToken, $additionalOptions); + $data['raceId'] = $race; + } + + $this->copyfrom($data, ['id', 'name', 'systemId', 'typeId', 'corporationId', 'raceId', 'services', 'position']); + $this->save(); + } + } + +} \ No newline at end of file diff --git a/app/Model/Universe/StructureModel.php b/app/Model/Universe/StructureModel.php new file mode 100644 index 000000000..3ea0828a4 --- /dev/null +++ b/app/Model/Universe/StructureModel.php @@ -0,0 +1,126 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'typeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\TypeModel', + 'constraint' => [ + [ + 'table' => 'type', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'x' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ], + 'y' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ], + 'z' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ] + ]; + + /** + * get data from object + * -> more fields can be added in here if needed + * @return \stdClass + */ + public function getData(): \stdClass { + $data = (object) []; + if($this->valid()){ + $data->id = $this->_id; + $data->name = $this->name; + $data->type = $this->typeId->getData(); + } + return $data; + } + + /** + * load data from API into $this and save $this + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getUniverseStructure', $id, $accessToken); + if(!empty($data) && !isset($data['error'])){ + /** + * @var $type TypeModel + */ + $type = $this->rel('typeId'); + $type->loadById($data['typeId'], $accessToken, $additionalOptions); + $data['typeId'] = $type; + + $this->copyfrom($data, ['id', 'name', 'systemId', 'typeId', 'position']); + $this->save(); + } + } + + /** + * overwrites parent + * @param null|SQL $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + //change `id` column to BigInt + $schema = new Schema($db); + $typeQuery = $schema->findQuery($schema->dataTypes[Schema::DT_BIGINT]); + $db->exec("ALTER TABLE " . $db->quotekey('structure') . + " MODIFY COLUMN " . $db->quotekey('id') . " " . $typeQuery . " NOT NULL"); + } + return $status; + } + +} \ No newline at end of file diff --git a/app/Model/Universe/SystemModel.php b/app/Model/Universe/SystemModel.php new file mode 100644 index 000000000..3775e0279 --- /dev/null +++ b/app/Model/Universe/SystemModel.php @@ -0,0 +1,577 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'constellationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\ConstellationModel', + 'constraint' => [ + [ + 'table' => 'constellation', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'starId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\StarModel', + 'constraint' => [ + [ + 'table' => 'star', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'security' => [ + 'type' => Schema::DT_VARCHAR128 + ], + 'trueSec' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 1 + ], + 'securityStatus' => [ + 'type' => Schema::DT_DOUBLE, + 'nullable' => false, + 'default' => 1 + ], + 'securityClass' => [ + 'type' => Schema::DT_VARCHAR128, + ], + 'effect' => [ + 'type' => Schema::DT_VARCHAR128 + ], + 'x' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'y' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'z' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'planets' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\PlanetModel', 'systemId'] + ], + 'statics' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\SystemStaticModel', 'systemId'] + ], + 'stargates' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\StargateModel', 'systemId'] + ], + 'stations' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\StationModel', 'systemId'] + ], + 'structures' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\StructureModel', 'systemId'] + ], + 'neighbour' => [ + 'has-one' => ['Exodus4D\Pathfinder\Model\Universe\SystemNeighbourModel', 'systemId'] + ], + 'sovereignty' => [ + 'has-one' => ['Exodus4D\Pathfinder\Model\Universe\SovereigntyMapModel', 'systemId'] + ], + 'factionWar' => [ + 'has-one' => ['Exodus4D\Pathfinder\Model\Universe\FactionWarSystemModel', 'systemId'] + ] + ]; + + /** + * get system data + * -> this includes constellation, region, star, planets as well + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + $data->id = $this->_id; + $data->name = $this->name; + $data->constellation = $this->constellationId->getData(); + $data->security = $this->security; + $data->trueSec = (float)$this->trueSec; + $data->effect = $this->effect; + $data->shattered = false; + + if($this->starId){ + $data->star = $this->starId->getData(); + } + + if($this->sovereignty){ + $data->sovereignty = $this->sovereignty->getData(); + } + + if($this->factionWar){ + $data->factionWar = $this->factionWar->getData(); + } + + if( !empty($planetsData = $this->getPlanetsData()) ){ + $data->planets = $planetsData; + + // 'Shattered' systems have ONLY planets named with '(shattered)' + // -> system 'Thera' has '(shattered)' AND other planets -> not shattered. + // -> system 'J164104, 'J115422' - the only non-shattered wormholes which have a shattered planet -> not shattered. + $data->shattered = count(array_filter($planetsData, function($planetData){ + return property_exists($planetData, 'type') && + (strpos(strtolower($planetData->type->name), '(shattered)') !== false); + })) == count($planetsData); + } + + if( !empty($staticsData = $this->getStaticsData()) ){ + $data->statics = $staticsData; + } + + if( !empty($stargatesData = $this->getStargatesData()) ){ + $data->stargates = $stargatesData; + } + + if( !empty($stationsData = $this->getStationsData()) ){ + $data->stations = $stationsData; + } + + return $data; + } + + /** + * setter for system name + * @param $name + * @return mixed + */ + public function set_name($name){ + // name should never change + // -> important for "Abyssal" systems where ESI don´t have correct system name + if(!empty($this->name)){ + $name = $this->name; + } + return $name; + } + + /** + * setter for row (un-formatted) trueSec + * @param $secStatus + * @return double + */ + public function set_securityStatus($secStatus){ + $secStatus = (double)$secStatus; + // round for trueSec + $positive = ($secStatus > 0); + $trueSec = round($secStatus, 1, PHP_ROUND_HALF_DOWN); + if($positive && $trueSec <= 0){ + $trueSec = 0.1; + } + $this->trueSec = $trueSec; + // set 'security' for NON wormhole systems! -> those get updated from csv import + // 'J1226-0' is also a wormhole with a '-' in the name! (single system) + if( + !preg_match('/^j(\d{6}|\d{4}-\d)$/i', $this->name) && + $this->name != 'Thera' + ){ + $constellationId = (int)$this->get('constellationId', true); + if($constellationId == 23000001){ + // "Pocket" system + $security = 'P'; + }elseif( + $constellationId >= 22000001 && + $constellationId <= 22000025 + ){ + // "Abyssal" system + $security = 'A'; + }else{ + // k-space system + if($trueSec <= 0){ + $security = '0.0'; + }elseif($trueSec < 0.5){ + $security = 'L'; + }else{ + $security = 'H'; + } + } + + $this->security = $security; + } + return $secStatus; + } + + /** + * setter for wormhole effect name + * @param $effect + * @return string|null + */ + public function set_effect($effect){ + $effect = (string)$effect; + return $effect ? : null; + } + + /** + * @param array $sovData + * @return bool true if sovereignty data changed + */ + public function updateSovereigntyData(array $sovData = []) : bool { + $hasChanged = false; + $systemId = (int)$sovData['systemId']; + $factionId = (int)$sovData['factionId']; + $allianceId = (int)$sovData['allianceId']; + $corporationId = (int)$sovData['corporationId']; + + if($this->valid()){ + if($systemId === $this->_id){ + // sov data belongs to this system + $validSovData = (bool)max($factionId, $allianceId, $corporationId); + if($validSovData){ + // at least one of these Ids must exist for a sovereignty relation + /** + * @var $sovereignty SovereigntyMapModel + */ + if(!$sovereignty = $this->sovereignty){ + // insert new sovereignty data + $sovereignty = $this->rel('sovereignty'); + } + + $sovData['systemId'] = $this; + + if($factionId){ + // HS, L - systems have "faction war" + $sovData['allianceId'] = null; + $sovData['corporationId'] = null; + + /** + * @var $faction FactionModel + */ + $faction = $sovereignty->rel('factionId'); + $faction->loadById($factionId); + $sovData['factionId'] = $faction; + }else{ + // 0.0 - systems have sovereignty data by corp and/or ally + $sovData['factionId'] = null; + + /** + * @var $alliance AllianceModel|null + */ + $alliance = null; + if($allianceId){ + $alliance = $sovereignty->rel('allianceId'); + $alliance->loadById($allianceId); + } + + /** + * @var $corporation CorporationModel|null + */ + $corporation = null; + if($corporationId){ + $corporation = $sovereignty->rel('corporationId'); + $corporation->loadById($corporationId); + } + + $sovData['allianceId'] = $alliance; + $sovData['corporationId'] = $corporation; + } + + $sovereignty->copyfrom($sovData, ['systemId', 'factionId', 'allianceId', 'corporationId']); + + // must be called before save(). Changed fields get reset after save() is called! + if($sovereignty->changed()){ + $hasChanged = true; + } + + $sovereignty->save(); + }elseif($this->sovereignty){ + // delete existing sov data + // -> hint: WH - systems never have sovereignty data + $this->sovereignty->erase(); + $hasChanged = true; + } + } + } + + return $hasChanged; + } + + /** + * @param array $fwData + * @return bool true if faction warfare data changed + */ + public function updateFactionWarData(array $fwData = []) : bool { + $hasChanged = false; + $systemId = (int)$fwData['systemId']; + $ownerFactionId = (int)$fwData['ownerFactionId']; + $occupierFactionId = (int)$fwData['occupierFactionId']; + + if($this->valid()){ + if($systemId === $this->_id){ + /** + * @var $factionWar FactionWarSystemModel + */ + if(!$factionWar = $this->factionWar){ + // insert new faction war data + $factionWar = $this->rel('factionWar'); + } + + $fwData['systemId'] = $this; + + if($ownerFactionId){ + /** + * @var $ownerFaction FactionModel + */ + $ownerFaction = $factionWar->rel('ownerFactionId'); + $ownerFaction->loadById($ownerFactionId); + $fwData['ownerFactionId'] = $ownerFaction; + } + + if($occupierFactionId){ + /** + * @var $occupierFaction FactionModel + */ + $occupierFaction = $factionWar->rel('occupierFactionId'); + $occupierFaction->loadById($occupierFactionId); + $fwData['occupierFactionId'] = $occupierFaction; + } + + $factionWar->copyfrom($fwData, ['systemId', 'ownerFactionId', 'occupierFactionId', 'contested', 'victoryPoints', 'victoryPointsThreshold']); + + // must be called before save(). Changed fields get reset after save() is called! + if($factionWar->changed()){ + $hasChanged = true; + } + + $factionWar->save(); + } + } + + return $hasChanged; + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterUpdateEvent($self, $pkeys){ + // build search index + $self->buildIndex(); + return parent::afterUpdateEvent($self, $pkeys); + } + + /** + * get data from all planets + * @return array + */ + protected function getPlanetsData() : array { + $planetsData = []; + + if($this->planets){ + /** + * @var $planet PlanetModel + */ + foreach($this->planets as &$planet){ + $planetsData[] = $planet->getData(); + } + } + return $planetsData; + } + + /** + * get data from all static wormholes + * @return array + */ + protected function getStaticsData() : array { + $staticsData = []; + + if($this->statics){ + /** + * @var $static SystemStaticModel + */ + foreach($this->statics as &$static){ + $staticsData[] = $static->getData(); + } + } + return $staticsData; + } + + /** + * get data from all stargates + * @return array + */ + protected function getStargatesData() : array { + $stargatesData = []; + + if($this->stargates){ + /** + * @var $stargate StargateModel + */ + foreach($this->stargates as &$stargate){ + $stargatesData[] = $stargate->getData(); + } + } + return $stargatesData; + } + + /** + * get data from all stations + * @return array + */ + protected function getStationsData() : array { + $stationsData = []; + + if($this->stations){ + /** + * @var $station StationModel + */ + foreach($this->stations as &$station){ + $data = $station->getData(); + if(!$data->race){ + // should never happen NPC stations always have a owning race + $data->race = (object) []; + $data->race->id = 0; + $data->race->name = 'unknown'; + $data->race->faction = (object) []; + $data->race->faction->id = 0; + $data->race->faction->name = 'unknown'; + } + + if(!array_key_exists($data->race->faction->id, $stationsData)){ + $stationsData[$data->race->faction->id] = [ + 'id' => $data->race->faction->id, + 'name' => $data->race->name, + 'stations' => [] + ]; + } + + $stationsData[$data->race->faction->id]['stations'][] = $data; + } + } + return $stationsData; + } + + /** + * update system from ESI + */ + public function updateModel(){ + if($this->valid()){ + $this->loadData($this->_id); + $this->loadPlanetsData(); + } + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getUniverseSystem', $id); + + if(!empty($data)){ + /** + * @var $constellation ConstellationModel + */ + $constellation = $this->rel('constellationId'); + $constellation->loadById($data['constellationId'], $accessToken, $additionalOptions); + $data['constellationId'] = $constellation; + + // starId is optional since ESI v4 (e.g. Abyssal systems) + if($data['starId']){ + /** + * @var $star StarModel + */ + $star = $this->rel('starId'); + $star->loadById($data['starId'], $accessToken, $additionalOptions); + $data['starId'] = $star; + } + + $this->copyfrom($data, ['id', 'name', 'constellationId', 'starId', 'securityStatus', 'securityClass', 'position']); + $this->save(); + } + } + + /** + * load planets data for this system + */ + public function loadPlanetsData(){ + if($this->valid()){ + $data = self::getF3()->ccpClient()->send('getUniverseSystem', $this->_id); + if($data['planets']){ + // planets are optional since ESI v4 (e.g. Abyssal systems) + foreach((array)$data['planets'] as $planetData){ + /** + * @var $planet PlanetModel + */ + $planet = $this->rel('planets'); + $planet->loadById($planetData->planet_id); + $planet->reset(); + } + } + } + } + + /** + * load stargates for this system + * -> stargates to destination system which is not in DB get ignored + */ + public function loadStargatesData(){ + if($this->valid()){ + $data = self::getF3()->ccpClient()->send('getUniverseSystem', $this->_id); + if($data['stargates']){ + foreach((array)$data['stargates'] as $stargateId){ + /** + * @var $stargate StargateModel + */ + $stargate = $this->rel('stargates'); + $stargate->loadById($stargateId); + $stargate->reset(); + } + } + } + } + + /** + * load NPC owned stations for this system + */ + public function loadStationsData(){ + if($this->valid()){ + $data = self::getF3()->ccpClient()->send('getUniverseSystem', $this->_id); + if($data['stations']){ + foreach((array)$data['stations'] as $stationId){ + /** + * @var $station SystemModel + */ + $station = $this->rel('stations'); + $station->loadById($stationId); + $station->reset(); + } + } + } + } +} \ No newline at end of file diff --git a/app/Model/Universe/SystemNeighbourModel.php b/app/Model/Universe/SystemNeighbourModel.php new file mode 100644 index 000000000..0ef5a20c0 --- /dev/null +++ b/app/Model/Universe/SystemNeighbourModel.php @@ -0,0 +1,92 @@ + used on /setup page or index build + * @var bool + */ + protected $allowTruncate = true; + + /** + * @var array + */ + protected $fieldConf = [ + 'regionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\RegionModel', + 'constraint' => [ + [ + 'table' => 'region', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'constellationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\ConstellationModel', + 'constraint' => [ + [ + 'table' => 'constellation', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'unique' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'systemName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'jumpNodes' => [ + 'type' => Schema::DT_VARCHAR512, + 'nullable' => false, + 'default' => '' + ], + 'trueSec' => [ + 'type' => Schema::DT_DECIMAL, + 'nullable' => false, + 'default' => 0 + ] + ]; + + /** + * No static columns added + * @var bool + */ + protected $addStaticFields = false; + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){} +} \ No newline at end of file diff --git a/app/Model/Universe/SystemStaticModel.php b/app/Model/Universe/SystemStaticModel.php new file mode 100644 index 000000000..c62b7b523 --- /dev/null +++ b/app/Model/Universe/SystemStaticModel.php @@ -0,0 +1,85 @@ + [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'typeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\TypeModel', + 'constraint' => [ + [ + 'table' => 'type', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ] + ]; + + /** + * No static columns added + * @var bool + */ + protected $addStaticFields = false; + + /** + * get static data + * @return null|string + */ + public function getData(){ + return $this->typeId ? $this->typeId->getWormholeName() : null; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){} + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['systemId', 'typeId'], true); + } + return $status; + } +} \ No newline at end of file diff --git a/app/Model/Universe/TypeAttributeModel.php b/app/Model/Universe/TypeAttributeModel.php new file mode 100644 index 000000000..41011af64 --- /dev/null +++ b/app/Model/Universe/TypeAttributeModel.php @@ -0,0 +1,92 @@ + [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\TypeModel', + 'constraint' => [ + [ + 'table' => 'type', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'attributeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\DogmaAttributeModel', + 'constraint' => [ + [ + 'table' => 'dogma_attribute', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'value' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ] + ]; + + /** + * No static columns added + * @var bool + */ + protected $addStaticFields = false; + + /** + * @return \stdClass + */ + public function getData(){ + $typeAttributeData = $this->attributeId->getData(); + $typeAttributeData->value = (float)$this->value; + + return $typeAttributeData; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){} + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['typeId', 'attributeId'], true); + } + return $status; + } +} \ No newline at end of file diff --git a/app/Model/Universe/TypeModel.php b/app/Model/Universe/TypeModel.php new file mode 100644 index 000000000..88b78f75b --- /dev/null +++ b/app/Model/Universe/TypeModel.php @@ -0,0 +1,378 @@ + set to true will store all typeAttributes from ESI for a type + * -> should be enabled for specific types, where data is used by Pathfinder + */ + const DEFAULT_STORE_DOGMA_ATTRIBUTES = false; + + /** + * @var bool + */ + public $storeDogmaAttributes = self::DEFAULT_STORE_DOGMA_ATTRIBUTES; + + /** + * @var array + */ + protected $fieldConf = [ + 'name' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'description' => [ + 'type' => Schema::DT_TEXT + ], + 'published' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'radius' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ], + 'volume' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ], + 'capacity' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ], + 'mass' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ], + 'groupId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Exodus4D\Pathfinder\Model\Universe\GroupModel', + 'constraint' => [ + [ + 'table' => 'group', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry', + ], + 'marketGroupId' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0, + 'index' => true + ], + 'packagedVolume' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ], + 'portionSize' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ], + 'graphicId' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0, + 'index' => true + ], + 'stations' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\StationModel', 'typeId'] + ], + 'structures' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\StructureModel', 'typeId'] + ], + 'planets' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\PlanetModel', 'typeId'] + ], + 'stars' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\StarModel', 'typeId'] + ], + 'attributes' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\TypeAttributeModel', 'typeId'] + ], + 'stargates' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\StargateModel', 'typeId'] + ], + 'statics' => [ + 'has-many' => ['Exodus4D\Pathfinder\Model\Universe\SystemStaticModel', 'typeId'] + ] + ]; + + /** + * set 'dogma_attributes' during ESI import process to a virtual field + * -> 'dogma_attributes' get imported after type is saved + * @see loadData() + * @param $dogmaAttributesData + * @return null + */ + public function set_dogma_attributes($dogmaAttributesData){ + $this->virtual('dogmaAttributes', (array)$dogmaAttributesData); + return null; + } + + /** + * special getter for 'wormhole' types + * @return string|null + */ + public function getWormholeName(){ + return self::formatWormholeName($this->name); + } + + /** + * @param bool $mapper + * @return NULL|void + */ + public function reset($mapper = true){ + $this->clearVirtual('dogmaAttributes'); + parent::reset($mapper); + } + + /** + * get type data + * @param array $additionalData + * @return null|object + */ + public function getData(array $additionalData = []){ + $typeData = (object) []; + $typeData->id = $this->_id; + $typeData->name = $this->name; + + foreach($additionalData as $key){ + if($key == 'attributes'){ + // add 'dogma' typeAttributes data + $typeData->$key = $this->getAttributesData(); + }elseif($this->exists($key)){ + $typeData->$key = $this->$key; + } + } + + return $typeData; + } + + /** + * get wormholeData from object + * @return \stdClass + */ + public function getWormholeData() : \stdClass { + $wormholeData = (object) []; + if($this->valid()){ + $wormholeData->name = $this->getWormholeName(); + $wormholeData->static = $this->statics ? (bool)count($this->statics) : false; + $wormholeData->security = ''; + $wormholeData->massTotal = null; + $wormholeData->massIndividual = null; + $wormholeData->maxStableTime = null; + foreach($this->getAttributesData() as $id => $attributesData){ + switch($id){ + case 1381: // 'wormholeTargetSystemClass' -> 'security' + $wormholeData->security = self::getSystemSecurityFromId((int)$attributesData['value']); + break; + case 1383: // 'wormholeMaxStableMass' -> 'massTotal' + $wormholeData->massTotal = $attributesData['value']; + break; + case 1385: // 'wormholeMaxJumpMass' -> 'massIndividual' + $wormholeData->massIndividual = $attributesData['value']; + break; + case 1384: // 'wormholeMassRegeneration' -> 'massRegeneration' + if($attributesData['value']){ + $wormholeData->massRegeneration = $attributesData['value']; + } + break; + case 1382: // 'wormholeMaxStableTime' -> 'maxStableTime' + $wormholeData->maxStableTime = $attributesData['value'] / 60; + break; + case Config::ESI_DOGMA_ATTRIBUTE_SCANWHSTRENGTH_ID: // 'scanWormholeStrength' -> 'scanWormholeStrength' + $wormholeData->scanWormholeStrength = $attributesData['value']; + break; + } + } + } + return $wormholeData; + } + + /** + * get shipData from object + * -> more fields can be added in here if needed + * @return \stdClass + */ + public function getShipData() : \stdClass { + $shipData = (object) []; + if($this->valid()){ + $shipData->typeId = $this->_id; + $shipData->typeName = $this->name; + $shipData->mass = $this->mass; + } + return $shipData; + } + + /** + * @return array + */ + protected function getAttributesData() : array { + $attributesData = []; + + if($this->attributes){ + foreach($this->attributes as $typeAttribute){ + /** + * @var $typeAttribute TypeAttributeModel + */ + $attributesData[] = get_object_vars($typeAttribute->getData()); + } + } + return Util::arrayGetBy($attributesData, 'id'); + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterInsertEvent($self, $pkeys){ + $self->syncDogmaAttributes(); + + return parent::afterInsertEvent($self, $pkeys); + } + + /** + * Event "Hook" function + * @param self $self + * @param $pkeys + */ + public function afterUpdateEvent($self, $pkeys){ + $self->syncDogmaAttributes(); + + return parent::afterUpdateEvent($self, $pkeys); + } + + /** + * sync existing 'dogma' typeAttributes data with "new/updated" typeAttributes + * -> $this->dogmaAttributes must be set before calling this method + */ + protected function syncDogmaAttributes(){ + if( + $this->storeDogmaAttributes && + !empty($dogmaAttributesData = (array)$this->dogmaAttributes) + ){ + foreach((array)$this->attributes as $typeAttribute){ + $key = array_search($typeAttribute->get('attributeId', true), array_column($dogmaAttributesData, 'attributeId')); + if($key !== false){ + // attribute still belongs to this 'type' -> update value + $typeAttribute->copyfrom($dogmaAttributesData[$key], ['value']); + $typeAttribute->save(); + + unset($dogmaAttributesData[$key]); + $dogmaAttributesData = array_values($dogmaAttributesData); + }else{ + // attribute no longer belongs to this 'type' + $typeAttribute->erase(); + } + } + + // add new dogmaTypes + foreach($dogmaAttributesData as $dogmaAttributeData){ + /** + * @var $typeAttribute TypeAttributeModel + * @var $dogmaAttribute DogmaAttributeModel + */ + $typeAttribute = $this->rel('attributes'); + $dogmaAttribute = $typeAttribute->rel('attributeId'); + $dogmaAttribute->loadById($dogmaAttributeData['attributeId']); + if($dogmaAttribute->valid()){ + $typeAttribute->typeId = $this; + $typeAttribute->attributeId = $dogmaAttribute; + $typeAttribute->value = $dogmaAttributeData['value']; + $typeAttribute->save(); + } + } + } + } + + /** + * manipulate 'dogma_attributes' array be reference + * -> used to inject custom attributes (not available from ESI) + * @param array $data + */ + private function manipulateDogmaAttributes(array &$data){ + if(!$this->storeDogmaAttributes){ + // attributes should not get saved + unset($data['dogma_attributes']); + }elseif(!empty($data['dogma_attributes'])){ + switch($data['groupId']){ + case Config::ESI_GROUP_WORMHOLE_ID: + if( + !empty($wormholesCSVData = static::getCSVData('wormhole', 'name')) && + !empty($wormholeCSVData = $wormholesCSVData[self::formatWormholeName($data['name'])]) + ){ + // found relevant wormhole data in *.csv for current type + if(!empty($scanWormholeStrength = (float)$wormholeCSVData['scanWormholeStrength'])){ + $data['dogma_attributes'][] = [ + 'attributeId' => Config::ESI_DOGMA_ATTRIBUTE_SCANWHSTRENGTH_ID, + 'value' => $scanWormholeStrength + ]; + } + } + break; + } + } + } + + /** + * load data from API into $this and save $this + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->send('getUniverseType', $id); + if(!empty($data)){ + $this->manipulateDogmaAttributes($data); + + /** + * @var $group GroupModel + */ + $group = $this->rel('groupId'); + $group->loadById($data['groupId'], $accessToken, $additionalOptions); + $data['groupId'] = $group; + + $this->copyfrom($data); + $this->save(); + } + } + + /** + * @param string|null $name + * @return string|null + */ + public static function formatWormholeName(?string $name) : ?string { + return (!empty($name) && !empty($format = @end(explode(' ', $name)))) ? $format : null; + } +} \ No newline at end of file diff --git a/app/Model/Universe/abstractuniversemodel.php b/app/Model/Universe/abstractuniversemodel.php new file mode 100644 index 000000000..cc5c3a9a3 --- /dev/null +++ b/app/Model/Universe/abstractuniversemodel.php @@ -0,0 +1,228 @@ + should "never" expire + * -> until manual remove and or global cache clear + */ + const CACHE_INDEX_EXPIRE_KEY = 86400 * 356 * 5; + + /** + * get model data -> should be overwritten + * @return null + */ + public function getData(){ + return null; + } + + /** + * setter for positions array (x/y/z) + * @param $position + * @return null + */ + public function set_position($position){ + $position = (array)$position; + if(count($position) === 3){ + $this->x = $position['x']; + $this->y = $position['y']; + $this->z = $position['z']; + } + return null; + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + * @return bool + */ + public function beforeUpdateEvent($self, $pkeys) : bool { + // if model changed, 'update' col needs to be updated as well + // -> data no longer "outdated" + $self->touch('updated'); + + return parent::beforeUpdateEvent($self, $pkeys); + } + + /** + * get hashKey for search index build + * -> used by the cache backend + * @param string $column + * @return bool|string + */ + public function getHashKey(string $column = '_id'){ + $key = false; + if($this->valid() && $this->exists($column)){ + $key = self::generateHashKeyRow($this->getTable(), $this->$column); + } + return $key; + } + + /** + * calculate time period (in seconds) from now on, until data get expired + * @return int + */ + /* + public function calcTtl() : int { + $ttl = 0; + if(!$this->dry()){ + $timezone = $this->getF3()->get('getTimeZone')(); + $currentTime = new \DateTime('now', $timezone); + $updateTime = \DateTime::createFromFormat( + 'Y-m-d H:i:s', + $this->updated, + $timezone + ); + // add expire period to last updated timestamp + $updateTime->modify('+' . self::CACHE_MAX_DAYS . ' day'); + + $seconds = $updateTime->getTimestamp() - $currentTime->getTimestamp(); + if($seconds > 0){ + $ttl = $seconds; + } + } + + return $ttl; + } +*/ + /** + * build up a "search" index for this model + * -> stores getData() result into Cache (RAM) for faster access + * @return null|\stdClass + */ + public function buildIndex(){ + $data = null; + if($hashKeyId = $this->getHashKey()){ + $data = $this->getData(); + $this->getF3()->set($hashKeyId, $data, self::CACHE_INDEX_EXPIRE_KEY); + + // ... add hashKey for this rows to tableIndex as well + self::buildTableIndex($this, [$hashKeyId]); + } + + return $data; + } + + /** + * get data from "search" index for this model + * -> if data not found -> try to build up index for this model + * @return null|\stdClass + */ + public function fromIndex(){ + $data = null; + if($hashKeyId = $this->getHashKey()){ + if( !self::existsCacheValue($hashKeyId, $data)){ + $data = $this->buildIndex(); + } + } + + return $data; + } + + /** + * load object by $id + * -> if $id not exists in DB -> query API + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + public function loadById(int $id, string $accessToken = '', array $additionalOptions = []){ + /** + * @var $model self + */ + $this->getById($id, 0); + if($this->isOutdated()){ + $this->loadData($id, $accessToken, $additionalOptions); + } + } + + /** + * load data from API into $this and save $this + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + abstract protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []); + + /** + * convert CCPs ids for system security into Pathfinder security label + * -> used e.g. in "Dogma Attributes" (wormholeTargetSystemClass) for wormhole types + * @param int $id + * @return string|null + */ + public static function getSystemSecurityFromId(int $id) : ?string { + $security = null; + if( + ($id >= 1 && $id <= 6) || + ($id >= 12 && $id <= 18) + ){ + $security = 'C' . $id; + }elseif($id == 7){ + $security = 'H'; + }elseif($id == 8){ + $security = 'L'; + }elseif($id == 9){ + $security = '0.0'; + } + + return $security; + } + + /** + * add $rowKeys (hashKeys) to a search index that holds all rowKeys of a table + * @param AbstractUniverseModel $model + * @param array $rowKeys + */ + public static function buildTableIndex(AbstractUniverseModel $model, array $rowKeys = []){ + $hashKeyTable = static::generateHashKeyTable($model->getTable()); + if( !self::getF3()->exists($hashKeyTable, $cachedData) ){ + $cachedData = []; + } + $cachedData = array_unique(array_merge($cachedData, $rowKeys)); + + self::getF3()->set($hashKeyTable, $cachedData, self::CACHE_INDEX_EXPIRE_KEY); + } + + /** + * generate hashKey for a table row data for search index build + * @param string $table + * @param $value + * @return string + */ + public static function generateHashKeyRow(string $table, $value) : string { + return static::generateHashKeyTable($table) . '_' . md5(strtolower((string)$value)); + } + + /** + * generate hashKey for a complete table + * -> should hold hashKeys for multiple rows + * @param string $table + * @param string $prefix + * @return string + */ + public static function generateHashKeyTable(string $table, string $prefix = self::CACHE_KEY_PREFIX) : string { + return parent::generateHashKeyTable($table, $prefix); + } +} \ No newline at end of file diff --git a/app/app/cortex.php b/app/app/cortex.php deleted file mode 100644 index 6b5cabcb2..000000000 --- a/app/app/cortex.php +++ /dev/null @@ -1,54 +0,0 @@ -set('AUTOLOAD', $f3->get('AUTOLOAD').';app/cortex/'); - $f3->set('QUIET', false); - - $dbs = array( - 'sql' => new \DB\SQL('mysql:host=localhost;port=3306;dbname=fatfree', 'fatfree', ''), -// 'sql-sqlite' => new \DB\SQL('sqlite:data/sqlite.db'), -// 'sql-pgsql' => new \DB\SQL('pgsql:host=localhost;dbname=fatfree', 'fatfree', 'fatfree'), - 'jig' => new \DB\Jig('data/'), - 'mongo' => new \DB\Mongo('mongodb://localhost:27017', 'testdb'), -// 'sqlsrv2012' => new \DB\SQL('sqlsrv:SERVER=LOCALHOST\SQLEXPRESS2012;Database=fatfree','fatfree', 'fatfree'), -// 'sqlsrv2008' => new \DB\SQL('sqlsrv:SERVER=LOCALHOST\SQLEXPRESS2008;Database=fatfree','fatfree', 'fatfree'), - ); - $results = array(); - - // Test Syntax - foreach ($dbs as $type => $db) { - $test = new \Test_Syntax(); - $results = array_merge((array) $results, (array) $test->run($db, $type)); - } - - // Test Relations - foreach ($dbs as $type => $db) { - $f3->set('DB',$db); - $test = new \Test_Relation(); - $results = array_merge((array) $results, (array) $test->run($db, $type)); - } - - // Test Filter - foreach ($dbs as $type => $db) { - $f3->set('DB',$db); - $test = new \Test_Filter(); - $results = array_merge((array) $results, (array) $test->run($db, $type)); - } - - // Further Common Tests - if (isset($dbs['sql'])) { - $test = new \Test_Common(); - $f3->set('DB', $dbs['sql']); - $results = array_merge((array) $results, (array) $test->run()); - } - $f3->set('results', $results); - } - - -} \ No newline at end of file diff --git a/app/app/cortex/authormodel.php b/app/app/cortex/authormodel.php deleted file mode 100644 index 0aa19264d..000000000 --- a/app/app/cortex/authormodel.php +++ /dev/null @@ -1,27 +0,0 @@ - array( - 'type' => \DB\SQL\Schema::DT_VARCHAR256 - ), - 'mail' => array( - 'type' => \DB\SQL\Schema::DT_VARCHAR256 - ), - 'website' => array( - 'type' => \DB\SQL\Schema::DT_VARCHAR256 - ), - 'news' => array( - 'has-many' => array('\NewsModel','author'), - ), - 'profile' => array( - 'has-one' => array('\ProfileModel','author'), - ), - ), -// $primary = 'aid', - $table = 'author', - $db = 'DB'; - -} diff --git a/app/app/cortex/newsmodel.php b/app/app/cortex/newsmodel.php deleted file mode 100644 index 33b6ccff6..000000000 --- a/app/app/cortex/newsmodel.php +++ /dev/null @@ -1,28 +0,0 @@ - array( - 'type' => \DB\SQL\Schema::DT_VARCHAR128 - ), - 'text' => array( - 'type' => \DB\SQL\Schema::DT_TEXT - ), - 'author' => array( - 'belongs-to-one' => '\AuthorModel', - ), - 'tags' => array( - 'belongs-to-many' => '\TagModel', - ), - 'tags2' => array( - 'has-many' => array('\TagModel','news','news_tags'), -// 'has-many' => array('\TagModel','news'), - ), - ), -// $primary='nid', - $table = 'news', - $db = 'DB'; - -} \ No newline at end of file diff --git a/app/app/cortex/profilemodel.php b/app/app/cortex/profilemodel.php deleted file mode 100644 index e749cf33d..000000000 --- a/app/app/cortex/profilemodel.php +++ /dev/null @@ -1,21 +0,0 @@ - array( - 'type' => \DB\SQL\Schema::DT_TEXT - ), - 'image' => array( - 'type' => \DB\SQL\Schema::DT_VARCHAR256 - ), - 'author' => array( - 'belongs-to-one' => '\AuthorModel' - ) - ), -// $primary = 'profile_id', - $table = 'profile', - $db = 'DB'; - -} \ No newline at end of file diff --git a/app/app/cortex/tagmodel.php b/app/app/cortex/tagmodel.php deleted file mode 100644 index 668bd34e5..000000000 --- a/app/app/cortex/tagmodel.php +++ /dev/null @@ -1,18 +0,0 @@ - array( - 'type' => \DB\SQL\Schema::DT_VARCHAR128 - ), - 'news' => array( - 'has-many' => array('\NewsModel','tags2','news_tags'), - ), - ), -// $primary = 'tid', - $table = 'tags', - $db = 'DB'; - -} \ No newline at end of file diff --git a/app/app/cortex/test_common.php b/app/app/cortex/test_common.php deleted file mode 100644 index bdcc74779..000000000 --- a/app/app/cortex/test_common.php +++ /dev/null @@ -1,125 +0,0 @@ -load(); - - $dummy = array( - 'title'=>'copy test', - 'text'=>'Lorem ipsum dolor sit amet.', - 'author'=>1, - 'tags'=>array(3) - ); - $f3->set('record1', $dummy); - $news->copyto('record2'); - - $test->expect( - $f3->exists('record2'), - 'copyto: raw record copied to hive' - ); - - $news->reset(); - - $news->copyfrom('record1'); - - $test->expect( - $news->title = 'copy test' && - $news->text = 'Lorem ipsum dolor sit amet.', - 'copyfrom: hydrate from hive key' - ); - $test->expect( - $news->author instanceof AuthorModel - && !$news->author->dry() && - $news->tags instanceof \DB\CortexCollection, - 'copyfrom: relations hydrated successful' - ); - - $test->expect( - $news->get('author',true) == 1, - 'get raw data from relational field' - ); - - $news->reset(); - $news->copyfrom('record2','title;author'); - - $test->expect( - $news->title = 'Responsive Images' && - $news->get('author',true) == 2 && - $news->text == NULL, - 'copyfrom: limit fields with split-able string' - ); - - $news->reset(); - $news->copyfrom('record2',array('title')); - - $test->expect( - $news->title = 'Responsive Images' && $news->text == NULL, - 'copyfrom: limit fields by array' - ); - - $news->reset(); - $news->copyfrom($dummy,function($fields) { - return array_intersect_key($fields,array_flip(array('title'))); - }); - - $test->expect( - $news->title = 'copy test', - 'copyfrom: copy from array instead of hive key' - ); - - $test->expect( - $news->title = 'copy test' && $news->text == NULL, - 'copyfrom: limit fields by callback function' - ); - - $all = $news->find(); - $allTitle = $all->getAll('title'); - - $test->expect( - count($allTitle) == 3 && - $allTitle[0] == 'Responsive Images' && - $allTitle[1] == 'CSS3 Showcase' && - $allTitle[2] == 'Touchable Interfaces', - 'collection getAll returns all values of selected field' - ); - - $newsByID = $all->getBy('_id'); - $test->expect( - array_keys($newsByID) == array(1,2,3), - 'collection getBy sorts by given field' - ); - - $newsByAuthorID = $all->getBy('author',true); - $test->expect( - array_keys($newsByAuthorID) == array(2, 1) && - count($newsByAuthorID[2]) == 2 && - count($newsByAuthorID[1]) == 1, - 'collection getBy nested sort by author' - ); - - $allTitle = array(); - foreach($all as $record) - $allTitle[] = $record->title; - - $test->expect( - count($allTitle) == 3 && - $allTitle[0] == 'Responsive Images' && - $allTitle[1] == 'CSS3 Showcase' && - $allTitle[2] == 'Touchable Interfaces', - 'collection is traversable' - ); - - - /////////////////////////////////// - return $test->results(); - } -} \ No newline at end of file diff --git a/app/app/cortex/test_filter.php b/app/app/cortex/test_filter.php deleted file mode 100644 index 9047cfea4..000000000 --- a/app/app/cortex/test_filter.php +++ /dev/null @@ -1,302 +0,0 @@ -find()->getAll('_id'); - $all = $news->find(); - $newsIDs = $all->getAll('_id'); - $profileIDs = $profile->find()->getAll('_id'); - $tagIDs = $tag->find()->getAll('_id'); - - // add another relation - $news->load(array('title = ?','CSS3 Showcase')); - $news->author = $author->load(array($author_pk.' = ?',$authorIDs[0])); - $news->save(); - $news->reset(); - $author->reset(); - - - // has-filter on belongs-to relation - /////////////////////////////////// - - $result = $author->has('news', array('title like ?', '%Image%'))->afind(); - - $test->expect( - count($result) == 1 && - $result[0]['name'] == 'Johnny English', - $type.': has filter on many-to-one field' - ); - $test->expect( - count($result[0]['news']) == 2 && - $result[0]['news'][0]['title'] == 'Responsive Images' && - $result[0]['news'][1]['title'] == 'CSS3 Showcase', - $type.': has filter does not prune relation set' - ); - - $result = $news->has('author', array('name = ?', 'Johnny English'))->afind(); - $test->expect( - count($result) == 2 && // has 2 news - $result[0]['title'] == 'Responsive Images' && - $result[1]['title'] == 'CSS3 Showcase', - $type.': has filter on one-to-many field' - ); - - // add another profile - $profile->message = 'Beam me up, Scotty!'; - $profile->author = $authorIDs[2]; - $profile->save(); - $profile->reset(); - - $result = $author->has('profile',array('message LIKE ?','%Scotty%'))->afind(); - $test->expect( - count($result) == 1 && - $result[0]['name'] == 'James T. Kirk' && - $result[0]['profile']['message'] == 'Beam me up, Scotty!', - $type.': has filter on one-to-one field' - ); - - $result = $profile->has('author',array('name LIKE ?','%Kirk%'))->afind(); - $test->expect( - count($result) == 1 && - $result[0]['message'] == 'Beam me up, Scotty!' && - $result[0]['author']['name'] == 'James T. Kirk', - $type.': has filter on one-to-one field, inverse' - ); - - // add mm tags - $news->load(array('title = ?','Responsive Images')); - $news->tags2 = array($tagIDs[0],$tagIDs[1]); - $news->save(); - $news->load(array('title = ?','CSS3 Showcase')); - $news->tags2 = array($tagIDs[1],$tagIDs[2]); - $news->save(); - $news->reset(); - - $result = $news->has('tags2',array('title like ?','%Design%'))->find(); - $test->expect( - count($result) == 1 && - $result[0]['title'] == 'Responsive Images', - $type.': has filter on many-to-many field' - ); - - $result = $news->has('tags2',array('title = ?','Responsive'))->find(); - $test->expect( - count($result) == 2 && - $result[0]['title'] == 'Responsive Images' && - $result[1]['title'] == 'CSS3 Showcase', - $type.': has filter on many-to-many field, additional test' - ); - - - $result = $tag->has('news',array('title = ?','Responsive Images'))->find(); - $test->expect( - count($result) == 2 && - $result[0]['title'] == 'Web Design' && - $result[1]['title'] == 'Responsive', - $type.': has filter on many-to-many field, inverse' - ); - - // add another tag - $news->load(array('title = ?', 'Touchable Interfaces')); - $news->tags2 = array($tagIDs[1]); - $news->save(); - $news->reset(); - - $tag->has('news',array('text LIKE ? and title LIKE ?', '%Lorem%', '%Interface%')); - $result = $tag->find(); - $test->expect( - count($result) == 1 && - $result[0]['title'] == 'Responsive', - $type.': has filter with multiple conditions' - ); - - $news->has('tags2', array('title = ? OR title = ?', 'Usability', 'Web Design')); - $result = $news->afind(array('text = ?', 'Lorem Ipsun')); - $test->expect( - count($result) == 1 && - $result[0]['title'] == 'Responsive Images', - $type.': find with condition and has filter' - ); - - $news->load(array('title = ?', 'Responsive Images')); - $news->author = $authorIDs[1]; - $news->save(); - $news->reset(); - - - $news->has('tags2', array('title = ? OR title = ?', 'Usability', 'Web Design')); - $news->has('author', array('name = ?', 'Ridley Scott')); - $result = $news->afind(); - $test->expect( - count($result) == 1 && - $result[0]['title'] == 'Responsive Images', - $type.': find with multiple has filters on different relations' - ); - - // add another news to author 2 - $news->load(array($news_pk.' = ?',$newsIDs[2])); - $news->author = $authorIDs[1]; - $news->save(); - - $news->reset(); - $news->has('author', array('name = ?', 'Ridley Scott')); - $news->load(); - $res = array(); - while (!$news->dry()) { - $res[] = $news->title; - $news->next(); - } - - $test->expect( - count($res) == 2 && - $res[0] == 'Responsive Images' && - $res[1] == 'Touchable Interfaces' - , - $type.': has filter in load context' - ); - - $news->reset(); - $news->fields(array('title')); - $news->load(); - - $test->expect( - !empty($news->title) && - empty($news->author) && - empty($news->text) && - empty($news->tags) && - empty($news->tags2), - $type.': use a whitelist to restrict fields' - ); - - unset($news); - $news = new \NewsModel(); - - $news->fields(array('title','tags','tags2','author'),true); - $news->load(); - - $test->expect( - empty($news->title) && - empty($news->author) && - !empty($news->text) && - empty($news->tags) && - empty($news->tags2), - $type.': use a blacklist to restrict fields' - ); - - unset($news); - $news = new \NewsModel(); - - $news->fields(array('tags.title')); - $news->load(); - - $test->expect( - !empty($news->tags[0]->title) && - empty($news->tags[0]->news), - $type.': set restricted fields to related mappers' - ); - - $news->filter('tags2',null,array('order'=>'title ASC')); - $news->load(array('title = ?','Responsive Images')); - $test->expect( - $news->tags2[0]->title == 'Responsive' && - $news->tags2[1]->title == 'Web Design', - $type.': filter with sorting of related records' - ); - - // get all tags sorted by their usage in news articles - $tag->reset(); - $tag->countRel('news'); - $result = $tag->find(null,array('order'=>'count_news DESC, title'))->castAll(0); - - $test->expect( - $result[0]['title'] == 'Responsive' && - $result[0]['count_news'] == 3 && - $result[1]['title'] == 'Usability' && - $result[1]['count_news'] == 1 && - $result[2]['title'] == 'Web Design' && - $result[2]['count_news'] == 1, - $type.': count and sort on many-to-many relation' - ); - - // get all authors sorted by the amount of news they have written - $author->reset(); - $author->countRel('news'); - $result = $author->find(null,array('order'=>'count_news DESC'))->castAll(0); - - $test->expect( - $result[0]['name'] == 'Ridley Scott' && - $result[0]['count_news'] == 2 && - $result[1]['name'] == 'Johnny English' && - $result[1]['count_news'] == 1 && - $result[2]['name'] == 'James T. Kirk' && - $result[2]['count_news'] == null, - $type.': count and sort on one-to-many relation' - ); - - $tag->reset(); - $tag->countRel('news'); - $result = $tag->find(null,array('order'=>'count_news DESC, title DESC','limit'=>1,'offset'=>1))->castAll(0); - - $test->expect( - $result[0]['title'] == 'Web Design' && - $result[0]['count_news'] == 1, - $type.': apply limit and offset on aggregated collection' - ); - - - $author->reset(); - $author->countRel('news'); - $author->has('news',array('text like ?','%Lorem%')); - $result = $author->find()->castAll(0); - - $test->expect( - count($result) == 1 && - $result[0]['name'] == 'Ridley Scott' && - $result[0]['count_news'] == 2 , - $type.': has-filter and 1:M relation counter' - ); - - - $author->reset(); - $id = $author->load()->next()->_id; - $tag->reset(); - $tag->countRel('news'); - $tag->has('news',array('author = ?',$id)); - $result = $tag->find(null,array('order'=>'count_news desc'))->castAll(0); - - $test->expect( - count($result) == 2 && - $result[0]['title'] == 'Responsive' && - $result[0]['count_news'] == 3 && - $result[1]['title'] == 'Web Design' && - $result[1]['count_news'] == 1, - $type.': has-filter and M:M relation counter' - ); - - /////////////////////////////////// - return $test->results(); - } - -} \ No newline at end of file diff --git a/app/app/cortex/test_relation.php b/app/app/cortex/test_relation.php deleted file mode 100644 index 08d319ab6..000000000 --- a/app/app/cortex/test_relation.php +++ /dev/null @@ -1,335 +0,0 @@ -cast(); - unset($row['_id']); - unset($row['id']); - unset($row['aid']); - unset($row['uid']); - unset($row['nid']); - unset($row['tid']); - unset($row['pid']); - unset($row['profile_id']); - foreach ($row as $col => $val) { - if (empty($val) || is_null($val)) - unset($row[$col]); - } - $out[] = $row; - } - return $out; - } - - function run($db,$type) - { - $test = new \Test(); - - // clear existing data - \AuthorModel::setdown(); - \TagModel::setdown(); - \NewsModel::setdown(); - \ProfileModel::setdown(); - - // setup models - \AuthorModel::setup(); - \TagModel::setup(); - \NewsModel::setup(); - \ProfileModel::setup(); - - // setup Author - /////////////////////////////////// - $author_id = array(); - - $author = new \AuthorModel(); - $ac=$author::resolveConfiguration(); - $author_pk = (is_int(strpos($type,'sql'))?$ac['primary']:'_id'); - - $author->name = 'Johnny English'; - $author->save(); - $author_id[] = $author->_id; - $author->reset(); - $author->name = 'Ridley Scott'; - $author->save(); - $author_id[] = $author->_id; - $author->reset(); - $author->name = 'James T. Kirk'; - $author->save(); - $author_id[] = $author->_id; - $author->reset(); - - $allauthors = $author->find()->castAll(); - $allauthors = $this->getResult($allauthors); - $test->expect( - json_encode($allauthors) == - '[{"name":"Johnny English"},{"name":"Ridley Scott"},{"name":"James T. Kirk"}]', - $type.': all AuthorModel items created' - ); - - // setup Tags - /////////////////////////////////// - $tag_id = array(); - - $tag = new \TagModel(); - $tc=$tag::resolveConfiguration(); - $tag_pk = (is_int(strpos($type,'sql'))?$tc['primary']:'_id'); - - $tag->title = 'Web Design'; - $tag->save(); - $tag_id[] = $tag->_id; - $tag->reset(); - $tag->title = 'Responsive'; - $tag->save(); - $tag_id[] = $tag->_id; - $tag->reset(); - $tag->title = 'Usability'; - $tag->save(); - $tag_id[] = $tag->_id; - $tag->reset(); - - $allTags = $this->getResult($tag->find()); - $test->expect( - json_encode($allTags) == - '[{"title":"Web Design"},{"title":"Responsive"},{"title":"Usability"}]', - $type.': all TagModel items created' - ); - - // setup News - /////////////////////////////////// - $news_id = array(); - - $news = new \NewsModel(); - $nc=$news::resolveConfiguration(); - $news_pk = (is_int(strpos($type,'sql'))?$nc['primary']:'_id'); - - $news->title = 'Responsive Images'; - $news->text = 'Lorem Ipsun'; - $news->save(); - $news_id[] = $news->_id; - $news->reset(); - $news->title = 'CSS3 Showcase'; - $news->text = 'News Text 2'; - $news->save(); - $news_id[] = $news->_id; - $news->reset(); - $news->title = 'Touchable Interfaces'; - $news->text = 'Lorem Foo'; - $news->save(); - $news_id[] = $news->_id; - $news->reset(); - - $allnews = $this->getResult($news->find()); - $test->expect( - json_encode($allnews) == - '[{"title":"Responsive Images","text":"Lorem Ipsun"},{"title":"CSS3 Showcase","text":"News Text 2"},{"title":"Touchable Interfaces","text":"Lorem Foo"}]', - $type.': all NewsModel items created' - ); - - // belongs-to author relation - /////////////////////////////////// - - $author->load(); - $news->load(array($news_pk.' = ?',$news_id[0])); - $news->author = $author; - $news->save(); - $news->reset(); - $news->load(array($news_pk.' = ?', $news_id[0])); - $test->expect( - $news->author->name == 'Johnny English', - $type.': belongs-to-one: author relation created' - ); - - $news->author = NULL; - $news->save(); - $news->reset(); - $news->load(array($news_pk.' = ?', $news_id[0])); - $test->expect( - empty($news->author), - $type.': belongs-to-one: author relation released' - ); - - $news->author = $author->_id; - $news->save(); - $news->reset(); - $news->load(array($news_pk.' = ?', $news_id[0])); - $test->expect( - $news->author->name == 'Johnny English', - $type.': belongs-to-one: relation created by raw id' - ); - - // belongs-to-many tag relation - /////////////////////////////////// - - $tag1 = new \TagModel(); - $tag1->load(array($tag_pk.' = ?', $tag_id[0])); - $tag2 = new \TagModel(); - $tag2->load(array($tag_pk.' = ?', $tag_id[1])); - $news->tags = array($tag1,$tag2); - $news->save(); - $news->reset(); - $news->load(array($news_pk.' = ?', $news_id[0])); - $test->expect( - $news->tags[0]->title == 'Web Design' && $news->tags[1]->title == 'Responsive', - $type.': belongs-to-many: relations created with array of mapper objects' - ); - - $news->reset(); - $news->load(array($news_pk.' = ?', $news_id[1])); - $news->tags = array($tag_id[1],$tag_id[2]); - $news->save(); - $news->reset(); - $news->load(array($news_pk.' = ?', $news_id[1])); - $test->expect( - $news->tags[0]->title == 'Responsive' && $news->tags[1]->title == 'Usability', - $type.': belongs-to-many: relations created with array of IDs' - ); - - $news->tags = null; - $news->save(); - $news->reset(); - $news->load(array($news_pk.' = ?', $news_id[1])); - $test->expect( - empty($news->tags), - $type.': belongs-to-many: relations released' - ); - - $tag->reset(); - $news->load(array($news_pk.' = ?', $news_id[1])); - $news->tags = $tag->load(array($tag_pk.' != ?',$tag_id[0])); - $news->save(); - $news->reset(); - $news->load(array($news_pk.' = ?', $news_id[1])); - $test->expect( - $news->tags[0]->title == 'Responsive' && $news->tags[1]->title == 'Usability', - $type.': belongs-to-many: relations created with hydrated mapper' - ); - - - $news->reset(); - $tag->reset(); - $news->load(array($news_pk.' = ?', $news_id[2])); - $news->tags = $tag_id[0].';'.$tag_id[2]; - $news->save(); - $news->reset(); - $news->load(array($news_pk.' = ?', $news_id[2])); - $test->expect( - $news->tags[0]->title == 'Web Design' && $news->tags[1]->title == 'Usability', - $type.': belongs-to-many: relations created with split-able string' - ); - $test->expect( - is_object($news->tags) && $news->tags instanceof \DB\CortexCollection, - $type.': belongs-to-many: result is collection' - ); - - - // has-one relation - /////////////////////////////////// - $profile = new ProfileModel(); - $pc=$profile::resolveConfiguration(); - $profile_pk = (is_int(strpos($type,'sql'))?$pc['primary']:'_id'); - - $profile->message = 'Hello World'; - $profile->author = $author->load(array($author_pk.' = ?',$author_id[0])); - $profile->save(); - $profile_id = $profile->_id; - $profile->reset(); - $author->reset(); - $author->load(array($author_pk.' = ?', $author_id[0])); - $profile->load(array($profile_pk.' = ?', $profile_id)); - $test->expect( - $author->profile->message == 'Hello World' && - $profile->author->name == "Johnny English", - $type.': has-one: relation assigned' - ); - - $profile->reset(); - $profile->message = 'I\'m feeling lucky'; - $profile->image = 'lolcat.jpg'; - $author->reset(); - $author->load(array($author_pk.' = ?',$author_id[1])); - $author->profile = $profile; - $author->save(); - $profile->reset(); - $author->reset(); - $author->load(array($author_pk.' = ?', $author_id[1])); - $test->expect( - $author->profile->message == 'I\'m feeling lucky', - $type.': has-one: inverse relation' - ); - - - // has-many relation - /////////////////////////////////// - - $author->load(array($author_pk.' = ?', $author_id[0])); - $result = $this->getResult($author->news); - $test->expect( - $result[0]['title'] == "Responsive Images" && - $result[0]['tags'][0]['title'] == 'Web Design' && - $result[0]['tags'][1]['title'] == 'Responsive', - $type.': has-many inverse relation' - ); - - // many to many relation - /////////////////////////////////// - - $news->load(array($news_pk.' = ?',$news_id[0])); - $news->tags2 = array($tag_id[0],$tag_id[1]); - $news->save(); - $news->reset(); - $news->load(array($news_pk.' = ?',$news_id[0])); - $test->expect( - $news->tags2[0]['title'] == 'Web Design' && - $news->tags2[1]['title'] == 'Responsive', - $type.': many-to-many relation created' - ); - - $test->expect( - is_object($news->tags2) && $news->tags2 instanceof \DB\CortexCollection, - $type.': many-to-many: result is collection' - ); - - $news->load(array($news_pk.' = ?', $news_id[0])); - $news->tags2 = NULL; - $news->save(); - $news->reset(); - $news->load(array($news_pk.' = ?', $news_id[0])); - $test->expect( - is_null($news->tags2), - $type.': many-to-many relation released' - ); - - $all = $news->find(); - $test->expect( - $all[1]->tags2 === NULL - && $all[2]->author === NULL, - $type.': empty relations are NULL' - ); - - $arr = $news->cast(); - $test->expect( - is_array($arr['tags']), - $type.': collection becomes array in casted model' - ); - - if ($type == 'mongo') { - $test->expect( - is_string($arr['_id']), - $type.': id becomes string in casted model' - ); - } - - /////////////////////////////////// - return $test->results(); - } - -} \ No newline at end of file diff --git a/app/app/cortex/test_syntax.php b/app/app/cortex/test_syntax.php deleted file mode 100644 index 9efa7e28c..000000000 --- a/app/app/cortex/test_syntax.php +++ /dev/null @@ -1,364 +0,0 @@ - array('type' => \DB\SQL\Schema::DT_TEXT), - 'num1' => array('type' => \DB\SQL\Schema::DT_INT4), - 'num2' => array('type' => \DB\SQL\Schema::DT_INT4), - ); - \DB\Cortex::setup($db, $tname, $fields); - - // adding some testing data - $cx = new \DB\Cortex($db, $tname); - $cx->title = 'bar1'; - $cx->save(); - $cx->reset(); - - $cx->title = 'baz2'; - $cx->num1 = 1; - $cx->save(); - $cx->reset(); - - $cx->title = 'foo3'; - $cx->num1 = 4; - $cx->save(); - $cx->reset(); - - $cx->title = 'foo4'; - $cx->num1 = 3; - $cx->save(); - $cx->reset(); - - $cx->title = 'foo5'; - $cx->num1 = 3; - $cx->num2 = 5; - $cx->save(); - $cx->reset(); - - $cx->title = 'foo6'; - $cx->num1 = 3; - $cx->num2 = 1; - $cx->save(); - $cx->reset(); - - $cx->title = 'foo7'; - $cx->num1 = 3; - $cx->num2 = 10; - $cx->save(); - $cx->reset(); - - $cx->title = 'foo8'; - $cx->num1 = 5; - $cx->save(); - $cx->reset(); - - $cx->title = 'foo9'; - $cx->num1 = 8; - $cx->save(); - $cx->reset(); - - $result = $this->getResult($cx->find()); - - $expected = array( - 0 => array( - 'title' => 'bar1', - ), - 1 => array( - 'num1' => 1, - 'title' => 'baz2', - ), - 2 => array( - 'num1' => 4, - 'title' => 'foo3', - ), - 3 => array( - 'num1' => 3, - 'title' => 'foo4', - ), - 4 => array( - 'num1' => 3, - 'num2' => 5, - 'title' => 'foo5', - ), - 5 => array( - 'num1' => 3, - 'num2' => 1, - 'title' => 'foo6', - ), - 6 => array( - 'num1' => 3, - 'num2' => 10, - 'title' => 'foo7', - ), - 7 => array( - 'num1' => 5, - 'title' => 'foo8', - ), - 8 => array( - 'num1' => 8, - 'title' => 'foo9', - ), - ); - - $test->expect( - json_encode($result) == json_encode($expected), - $type.': init mapper, adding records' - ); - - // operator = - $result = $this->getResult($cx->find(array('title = ?', 'foo7'))); - $expected = array( - 0 => array( - 'num1' => 3, - 'num2' => 10, - 'title' => 'foo7', - ), - ); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': operator check: =' - ); - - // operator > - $result = $this->getResult($cx->find(array('num1 > ?', 4))); - $expected = array( - 0 => array( - 'num1' => 5, - 'title' => 'foo8', - ), - 1 => array( - 'num1' => 8, - 'title' => 'foo9', - ), - ); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': operator check: >' - ); - - // operator >= - $result = $this->getResult($cx->find(array('num1 >= ?', 5))); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': operator check: >=' - ); - - // operator < - $result = $this->getResult($cx->find(array('num2 < ?', 2))); - $expected = array( - 0 => array( - 'num1' => 3, - 'num2' => 1, - 'title' => 'foo6', - ), - ); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': operator check: <' - ); - - // operator <= - $result = $this->getResult($cx->find(array('num2 <= ?', 1))); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': operator check: <=' - ); - - // operator without binding - $result = $this->getResult($cx->find(array('num1 > 4'))); - $expected = array( - 0 => array( - 'num1' => 5, - 'title' => 'foo8', - ), - 1 => array( - 'num1' => 8, - 'title' => 'foo9', - ), - ); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': operator without binding' - ); - - // field comparision - $result = $this->getResult($cx->find( - array('num2 > num1', 1))); - $expected = array( - 0 => array( - 'num1' => 3, - 'num2' => 5, - 'title' => 'foo5', - ), - 1 => array( - 'num1' => 3, - 'num2' => 10, - 'title' => 'foo7', - ), - ); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': check field comparision' - ); - - // lookahead search - $result = $this->getResult($cx->find(array('title like ?', '%o6'))); - $expected = array( - 0 => array( - 'num1' => 3, - 'num2' => 1, - 'title' => 'foo6', - ), - ); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': lookahead search' - ); - - // lookbehind search - $result = $this->getResult($cx->find(array('title like ?', 'bar%'))); - $expected = array( - 0 => array( - 'title' => 'bar1', - ), - ); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': lookbehind search' - ); - - // full search - $result = $this->getResult($cx->find(array('title like ?', '%a%'))); - $expected = array( - 0 => array( - 'title' => 'bar1', - ), - 1 => array( - 'num1' => 1, - 'title' => 'baz2', - ), - ); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': full search' - ); - - // negated search - $result = $this->getResult($cx->find(array('title not like ?', 'foo%'))); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': negated search' - ); - - // AND / OR chaining - $result = $this->getResult($cx->find( - array('(num2 < ? AND num1 > ?) OR title like ?', 2, 1, '%o9'))); - $expected = array( - 0 => array( - 'num1' => 3, - 'num2' => 1, - 'title' => 'foo6', - ), - 1 => array( - 'num1' => 8, - 'title' => 'foo9', - ), - ); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': check logical operator chaining' - ); - - // check limit - $result = $this->getResult($cx->find( - null, array('limit' => '2'))); - $expected = array( - 0 => array( - 'title' => 'bar1', - ), - 1 => array( - 'num1' => 1, - 'title' => 'baz2', - ), - ); - $test->expect( - json_encode($result) == json_encode($expected), - $type.': check limit' - ); - - // check order - $result = $this->getResult($cx->find( - array('num2 >= ?', 1), array('order' => 'num2 desc'))); - $expected = array( - 0 => array( - 'num1' => 3, - 'num2' => 10, - 'title' => 'foo7', - ), - 1 => array( - 'num1' => 3, - 'num2' => 5, - 'title' => 'foo5', - ), - 2 => array( - 'num1' => 3, - 'num2' => 1, - 'title' => 'foo6', - ), - ); - - $test->expect( - json_encode($result) == json_encode($expected), - $type.': check order' - ); - - // IN search - $rc = $cx->find(array('num1 IN ?',array(4,5,8))); - $result = $rc->getAll('title'); - sort($result); - $test->expect( - json_encode($result) == json_encode(array('foo3','foo8','foo9')), - $type.': IN operator' - ); - - $rc = $cx->find(array('num1 IN ? && num2 > ? && num2 NOT IN ?',array(3,4),1,array(10))); - $result = $rc->getAll('title'); - $test->expect( - json_encode($result) == json_encode(array('foo5')), - $type.': enhanced IN, NOT IN operator' - ); - - /////////////////////////////////// - return $test->results(); - } - - /** - * unify results for better comparison - */ - private function getResult($result) - { - $out = array(); - foreach ($result as $row) { - $row = $row->cast(); - unset($row['_id']); - unset($row['id']); - ksort($row); - foreach ($row as $col => $val) { - if (empty($val) || is_null($val)) - unset($row[$col]); - } - $out[] = $row; - } - return $out; - } -} \ No newline at end of file diff --git a/app/app/schema.php b/app/app/schema.php deleted file mode 100644 index ea6bbfd75..000000000 --- a/app/app/schema.php +++ /dev/null @@ -1,544 +0,0 @@ -f3->get('timer') - $this->roundTime; - $this->roundTime = microtime(TRUE) - $this->f3->get('timer'); - return ' [ '.sprintf('%.3f', $time).'s ]'; - } - - private function getTestDesc($text) - { - return $this->getTime().' '.$this->current_engine.': #'.$this->current_test++.' - '.$text; - } - - function get() - { - $this->f3 = \Base::instance(); - $this->test = new \Test; - - $this->f3->set('QUIET', false); - $this->f3->set('CACHE', false); - - $dbs = array( - /*'mysql' => new \DB\SQL( - 'mysql:host=localhost;port=3306;dbname=fatfree', 'fatfree', '' - ),*/ - 'sqlite' => new \DB\SQL( - 'sqlite::memory:' - // 'sqlite:db/sqlite.db' - ), - /*'pgsql' => new \DB\SQL( - 'pgsql:host=localhost;dbname=fatfree', 'fatfree', 'fatfree' - ),*/ - /*'sqlsrv2012' => new \DB\SQL( - 'sqlsrv:SERVER=LOCALHOST\SQLEXPRESS2012;Database=fatfree','fatfree', 'fatfree' - ),*/ - /*'sqlsrv2008' => new \DB\SQL( - 'sqlsrv:SERVER=LOCALHOST\SQLEXPRESS2008;Database=fatfree','fatfree', 'fatfree' - )*/ - ); - - $this->roundTime = microtime(TRUE) - \Base::instance()->get('timer'); - $this->tname = 'test_table'; - - foreach ($dbs as $type => $db) { - $this->current_engine = $type; - $this->runTestSuite($db); - $this->current_test = 1; - } - $this->f3->set('results', $this->test->results()); - } - - private function runTestSuite($db) - { - $schema = new \DB\SQL\Schema($db); - - $schema->dropTable($this->tname); - - // create table - $table = $schema->createTable($this->tname); - $table = $table->build(); - $result = $schema->getTables(); - $this->test->expect( - in_array($this->tname, $result), - $this->getTestDesc('create default table') - ); - unset($result); - - $this->test->expect( - $table instanceof \DB\SQL\TableModifier, - $this->getTestDesc('$table->build() returns TableModifier') - ); - - // drop table - $table->drop(); - $this->test->expect( - in_array($this->tname, $schema->getTables()) == false, - $this->getTestDesc('drop table') - ); - unset($table); - - // create table with columns - $table = $schema->createTable($this->tname); - $table->addColumn('title')->type($schema::DT_VARCHAR128); - $table->addColumn('number')->type($schema::DT_INT4); - $table = $table->build(); - $r1 = $schema->getTables(); - $r2 = $table->getCols(); - $this->test->expect( - in_array($this->tname, $r1) && in_array('id', $r2) - && in_array('title', $r2) && in_array('number', $r2), - $this->getTestDesc('create new table with additional columns') - ); - unset($r1,$r2); - - // testing all datatypes - foreach (array_keys($schema->dataTypes) as $index => $field) { - // testing column type - $table->addColumn('column_'.$index)->type($field); - $table->build(); - $r1 = $table->getCols(); - $this->test->expect( - in_array('column_'.$index, $r1), - $this->getTestDesc('adding column ['.$field.'], nullable') - ); - } - unset($r1); - - // adding some testing data - $mapper = new \DB\SQL\Mapper($db, $this->tname); - $mapper->column_7 = 'hello world'; - $mapper->save(); - $mapper->reset(); - $result = $mapper->findone(array('column_7 = ?', 'hello world'))->cast(); - unset($mapper); - $this->test->expect( - $result['column_7'] == 'hello world', - $this->getTestDesc('mapping dummy data') - ); - - // default value text, not nullable - $table->addColumn('text_default_not_null') - ->type($schema::DT_VARCHAR128) - ->nullable(false)->defaults('foo bar'); - $table->build(); - $r1 = $table->getCols(true); - $this->test->expect( - in_array('text_default_not_null', array_keys($r1)) && - $r1['text_default_not_null']['default'] == 'foo bar' && - $r1['text_default_not_null']['nullable'] == false, - $this->getTestDesc('adding column [VARCHAR128], not nullable with default value') - ); - unset($r1); - - // some testing dummy data - $mapper = new \DB\SQL\Mapper($db, $this->tname); - $mapper->column_7 = 'tanduay'; - $mapper->save(); - $mapper->reset(); - $result = $mapper->findone(array('column_7 = ?','tanduay'))->cast(); - $this->test->expect( - $result['column_7'] == 'tanduay' && - $result['text_default_not_null'] == 'foo bar', - $this->getTestDesc('mapping dummy data') - ); - unset($mapper,$result); - - // default value numeric, not nullable - $table->addColumn('int_default_not_null') - ->type($schema::DT_INT4)->nullable(false)->defaults(123); - $table->build(); - $r1 = $table->getCols(true); - $this->test->expect( - in_array('int_default_not_null', array_keys($r1)) && - $r1['int_default_not_null']['default'] == 123 && - $r1['int_default_not_null']['nullable'] == false, - $this->getTestDesc('adding column [INT4], not nullable with default value') - ); - unset($r1); - - // adding testing data - $mapper = new \DB\SQL\Mapper($db, $this->tname); - $mapper->column_7 = 'test3'; - $mapper->save(); - $mapper->reset(); - $r1 = $mapper->findone(array('column_7 = ?','test3'))->cast(); - $this->test->expect( - $r1['column_7'] == 'test3' && - $r1['int_default_not_null'] == 123, - $this->getTestDesc('mapping dummy data') - ); - unset($mapper,$r1); - - - // default value text, nullable - $table->addColumn('text_default_nullable') - ->type($schema::DT_VARCHAR128) - ->defaults('foo bar'); - $table->build(); - $r1 = $table->getCols(true); - $this->test->expect( - in_array('text_default_nullable', array_keys($r1)) && - $r1['text_default_nullable']['default'] == 'foo bar', - $this->getTestDesc('adding column [VARCHAR128], nullable with default value') - ); - unset($r1); - - // adding some dummy data - $mapper = new \DB\SQL\Mapper($db, $this->tname); - $mapper->column_7 = 'test4'; - $mapper->save(); - $mapper->reset(); - $mapper->column_7 = 'test5'; - $mapper->text_default_nullable = null; - $mapper->save(); - $mapper->reset(); - $result = $mapper->find(array('column_7 = ? OR column_7 = ?','test4','test5')); - foreach ($result as &$r) - $r = $r->cast(); - - $this->test->expect( - array_key_exists(0, $result) && array_key_exists(1, $result) && - $result[0]['column_7'] == 'test4' && $result[0]['text_default_nullable'] == 'foo bar' && - $result[1]['column_7'] == 'test5' && $result[1]['text_default_nullable'] === null, - $this->getTestDesc('mapping dummy data') - ); - unset($mapper, $result); - - // default value numeric, nullable - $table->addColumn('int_default_nullable')->type($schema::DT_INT4)->defaults(123); - $table->build(); - $r1 = $table->getCols(true); - $this->test->expect( - in_array('int_default_nullable', array_keys($r1)) == true && - $r1['int_default_nullable']['default'] == 123, - $this->getTestDesc('adding column [INT4], nullable with default value') - ); - unset($r1); - - // adding dummy data - $mapper = new \DB\SQL\Mapper($db, $this->tname); - $mapper->column_7 = 'test6'; - $mapper->save(); - $mapper->reset(); - $mapper->column_7 = 'test7'; - $mapper->int_default_nullable = null; - $mapper->save(); - $mapper->reset(); - $result = $mapper->find(array('column_7 = ? OR column_7 = ?', 'test6', 'test7')); - foreach ($result as &$r) - $r = $r->cast(); - - $this->test->expect( - array_key_exists(0, $result) && array_key_exists(1, $result) && - $result[0]['column_7'] == 'test6' && $result[0]['int_default_nullable'] === 123 && - $result[1]['column_7'] == 'test7' && $result[1]['int_default_nullable'] === null, - $this->getTestDesc('mapping dummy data') - ); - unset($mapper, $result); - - // current timestamp - $table->addColumn('stamp') - ->type($schema::DT_TIMESTAMP) - ->nullable(false) - ->defaults($schema::DF_CURRENT_TIMESTAMP); - $table->build(); - $r1 = $table->getCols(true); - $this->test->expect( - in_array('stamp', array_keys($r1)) && - $r1['stamp']['default'] == $schema::DF_CURRENT_TIMESTAMP, - $this->getTestDesc( - 'adding column [TIMESTAMP], not nullable with current_timestamp default value') - ); - unset($r1); - - - // datetime nullable - $table->addColumn('datetime')->type_datetime()->nullable(true); - $table->build(); - $r1 = $table->getCols(true); - $this->test->expect( - in_array('datetime', array_keys($r1)) && - $r1['datetime']['nullable'] == true, - $this->getTestDesc( - 'adding column [DATETIME], nullable, no default') - ); - unset($r1); - - - // adding dummy data - $mapper = new \DB\SQL\Mapper($db, $this->tname); - $mapper->column_7 = 'test_datetime'; - $mapper->datetime = NULL; - $mapper->save(); - $mapper->reset(); - $result = $mapper->find(array('column_7 = ?', 'test_datetime')); - foreach ($result as &$r) - $r = $r->cast(); - - $this->test->expect( - array_key_exists(0, $result) && $result[0]['column_7'] == 'test_datetime' && - $result[0]['datetime'] === null, - $this->getTestDesc('mapping dummy data') - ); - unset($mapper, $result); - - - // rename column - $table->renameColumn('text_default_not_null', 'title123'); - $table->build(); - $r1 = $table->getCols(); - $this->test->expect( - in_array('title123', $r1) && !in_array('text_default_not_null', $r1), - $this->getTestDesc('renaming column') - ); - unset($r1); - - // adding dummy data - $mapper = new \DB\SQL\Mapper($db, $this->tname); - $mapper->title123 = 'test8'; - $mapper->save(); - $mapper->reset(); - $result = $mapper->findone(array('title123 = ?','test8')); - $this->test->expect( - !$result->dry(), - $this->getTestDesc('mapping dummy data') - ); - $table->renameColumn('title123', 'text_default_not_null'); - $table->build(); - unset($result,$mapper); - - // remove column - $table->dropColumn('column_1'); - $table->build(); - $r1 = $table->getCols(); - $this->test->expect( - !in_array('column_1', $r1), - $this->getTestDesc('removing column') - ); - unset($r1); - - // rename table - $schema->dropTable('test123'); - $table->rename('test123'); - $result = $schema->getTables(); - $this->test->expect( - in_array('test123', $result) && !in_array($this->tname, $result), - $this->getTestDesc('renaming table') - ); - $table->rename($this->tname); - unset($result); - - // check record count - $mapper = new \DB\SQL\Mapper($db, $this->tname); - $this->test->expect( - count($mapper->find()) == 9, - $this->getTestDesc('check record count') - ); - unset($mapper); - - // adding composite primary keys - $table->addColumn('version')->type($schema::DT_INT4)->nullable(false)->defaults(1); - $table->primary(array('id', 'version')); - $table->build(); - $r1 = $table->getCols(true); - $this->test->expect(!empty($r1) && isset($r1['version']) && - $r1['id']['pkey'] == true && $r1['version']['pkey'] == true, - $this->getTestDesc('adding composite primary-keys') - ); - unset($r1); - - // check record count - $mapper = new \DB\SQL\Mapper($db, $this->tname); - $this->test->expect( - count($mapper->find()) == 9, - $this->getTestDesc('check record count') - ); - unset($mapper); - - // drop table - $schema->dropTable($this->tname); - $this->test->expect( - !in_array($this->tname, $schema->getTables()), - $this->getTestDesc('drop table') - ); - - // adding composite primary keys - $table = $schema->createTable($this->tname); - $table->addColumn('version')->type($schema::DT_INT4) - ->defaults(1)->nullable(false); - $table->primary(array('id', 'version')); - $table = $table->build(); - $r1 = $table->getCols(true); - - $this->test->expect(!empty($r1) && - $r1['id']['pkey'] == true && $r1['version']['pkey'] == true, - $this->getTestDesc('creating new table with composite key') - ); - $this->test->expect(!empty($r1) && - $r1['version']['default'] == '1', - $this->getTestDesc('default value on composite primary key') - ); - unset($r1); - - // more fields to composite primary key table - $table->addColumn('title')->type($schema::DT_VARCHAR256); - $table->addColumn('title2')->type($schema::DT_TEXT); - $table->addColumn('title_notnull') - ->type($schema::DT_VARCHAR128)->nullable(false)->defaults("foo"); - $table->build(); - $r1 = $table->getCols(true); - $this->test->expect( - array_key_exists('title', $r1) && - array_key_exists('title_notnull', $r1) && - $r1['id']['pkey'] == true && $r1['version']['pkey'] == true, - $this->getTestDesc('adding more fields to composite pk table') - ); - unset($r1); - - // testing primary keys with inserted data - $mapper = new \DB\SQL\Mapper($db, $this->tname); - $mapper->title = 'test1'; - $mapper->save(); - $mapper->reset(); - - $mapper->id = 1; - $mapper->title = 'nullable'; - $mapper->version = 2; - $mapper->save(); - $mapper->reset(); - - $mapper->title = 'test3'; - $mapper->title2 = 'foobar'; - $mapper->title_notnull = 'bar'; - $mapper->save(); - - $result = array_map(array($mapper,'cast'),$mapper->find()); - - $cpk_expected = array( - 0=>array( - 'id' => 1, - 'version' => 1, - 'title' => 'test1', - 'title2' => NULL, - 'title_notnull' => 'foo', - ), - 1=>array( - 'id' => 1, - 'version' => 2, - 'title' => 'nullable', - 'title2' => NULL, - 'title_notnull' => 'foo', - ), - 2=>array( - 'id' => 2, - 'version' => 1, - 'title' => 'test3', - 'title2' => 'foobar', - 'title_notnull' => 'bar', - ), - ); - foreach ($result as &$r) - ksort($r); - foreach ($cpk_expected as &$r) - ksort($r); - $this->test->expect( - json_encode($result) == json_encode($cpk_expected), - $this->getTestDesc('adding items with composite primary-keys') - ); - - $schema->dropTable($this->tname); - - // indexes - $table = $schema->createTable($this->tname); - $table->addColumn('rawtest', array('type' => $schema::DT_VARCHAR256, 'default' => 'foo')); - $table->addColumn('text')->type($schema::DT_TEXT); - $table->addColumn('foo')->type($schema::DT_VARCHAR128)->index(); - $table = $table->build(); - $r1 = $table->getCols(true); - $this->test->expect( - isset($r1['rawtest']) && $r1['rawtest']['default'] = 'foo', - $this->getTestDesc('adding column with options array') - ); - $indexes = $table->listIndex(); - $this->test->expect( - isset($indexes[$table->name.'___foo']), - $this->getTestDesc('column index on table creation') - ); - $table->addColumn('bar')->type($schema::DT_VARCHAR128)->index(true); - $table->addColumn('baz')->type($schema::DT_VARCHAR128); - $table->addIndex(array('foo', 'baz')); - $table->build(); - $indexes = $table->listIndex(); - $this->test->expect( - isset($indexes[$table->name.'___bar']), - $this->getTestDesc('column index on table alteration') - ); - $this->test->expect( - isset($indexes[$table->name.'___bar']) && $indexes[$table->name.'___bar']['unique'] == true, - $this->getTestDesc('unique index') - ); - $this->test->expect( - isset($indexes[$table->name.'___foo__baz']), - $this->getTestDesc('index on combined columns') - ); - - if($this->current_engine == 'sqlite') { - $table->dropColumn('rawtest'); - $table->build(); - $indexes = $table->listIndex(); - $this->test->expect( - isset($indexes[$table->name.'___foo__baz']) && isset($indexes[$table->name.'___bar']) - && $indexes[$table->name.'___bar']['unique'], - $this->getTestDesc('preserve indexes after table rebuild') - ); - } - - $table->dropIndex($table->name.'___bar'); - $table->build(); - $indexes = $table->listIndex(); - $this->test->expect( - !array_key_exists($table->name.'___bar',$indexes), - $this->getTestDesc('drop index') - ); - - // update column - $table->updateColumn('bar',$schema::DT_TEXT); - $table->build(); - $r1 = $table->getCols(true); - $this->test->expect( - array_key_exists('bar', $r1) && $r1['bar']['type'] == 'text', - $this->getTestDesc('update column') - ); - - // create table with text not nullable column - $table2 = $schema->createTable($this->tname.'_notnulltext'); - $table2->addColumn('desc')->type($schema::DT_TEXT)->nullable(false); - $table2 = $table2->build(); - $r1 = $schema->getTables(); - $r2 = $table2->getCols(true); - $this->test->expect( - in_array($this->tname.'_notnulltext', $r1) && array_key_exists('desc', $r2) - && $r2['desc']['nullable']==false, - $this->getTestDesc('create new table with not nullable text column') - ); - $table2->drop(); - - - } - -} \ No newline at end of file diff --git a/app/config.ini b/app/config.ini index 39e215076..59b0dee6c 100644 --- a/app/config.ini +++ b/app/config.ini @@ -1,33 +1,144 @@ +; Global Framework Config + +[SERVER] +SERVER_NAME = PATHFINDER + [globals] -; Default Verbosity level of the stack trace. -; Assign values between 0 to 3 for increasing verbosity levels. Check "PATHFINDER" config for overwriting -DEBUG = 0 +; Verbosity level of error stack trace for errors +; This affects error logging and stack traces returned to clients on error. +; DEBUG level can be overwritten in environment.ini +; Syntax: 0 | 1 | 2 | 3 +; Default: 0 +DEBUG = 0 + +; How to behave on 'non-fatal' errors +; If TRUE, the framework, after having logged stack trace and errors, stops execution +; (die without any status) when a non-fatal error is detected. +; Hint: You should not change this. +; Syntax: TRUE | FALSE +; Default: FALSE +HALT = FALSE + +; Timezone to use +; Sync Pathfinder with EVE server time. +; Hint: You should not change this. +; Default: UTC +TZ = UTC -; If TRUE, the framework, after having logged stack trace and errors, stops execution (die without any status) when a non-fatal error is detected. -HALT = FALSE +; Default language +; Overwrites HTTP Accept-Language request header. +; Used by setlocale() and affects number formatting. +; Syntax: String +; Default: en-US +LANGUAGE = en-US -ONERROR = "Controller\MapController->showError" +; Cache key prefix +; Same for all cache values for this installation. +; CLI (cronjob) scripts use it for cache manipulation. +; Hint: You should not change this. +; Syntax String +; Default: {{ md5(@SERVER.SERVER_NAME) }} +SEED = {{ md5(@SERVER.SERVER_NAME) }} -; Timezone to use. Sync program with eve server time -TZ = "UTC" +; Cache backend +; This sets the primary cache backend for Pathfinder. Used for e.g.: +; DB query, DB schema, HTTP response, or even simple key->value caches +; Can handle Redis, Memcache module, APC, WinCache, XCache and a filesystem-based cache. +; Hint: Redis is recommended and gives the best performance. +; Syntax: folder=[DIR] | redis=[SERVER] +; Default: folder=tmp/cache/ +; Value: FALSE +; - Disables caching +; folder=[DIR] +; - Cache data is stored on disc +; redis=[SERVER] +; - Cache data is stored in Redis. redis=[host]:[port]:[db]:[auth] (e.g. redis=localhost:6379:1:myPass) +CACHE = folder=tmp/cache/ -; Cache backend. Can handle Memcache module, APC, WinCache, XCache and a filesystem-based cache. -CACHE = TRUE +; Cache backend for API data +; This sets the cache backend for API response data and other temp data relates to API requests. +; Response data with proper 'Expire' HTTP Header will be cached here and speed up further requests. +; As default 'API_CACHE' and 'CACHE' share the same backend (cache location) +; Hint1: You can specify e.g. a dedicated Redis DB here, then 'CACHE' and 'API_CACHE' can be cleared independently +; Hint2: Redis is recommended and gives the best performance. +; Default: {{@CACHE}} +; Value: FALSE +; - Disables caching +; folder=[DIR] +; - Cache data is stored on disc +; redis=[SERVER] +; - Cache data is stored in Redis. redis=[host]:[port]:[db]:[auth] (e.g. redis=localhost:6379:2:myPass) +API_CACHE = {{@CACHE}} -; Path configurations ================================================================================== -; relative to "BASE" dir +; Cache backend used by PHPs Session handler. +; Hint1: Best performance and recommended configuration for Pathfinder is to configured Redis as PHPs default Session handler +; in your php.ini and set 'default' value here in order to use Redis (fastest) +; Hint2: If Redis is not available for you, leave this at 'mysql' (faster than PHPs default files bases Sessions) +; Syntax: mysql | default +; Default: mysql +; Value: mysql +; - Session data get stored in 'pathfinder'.'sessions' table (environment.ini → DB_PF_NAME). +; Table `sessions` is auto created if not exist. +; default +; - Session data get stored in PHPs default Session handler (php.ini → session.save_handler and session.save_path) +; PHPs default session.save_handler is `files` and each Session is written to disc (slowest) +SESSION_CACHE = mysql -; Temporary folder for cache, filesystem locks, compiled F3 templates, etc. -TEMP = tmp/ +; Callback functions ============================================================================== +ONERROR = {{ @NAMESPACE }}\Controller\Controller->showError +UNLOAD = {{ @NAMESPACE }}\Controller\Controller->unload + +; Path configurations ============================================================================= +; All path configurations are relative to BASE dir and should NOT be changed + +; Temporary folder for cache +; Used for compiled templates. +; Syntax: [DIR] +; Default: tmp/ +TEMP = tmp/ ; Log file folder -LOGS = logs/ +; Syntax: [DIR] +; Default: logs/ +LOGS = logs/ + +; UI folder +; Where all the public assets (templates, images, styles, scripts) are located. +; Syntax: [DIR] +; Default: public/ +UI = public/ -; Search path for user interface files used by the View and Template classes' render() method. -UI = public/ +; Autoload folder +; Where PHP attempts to autoload PHP classes at runtime. +; Syntax: [DIR] +; Default: app/ +;AUTOLOAD = app/ -; Search path(s) for user-defined PHP classes that the framework will attempt to autoload at runtime -AUTOLOAD = app/main/ +; Favicon folder +; Syntax: [DIR] +; Default: favicon/ +FAVICON = favicon/ +; Export folder +; Where DB dump files are located/created at. +; Syntax: [DIR] +; Default: export/ +EXPORT = export/ +; Custom *.ini file folder +; Can be used to overwrite default *.ini files and settings +; See: https://github.com/exodus4d/pathfinder/wiki/Configuration#custom-confpathfinderini +; Syntax: [DIR] +CONF.CUSTOM = conf/ +CONF.DEFAULT = app/ +; Load additional config files +; DO NOT load environment.ini, it is loaded automatically +[configs] +{{@CONF.DEFAULT}}routes.ini = true +{{@CONF.DEFAULT}}pathfinder.ini = true +{{@CONF.DEFAULT}}plugin.ini = true +{{@CONF.CUSTOM}}pathfinder.ini = true +{{@CONF.CUSTOM}}plugin.ini = true +{{@CONF.DEFAULT}}requirements.ini = true +{{@CONF.DEFAULT}}cron.ini = true \ No newline at end of file diff --git a/app/cron.ini b/app/cron.ini index 89ae8f632..f7c841e15 100644 --- a/app/cron.ini +++ b/app/cron.ini @@ -1,24 +1,72 @@ [CRON] -log = TRUE -cli = TRUE -web = TRUE +log = TRUE +cli = TRUE +web = FALSE +silent = TRUE [CRON.presets] ; run every minute -instant = * * * * * +instant = * * * * * -; run in downtime 11:00 GMT/UTC -downtime = 0 11 * * * +; 12 times per hour (each 5min) +fiveMinutes = */5 * * * * + +; 6 times per hour (each 10min) +tenMinutes = */10 * * * * + +; 2 times per hour (each 30min) +halfHour = */30 * * * * + +; 1 times per hour (12:30, 13:30, 14:30,...) +halfPastHour = 30 * * * * + +; run on EVE downtime 11:00 GMT/UTC +downtime = 0 11 * * * [CRON.jobs] +; delete EOL connections +deleteEolConnections = {{ @NAMESPACE }}\Cron\MapUpdate->deleteEolConnections, @fiveMinutes + +; delete expired wh connections +deleteExpiredConnections = {{ @NAMESPACE }}\Cron\MapUpdate->deleteExpiredConnections, @hourly + +; delete character log data +deleteLogData = {{ @NAMESPACE }}\Cron\CharacterUpdate->deleteLogData, @instant + +; delete expired signatures +deleteSignatures = {{ @NAMESPACE }}\Cron\MapUpdate->deleteSignatures, @halfHour + ; import system data (jump, kill,..) from CCP API -importSystemData = Cron\CcpSystemsUpdate->importSystemData, @hourly +importSystemData = {{ @NAMESPACE }}\Cron\CcpSystemsUpdate->importSystemData, @halfPastHour ; disable outdated maps -deactivateMapData = Cron\MapUpdate->deactivateMapData, @hourly +deactivateMapData = {{ @NAMESPACE }}\Cron\MapUpdate->deactivateMapData, @hourly + +; clean up character data (kick, ban,..) +cleanUpCharacterData = {{ @NAMESPACE }}\Cron\CharacterUpdate->cleanUpCharacterData, @hourly ; delete disabled maps -deleteMapData = Cron\MapUpdate->deleteMapData, @downtime +deleteMapData = {{ @NAMESPACE }}\Cron\MapUpdate->deleteMapData, @downtime -; delete character log data -deleteLogData = Cron\CharacterUpdate->deleteLogData, @downtime \ No newline at end of file +; delete expired character cookie authentication data +deleteAuthenticationData = {{ @NAMESPACE }}\Cron\CharacterUpdate->deleteAuthenticationData, @downtime + +; delete expired cache files +deleteExpiredCacheData = {{ @NAMESPACE }}\Cron\Cache->deleteExpiredCacheData, @downtime + +; delete old statistics (activity log) data +deleteStatisticsData = {{ @NAMESPACE }}\Cron\StatisticsUpdate->deleteStatisticsData, @weekly + +; truncate map history log files +truncateMapHistoryLogFiles = {{ @NAMESPACE }}\Cron\MapHistory->truncateMapHistoryLogFiles, @halfHour + +; sync "sovereignty" and "faction warfare" data from CCP´s ESI API +updateSovereigntyData = {{ @NAMESPACE }}\Cron\Universe->updateSovereigntyData, @halfPastHour + +; sync static system data from CCP´s ESI API +; -> Job is WIP! +;updateUniverseSystems = {{ @NAMESPACE }}\Cron\Universe->updateUniverseSystems, @instant + +; bootstrap job for "eve_universe" DB from CCP´s ESI API +; -> Only for development! This job is used to build the initial export/sql/eve_universe.sql +;setup = {{ @NAMESPACE }}\Cron\Universe->setup, @instant \ No newline at end of file diff --git a/app/environment.ini b/app/environment.ini new file mode 100644 index 000000000..54075b4cf --- /dev/null +++ b/app/environment.ini @@ -0,0 +1,106 @@ +; Environment Config + +[ENVIRONMENT] +; project environment (DEVELOP || PRODUCTION). +; This effects: DB connection, Mail-Server, SSO, ESI configurations in this file +; configuration below +SERVER = DEVELOP + +[ENVIRONMENT.DEVELOP] +; path to index.php (Default: leave blank == "auto-detect") +; -> e.g. set /pathfinder if your URL looks like https://www.[YOUR_DOMAIN]/pathfinder (subfolder) +BASE = +; deployment URL (e.g. http://localhost) +URL = {{@SCHEME}}://local.pathfinder +; level of debug/error stack trace +DEBUG = 3 +; Pathfinder database +DB_PF_DNS = mysql:host=localhost;port=3306;dbname= +DB_PF_NAME = pathfinder +DB_PF_USER = root +DB_PF_PASS = + +; Universe data (New Eden) cache DB for ESI API respons +DB_UNIVERSE_DNS = mysql:host=localhost;port=3306;dbname= +DB_UNIVERSE_NAME = eve_universe +DB_UNIVERSE_USER = root +DB_UNIVERSE_PASS = + +; CCP SSO (OAuth2) - visit: https://developers.eveonline.com/applications +CCP_SSO_URL = https://sisilogin.testeveonline.com +CCP_SSO_CLIENT_ID = +CCP_SSO_SECRET_KEY = +CCP_SSO_DOWNTIME = 11:00 + +; CCP ESI API +CCP_ESI_URL = https://esi.evetech.net +CCP_ESI_DATASOURCE = singularity +CCP_ESI_SCOPES = esi-location.read_online.v1,esi-location.read_location.v1,esi-location.read_ship_type.v1,esi-ui.write_waypoint.v1,esi-ui.open_window.v1,esi-universe.read_structures.v1,esi-corporations.read_corporation_membership.v1,esi-clones.read_clones.v1,esi-characters.read_corporation_roles.v1 +CCP_ESI_SCOPES_ADMIN = + +; SMTP settings (optional) +SMTP_HOST = localhost +SMTP_PORT = 25 +SMTP_SCHEME = TLS +SMTP_USER = pathfinder +SMTP_PASS = root + +SMTP_FROM = pathfinder@localhost.com +SMTP_ERROR = pathfinder@localhost.com + +; TCP Socket configuration (optional) (advanced) +;SOCKET_HOST = 127.0.0.1 +;SOCKET_PORT = 5555 + + +[ENVIRONMENT.PRODUCTION] +; path to index.php (Default: leave blank == "auto-detect") +; -> e.g. set /pathfinder if your URL looks like https://www.[YOUR_DOMAIN]/pathfinder (subfolder) +BASE = +; deployment URL (e.g. https://www.pathfinder-w.space) +URL = {{@SCHEME}}://www.pathfinder-w.space +; level of debug/error stack trace +DEBUG = 0 +; Pathfinder database +DB_PF_DNS = mysql:host=localhost;port=3306;dbname= +DB_PF_NAME = +DB_PF_USER = +DB_PF_PASS = + +; Universe data (New Eden) cache DB for ESI API respons +DB_UNIVERSE_DNS = mysql:host=localhost;port=3306;dbname= +DB_UNIVERSE_NAME = +DB_UNIVERSE_USER = +DB_UNIVERSE_PASS = + +; EVE-Online CCP Database export +DB_CCP_DNS = mysql:host=localhost;port=3306;dbname= +DB_CCP_NAME = +DB_CCP_USER = +DB_CCP_PASS = + +; CCP SSO +CCP_SSO_URL = https://login.eveonline.com +CCP_SSO_CLIENT_ID = +CCP_SSO_SECRET_KEY = +CCP_SSO_DOWNTIME = 11:00 + +; CCP ESI API +CCP_ESI_URL = https://esi.evetech.net +CCP_ESI_DATASOURCE = tranquility +CCP_ESI_SCOPES = esi-location.read_online.v1,esi-location.read_location.v1,esi-location.read_ship_type.v1,esi-ui.write_waypoint.v1,esi-ui.open_window.v1,esi-universe.read_structures.v1,esi-corporations.read_corporation_membership.v1,esi-clones.read_clones.v1,esi-characters.read_corporation_roles.v1 +CCP_ESI_SCOPES_ADMIN = + +; SMTP settings (optional) +SMTP_HOST = localhost +SMTP_PORT = 25 +SMTP_SCHEME = TLS +SMTP_USER = +SMTP_PASS = + +SMTP_FROM = registration@pathfinder-w.space +SMTP_ERROR = admin@pathfinder-w.space + +; TCP Socket configuration (optional) (advanced) +;SOCKET_HOST = 127.0.0.1 +;SOCKET_PORT = 5555 diff --git a/app/lib/CHANGELOG b/app/lib/CHANGELOG deleted file mode 100644 index d7179d106..000000000 --- a/app/lib/CHANGELOG +++ /dev/null @@ -1,577 +0,0 @@ -CHANGELOG - -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 3c7236c80..000000000 --- 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 3c7236c80..000000000 --- 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 1338ca8cc..000000000 --- a/app/lib/audit.php +++ /dev/null @@ -1,190 +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=array(); - 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()->get('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()->get('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()->get('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}$/',$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 5f7e05084..000000000 --- a/app/lib/auth.php +++ /dev/null @@ -1,241 +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; - - /** - * Jig storage handler - * @return bool - * @param $id string - * @param $pw string - * @param $realm string - **/ - protected function _jig($id,$pw,$realm) { - return (bool) - call_user_func_array( - array($this->mapper,'load'), - array( - array_merge( - array( - '@'.$this->args['id'].'==? AND '. - '@'.$this->args['pw'].'==?'. - (isset($this->args['realm'])? - (' AND @'.$this->args['realm'].'==?'):''), - $id,$pw - ), - (isset($this->args['realm'])?array($realm):array()) - ) - ) - ); - } - - /** - * MongoDB storage handler - * @return bool - * @param $id string - * @param $pw string - * @param $realm string - **/ - protected function _mongo($id,$pw,$realm) { - return (bool) - $this->mapper->load( - array( - $this->args['id']=>$id, - $this->args['pw']=>$pw - )+ - (isset($this->args['realm'])? - array($this->args['realm']=>$realm):array()) - ); - } - - /** - * SQL storage handler - * @return bool - * @param $id string - * @param $pw string - * @param $realm string - **/ - protected function _sql($id,$pw,$realm) { - return (bool) - call_user_func_array( - array($this->mapper,'load'), - array( - array_merge( - array( - $this->args['id'].'=? AND '. - $this->args['pw'].'=?'. - (isset($this->args['realm'])? - (' AND '.$this->args['realm'].'=?'):''), - $id,$pw - ), - (isset($this->args['realm'])?array($realm):array()) - ) - ) - ); - } - - /** - * LDAP storage handler - * @return bool - * @param $id string - * @param $pw string - **/ - protected function _ldap($id,$pw) { - $dc=@ldap_connect($this->args['dc']); - 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'], - 'uid='.$id)) && - ldap_count_entries($dc,$result) && - ($info=ldap_get_entries($dc,$result)) && - @ldap_bind($dc,$info[0]['dn'],$pw) && - @ldap_close($dc)) { - return $info[0]['uid'][0]==$id; - } - 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->get('HOST')); - if (strtolower($this->args['scheme'])=='tls') { - $dialog('STARTTLS'); - stream_socket_enable_crypto( - $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); - $dialog('EHLO '.$fw->get('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->get('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 - **/ - function __construct($storage,array $args=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; - } - -} diff --git a/app/lib/base.php b/app/lib/base.php deleted file mode 100644 index 0855f9d1e..000000000 --- a/app/lib/base.php +++ /dev/null @@ -1,3087 +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.5.0-Release'; - //@} - - //@{ HTTP status codes (RFC 2616) - const - HTTP_100='Continue', - HTTP_101='Switching Protocols', - 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', - //! Default directory permissions - MODE=0755, - //! Syntax highlighting stylesheet - CSS='code.css'; - - //@{ HTTP request types - const - REQ_SYNC=1, - REQ_AJAX=2; - //@} - - //@{ 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, - //! 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 $params array - **/ - function build($url,$params=array()) { - $params+=$this->hive['PARAMS']; - if (is_array($url)) - foreach ($url as &$var) { - $var=$this->build($var,$params); - unset($var); - } - else { - $i=0; - $url=preg_replace_callback('/@(\w+)|\*/', - function($match) use(&$i,$params) { - $i++; - if (isset($match[1]) && - array_key_exists($match[1],$params)) - return $params[$match[1]]; - return array_key_exists($i,$params)? - $params[$i]: - $match[0]; - },$url); - } - return $url; - } - - /** - * assemble url from alias name - * @return NULL - * @param $name string - * @param $params array|string - **/ - function alias($name,$params=array()) { - 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); - 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=array(); - foreach ($pairs as $pair) - $out[$pair[1]]=trim($pair[2]); - return $out; - } - - /** - * Convert JS-style token to PHP expression - * @return string - * @param $str string - **/ - function compile($str) { - $fw=$this; - return preg_replace_callback( - '/(?|::)*)/', - function($var) use($fw) { - return '$'.preg_replace_callback( - '/\.(\w+)\(|\.(\w+)|\[((?:[^\[\]]*|(?R))*)\]/', - function($expr) use($fw) { - return $expr[1]? - ((function_exists($expr[1])? - ('.'.$expr[1]): - ('['.var_export($expr[1],TRUE).']')).'('): - ('['.var_export( - isset($expr[3])? - $fw->compile($expr[3]): - (ctype_digit($expr[2])? - (int)$expr[2]: - $expr[2]),TRUE).']'); - }, - $var[1] - ); - }, - $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 - **/ - function &ref($key,$add=TRUE) { - $null=NULL; - $parts=$this->cut($key); - if ($parts[0]=='SESSION') { - @session_start(); - $this->sync('SESSION'); - } - elseif (!preg_match('/^\w+$/',$parts[0])) - user_error(sprintf(self::E_Hive,$this->stringify($key)), - E_USER_ERROR); - 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=array(); - if ($add || array_key_exists($part,$var)) - $var=&$var[$part]; - else { - $var=&$null; - break; - } - } - if ($parts[0]=='ALIASES') - $var=$this->build($var); - 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 - * @return bool - * @param $key string - **/ - function devoid($key) { - $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=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'])); - if ($ttl) - $jar['expire']=$time+$ttl; - call_user_func_array('setcookie',array($parts[1],$val)+$jar); - return $val; - } - } - else switch ($key) { - case 'CACHE': - $val=Cache::instance()->load($val,TRUE); - 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']); - case 'LOCALES': - if (isset($lex) || $lex=$this->lexicon($val)) - $this->mset($lex,$this->hive['PREFIX'],$ttl); - break; - case 'TZ': - date_default_timezone_set($val); - break; - } - $ref=&$this->ref($key); - $ref=$val; - if (preg_match('/^JAR\b/',$key)) { - $jar=$this->unserialize($this->serialize($this->hive['JAR'])); - $jar['expire']-=$time; - call_user_func_array('session_set_cookie_params',$jar); - } - $cache=Cache::instance(); - if ($cache->exists($hash=$this->hash($key).'.var') || $ttl) - // Persist the key-value pair - $cache->set($hash,$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( - array($this,'format'), - array_merge(array($val),is_array($args)?$args:array($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']; - $jar['expire']=strtotime('-1 year'); - call_user_func_array('setcookie', - array_merge(array($parts[1],''),$jar)); - unset($_COOKIE[$parts[1]]); - } - } - elseif ($parts[0]=='SESSION') { - @session_start(); - if (empty($parts[1])) { - // End session - session_unset(); - session_destroy(); - unset($_COOKIE[session_name()]); - header_remove('Set-Cookie'); - } - $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 - **/ - function merge($key,$src) { - $ref=&$this->ref($key); - return array_merge($ref,is_string($src)?$this->hive[$src]:$src); - } - - /** - * 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=array(); - switch (gettype($arg)) { - case 'object': - $str=''; - foreach (get_object_vars($arg) as $key=>$val) - $str.=($str?',':''). - var_export($key,TRUE).'=>'. - $this->stringify($val, - array_merge($stack,array($arg))); - return get_class($arg).'::__set_state(array('.$str.'))'; - case 'array': - $str=''; - $num=isset($arg[0]) && - ctype_digit(implode('',array_keys($arg))); - foreach ($arg as $key=>$val) - $str.=($str?',':''). - ($num?'':(var_export($key,TRUE).'=>')). - $this->stringify($val, - array_merge($stack,array($arg))); - return 'array('.$str.')'; - default: - return var_export($arg,TRUE); - } - } - - /** - * Flatten array values and return as CSV string - * @return string - * @param $args array - **/ - function csv(array $args) { - return implode(',',array_map('stripcslashes', - array_map(array($this,'stringify'),$args))); - } - - /** - * Convert snakecase string to camelcase - * @return string - * @param $str string - **/ - function camelcase($str) { - return preg_replace_callback( - '/_(\w)/', - function($match) { - return strtoupper($match[1]); - }, - $str - ); - } - - /** - * Convert camelcase string to snakecase - * @return string - * @param $str string - **/ - function snakecase($str) { - return strtolower(preg_replace('/[[:upper:]]/','_\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; - } - - /** - * Convert class constants to array - * @return array - * @param $class object|string - * @param $prefix string - **/ - function constants($class,$prefix='') { - $ref=new ReflectionClass($class); - $out=array(); - foreach (preg_grep('/^'.$prefix.'/',array_keys($ref->getconstants())) - as $val) { - $out[$key=substr($val,strlen($prefix))]= - constant((is_object($class)?get_class($class):$class).'::'.$prefix.$key); - } - unset($ref); - return $out; - } - - /** - * 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=NULL) { - if ($stack) { - foreach ($stack as $node) - if ($arg===$node) - return $arg; - } - else - $stack=array(); - switch (gettype($arg)) { - case 'object': - if (method_exists('ReflectionClass','iscloneable')) { - $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,array($arg))); - } - } - return $arg; - case 'array': - $copy=array(); - foreach ($arg as $key=>$val) - $copy[$key]=$this->recursive($val,$func, - array_merge($stack,array($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) { - $fw=$this; - return $this->recursive($arg, - function($val) use($fw,$tags) { - if ($tags!='*') - $val=trim(strip_tags($val, - '<'.implode('><',$fw->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( - '/\{(?P\d+)\s*(?:,\s*(?P\w+)\s*'. - '(?:,\s*(?P(?:\w+(?:\s*\{.+?\}\s*,?)?)*)'. - '(?:,\s*(?P.+?))?)?)?\}/', - function($expr) use($args,$conv) { - extract($expr); - extract($conv); - if (!array_key_exists($pos,$args)) - return $expr[0]; - if (isset($type)) - switch ($type) { - case 'plural': - preg_match_all('/(?\w+)'. - '(?:\s*\{\s*(?.+?)\s*\})/', - $mod,$matches,PREG_SET_ORDER); - $ord=array('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': - if (function_exists('money_format')) - return money_format( - '%n',$args[$pos]); - $fmt=array( - 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( - array('+','n','c'), - array($sgn,number_format( - abs($args[$pos]), - $frac_digits, - $decimal_point, - $thousands_sep), - $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).'%'; - case 'decimal': - return number_format( - $args[$pos],$prop,$decimal_point, - $thousands_sep); - } - break; - case 'date': - if (empty($mod) || $mod=='short') - $prop='%x'; - elseif ($mod=='long') - $prop='%A, %d %B %Y'; - return strftime($prop,$args[$pos]); - case 'time': - if (empty($mod) || $mod=='short') - $prop='%X'; - return strftime($prop,$args[$pos]); - default: - return $expr[0]; - } - return $args[$pos]; - }, - $val - ); - } - - /** - * 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=array(); - 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=array(); - $windows=preg_match('/^win/i',PHP_OS); - foreach ($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; - } - $locales[]=$locale; - $locales[]=$locale.'.'.ini_get('default_charset'); - } - setlocale(LC_ALL,str_replace('-','_',$locales)); - return implode(',',$this->languages); - } - - /** - * Return lexicon entries - * @return array - * @param $path string - **/ - function lexicon($path) { - $lex=array(); - foreach ($this->languages?:explode(',',$this->fallback) 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/','',$match['rval'])); - } - } - 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 (PHP_SAPI!='cli') - header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason); - return $reason; - } - - /** - * Send cache metadata to HTTP client - * @return NULL - * @param $secs int - **/ - function expire($secs=0) { - if (PHP_SAPI!='cli') { - header('X-Content-Type-Options: nosniff'); - header('X-Frame-Options: '.$this->hive['XFRAME']); - header('X-Powered-By: '.$this->hive['PACKAGE']); - header('X-XSS-Protection: 1; mode=block'); - if ($secs) { - $time=microtime(TRUE); - header_remove('Pragma'); - header('Expires: '.gmdate('r',$time+$secs)); - header('Cache-Control: max-age='.$secs); - header('Last-Modified: '.gmdate('r')); - } - else - header('Cache-Control: no-cache, no-store, must-revalidate'); - } - } - - /** - * 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'])? - $headers['X-Forwarded-For']: - (isset($_SERVER['REMOTE_ADDR'])? - $_SERVER['REMOTE_ADDR']:'')); - } - - /** - * Return formatted stack trace - * @return string - * @param $trace array|NULL - **/ - function trace(array $trace=NULL) { - if (!$trace) { - $trace=debug_backtrace(FALSE); - $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 $debug && isset($frame['file']) && - ($frame['file']!=__FILE__ || $debug>1) && - (empty($frame['function']) || - !preg_match('/^(?:(?:trigger|user)_error|'. - '__call|call_user_func)/',$frame['function'])); - } - ); - $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 - **/ - function error($code,$text='',array $trace=NULL) { - $prior=$this->hive['ERROR']; - $header=$this->status($code); - $req=$this->hive['VERB'].' '.$this->hive['PATH']; - if (!$text) - $text='HTTP '.$code.' ('.$req.')'; - error_log($text); - $trace=$this->trace($trace); - foreach (explode("\n",$trace) as $nexus) - if ($nexus) - error_log($nexus); - if ($highlight=PHP_SAPI!='cli' && !$this->hive['AJAX'] && - $this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS)) - $trace=$this->highlight($trace); - $this->hive['ERROR']=array( - 'status'=>$header, - 'code'=>$code, - 'text'=>$text, - 'trace'=>$trace - ); - $handler=$this->hive['ONERROR']; - $this->hive['ONERROR']=NULL; - $eol="\n"; - if ((!$handler || - $this->call($handler,array($this,$this->hive['PARAMS']), - 'beforeroute,afterroute')===FALSE) && - !$prior && PHP_SAPI!='cli' && !$this->hive['QUIET']) - echo $this->hive['AJAX']? - json_encode($this->hive['ERROR']): - (''.$eol. - ''.$eol. - ''. - ''.$code.' '.$header.''. - ($highlight? - (''):''). - ''.$eol. - ''.$eol. - '

'.$header.'

'.$eol. - '

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

'.$eol. - ($this->hive['DEBUG']?('
'.$trace.'
'.$eol):''). - ''.$eol. - ''); - if ($this->hive['HALT']) - die; - } - - /** - * 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=array(); - $types=array('sync','ajax'); - 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]):array()); - } - 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:array(); - $GLOBALS['_REQUEST']=array_merge($GLOBALS['_GET'],$GLOBALS['_POST']); - foreach ($headers?:array() as $key=>$val) - $_SERVER['HTTP_'.strtr(strtoupper($key),'-','_')]=$val; - $this->hive['VERB']=$verb; - $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]); - return $this->run(); - } - - /** - * 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=array('sync','ajax'); - $alias=null; - if (is_array($pattern)) { - foreach ($pattern as $item) - $this->route($item,$handler,$ttl,$kbps); - return; - } - preg_match('/([\|\w]+)\h+(?:(?:@(\w+)\h*:\h*)?(@(\w+)|[^\h]+))'. - '(?:\h+\[('.implode('|',$types).')\])?/',$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])? - self::REQ_SYNC|self::REQ_AJAX: - 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)]= - array($handler,$ttl,$kbps,$alias); - } - } - - /** - * Reroute to specified URI - * @return NULL - * @param $url string - * @param $permanent bool - **/ - function reroute($url=NULL,$permanent=FALSE) { - if (!$url) - $url=$this->hive['REALM']; - if (preg_match('/^(?:@(\w+)(?:(\(.+?)\))*)/',$url,$parts)) { - if (empty($this->hive['ALIASES'][$parts[1]])) - user_error(sprintf(self::E_Named,$parts[1]),E_USER_ERROR); - $url=$this->hive['ALIASES'][$parts[1]]; - } - $url=$this->build($url, - isset($parts[2])?$this->parse($parts[2]):array()); - if (($handler=$this->hive['ONREROUTE']) && - $this->call($handler,array($url,$permanent))!==FALSE) - return; - if ($url[0]=='/') - $url=$this->hive['BASE'].$url; - if (PHP_SAPI!='cli') { - header('Location: '.$url); - $this->status($permanent?301:302); - die; - } - $this->mock('GET '.$url); - } - - /** - * 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): - array($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':''; - preg_match('/^'. - preg_replace('/@(\w+\b)/','(?P<\1>[^\/\?]+)', - str_replace('\*','([^\?]+)',preg_quote($pattern,'/'))). - '\/?(?:\?.*)?$/'.$case.'um',$url,$args); - 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=array(); - foreach ($keys=array_keys($this->hive['ROUTES']) as $key) - $paths[]=str_replace('@','*@',$key); - $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=$this->rel($this->hive['URI']); - 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: '. - ($cors['credentials']?'true':'false')); - } - $allowed=array(); - foreach ($this->hive['ROUTES'] as $pattern=>$routes) { - if (!$args=$this->mask($pattern,$req)) - continue; - ksort($args); - $route=NULL; - if (isset( - $routes[$ptr=$this->hive['AJAX']+1][$this->hive['VERB']])) - $route=$routes[$ptr]; - elseif (isset($routes[self::REQ_SYNC|self::REQ_AJAX])) - $route=$routes[self::REQ_SYNC|self::REQ_AJAX]; - if (!$route) - continue; - if ($this->hive['VERB']!='OPTIONS' && - isset($route[$this->hive['VERB']])) { - $parts=parse_url($req); - if ($this->hive['VERB']=='GET' && - preg_match('/.+\/$/',$parts['path'])) - $this->reroute(substr($parts['path'],0,-1). - (isset($parts['query'])?('?'.$parts['query']):'')); - list($handler,$ttl,$kbps,$alias)=$route[$this->hive['VERB']]; - if (is_bool(strpos($pattern,'/*'))) - foreach (array_keys($args) as $key) - if (is_numeric($key) && $key) - unset($args[$key]); - // Capture values of route pattern tokens - $this->hive['PARAMS']=$args=array_map('urldecode',$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)/', - function($id) use($args) { - return isset($args[$id[1]])?$args[$id[1]]:$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 && $cached[0]+$ttl>$now) { - 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 (PHP_SAPI!='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,array($this,$args), - 'beforeroute,afterroute'); - $body=ob_get_clean(); - if (isset($cache) && !error_get_last()) { - // Save to cache backend - $cache->set($hash,array( - // 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; - } - return $result; - } - $allowed=array_merge($allowed,array_keys($route)); - } - if (!$allowed) - // URL doesn't match any route - $this->error(404); - elseif (PHP_SAPI!='cli') { - // Unhandled HTTP method - header('Allow: '.implode(',',array_unique($allowed))); - if ($cors) { - header('Access-Control-Allow-Methods: OPTIONS,'.implode(',',$allowed)); - if ($cors['headers']) - 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=array(); - $time=time(); - $limit=max(0,min($timeout,$max=ini_get('max_execution_time')-1)); - $out=''; - $flag=FALSE; - // Not for the weak of heart - while ( - // Still alive? - !connection_aborted() && - // Got time left? - (time()-$time+1<$limit) && - // Restart session - $flag=@session_start() && - // CAUTION: Callback will kill host if it never becomes truthy! - !($out=$this->call($func,$args))) { - session_commit(); - ob_flush(); - flush(); - // Hush down - sleep(1); - } - if ($flag) { - session_commit(); - ob_flush(); - flush(); - } - return $out; - } - - /** - * Disconnect HTTP client - **/ - function abort() { - @session_start(); - session_commit(); - header('Content-Length: 0'); - while (ob_get_level()) - ob_end_clean(); - 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'); - else { - $ref=new ReflectionClass($parts[1]); - $parts[1]=method_exists($parts[1],'__construct')? - $ref->newinstanceargs($args): - $ref->newinstance(); - } - } - $func=array($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=array($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=array(); - 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(array($func[0],$hook),$args)===FALSE) - return FALSE; - // Execute callback - $out=call_user_func_array($func,$args?:array()); - 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(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=array(); - 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=array($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 $file string - * @param $allow bool - **/ - function config($file,$allow=FALSE) { - 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'; - 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); - } - else { - if ($allow) { - $match['lval']=Preview::instance()-> - resolve($match['lval']); - $match['rval']=Preview::instance()-> - resolve($match['rval']); - } - if (preg_match('/^(config|route|map|redirect)s\b/i', - $sec,$cmd)) { - call_user_func_array( - array($this,$cmd[1]), - array_merge(array($match['lval']), - str_getcsv($match['rval']))); - } - else { - $args=array_map( - function($val) { - if (is_numeric($val)) - return $val+0; - $val=ltrim($val); - if (preg_match('/^\w+$/i',$val) && - defined($val)) - return constant($val); - return trim(preg_replace( - array('/\\\\"/','/\\\\\h*(\r?\n)/'), - array('"','\1'),$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=array($this->call($func, - count($args)>1?array($args):$args)); - call_user_func_array( - array($this,'set'), - array_merge( - array( - ($custom?($parts['section'].'.'):''). - $match['lval'] - ), - count($args)>1?array($args):$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->hash($this->hive['ROOT'].$this->hive['BASE']).'.'. - $this->hash($id).'.lock') && - filemtime($lock)+ini_get('max_execution_time')call($func,$args); - fclose($handle); - @unlink($lock); - 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 (!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 - * @return NULL - * @param $cwd string - **/ - function unload($cwd) { - chdir($cwd); - if (!$error=error_get_last()) - @session_commit(); - $handler=$this->hive['UNLOAD']; - if ((!$handler || $this->call($handler,$this)===FALSE) && - $error && in_array($error['type'], - array(E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR))) - // Fatal error detected - $this->error(500,sprintf(self::E_Fatal,$error['message']), - array($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,$args) { - return call_user_func_array($this->get($key),$args); - } - - //! 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 - error_reporting((E_ALL|E_STRICT)&~(E_NOTICE|E_USER_NOTICE)); - $fw=$this; - set_exception_handler( - function($obj) use($fw) { - $fw->hive['EXCEPTION']=$obj; - $fw->error(500,$obj->getmessage(),$obj->gettrace()); - } - ); - set_error_handler( - function($code,$text) use($fw) { - if ($code & error_reporting()) - $fw->error(500,$text); - } - ); - if (!isset($_SERVER['SERVER_NAME'])) - $_SERVER['SERVER_NAME']=gethostname(); - if (PHP_SAPI=='cli') { - // Emulate HTTP request - if (isset($_SERVER['argc']) && $_SERVER['argc']<2) { - $_SERVER['argc']++; - $_SERVER['argv'][1]='/'; - } - $_SERVER['REQUEST_METHOD']='GET'; - $_SERVER['REQUEST_URI']=$_SERVER['argv'][1]; - } - $headers=array(); - if (PHP_SAPI!='cli') - 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']=$_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=array('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 (PHP_SAPI!='cli') - $base=rtrim($this->fixslashes( - dirname($_SERVER['SCRIPT_NAME'])),'/'); - $uri=parse_url($_SERVER['REQUEST_URI']); - $path=preg_replace('/^'.preg_quote($base,'/').'/','',$uri['path']); - call_user_func_array('session_set_cookie_params', - $jar=array( - 'expire'=>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=0; - if (isset($_SERVER['SERVER_PORT'])) - $port=$_SERVER['SERVER_PORT']; - // Default configuration - $this->hive+=array( - 'AGENT'=>$this->agent(), - 'AJAX'=>$this->ajax(), - 'ALIAS'=>NULL, - 'ALIASES'=>array(), - 'AUTOLOAD'=>'./', - 'BASE'=>$base, - 'BITMASK'=>ENT_COMPAT, - 'BODY'=>NULL, - 'CACHE'=>FALSE, - 'CASELESS'=>TRUE, - 'CONFIG'=>NULL, - 'CORS'=>array( - 'headers'=>'', - 'origin'=>false, - 'credentials'=>false, - 'expose'=>false, - 'ttl'=>0), - 'DEBUG'=>0, - 'DIACRITICS'=>array(), - 'DNSBL'=>'', - 'EMOJI'=>array(), - 'ENCODING'=>$charset, - 'ERROR'=>NULL, - 'ESCAPE'=>TRUE, - 'EXCEPTION'=>NULL, - 'EXEMPT'=>NULL, - 'FALLBACK'=>$this->fallback, - 'FRAGMENT'=>isset($uri['fragment'])?$uri['fragment']:'', - 'HALT'=>TRUE, - 'HIGHLIGHT'=>TRUE, - 'HOST'=>$_SERVER['SERVER_NAME'], - 'IP'=>$this->ip(), - 'JAR'=>$jar, - 'LANGUAGE'=>isset($headers['Accept-Language'])? - $this->language($headers['Accept-Language']): - $this->fallback, - 'LOCALES'=>'./', - 'LOGS'=>'./', - 'ONERROR'=>NULL, - 'ONREROUTE'=>NULL, - 'PACKAGE'=>self::PACKAGE, - 'PARAMS'=>array(), - '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 && $port!=80 && $port!=443? - (':'.$port):'').$_SERVER['REQUEST_URI'], - 'RESPONSE'=>'', - 'ROOT'=>$_SERVER['DOCUMENT_ROOT'], - 'ROUTES'=>array(), - 'SCHEME'=>$scheme, - 'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php', - 'TEMP'=>'tmp/', - 'TIME'=>microtime(TRUE), - 'TZ'=>(@ini_get('date.timezone'))?:'UTC', - 'UI'=>'./', - 'UNLOAD'=>NULL, - 'UPLOADS'=>'./', - 'URI'=>&$_SERVER['REQUEST_URI'], - 'VERB'=>&$_SERVER['REQUEST_METHOD'], - 'VERSION'=>self::VERSION, - 'XFRAME'=>'SAMEORIGIN' - ); - if (PHP_SAPI=='cli-server' && - preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI'])) - $this->reroute('/'); - if (ini_get('auto_globals_jit')) - // Override setting - $GLOBALS+=array('_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+=array( - $global=>preg_match('/SERVER|ENV/',$global)?$sync:array() - ); - } - if ($error=error_get_last()) - // Error detected - $this->error(500,sprintf(self::E_Fatal,$error['message']), - array($error)); - date_default_timezone_set($this->hive['TZ']); - // Register framework autoloader - spl_autoload_register(array($this,'autoload')); - // Register shutdown handler - register_shutdown_function(array($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=apc_fetch($ndx); - break; - case 'redis': - $raw=$this->ref->get($ndx); - break; - case 'memcache': - $raw=memcache_get($this->ref,$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 array($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; - $time=microtime(TRUE); - if ($cached=$this->exists($key)) - list($time,$ttl)=$cached; - $data=$fw->serialize(array($val,$time,$ttl)); - $parts=explode('=',$this->dsn,2); - switch ($parts[0]) { - case 'apc': - case 'apcu': - return apc_store($ndx,$data,$ttl); - case 'redis': - return $this->ref->set($ndx,$data,array('ex'=>$ttl)); - case 'memcache': - return memcache_set($this->ref,$ndx,$data,0,$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].$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 apc_delete($ndx); - case 'redis': - return $this->ref->del($ndx); - case 'memcache': - return memcache_delete($this->ref,$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 - * @param $lifetime int - **/ - function reset($suffix=NULL,$lifetime=0) { - 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=apc_cache_info('user'); - if (!empty($info['cache_list'])) { - $key=array_key_exists('info',$info['cache_list'][0])?'info':'key'; - $mtkey=array_key_exists('mtime',$info['cache_list'][0])? - 'mtime':'modification_time'; - foreach ($info['cache_list'] as $item) - if (preg_match($regex,$item[$key]) && - $item[$mtkey]+$lifetimeref->keys($this->prefix.'.*'.$suffix); - foreach($keys as $key) { - $val=$fw->unserialize($this->ref->get($key)); - if ($val[1]+$lifetimeref->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 ($data as $key=>$val) - if (preg_match($regex,$key) && - $val[1]+$lifetimeref,$key); - return TRUE; - case 'wincache': - $info=wincache_ucache_info(); - foreach ($info['ucache_entries'] as $item) - if (preg_match($regex,$item['key_name']) && - $item['use_time']+$lifetime1) - list($host,$port)=$parts; - else - $host=$parts[0]; - $this->ref=new Redis; - if(!$this->ref->connect($host,$port,2)) - $this->ref=NULL; - } - elseif (preg_match('/^memcache=(.+)/',$dsn,$parts) && - extension_loaded('memcache')) - foreach ($fw->split($parts[1]) as $server) { - $port=11211; - $parts=explode(':',$server,2); - if (count($parts)>1) - list($host,$port)=$parts; - else - $host=$parts[0]; - if (empty($this->ref)) - $this->ref=@memcache_connect($host,$port)?:NULL; - else - memcache_add_server($this->ref,$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->get('TEMP').'cache/'); - if (preg_match('/^folder\h*=\h*(.+)/',$dsn,$parts) && - !is_dir($parts[1])) - mkdir($parts[1],Base::MODE,TRUE); - } - $this->prefix=$fw->hash($_SERVER['SERVER_NAME'].$fw->get('BASE')); - return $this->dsn=$dsn; - } - - /** - * Class constructor - * @return object - * @param $dsn bool|string - **/ - function __construct($dsn=FALSE) { - if ($dsn) - $this->load($dsn); - } - -} - -//! View handler -class View extends Prefab { - - protected - //! Template file - $view, - //! post-rendering handler - $trigger, - //! Nesting level - $level=0; - - /** - * Encode characters to equivalent HTML entities - * @return string - * @param $arg mixed - **/ - function esc($arg) { - $fw=Base::instance(); - return $fw->recursive($arg, - function($val) use($fw) { - return is_string($val)?$fw->encode($val):$val; - } - ); - } - - /** - * Decode HTML entities to equivalent characters - * @return string - * @param $arg mixed - **/ - function raw($arg) { - $fw=Base::instance(); - return $fw->recursive($arg, - function($val) use($fw) { - return is_string($val)?$fw->decode($val):$val; - } - ); - } - - /** - * Create sandbox for template execution - * @return string - * @param $hive array - **/ - protected function sandbox(array $hive=NULL) { - $this->level++; - $fw=Base::instance(); - $implicit=false; - if ($hive === null) { - $implicit=true; - $hive=$fw->hive(); - } - if ($this->level<2 || $implicit) { - if ($fw->get('ESCAPE')) - $hive=$this->esc($hive); - if (isset($hive['ALIASES'])) - $hive['ALIASES']=$fw->build($hive['ALIASES']); - } - unset($fw, $implicit); - extract($hive); - unset($hive); - ob_start(); - require($this->view); - $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=Base::instance(); - $cache=Cache::instance(); - $cached=$cache->exists($hash=$fw->hash($file),$data); - if ($cached && $cached[0]+$ttl>microtime(TRUE)) - return $data; - foreach ($fw->split($fw->get('UI').';./') as $dir) - if (is_file($this->view=$fw->fixslashes($dir.$file))) { - if (isset($_COOKIE[session_name()])) - @session_start(); - $fw->sync('SESSION'); - if ($mime && PHP_SAPI!='cli' && !headers_sent()) - header('Content-Type: '.$mime.'; '. - 'charset='.$fw->get('ENCODING')); - $data=$this->sandbox($hive); - if(isset($this->trigger['afterrender'])) - foreach($this->trigger['afterrender'] as $func) - $data=$fw->call($func,$data); - if ($ttl) - $cache->set($hash,$data); - 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 - //! MIME type - $mime, - //! token filter - $filter = array( - 'esc'=>'$this->esc', - 'raw'=>'$this->raw', - 'alias'=>'\Base::instance()->alias', - 'format'=>'\Base::instance()->format' - ); - - /** - * Convert token to variable - * @return string - * @param $str string - **/ - function token($str) { - return trim(preg_replace('/\{\{(.+?)\}\}/s',trim('\1'), - Base::instance()->compile($str))); - } - - /** - * register token filter - * @param string $key - * @param string $func - * @return array - */ - function filter($key=NULL,$func=NULL) { - if (!$key) - return array_keys($this->filter); - if (!$func) - return $this->filter[$key]; - $this->filter[$key]=$func; - } - - /** - * Assemble markup - * @return string - * @param $node string - **/ - protected function build($node) { - $self=$this; - return preg_replace_callback( - '/\{\-(.+?)\-\}|\{\{(.+?)\}\}(\n+)?/s', - function($expr) use($self) { - if ($expr[1]) - return $expr[1]; - $str=trim($self->token($expr[2])); - if (preg_match('/^([^|]+?)\h*\|(\h*\w+(?:\h*[,;]\h*\w+)*)/', - $str,$parts)) { - $str=$parts[1]; - foreach (Base::instance()->split($parts[2]) as $func) - $str=$self->filter($func).'('.$str.')'; - } - return ''. - (isset($expr[3])?$expr[3]."\n":''); - }, - preg_replace_callback( - '/\{~(.+?)~\}/s', - function($expr) use($self) { - return 'token($expr[1]).' ?>'; - }, - $node - ) - ); - } - - /** - * Render template string - * @return string - * @param $str string - * @param $hive array - **/ - function resolve($str,array $hive=NULL) { - if (!$hive) - $hive=\Base::instance()->hive(); - extract($hive); - ob_start(); - eval(' ?>'.$this->build($str).'get('TEMP'))) - mkdir($tmp,Base::MODE,TRUE); - foreach ($fw->split($fw->get('UI')) as $dir) { - $cached=$cache->exists($hash=$fw->hash($dir.$file),$data); - if ($cached && $cached[0]+$ttl>microtime(TRUE)) - return $data; - if (is_file($view=$fw->fixslashes($dir.$file))) { - if (!is_file($this->view=($tmp. - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash($view).'.php')) || - filemtime($this->view)\h*'. - '(?!["\'])|\{\*.+?\*\}/is','', - $fw->read($view)); - if (method_exists($this,'parse')) - $text=$this->parse($text); - $fw->write($this->view,$this->build($text)); - } - if (isset($_COOKIE[session_name()])) - @session_start(); - $fw->sync('SESSION'); - if ($mime && PHP_SAPI!='cli' && !headers_sent()) - header('Content-Type: '.($this->mime=$mime).'; '. - 'charset='.$fw->get('ENCODING')); - $data=$this->sandbox($hive); - if(isset($this->trigger['afterrender'])) - foreach ($this->trigger['afterrender'] as $func) - $data = $fw->call($func, $data); - if ($ttl) - $cache->set($hash,$data); - return $data; - } - } - user_error(sprintf(Base::E_Open,$file),E_USER_ERROR); - } - -} - -//! 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 7445e1434..000000000 --- a/app/lib/basket.php +++ /dev/null @@ -1,237 +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=array(); - - /** - * 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=array(); - if (isset($_SESSION[$this->key])) { - foreach ($_SESSION[$this->key] as $id=>$item) - if (!isset($key) || - array_key_exists($key,$item) && $item[$key]==$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 array(); - } - - /** - * 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=array(); - } - - /** - * 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()->get($var); - foreach ($var as $key=>$val) - $this->item[$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 array(); - } - - /** - * Instantiate class - * @return void - * @param $key string - **/ - function __construct($key='basket') { - $this->key=$key; - @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 6ecd61e47..000000000 --- a/app/lib/bcrypt.php +++ /dev/null @@ -1,96 +0,0 @@ -. - -*/ - -//! Lightweight password hashing library -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 (extension_loaded('mcrypt')) - $iv=mcrypt_create_iv($raw,MCRYPT_DEV_URANDOM); - 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 6a4ddd298..000000000 --- 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/code.css b/app/lib/code.css deleted file mode 100644 index 618703f91..000000000 --- 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/cron.php b/app/lib/cron.php deleted file mode 100644 index 28a619b60..000000000 --- a/app/lib/cron.php +++ /dev/null @@ -1,228 +0,0 @@ -'0 0 1 1 *', - 'annually'=>'0 0 1 1 *', - 'monthly'=>'0 0 1 * *', - 'weekly'=>'0 0 * * 0', - 'daily'=>'0 0 * * *', - 'hourly'=>'0 * * * *', - ); - - /** - * 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 - */ - function execute($job,$async=TRUE) { - if (!isset($this->jobs[$job])) - return; - $f3=\Base::instance(); - if (is_string($func=$this->jobs[$job][0])){ - $func=$f3->grab($func); - } - - if (!is_callable($func)) - return; - if ($async && $this->async) { - // 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=''; - $file='index.php'; - if ($this->clipath) { - $dir=dirname($this->clipath); - $file=basename($this->clipath); - } - if (@$dir[0]!='/') - $dir=getcwd().'/'.$dir; - exec(sprintf('cd "%s";php %s /cron/%s > /dev/null 2>/dev/null &',$dir,$file,$job)); - } else { - $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)); - } - } - } - - /** - * Run scheduler, i.e executes all due jobs at a given time - * @param int $time - * @param bool $async - */ - function run($time=NULL,$async=TRUE) { - if (!isset($time)) - $time=time(); - foreach(array_keys($this->jobs) as $job) - if ($this->isDue($job,$time)) - $this->execute($job,$async); - } - - /** - * Route controller code - * @param \Base $f3 - * @param array $params - */ - function route($f3,$params) { - - if (PHP_SAPI=='cli'?!$this->cli:!$this->web) - $f3->error(404); - if (isset($params['job'])) - $this->execute($params['job'],FALSE); - else{ - // IMPORTANT! async does not work on Windows - // -> my development environment is Windows :(( - $async = FALSE; - $this->run(NULL, $async); - } - } - - /** - * 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('jobs','async','presets'))) - return $this->$name; - trigger_error(sprintf(self::E_Undefined,__CLASS__,$name)); - } - - //! Constructor - function __construct() { - $f3=\Base::instance(); - $config=(array)$f3->get('CRON'); - - foreach(array('log','cli','web') as $k) - if (isset($config[$k])) - $this->$k=(bool)$config[$k]; - foreach(array('clipath') as $k) - if (isset($config[$k])) - $this->$k=(string)$config[$k]; - 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 (function_exists('exec') && exec('php -r "echo 1+3;"')=='4') - $this->async=TRUE; - if ($this->cli || $this->web) - $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 432091eb4..000000000 --- a/app/lib/db/cortex.php +++ /dev/null @@ -1,2603 +0,0 @@ - - * https://github.com/ikkez/F3-Sugar/ - * - * @package DB - * @version 1.4.0 - * @since 24.04.2012 - * @date 04.06.2015 - */ - -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 - - /** @var Cursor */ - protected $mapper; - - /** @var CortexQueryParser */ - protected $queryParser; - - static - $init = false; // just init without mapper - - 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))) - trigger_error(self::E_CONNECTION); - 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'; - if (!$this->table && !$this->fluid) - trigger_error(self::E_NO_TABLE); - $this->ttl = $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->getTable(),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': - $this->mapper = new SQL\Mapper($this->db, $this->table, $this->whitelist, - ($this->fluid)?0:$this->ttl); - break; - case 'mongo': - $this->mapper = new Mongo\Mapper($this->db, $this->table); - break; - default: - trigger_error(sprintf(self::E_UNKNOWN_DB_ENGINE,$this->dbsType)); - } - $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); - unset($conf); - } - } - - /** - * get fields or set whitelist / blacklist of fields - * @param array $fields - * @param bool $exclude - * @return array - */ - public function fields(array $fields=array(), $exclude=false) - { - if ($fields) - // collect 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]); - } - $schema = $this->whitelist ?: $this->mapper->fields(); - if (!$schema && !$this->dbsType != 'sql' && $this->dry()) { - $schema = $this->load()->mapper->fields(); - $this->reset(); - } - if (!$this->whitelist && $this->fieldConf) - $schema=array_unique(array_merge($schema,array_keys($this->fieldConf))); - 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)) - $this->whitelist[]=$id; - $this->initMapper(); - return $this->whitelist; - } - - /** - * 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, - ); - 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) - $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(); - 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); - if (strlen($table=$table?:$df['table'])==0) - trigger_error(self::E_NO_TABLE); - if (is_null($fields)) - if (!empty($df['fieldConf'])) - $fields = $df['fieldConf']; - elseif(!$df['fluid']) { - trigger_error(self::E_FIELD_SETUP); - return false; - } else - $fields = array(); - if ($db instanceof SQL) { - $schema = new Schema($db); - // prepare field configuration - if (!empty($fields)) - 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'])) { - unset($fields[$key]); - continue; - } - $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[1], $table, $key, - $rel['fieldConf'][$relConf[1]]['has-many']); - if (!in_array($mmTable,$schema->getTables())) { - $mmt = $schema->createTable($mmTable); - $mmt->addColumn($relConf[1])->type($relConf['relFieldType']); - $mmt->addColumn($key)->type($field['type']); - $index = array($relConf[1],$key); - sort($index); - $mmt->addIndex($index); - $mmt->build(); - } - } - } - // 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); - 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); - if (strlen($table=strtolower($table?:$df['table']))==0) - trigger_error(self::E_NO_TABLE); - 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 (!is_int(strpos($fclass, $self))) - trigger_error(sprintf(self::E_MM_REL_CLASS, $fclass, $self)); - if ($pfkey != $pkey) - trigger_error(sprintf(self::E_MM_REL_FIELD, - $fclass.'.'.$pfkey, $self.'.'.$pkey)); - } - $mmTable = array($ftable.'__'.$fkey, $ptable.'__'.$pkey); - natcasesort($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->getTable(), $key, $fConf); - $this->fieldConf[$key]['has-many']['refTable'] = $mmTable; - } else - $mmTable = $conf['refTable']; - return $mmTable; - } - - /** - * resolve relation field types - * @param $field - * @return mixed - */ - protected static function resolveRelationConf($field) - { - 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]])) { - $field['has-many']['hasRel'] = 'has-many'; - $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[3])?$relConf[3]:$rel['primary']; - } else { - $field['has-many']['hasRel'] = 'belongs-to-one'; - $toConf=$rel['fieldConf'][$relConf[1]]['belongs-to-one']; - if (is_array($toConf)) - $field['has-many']['relField'] = $toConf[1]; - } - } 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 - */ - public function find($filter = NULL, array $options = NULL, $ttl = 0) - { - $sort=false; - if ($this->dbsType!='sql') { - if (!empty($this->countFields)) - // 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); - } - if (!empty($this->countFields)) - // 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|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)); - $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)); - $options['group'] = (isset($options['group'])?$options['group'].',':''). - $this->db->quotekey($this->table.'.'.$this->primary); - $groupFields = explode(',', preg_replace('/"/','',$options['group'])); - // 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->db->quotekey($this->table.'.'.$field); - } - elseif ($result = $this->_hasRefsInMM($key,$has_filter,$has_options,$ttl)) - $addToFilter = array($id.' IN ?', $result); - } // *-to-one - 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); - } - 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->_sql_quoteCondition($cond,$this->db->quotekey($this->getTable())); - $filter[0] .= '('.$cond.')'; - $filter = array_merge($filter, $addToFilter); - } - } - $this->hasCond = null; - } - $filter = $this->queryParser->prepareFilter($filter,$this->dbsType,$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/i',' DESC NULLS LAST',$options['order']); - if (!empty($hasJoin)) { - // assemble full sql query - $adhoc=''; - if ($count) - $sql = 'SELECT COUNT(*) AS '.$this->db->quotekey('rows').' FROM '.$qtable; - else { - if (!empty($this->preBinds)) { - $crit = array_shift($filter); - $filter = array_merge($this->preBinds,$filter); - array_unshift($filter,$crit); - } - if (!empty($m_refl_adhoc)) - foreach ($m_refl_adhoc as $key=>$val) - $adhoc.=', '.$val['expr'].' AS '.$key; - $sql = 'SELECT '.$qtable.'.*'.$adhoc.' FROM '.$qtable; - } - $sql .= ' '.implode(' ',$hasJoin).' WHERE '.$filter[0]; - if (!$count) { - if (isset($options['group'])) - $sql .= ' GROUP BY '.$this->_sql_quoteCondition($options['group'], $this->table); - if (isset($options['order'])) - $sql .= ' ORDER BY '.$options['order']; - 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']; - } - } - 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(); - // TODO: refactor this. Reflection can be removed for F3 >= v3.4.1 - $mapper->query= array($record); - $m_adhoc = empty($adhoc) ? array() : $m_refl_adhoc; - foreach ($record as $key=>$val) - if (isset($m_refl_adhoc[$key])) - $m_adhoc[$key]['value']=$val; - else - $mapper->set($key, $val); - if (!empty($adhoc)) { - $refl = new \ReflectionObject($mapper); - $prop = $refl->getProperty('adhoc'); - $prop->setAccessible(true); - $prop->setValue($mapper,$m_adhoc); - $prop->setAccessible(false); - } - $record = $mapper; - unset($record, $mapper); - } - return $result; - } elseif (!empty($this->preBinds) && !$count) { - // 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); - } - } - return ($count) - ? $this->mapper->count($filter,$ttl) - : $this->mapper->find($filter,$this->queryParser->prepareOptions($options,$this->dbsType),$ttl); - } - - /** - * Retrieve first object that satisfies criteria - * @param null $filter - * @param array $options - * @param int $ttl - * @return Cortex - */ - 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; - } - - /** - * 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())); - if (!isset($this->fieldConf[$key]['relType'])) - trigger_error(self::E_HAS_COND); - $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; - } - - /** - * 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)); - $pivotSet = $pivot->find(array($key.' IN ?',$hasIDs),null,$ttl); - if ($pivotSet) - $result = array_unique($pivotSet->getAll($fieldConf['relField'],true)); - } - 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']; - $hasJoin = array(); - $mmTable = $this->mmTable($fieldConf,$key); - $hasJoin[] = $this->_sql_left_join($this->primary,$this->table,$fieldConf['relField'],$mmTable); - $hasJoin[] = $this->_sql_left_join($key,$mmTable,$fieldConf['relPK'],$fieldConf['relTable']); - $this->_sql_mergeRelCondition($hasCond,$fieldConf['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; - $query = $this->_sql_left_join($key,$this->table,$fkey,$table); - $this->_sql_mergeRelCondition($cond,$table,$filter,$options); - return $query; - } - - /** - * assemble SQL join query string - */ - protected function _sql_left_join($skey,$sTable,$fkey,$fTable) - { - $skey = $this->db->quotekey($skey); - $sTable = $this->db->quotekey($sTable); - $fkey = $this->db->quotekey($fkey); - $fTable = $this->db->quotekey($fTable); - 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 - */ - protected function _sql_mergeRelCondition($cond, $table, &$filter, &$options) - { - $table = $this->db->quotekey($table); - if (!empty($cond[0])) { - $whereClause = '('.array_shift($cond[0]).')'; - $whereClause = $this->_sql_quoteCondition($whereClause,$table); - if (!$filter) - $filter = array($whereClause); - elseif (!empty($filter[0])) - $filter[0] = '('.$this->_sql_quoteCondition($filter[0], - $this->db->quotekey($this->table)).') and '.$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; - } - } - - protected function _sql_quoteCondition($cond, $table) - { - $db = $this->db; - if (preg_match('/[`\'"\[\]]/i',$cond)) - return $cond; - return preg_replace_callback('/\w+/i',function($match) use($table,$db) { - if (preg_match('/\b(AND|OR|IN|LIKE|NOT)\b/i',$match[0])) - return $match[0]; - return $table.'.'.$db->quotekey($match[0]); - }, $cond); - } - - /** - * 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])) - { - $filter = $this->relFilter[$key][0]; - $crit[0] .= ' and '.array_shift($filter); - $crit = array_merge($crit, $filter); - } - return $crit; - } - - /** - * 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 void - */ - public function erase($filter = null) - { - $filter = $this->queryParser->prepareFilter($filter, $this->dbsType); - if (!$filter && $this->emit('beforeerase')!==false) { - if ($this->fieldConf) { - foreach($this->fieldConf as $field => $conf) - if (isset($conf['has-many']) && - $conf['has-many']['hasRel']=='has-many') - $this->set($field,null); - $this->save(); - } - $this->mapper->erase(); - $this->emit('aftererase'); - } elseif($filter) - $this->mapper->erase($filter); - } - - /** - * Save mapped record - * @return mixed - **/ - function save() - { - 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(); - } - // 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)); - - // 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']; - $mmTable = $this->mmTable($relConf,$key); - $rel = $this->getRelInstance(null, array('db'=>$this->db, 'table'=>$mmTable)); - $id = $this->get($relConf['relPK'],true); - // delete all refs - if (is_null($val)) - $rel->erase(array($relConf['relField'].' = ?', $id)); - // update refs - elseif (is_array($val)) { - $rel->erase(array($relConf['relField'].' = ?', $id)); - foreach($val as $v) { - $rel->set($key,$v); - $rel->set($relConf['relField'],$id); - $rel->save(); - $rel->reset(); - } - } - unset($rel); - } 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 int $ttl - * @return mixed - */ - public function count($filter = NULL, $ttl = 60) - { - $has=$this->hasCond; - $count=$this->filteredFind($filter,null,$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) { - if (isset($this->fieldConf[$key])){ - // one-to-one, one-to-many - if ($this->fieldConf[$key]['relType'] == 'belongs-to-one') { - if ($this->dbsType == 'sql') { - $this->set('count_'.$key,'count('.$key.')'); - $this->grp_stack=(!$this->grp_stack)?$key:$this->grp_stack.','.$key; - } elseif ($this->dbsType == 'mongo') - $this->_mongo_addGroup(array( - 'keys'=>array($key=>1), - 'reduce' => 'prev.count_'.$key.'++;', - "initial" => array("count_".$key => 0) - )); - else - trigger_error('Cannot add direct relational counter.'); - } 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($this->db->quotekey($mmTable).'.'.$this->db->quotekey($relConf['relField']) - .' = '.$this->db->quotekey($this->getTable()).'.'.$this->db->quotekey($this->primary)); - $from=$mmTable; - if (array_key_exists($key, $this->relFilter) && - !empty($this->relFilter[$key][0])) { - $options=array(); - $from = $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->fieldConf); - $crit = array_shift($filter); - if (count($filter)>0) - $this->preBinds+=$filter; - $this->set('count_'.$key,'(select count('.$mmTable.'.'.$relConf['relField'].') from '.$from. - ' where '.$crit.' group by '.$mmTable.'.'.$relConf['relField'].')'); - } 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=$this->db->quotekey($fConf['table']); - $fKey=$this->db->quotekey($fConf['primary']); - $rKey=$this->db->quotekey($relConf[1]); - $pKey=$this->db->quotekey($this->primary); - $table=$this->db->quotekey($this->getTable()); - $crit = $fTable.'.'.$rKey.' = '.$table.'.'.$pKey; - $filter = $this->mergeWithRelFilter($key,array($crit)); - $filter = $this->queryParser->prepareFilter($filter,$this->dbsType,$this->fieldConf); - $crit = array_shift($filter); - if (count($filter)>0) - $this->preBinds+=$filter; - $this->set('count_'.$key,'(select count('.$fTable.'.'.$fKey.') from '.$fTable.' where '. - $crit.' group by '.$fTable.'.'.$rKey.')'); - } else { - // count rel - $this->countFields[]=$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 || Schema::DT_TIMESTAMP) - $this->set($key,date($date.' H:i:s')); - elseif ($type == Schema::DT_DATE) - $this->set($key,date($date)); - } - } - - /** - * Bind value to key - * @return mixed - * @param $key string - * @param $val mixed - */ - function set($key, $val) - { - $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 (is_null($val)) - $val = NULL; - elseif (is_object($val) && - !($this->dbsType=='mongo' && $val instanceof \MongoId)) { - // fetch fkey from mapper - if (!$val instanceof Cortex || $val->dry()) - trigger_error(self::E_INVALID_RELATION_OBJECT); - 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' && !$val instanceof \MongoId) - $val = new \MongoId($val); - } elseif (isset($fields[$key]['has-one'])){ - $relConf = $fields[$key]['has-one']; - if (is_null($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'])) { - // many-to-many, bidirectional - $relConf = $fields[$key]['has-many']; - if ($relConf['hasRel'] == 'has-many') { - // custom setter - $val = $this->emit('set_'.$key, $val); - $val = $this->getForeignKeysArray($val,'_id',$key); - $this->saveCsd[$key] = $val; // array of keys - return $val; - } elseif ($relConf['hasRel'] == 'belongs-to-one') { - // TODO: many-to-one, bidirectional, inverse way - trigger_error("not implemented"); - } - } - // 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)); - // 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)); - // MongoId shorthand - if ($this->dbsType == 'mongo' && !$val instanceof \MongoId) { - if ($key == '_id') - $val = new \MongoId($val); - elseif (preg_match('/INT/i',$fields[$key]['type']) - && !isset($fields[$key]['relType'])) - $val = (int) $val; - } - if (preg_match('/BOOL/i',$fields[$key]['type'])) { - $val = !$val || $val==='false' ? false : (bool) $val; - if ($this->dbsType == 'sql') - $val = (int) $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 (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]; - $refl = new \ReflectionObject($this->mapper); - $prop = $refl->getProperty('fields'); - $prop->setAccessible(true); - $fields = $prop->getValue($this->mapper); - $fields[$key] = $newField + array('value'=>NULL,'changed'=>NULL); - $prop->setValue($this->mapper,$fields); - } - } - // custom setter - $val = $this->emit('set_'.$key, $val); - 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 - * @return mixed|null - */ - public function virtual($key,$val) { - $this->vFields[$key]=$val; - } - - /** - * 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; - 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)); - $rel = $this->getRelInstance($fromConf[0],null,$key,true); - $relFieldConf = $rel->getFieldConfiguration(); - $relType = key($relFieldConf[$fromConf[1]]); - // 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; - } else { - $crit = array($fromConf[1].' = ?', $this->get($toConf[1],true)); - $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; - } - } - // 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 - $mmRes = $rel->find(array($fromConf['relField'].' IN ?', $relKeys),null,$this->_ttl); - if (!$mmRes) - $cx->setRelSet($key, NULL); - else { - $pivotRel = array(); - $pivotKeys = array(); - foreach($mmRes as $model) { - $val = $model->get($key,true); - $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($toConf['relPK'].' 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 - $results = $rel->find( - array($fromConf['relField'].' = ?', $this->get($fromConf['relPK'],true)),null,$this->_ttl); - if(!$results) - $this->fieldsCache[$key] = NULL; - else { - $fkeys = $results->getAll($key,true); - // create foreign table mapper - unset($rel); - $rel = $this->getRelInstance($fromConf[0],null,$key,true); - // load foreign models - $filter = array($toConf['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->exists($key) ? null :$this->mapper->get($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 - $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); - } - // 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 - $val = array_key_exists($key,$this->fieldsCache) ? $this->fieldsCache[$key] - : (($this->exists($key)) ? $this->mapper->{$key} : null); - if ($this->dbsType == 'mongo' && $val instanceof \MongoId) { - // conversion to string makes further processing in template, etc. much easier - $val = (string) $val; - } - // custom getter - $out = $this->emit('get_'.$key, $val); - return $out; - } - - /** - * 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->expose(); - 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)); - // 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 && $item instanceof \MongoId)) { - if (!$item instanceof Cortex || $item->dry()) - trigger_error(self::E_INVALID_RELATION_OBJECT); - else $item = $item->get($rel_field,true); - } - if ($isMongo && $rel_field == '_id' && is_string($item)) - $item = new \MongoId($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); - $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); - \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 $fieldConf - * @param $key - * @return Cortex - */ - protected function getRelFromConf(&$fieldConf, $key) { - if (!is_array($fieldConf)) - $fieldConf = array($fieldConf, '_id'); - $rel = $this->getRelInstance($fieldConf[0],null,$key,true); - if($this->dbsType=='sql' && $fieldConf[1] == '_id') - $fieldConf[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 (!empty($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=$type[0]; - // 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|array $key array of mapper objects, or 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)); - } - - 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=array(); - $this->saveCsd=array(); - $this->countFields=array(); - $this->preBinds=array(); - $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); - } - } - - /** - * 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'])); - } - - /** - * 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 __destruct() { - unset($this->mapper); - } - - 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 null $fieldConf - * @return array|bool|null - */ - public function prepareFilter($cond, $engine,$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 (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, $fieldConf); - unset($part); - } - $ncond = $this->_mongo_parse_logical_op($parts); - break; - case 'sql': - // preserve identifier - $where = preg_replace('/(?!\B)_id/', 'id', $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 (!is_array($val) || empty($val)) - trigger_error(self::E_INBINDVALUE); - $bindMarks = str_repeat('?,', count($val) - 1).'?'; - $part = substr($part, 0, $pos).'IN ('.$bindMarks.')'; - $ncond = array_merge($ncond, $val); - } else - $ncond[] = $val; - } - unset($part); - } - array_unshift($ncond, implode(' ', $parts)); - break; - default: - trigger_error(self::E_ENGINEERROR); - } - $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*((?splitLogical($where); - if (is_int(strpos($where, ':'))) - list($parts, $args) = $this->convertNamedParams($parts, $args); - $ncond = array(); - foreach ($parts as &$part) { - if (in_array(strtoupper($part), array('AND', 'OR'))) - continue; - // prefix field names - $part = preg_replace('/([a-z_-]+)/i', '@$1', $part, -1, $count); - // value comparison - if (is_int(strpos($part, '?'))) { - $val = array_shift($args); - preg_match('/(@\w+)/i', $part, $match); - // 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); - $part = ($not ? '!' : '').'preg_match(?,'.$match[0].')'; - } // find IN operator - else if (is_int($pos = strpos($upart, ' @IN '))) { - if ($not = is_int($npos = strpos($upart, '@NOT'))) - $pos = $npos; - $part = ($not ? '!' : '').'in_array('.substr($part, 0, $pos). - ',array(\''.implode('\',\'', $val).'\'))'; - unset($val); - } - // add existence check - $part = '(isset('.$match[0].') && '.$part.')'; - if (isset($val)) - $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 ($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); - else - // add sub-bracket to parse array - $child[] = $part; - } // add to parse array - elseif ($b_offset > 0) - $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); - 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 null $fieldConf - * @return array|null - */ - protected function _mongo_parse_relational_op($part, &$args, $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 (!$id instanceof \MongoId) - $id = new \MongoId($id); - unset($id); - } - elseif(!$var instanceof \MongoId) - $var = new \MongoId($var); - } - // find LIKE operator - if (in_array($upart, array('LIKE','NOT LIKE'))) { - $rgx = $this->_likeValueToRegEx($var); - $var = new \MongoRegex($rgx); - if ($upart == 'NOT LIKE') - $var = array('$not' => $var); - } // find IN operator - elseif (in_array($upart, array('IN','NOT IN'))) { - $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 - * @return array|null - */ - public function prepareOptions($options, $engine) - { - if (empty($options) || !is_array($options)) - return null; - switch ($engine) { - case 'jig': - if (array_key_exists('order', $options)) - $options['order'] = str_replace(array('asc', 'desc'), - array('SORT_ASC', 'SORT_DESC'), strtolower($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; - } - 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))); - if (!$this->hasRelSet($prop) || !($relSet = $this->getRelSet($prop))) - return null; - foreach ($keys as &$key) { - if ($key instanceof \MongoId) - $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->exists($prop,true)) { - $val = $model->get($prop, $raw); - if (!empty($val)) - $out[] = $val; - } - } - 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]); - } - - 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 c218fa478..000000000 --- a/app/lib/db/cursor.php +++ /dev/null @@ -1,384 +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=array(), - //! Current position - $ptr=0, - //! Event listeners - $trigger=array(); - - /** - * 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 $ttl int - **/ - abstract function count($filter=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.5if 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 \DB\Cursor|FALSE - * @param $filter string|array - * @param $options array - * @param $ttl int - **/ - function findone($filter=NULL,array $options=NULL,$ttl=0) { - if (!$options) - $options=array(); - // 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 - **/ - function paginate( - $pos=0,$size=10,$filter=NULL,array $options=NULL,$ttl=0) { - $total=$this->count($filter,$ttl); - $count=ceil($total/$size); - $pos=max(0,min($pos,$count-1)); - return array( - 'subset'=>$this->find($filter, - array_merge( - $options?:array(), - array('limit'=>$size,'offset'=>$pos*$size) - ), - $ttl - ), - 'total'=>$total, - 'limit'=>$size, - 'count'=>$count, - 'pos'=>$pos<$count?$pos:0 - ); - } - - /** - * 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) { - return ($this->query=$this->find($filter,$options,$ttl)) && - $this->skip(0)?$this->query[$this->ptr=0]: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=array(); - $this->ptr=0; - } - -} diff --git a/app/lib/db/jig.php b/app/lib/db/jig.php deleted file mode 100644 index 736735c4b..000000000 --- a/app/lib/db/jig.php +++ /dev/null @@ -1,150 +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; - - /** - * 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]=array(); - 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) - return count($this->data[$file]=$data); - $fw=\Base::instance(); - switch ($this->format) { - case self::FORMAT_JSON: - $out=json_encode($data,@constant('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 - * @return string - **/ - function log() { - return $this->log; - } - - /** - * 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->dir) - $this->data=array(); - elseif ($glob=@glob($this->dir.'/*',GLOB_NOSORT)) - foreach ($glob as $file) - @unlink($file); - } - - /** - * Instantiate class - * @param $dir string - * @param $format int - **/ - function __construct($dir=NULL,$format=self::FORMAT_JSON) { - if ($dir && !is_dir($dir)) - mkdir($dir,\Base::MODE,TRUE); - $this->uuid=\Base::instance()->hash($this->dir=$dir); - $this->format=$format; - } - -} diff --git a/app/lib/db/jig/mapper.php b/app/lib/db/jig/mapper.php deleted file mode 100644 index e5b007c6d..000000000 --- a/app/lib/db/jig/mapper.php +++ /dev/null @@ -1,476 +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=array(); - - /** - * 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 - **/ - protected function factory($id,$row) { - $mapper=clone($this); - $mapper->reset(); - $mapper->id=$id; - foreach ($row as $field=>$val) - $mapper->document[$field]=$val; - $mapper->query=array(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+array('_id'=>$this->id); - } - - /** - * Convert tokens in string expression to variable names - * @return string - * @param $str string - **/ - function token($str) { - $self=$this; - $str=preg_replace_callback( - '/(?stringify(substr($expr[1],1)): - (preg_match('/^\w+/', - $mix=$self->token($expr[2]))? - $fw->stringify($mix): - $mix)). - ']'; - }, - $token[1] - ); - }, - $str - ); - return trim($str); - } - - /** - * Return records that match criteria - * @return \DB\JIG\Mapper[]|FALSE - * @param $filter array - * @param $options array - * @param $ttl int - * @param $log bool - **/ - function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) { - if (!$options) - $options=array(); - $options+=array( - 'order'=>NULL, - 'limit'=>0, - 'offset'=>0 - ); - $fw=\Base::instance(); - $cache=\Cache::instance(); - $db=$this->db; - $now=microtime(TRUE); - $data=array(); - if (!$fw->get('CACHE') || !$ttl || !($cached=$cache->exists( - $hash=$fw->hash($this->db->dir(). - $fw->stringify(array($filter,$options))).'.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:array(1=>$args); - $keys=$vals=array(); - $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['order'])) { - $cols=$fw->split($options['order']); - 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)=array($val1[$col],$val2[$col]); - if ($out=strnatcmp($v1,$v2)* - (($order==SORT_ASC)*2-1)) - return $out; - } - return 0; - } - ); - } - $data=array_slice($data, - $options['offset'],$options['limit']?:NULL,TRUE); - if ($fw->get('CACHE') && $ttl) - // Save to cache backend - $cache->set($hash,$data,$ttl); - } - $out=array(); - 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; - } - - /** - * Count records that match criteria - * @return int - * @param $filter array - * @param $ttl int - **/ - function count($filter=NULL,$ttl=0) { - $now=microtime(TRUE); - $out=count($this->find($filter,NULL,$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:array(); - $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=array('_id'=>$this->id); - if (isset($this->trigger['beforeinsert']) && - \Base::instance()->call($this->trigger['beforeinsert'], - array($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'], - array($this,$pkey)); - $this->load(array('@_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'], - array($this,array('_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'], - array($this,array('_id'=>$this->id))); - return $this->document; - } - - /** - * Delete current record - * @return bool - * @param $filter array - **/ - function erase($filter=NULL) { - $db=$this->db; - $now=microtime(TRUE); - $data=&$db->read($this->file); - $pkey=array('_id'=>$this->id); - if ($filter) { - foreach ($this->find($filter,NULL,FALSE) as $mapper) - if (!$mapper->erase()) - return FALSE; - return TRUE; - } - elseif (isset($this->id)) { - unset($data[$this->id]); - parent::erase(); - } - else - return FALSE; - if (isset($this->trigger['beforeerase']) && - \Base::instance()->call($this->trigger['beforeerase'], - array($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:array(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 (isset($this->trigger['aftererase'])) - \Base::instance()->call($this->trigger['aftererase'], - array($this,$pkey)); - return TRUE; - } - - /** - * Reset cursor - * @return NULL - **/ - function reset() { - $this->id=NULL; - $this->document=array(); - 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()->get($var); - if ($func) - $var=call_user_func($func,$var); - foreach ($var as $key=>$val) - $this->document[$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 7f9a6ffaf..000000000 --- a/app/lib/db/jig/session.php +++ /dev/null @@ -1,180 +0,0 @@ -. - -*/ - -namespace DB\Jig; - -//! Jig-managed session handler -class Session extends Mapper { - - protected - //! Session ID - $sid; - - /** - * Open session - * @return TRUE - * @param $path string - * @param $name string - **/ - function open($path,$name) { - return TRUE; - } - - /** - * Close session - * @return TRUE - **/ - function close() { - return TRUE; - } - - /** - * Return session data in serialized format - * @return string|FALSE - * @param $id string - **/ - function read($id) { - if ($id!=$this->sid) - $this->load(array('@session_id=?',$this->sid=$id)); - return $this->dry()?FALSE:$this->get('data'); - } - - /** - * Write session data - * @return TRUE - * @param $id string - * @param $data string - **/ - function write($id,$data) { - $fw=\Base::instance(); - $sent=headers_sent(); - $headers=$fw->get('HEADERS'); - if ($id!=$this->sid) - $this->load(array('@session_id=?',$this->sid=$id)); - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); - $this->set('session_id',$id); - $this->set('data',$data); - $this->set('csrf',$sent?$this->csrf():$csrf); - $this->set('ip',$fw->get('IP')); - $this->set('agent', - isset($headers['User-Agent'])?$headers['User-Agent']:''); - $this->set('stamp',time()); - $this->save(); - return TRUE; - } - - /** - * Destroy session - * @return TRUE - * @param $id string - **/ - function destroy($id) { - $this->erase(array('@session_id=?',$id)); - setcookie(session_name(),'',strtotime('-1 year')); - unset($_COOKIE[session_name()]); - header_remove('Set-Cookie'); - return TRUE; - } - - /** - * Garbage collector - * @return TRUE - * @param $max int - **/ - function cleanup($max) { - $this->erase(array('@stamp+?dry()?FALSE:$this->get('csrf'); - } - - /** - * Return IP address - * @return string|FALSE - **/ - function ip() { - return $this->dry()?FALSE:$this->get('ip'); - } - - /** - * Return Unix timestamp - * @return string|FALSE - **/ - function stamp() { - return $this->dry()?FALSE:$this->get('stamp'); - } - - /** - * Return HTTP user agent - * @return string|FALSE - **/ - function agent() { - return $this->dry()?FALSE:$this->get('agent'); - } - - /** - * Instantiate class - * @param $db object - * @param $file string - * @param $onsuspect callback - **/ - function __construct(\DB\Jig $db,$file='sessions',$onsuspect=NULL) { - parent::__construct($db,$file); - session_set_save_handler( - array($this,'open'), - array($this,'close'), - array($this,'read'), - array($this,'write'), - array($this,'destroy'), - array($this,'cleanup') - ); - register_shutdown_function('session_commit'); - @session_start(); - $fw=\Base::instance(); - $headers=$fw->get('HEADERS'); - if (($ip=$this->ip()) && $ip!=$fw->get('IP') || - ($agent=$this->agent()) && - (!isset($headers['User-Agent']) || - $agent!=$headers['User-Agent'])) { - if (isset($onsuspect)) - $fw->call($onsuspect,array($this)); - else { - session_destroy(); - $fw->error(403); - } - } - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); - if ($this->load(array('@session_id=?',$this->sid=session_id()))) { - $this->set('csrf',$csrf); - $this->save(); - } - } - -} diff --git a/app/lib/db/mongo.php b/app/lib/db/mongo.php deleted file mode 100644 index ffe772e0e..000000000 --- a/app/lib/db/mongo.php +++ /dev/null @@ -1,111 +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, - //! 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 - * @return string - **/ - function log() { - $cursor=$this->selectcollection('system.profile')->find(); - foreach (iterator_to_array($cursor) as $frame) - if (!preg_match('/\.system\..+$/',$frame['ns'])) - $this->log.=date('r',$frame['ts']->sec).' ('. - 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; - return $this->log; - } - - /** - * Intercept native call to re-enable profiler - * @return int - **/ - function drop() { - $out=$this->db->drop(); - $this->setprofilinglevel(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(array($this->db,$func),$args); - } - - /** - * 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); - $class=class_exists('\MongoClient')?'\MongoClient':'\Mongo'; - $this->db=new \MongoDB(new $class($dsn,$options?:array()),$dbname); - $this->setprofilinglevel(2); - } - -} diff --git a/app/lib/db/mongo/mapper.php b/app/lib/db/mongo/mapper.php deleted file mode 100644 index f4ef5170d..000000000 --- a/app/lib/db/mongo/mapper.php +++ /dev/null @@ -1,361 +0,0 @@ -. - -*/ - -namespace DB\Mongo; - -//! MongoDB mapper -class Mapper extends \DB\Cursor { - - protected - //! MongoDB wrapper - $db, - //! Mongo collection - $collection, - //! Mongo document - $document=array(), - //! Mongo cursor - $cursor; - - /** - * 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 \DB\Mongo\Mapper - * @param $row array - **/ - protected function factory($row) { - $mapper=clone($this); - $mapper->reset(); - foreach ($row as $key=>$val) - $mapper->document[$key]=$val; - $mapper->query=array(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 \DB\Mongo\Mapper[] - * @param $fields string - * @param $filter array - * @param $options array - * @param $ttl int - **/ - function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) { - if (!$options) - $options=array(); - $options+=array( - 'group'=>NULL, - 'order'=>NULL, - 'limit'=>0, - 'offset'=>0 - ); - $fw=\Base::instance(); - $cache=\Cache::instance(); - if (!($cached=$cache->exists($hash=$fw->hash($this->db->dsn(). - $fw->stringify(array($fields,$filter,$options))).'.mongo', - $result)) || !$ttl || $cached[0]+$ttlcollection->group( - $options['group']['keys'], - $options['group']['initial'], - $options['group']['reduce'], - array( - 'condition'=>$filter, - 'finalize'=>$options['group']['finalize'] - ) - ); - $tmp=$this->db->selectcollection( - $fw->get('HOST').'.'.$fw->get('BASE').'.'. - uniqid(NULL,TRUE).'.tmp' - ); - $tmp->batchinsert($grp['retval'],array('w'=>1)); - $filter=array(); - $collection=$tmp; - } - else { - $filter=$filter?:array(); - $collection=$this->collection; - } - $this->cursor=$collection->find($filter,$fields?:array()); - 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=array(); - while ($this->cursor->hasnext()) - $result[]=$this->cursor->getnext(); - if ($options['group']) - $tmp->drop(); - if ($fw->get('CACHE') && $ttl) - // Save to cache backend - $cache->set($hash,$result,$ttl); - } - $out=array(); - foreach ($result as $doc) - $out[]=$this->factory($doc); - return $out; - } - - /** - * Return records that match criteria - * @return \DB\Mongo\Mapper[] - * @param $filter array - * @param $options array - * @param $ttl int - **/ - function find($filter=NULL,array $options=NULL,$ttl=0) { - if (!$options) - $options=array(); - $options+=array( - 'group'=>NULL, - 'order'=>NULL, - 'limit'=>0, - 'offset'=>0 - ); - return $this->select(NULL,$filter,$options,$ttl); - } - - /** - * Count records that match criteria - * @return int - * @param $filter array - * @param $ttl int - **/ - function count($filter=NULL,$ttl=0) { - $fw=\Base::instance(); - $cache=\Cache::instance(); - if (!($cached=$cache->exists($hash=$fw->hash($fw->stringify( - array($filter))).'.mongo',$result)) || !$ttl || - $cached[0]+$ttlcollection->count($filter?:array()); - if ($fw->get('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:array(); - 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'], - array($this,array('_id'=>$this->document['_id'])))===FALSE) - return $this->document; - $this->collection->insert($this->document); - $pkey=array('_id'=>$this->document['_id']); - if (isset($this->trigger['afterinsert'])) - \Base::instance()->call($this->trigger['afterinsert'], - array($this,$pkey)); - $this->load($pkey); - return $this->document; - } - - /** - * Update current record - * @return array - **/ - function update() { - $pkey=array('_id'=>$this->document['_id']); - if (isset($this->trigger['beforeupdate']) && - \Base::instance()->call($this->trigger['beforeupdate'], - array($this,$pkey))===FALSE) - return $this->document; - $this->collection->update( - $pkey,$this->document,array('upsert'=>TRUE)); - if (isset($this->trigger['afterupdate'])) - \Base::instance()->call($this->trigger['afterupdate'], - array($this,$pkey)); - return $this->document; - } - - /** - * Delete current record - * @return bool - * @param $filter array - **/ - function erase($filter=NULL) { - if ($filter) - return $this->collection->remove($filter); - $pkey=array('_id'=>$this->document['_id']); - if (isset($this->trigger['beforeerase']) && - \Base::instance()->call($this->trigger['beforeerase'], - array($this,$pkey))===FALSE) - return FALSE; - $result=$this->collection-> - remove(array('_id'=>$this->document['_id'])); - parent::erase(); - if (isset($this->trigger['aftererase'])) - \Base::instance()->call($this->trigger['aftererase'], - array($this,$pkey)); - return $result; - } - - /** - * Reset cursor - * @return NULL - **/ - function reset() { - $this->document=array(); - 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()->get($var); - if ($func) - $var=call_user_func($func,$var); - foreach ($var as $key=>$val) - $this->document[$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 - **/ - function __construct(\DB\Mongo $db,$collection) { - $this->db=$db; - $this->collection=$db->selectcollection($collection); - $this->reset(); - } - -} diff --git a/app/lib/db/mongo/session.php b/app/lib/db/mongo/session.php deleted file mode 100644 index 3d7e1d261..000000000 --- a/app/lib/db/mongo/session.php +++ /dev/null @@ -1,180 +0,0 @@ -. - -*/ - -namespace DB\Mongo; - -//! MongoDB-managed session handler -class Session extends Mapper { - - protected - //! Session ID - $sid; - - /** - * Open session - * @return TRUE - * @param $path string - * @param $name string - **/ - function open($path,$name) { - return TRUE; - } - - /** - * Close session - * @return TRUE - **/ - function close() { - return TRUE; - } - - /** - * Return session data in serialized format - * @return string|FALSE - * @param $id string - **/ - function read($id) { - if ($id!=$this->sid) - $this->load(array('session_id'=>$this->sid=$id)); - return $this->dry()?FALSE:$this->get('data'); - } - - /** - * Write session data - * @return TRUE - * @param $id string - * @param $data string - **/ - function write($id,$data) { - $fw=\Base::instance(); - $sent=headers_sent(); - $headers=$fw->get('HEADERS'); - if ($id!=$this->sid) - $this->load(array('session_id'=>$this->sid=$id)); - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); - $this->set('session_id',$id); - $this->set('data',$data); - $this->set('csrf',$sent?$this->csrf():$csrf); - $this->set('ip',$fw->get('IP')); - $this->set('agent', - isset($headers['User-Agent'])?$headers['User-Agent']:''); - $this->set('stamp',time()); - $this->save(); - return TRUE; - } - - /** - * Destroy session - * @return TRUE - * @param $id string - **/ - function destroy($id) { - $this->erase(array('session_id'=>$id)); - setcookie(session_name(),'',strtotime('-1 year')); - unset($_COOKIE[session_name()]); - header_remove('Set-Cookie'); - return TRUE; - } - - /** - * Garbage collector - * @return TRUE - * @param $max int - **/ - function cleanup($max) { - $this->erase(array('$where'=>'this.stamp+'.$max.'<'.time())); - return TRUE; - } - - /** - * Return anti-CSRF token - * @return string|FALSE - **/ - function csrf() { - return $this->dry()?FALSE:$this->get('csrf'); - } - - /** - * Return IP address - * @return string|FALSE - **/ - function ip() { - return $this->dry()?FALSE:$this->get('ip'); - } - - /** - * Return Unix timestamp - * @return string|FALSE - **/ - function stamp() { - return $this->dry()?FALSE:$this->get('stamp'); - } - - /** - * Return HTTP user agent - * @return string|FALSE - **/ - function agent() { - return $this->dry()?FALSE:$this->get('agent'); - } - - /** - * Instantiate class - * @param $db object - * @param $table string - * @param $onsuspect callback - **/ - function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL) { - parent::__construct($db,$table); - session_set_save_handler( - array($this,'open'), - array($this,'close'), - array($this,'read'), - array($this,'write'), - array($this,'destroy'), - array($this,'cleanup') - ); - register_shutdown_function('session_commit'); - @session_start(); - $fw=\Base::instance(); - $headers=$fw->get('HEADERS'); - if (($ip=$this->ip()) && $ip!=$fw->get('IP') || - ($agent=$this->agent()) && - (!isset($headers['User-Agent']) || - $agent!=$headers['User-Agent'])) { - if (isset($onsuspect)) - $fw->call($onsuspect,array($this)); - else { - session_destroy(); - $fw->error(403); - } - } - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); - if ($this->load(array('session_id'=>$this->sid=session_id()))) { - $this->set('csrf',$csrf); - $this->save(); - } - } - -} diff --git a/app/lib/db/sql.php b/app/lib/db/sql.php deleted file mode 100644 index f4734c3c5..000000000 --- a/app/lib/db/sql.php +++ /dev/null @@ -1,455 +0,0 @@ -. - -*/ - -namespace DB; - -//! PDO wrapper -class SQL { - - //@{ Error messages - const - E_PKey='Table %s does not have a primary key'; - //@} - - 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; - } - - /** - * 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; - default: - return \PDO::PARAM_STR; - } - } - - /** - * Cast value to PHP type - * @return scalar - * @param $type string - * @param $val scalar - **/ - function value($type,$val) { - switch ($type) { - case \PDO::PARAM_NULL: - return (unset)$val; - case \PDO::PARAM_INT: - return (int)$val; - case \PDO::PARAM_BOOL: - return (bool)$val; - case \PDO::PARAM_STR: - return (string)$val; - } - } - - /** - * Execute SQL statement(s) - * @return array|int|FALSE - * @param $cmds string|array - * @param $args string|array - * @param $ttl int - * @param $log bool - **/ - function exec($cmds,$args=NULL,$ttl=0,$log=TRUE) { - $auto=FALSE; - if (is_null($args)) - $args=array(); - elseif (is_scalar($args)) - $args=array(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=array($cmds); - $args=array($args); - } - $fw=\Base::instance(); - $cache=\Cache::instance(); - $result=FALSE; - for ($i=0;$i<$count;$i++) { - $cmd=$cmds[$i]; - $arg=$args[$i]; - if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd)) - continue; - $now=microtime(TRUE); - $keys=$vals=array(); - if ($fw->get('CACHE') && $ttl && ($cached=$cache->exists( - $hash=$fw->hash($this->dsn.$cmd. - $fw->stringify($arg)).'.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.=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]); - $vals[]=$fw->stringify($this->value($val[1],$val[0])); - } - else { - // Convert to PDO data type - $query->bindvalue($key,$val, - $type=$this->type($val)); - $vals[]=$fw->stringify($this->value($type,$val)); - } - $keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). - '/'; - } - if ($log) - $this->log.=date('r').' ('. - sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. - preg_replace($keys,$vals, - str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; - $query->execute(); - $error=$query->errorinfo(); - if ($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*'. - '(?: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]=array(); - foreach ($rec as $key=>$val) - $result[$pos][trim($key,'\'"[]`')]=$val; - } - $this->rows=count($result); - if ($fw->get('CACHE') && $ttl) - // Save to cache backend - $cache->set($hash,$result,$ttl); - } - else - $this->rows=$result=$query->rowcount(); - $query->closecursor(); - unset($query); - } - else { - $error=$this->errorinfo(); - if ($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 - * @return string - **/ - function log() { - return $this->log; - } - - /** - * Retrieve schema of SQL table - * @return array|FALSE - * @param $table string - * @param $fields array|string - * @param $ttl int - **/ - function schema($table,$fields=NULL,$ttl=0) { - if (strpos($table,'.')) - list($schema,$table)=explode('.',$table); - // Supported engines - $cmd=array( - 'sqlite2?'=>array( - 'PRAGMA table_info("'.$table.'");', - 'name','type','dflt_value','notnull',0,'pk',TRUE), - 'mysql'=>array( - 'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`;', - 'Field','Type','Default','Null','YES','Key','PRI'), - 'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>array( - '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'=>array( - '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); - foreach ($cmd as $key=>$val) - if (preg_match('/'.$key.'/',$this->engine)) { - // Improve InnoDB performance on MySQL with - // SET GLOBAL innodb_stats_on_metadata=0; - // This requires SUPER privilege! - $rows=array(); - foreach ($this->exec($val[0],NULL,$ttl) as $row) { - if (!$fields || in_array($row[$val[1]],$fields)) - $rows[$row[$val[1]]]=array( - 'type'=>$row[$val[2]], - 'pdo_type'=> - preg_match('/int\b|integer/i',$row[$val[2]])? - \PDO::PARAM_INT: - (preg_match('/bool/i',$row[$val[2]])? - \PDO::PARAM_BOOL: - \PDO::PARAM_STR), - '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] - ); - } - 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 - **/ - function quotekey($key) { - $delims=array( - 'mysql'=>'``', - 'sqlite2?|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].implode($use[1].'.'.$use[0],explode('.',$key)).$use[1]; - } - - /** - * Redirect call to MongoDB object - * @return mixed - * @param $func string - * @param $args array - **/ - function __call($func,array $args) { - return call_user_func_array(array($this->pdo,$func),$args); - } - - /** - * 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=array(); - if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql') - $options+=array(\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '. - strtolower(str_replace('-','',$fw->get('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 3a13c65b4..000000000 --- a/app/lib/db/sql/mapper.php +++ /dev/null @@ -1,639 +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, - //! Last insert ID - $_id, - //! Defined fields - $fields, - //! Adhoc fields - $adhoc=array(); - - /** - * Return database type - * @return string - **/ - function dbtype() { - return 'SQL'; - } - - /** - * Return mapped table - * @return string - **/ - function table() { - return $this->source; - } - - /** - * Return TRUE if field is defined - * @return bool - * @param $key string - **/ - function exists($key) { - return array_key_exists($key,$this->fields+$this->adhoc); - } - - /** - * 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; - } - - /** - * 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]['value']!==$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; - else - // Parenthesize expression in case it's a subquery - $this->adhoc[$key]=array('expr'=>'('.$val.')','value'=>NULL); - 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']; - 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]); - } - - /** - * Get PHP type equivalent of PDO constant - * @return string - * @param $pdo string - **/ - function type($pdo) { - switch ($pdo) { - case \PDO::PARAM_NULL: - return 'unset'; - case \PDO::PARAM_INT: - return 'int'; - case \PDO::PARAM_BOOL: - return 'bool'; - case \PDO::PARAM_STR: - return 'string'; - } - } - - /** - * Convert array to mapper object - * @return object - * @param $row array - **/ - protected 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; - if ($var=='fields' && $mapper->{$var}[$key]['pkey']) - $mapper->{$var}[$key]['previous']=$val; - } - $mapper->query=array(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 execute - * @return \DB\SQL\Mapper[] - * @param $fields string - * @param $filter string|array - * @param $options array - * @param $ttl int - **/ - function select($fields,$filter=NULL,array $options=NULL,$ttl=0) { - if (!$options) - $options=array(); - $options+=array( - 'group'=>NULL, - 'order'=>NULL, - 'limit'=>0, - 'offset'=>0 - ); - $db=$this->db; - $sql='SELECT '.$fields.' FROM '.$this->table; - $args=array(); - if ($filter) { - 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:array(1=>$args); - list($filter)=$filter; - } - $sql.=' WHERE '.$filter; - } - if ($options['group']) { - $sql.=' GROUP BY '.implode(',',array_map( - function($str) use($db) { - return preg_replace_callback( - '/\b(\w+)\h*(HAVING.+|$)/i', - function($parts) use($db) { - return $db->quotekey($parts[1]); - }, - $str - ); - }, - explode(',',$options['group']))); - } - if ($options['order']) { - $sql.=' ORDER BY '.implode(',',array_map( - function($str) use($db) { - return preg_match('/^(\w+)(?:\h+(ASC|DESC))?\h*(?:,|$)/i', - $str,$parts)? - ($db->quotekey($parts[1]). - (isset($parts[2])?(' '.$parts[2]):'')):$str; - }, - explode(',',$options['order']))); - } - if (preg_match('/mssql|sqlsrv|odbc/', $this->engine) && - ($options['limit'] || $options['offset'])) { - $pkeys=array(); - foreach ($this->fields as $key=>$field) - if ($field['pkey']) - $pkeys[]=$key; - $ofs=$options['offset']?(int)$options['offset']:0; - $lmt=$options['limit']?(int)$options['limit']:0; - if (strncmp($db->version(),'11',2)>=0) { - // SQL Server 2012 - if (!$options['order']) - $sql.=' ORDER BY '.$db->quotekey($pkeys[0]); - $sql.=' OFFSET '.$ofs.' ROWS'; - if ($lmt) - $sql.=' FETCH NEXT '.$lmt.' ROWS ONLY'; - } - else { - // SQL Server 2008 - $sql=str_replace('SELECT', - 'SELECT '. - ($lmt>0?'TOP '.($ofs+$lmt):'').' ROW_NUMBER() '. - 'OVER (ORDER BY '. - $db->quotekey($pkeys[0]).') AS rnum,',$sql); - $sql='SELECT * FROM ('.$sql.') x WHERE rnum > '.($ofs); - } - } - else { - if ($options['limit']) - $sql.=' LIMIT '.(int)$options['limit']; - if ($options['offset']) - $sql.=' OFFSET '.(int)$options['offset']; - } - $result=$this->db->exec($sql,$args,$ttl); - $out=array(); - 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); - } - elseif (array_key_exists($field,$this->adhoc)) - $this->adhoc[$field]['value']=$val; - unset($val); - } - $out[]=$this->factory($row); - unset($row); - } - return $out; - } - - /** - * Return records that match criteria - * @return \DB\SQL\Mapper[] - * @param $filter string|array - * @param $options array - * @param $ttl int - **/ - function find($filter=NULL,array $options=NULL,$ttl=0) { - if (!$options) - $options=array(); - $options+=array( - '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(array($this->db,'quotekey'), - array_keys($this->fields)))).$adhoc,$filter,$options,$ttl); - } - - /** - * Count records that match criteria - * @return int - * @param $filter string|array - * @param $ttl int - **/ - function count($filter=NULL,$ttl=0) { - $sql='SELECT COUNT(*) AS '. - $this->db->quotekey('rows').' FROM '.$this->table; - $args=array(); - if ($filter) { - 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:array(1=>$args); - list($filter)=$filter; - } - $sql.=' WHERE '.$filter; - } - $result=$this->db->exec($sql,$args,$ttl); - return $result[0]['rows']; - } - - /** - * Return record at specified offset using same criteria as - * previous load() call and make it active - * @return array - * @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['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 (isset($this->trigger['load'])) - \Base::instance()->call($this->trigger['load'],$this); - return $out; - } - - /** - * Insert new record - * @return object - **/ - function insert() { - $args=array(); - $actr=0; - $nctr=0; - $fields=''; - $values=''; - $filter=''; - $pkeys=array(); - $nkeys=array(); - $ckeys=array(); - $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'], - array($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]=array($field['value'],$field['pdo_type']); - $nctr++; - } - if ($field['changed'] && $key!=$inc) { - $fields.=($actr?',':'').$this->db->quotekey($key); - $values.=($actr?',':'').'?'; - $args[$actr+1]=array($field['value'],$field['pdo_type']); - $actr++; - $ckeys[]=$key; - } - $field['changed']=FALSE; - unset($field); - } - if ($fields) { - $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.')',$args - ); - $seq=NULL; - if ($this->engine=='pgsql') { - $names=array_keys($pkeys); - $seq=$this->source.'_'.end($names).'_seq'; - } - if ($this->engine!='oci') - $this->_id=$this->db->lastinsertid($seq); - // Reload to obtain default and auto-increment field values - $this->load($inc? - array($inc.'=?',$this->db->value( - $this->fields[$inc]['pdo_type'],$this->_id)): - array($filter,$nkeys)); - if (isset($this->trigger['afterinsert'])) - \Base::instance()->call($this->trigger['afterinsert'], - array($this,$pkeys)); - } - return $this; - } - - /** - * Update current record - * @return object - **/ - function update() { - $args=array(); - $ctr=0; - $pairs=''; - $filter=''; - $pkeys=array(); - foreach ($this->fields as $key=>$field) - if ($field['pkey']) - $pkeys[$key]=$field['previous']; - if (isset($this->trigger['beforeupdate']) && - \Base::instance()->call($this->trigger['beforeupdate'], - array($this,$pkeys))===FALSE) - return $this; - foreach ($this->fields as $key=>$field) - if ($field['changed']) { - $pairs.=($pairs?',':'').$this->db->quotekey($key).'=?'; - $args[$ctr+1]=array($field['value'],$field['pdo_type']); - $ctr++; - } - foreach ($this->fields as $key=>$field) - if ($field['pkey']) { - $filter.=($filter?' AND ':' WHERE '). - $this->db->quotekey($key).'=?'; - $args[$ctr+1]=array($field['previous'],$field['pdo_type']); - $ctr++; - } - 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'], - array($this,$pkeys)); - } - return $this; - } - - /** - * Delete current record - * @return int - * @param $filter string|array - **/ - function erase($filter=NULL) { - if ($filter) { - $args=array(); - 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:array(1=>$args); - list($filter)=$filter; - } - return $this->db-> - exec('DELETE FROM '.$this->table.' WHERE '.$filter.';',$args); - } - $args=array(); - $ctr=0; - $filter=''; - $pkeys=array(); - foreach ($this->fields as $key=>&$field) { - if ($field['pkey']) { - $filter.=($filter?' AND ':'').$this->db->quotekey($key).'=?'; - $args[$ctr+1]=array($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'], - array($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'], - array($this,$pkeys)); - return $out; - } - - /** - * Reset cursor - * @return NULL - **/ - function reset() { - foreach ($this->fields as &$field) { - $field['value']=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()->get($var); - if ($func) - $var=call_user_func($func,$var); - foreach ($var as $key=>$val) - if (in_array($key,array_keys($this->fields))) { - $field=&$this->fields[$key]; - if ($field['value']!==$val) { - $field['value']=$val; - $field['changed']=TRUE; - } - unset($field); - } - } - - /** - * 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:array())); - } - - /** - * 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()); - } - - /** - * Instantiate class - * @param $db object - * @param $table string - * @param $fields array|string - * @param $ttl int - **/ - 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 eae536b38..000000000 --- a/app/lib/db/sql/schema.php +++ /dev/null @@ -1,1186 +0,0 @@ - - * https://github.com/ikkez/F3-Sugar/ - * - * @package DB - * @version 2.1.1 - **/ - - -namespace DB\SQL; - -use DB\SQL; - -class Schema extends DB_Utils { - - public - $dataTypes = array( - 'BOOLEAN' => array('mysql|sqlite2?|pgsql' => 'BOOLEAN', - 'mssql|sybase|dblib|odbc|sqlsrv' => 'bit', - 'ibm' => 'numeric(1,0)', - ), - 'INT1' => array('mysql' => 'TINYINT UNSIGNED', - 'sqlite2?' => 'integer', - 'mssql|sybase|dblib|odbc|sqlsrv' => 'tinyint', - 'pgsql|ibm' => 'smallint', - ), - 'INT2' => array('mysql' => 'SMALLINT', - 'sqlite2?' => 'integer', - 'pgsql|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'smallint', - ), - 'INT4' => array('sqlite2?|pgsql|sybase|odbc|sqlsrv|imb' => 'integer', - 'mysql|mssql|dblib' => 'int', - ), - 'INT8' => array('sqlite2?' => 'integer', - 'pgsql|mysql|mssql|sybase|dblib|odbc|sqlsrv|imb' => 'bigint', - ), - 'FLOAT' => array('mysql|sqlite2?' => 'FLOAT', - 'pgsql' => 'double precision', - 'mssql|sybase|dblib|odbc|sqlsrv' => 'float', - 'imb' => 'decfloat' - ), - 'DOUBLE' => array('mysql|sqlite2?|ibm' => 'DOUBLE', - 'pgsql|sybase|odbc|sqlsrv' => 'double precision', - 'mssql|dblib' => 'decimal', - ), - 'VARCHAR128' => array('mysql|pgsql|sqlite2?|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'varchar(128)', - ), - 'VARCHAR256' => array('mysql|pgsql|sqlite2?|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'varchar(255)', - ), - 'VARCHAR512' => array('mysql|pgsql|sqlite2?|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'varchar(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; - - /** @var \Base */ - protected $fw; - - 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->fw = \Base::instance(); - parent::__construct($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; - } - -} - -abstract class TableBuilder extends DB_Utils { - - protected $columns, $pkeys, $queries, $increments, $rebuild_cmd, $suppress; - public $name, $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 - * @return \DB\SQL\TableBuilder - */ - 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'; - parent::__construct($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)); - $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->db->quotekey($this->name.'___'.implode('__', $index_cols)); - $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; - } - - /** - * 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."; - - /** - * 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)); - 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)); - 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=utf8 COLLATE utf8_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 - */ - 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)); - - 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) { - // 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)); - 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)); - 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) { - // for backward compatibility - if ($col == $name) - unset($indexes[$name]); - // 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); - } else // for backward compatibility - $ci = explode('__', $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']))) - $col['type']=$this->rebuild_cmd['update'][$name]; - $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=$this->findQuery($this->schema->defaultTypes['CUR_STAMP']), - $default)) || is_int(strpos($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; - } - - /** - * 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.'); - if (in_array($new_name, array_keys($existing_columns))) - trigger_error('cannot rename column. new column already exist.'); - - 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 $name - * @param $datatype - * @param bool $force - * @return bool - */ - public function updateColumn($name, $datatype, $force = false) - { - 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] = $datatype; - } else { - $cmd = array( - 'mysql' => - "ALTER TABLE $table MODIFY COLUMN $column $datatype;", - 'pgsql' => - "ALTER TABLE $table ALTER COLUMN $column TYPE $datatype;", - 'sqlsrv|mssql|sybase|dblib|ibm' => - "ALTER TABLE $table ALTER COLUMN $column $datatype;", - ); - $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())); - 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 extends DB_Utils { - - public $name, $type, $nullable, $default, $after, $index, $unique, $passThrough, $pkey; - protected $table, $schema; - - const - TEXT_NoDataType = 'The specified datatype %s is not defined in %s driver', - 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; - parent::__construct($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; - } - - /** - * 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; - } - - /** - * generate SQL column definition query - * @return bool|string - */ - public function getColumnQuery() - { - if (!$this->type) - trigger_error(sprintf('Cannot build a column query for `%s`: no column type set',$this->name)); - // prepare column types - if ($this->passThrough) - $type_val = $this->type; - else { - $type_val = $this->findQuery($this->schema->dataTypes[strtoupper($this->type)]); - if (!$type_val) { - trigger_error(sprintf(self::TEXT_NoDataType, strtoupper($this->type), - $this->db->driver())); - return FALSE; - } - } - // build query - $query = $this->db->quotekey($this->name).' '.$type_val.' '. - ($this->nullable ? 'NULL' : 'NOT NULL'); - // default value - if ($this->default !== false) { - $def_cmds = array( - 'sqlite2?|mysql|pgsql|mssql|sybase|dblib|odbc|sqlsrv' => 'DEFAULT', - 'ibm' => 'WITH DEFAULT', - ); - $def_cmd = $this->findQuery($def_cmds).' '; - // 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' && // TODO: check that condition - ($this->passThrough && strtoupper($this->type) != strtoupper($stamp_type)) - ) - trigger_error(self::TEXT_CurrentStampDataType); - $def_cmd .= $this->findQuery($this->schema->defaultTypes[strtoupper($this->default)]); - } else { - // static defaults - $pdo_type = preg_match('/int|bool/i', $type_val, $parts) ? - constant('\PDO::PARAM_'.strtoupper($parts[0])) : \PDO::PARAM_STR; - $def_cmd .= ($this->default === NULL ? 'NULL' : - $this->db->quote(htmlspecialchars($this->default, ENT_QUOTES, - $this->f3->get('ENCODING')), $pdo_type)); - } - $query .= ' '.$def_cmd; - } - if (!empty($this->after)) { - // `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; - } -} - - -class DB_Utils { - - /** @var \DB\SQL */ - protected $db; - - /** @var \BASE */ - protected $f3; - - const - TEXT_ENGINE_NOT_SUPPORTED = 'DB Engine `%s` is not supported for this action.'; - - /** - * parse command array and return backend specific query - * @param $cmd - * @param $cmd array - * @return bool|string - */ - protected function findQuery($cmd) - { - $match = FALSE; - foreach ($cmd as $backend => $val) - if (preg_match('/'.$backend.'/', $this->db->driver())) { - $match = TRUE; - break; - } - if (!$match) { - trigger_error(sprintf(self::TEXT_ENGINE_NOT_SUPPORTED, $this->db->driver())); - return FALSE; - } - return $val; - } - - public function __construct(SQL $db) { - $this->db = $db; - $this->f3 = \Base::instance(); - } -} \ 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 12c27f425..000000000 --- a/app/lib/db/sql/session.php +++ /dev/null @@ -1,203 +0,0 @@ -. - -*/ - -namespace DB\SQL; - -//! SQL-managed session handler -class Session extends Mapper { - - protected - //! Session ID - $sid; - - /** - * Open session - * @return TRUE - * @param $path string - * @param $name string - **/ - function open($path,$name) { - return TRUE; - } - - /** - * Close session - * @return TRUE - **/ - function close() { - return TRUE; - } - - /** - * Return session data in serialized format - * @return string|FALSE - * @param $id string - **/ - function read($id) { - if ($id!=$this->sid) - $this->load(array('session_id=?',$this->sid=$id)); - return $this->dry()?FALSE:$this->get('data'); - } - - /** - * Write session data - * @return TRUE - * @param $id string - * @param $data string - **/ - function write($id,$data) { - $fw=\Base::instance(); - $sent=headers_sent(); - $headers=$fw->get('HEADERS'); - if ($id!=$this->sid) - $this->load(array('session_id=?',$this->sid=$id)); - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); - $this->set('session_id',$id); - $this->set('data',$data); - $this->set('csrf',$sent?$this->csrf():$csrf); - $this->set('ip',$fw->get('IP')); - $this->set('agent', - isset($headers['User-Agent'])?$headers['User-Agent']:''); - $this->set('stamp',time()); - $this->save(); - return TRUE; - } - - /** - * Destroy session - * @return TRUE - * @param $id string - **/ - function destroy($id) { - $this->erase(array('session_id=?',$id)); - setcookie(session_name(),'',strtotime('-1 year')); - unset($_COOKIE[session_name()]); - header_remove('Set-Cookie'); - return TRUE; - } - - /** - * Garbage collector - * @return TRUE - * @param $max int - **/ - function cleanup($max) { - $this->erase(array('stamp+?dry()?FALSE:$this->get('csrf'); - } - - /** - * Return IP address - * @return string|FALSE - **/ - function ip() { - return $this->dry()?FALSE:$this->get('ip'); - } - - /** - * Return Unix timestamp - * @return string|FALSE - **/ - function stamp() { - return $this->dry()?FALSE:$this->get('stamp'); - } - - /** - * Return HTTP user agent - * @return string|FALSE - **/ - function agent() { - return $this->dry()?FALSE:$this->get('agent'); - } - - /** - * Instantiate class - * @param $db object - * @param $table string - * @param $force bool - * @param $onsuspect callback - **/ - function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL) { - if ($force) { - $eol="\n"; - $tab="\t"; - $db->exec( - (preg_match('/mssql|sqlsrv|sybase/',$db->driver())? - ('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')? - ($name.'.'):''))). - $table.' ('.$eol. - $tab.$db->quotekey('session_id').' VARCHAR(40),'.$eol. - $tab.$db->quotekey('data').' TEXT,'.$eol. - $tab.$db->quotekey('csrf').' TEXT,'.$eol. - $tab.$db->quotekey('ip').' VARCHAR(40),'.$eol. - $tab.$db->quotekey('agent').' VARCHAR(255),'.$eol. - $tab.$db->quotekey('stamp').' INTEGER,'.$eol. - $tab.'PRIMARY KEY ('.$db->quotekey('session_id').')'.$eol. - ');' - ); - } - parent::__construct($db,$table); - session_set_save_handler( - array($this,'open'), - array($this,'close'), - array($this,'read'), - array($this,'write'), - array($this,'destroy'), - array($this,'cleanup') - ); - register_shutdown_function('session_commit'); - @session_start(); - $fw=\Base::instance(); - $headers=$fw->get('HEADERS'); - if (($ip=$this->ip()) && $ip!=$fw->get('IP') || - ($agent=$this->agent()) && - (!isset($headers['User-Agent']) || - $agent!=$headers['User-Agent'])) { - if (isset($onsuspect)) - $fw->call($onsuspect,array($this)); - else { - session_destroy(); - $fw->error(403); - } - } - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); - if ($this->load(array('session_id=?',$this->sid=session_id()))) { - $this->set('csrf',$csrf); - $this->save(); - } - } - -} diff --git a/app/lib/f3.php b/app/lib/f3.php deleted file mode 100644 index 96c7845e0..000000000 --- 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(array(self::$fw,$func),$args); - } - -} diff --git a/app/lib/image.php b/app/lib/image.php deleted file mode 100644 index 55a29ba98..000000000 --- a/app/lib/image.php +++ /dev/null @@ -1,597 +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_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 - **/ - function rgb($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,$height,$crop=TRUE,$enlarge=TRUE) { - // Adjust dimensions; retain aspect ratio - $ratio=($origw=imagesx($this->data))/($origh=imagesy($this->data)); - if (!$crop) { - if ($width/$ratio<=$height) - $height=$width/$ratio; - else - $width=$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=$origh*$width/$height; - imagecopyresampled($tmp,$this->data, - 0,0,($origw-$cropw)/2,0,$width,$height,$cropw,$origh); - } - else { - $croph=$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=array( - array(.5,1,1,0,1,1), - array(.5,0,1,0,.5,1,0,1), - array(.5,0,1,0,1,1,.5,1,1,.5), - array(0,.5,.5,0,1,.5,.5,1,.5,.5), - array(0,.5,1,0,1,1,0,1,1,.5), - array(1,0,1,1,.5,1,1,.5,.5,.5), - array(0,0,1,0,1,.5,0,0,.5,1,0,1), - array(0,0,.5,0,1,.5,.5,1,0,1,.5,.5), - array(.5,0,.5,.5,1,.5,1,1,.5,1,.5,.5,0,.5), - array(0,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1), - array(0,.5,.5,1,1,.5,.5,0,1,0,1,1,0,1), - array(.5,0,1,0,1,1,.5,1,1,.75,.5,.5,1,.25), - array(0,.5,.5,0,.5,.5,1,0,1,.5,.5,1,.5,.5,0,1), - array(0,0,1,0,1,1,0,1,1,.5,.5,.25,.5,.75,0,.5,.5,.25), - array(0,.5,.5,.5,.5,0,1,0,.5,.5,1,.5,.5,1,.5,.5,0,1), - array(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); - if ($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); - } - $sprite=imagerotate($sprite, - 90*(hexdec($hash[($j*$blocks+$i)*2+1])%4), - imagecolorallocatealpha($sprite,0,0,0,127)); - 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; - } - $fw=Base::instance(); - foreach ($fw->split($path?:$fw->get('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=array(); - 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->set($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()->get('PACKAGE')); - } - call_user_func_array('image'.$format, - array_merge(array($this->data),$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(array($this->data),$args)); - return ob_get_clean(); - } - - /** - * Save current state - * @return object - **/ - function save() { - $fw=Base::instance(); - if ($this->flag) { - if (!is_dir($dir=$fw->get('TEMP'))) - mkdir($dir,Base::MODE,TRUE); - $this->count++; - $fw->write($dir.'/'. - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $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->get('TEMP'). - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $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 - * @param $str string - **/ - function load($str) { - $this->data=imagecreatefromstring($str); - 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->get('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->get('TEMP'). - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $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 6583cedbe..000000000 --- a/app/lib/log.php +++ /dev/null @@ -1,67 +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(); - $fw->write( - $this->file, - date($format). - (isset($_SERVER['REMOTE_ADDR'])? - (' ['.$_SERVER['REMOTE_ADDR'].']'):'').' '. - trim($text).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->get('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 5669908a2..000000000 --- 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 9863d9117..000000000 --- a/app/lib/markdown.php +++ /dev/null @@ -1,577 +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->get('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]); - $self=$this; - $str=preg_replace_callback( - '/([^<>\[]+)?(<[\?%].+?[\?%]>|<.+?>|\[.+?\]\s*\(.+?\))|'. - '(.+)/s', - function($expr) use($self) { - $tmp=''; - if (isset($expr[4])) - $tmp.=$self->esc($expr[4]); - else { - if (isset($expr[1])) - $tmp.=$self->esc($expr[1]); - $tmp.=$expr[2]; - if (isset($expr[3])) - $tmp.=$self->esc($expr[3]); - } - return $tmp; - }, - $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( - '/(?'.$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) { - $self=$this; - return preg_replace_callback( - '/!(?:\[(.+?)\])?\h*\(?(?:\h*"(.*?)"\h*)?\)/', - function($expr) use($self) { - return ''.$self->esc($expr[1]).''; - }, - $str - ); - } - - /** - * Process anchor span - * @return string - * @param $str string - **/ - protected function _a($str) { - $self=$this; - return preg_replace_callback( - '/(??(?:\h*"(.*?)"\h*)?\)/', - function($expr) use($self) { - return ''.$self->scan($expr[1]).''; - }, - $str - ); - } - - /** - * Auto-convert links - * @return string - * @param $str string - **/ - protected function _auto($str) { - $self=$this; - return preg_replace_callback( - '/`.*?<(.+?)>.*?`|<(.+?)>/', - function($expr) use($self) { - if (empty($expr[1]) && parse_url($expr[2],PHP_URL_SCHEME)) { - $expr[2]=$self->esc($expr[2]); - return ''.$expr[2].''; - } - return $expr[0]; - }, - $str - ); - } - - /** - * Process code span - * @return string - * @param $str string - **/ - protected function _code($str) { - $self=$this; - return preg_replace_callback( - '/`` (.+?) ``|(?'. - $self->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=array( - '...'=>'…', - '(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()->get('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=array('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=array( - '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' - ); - } - $self=$this; - // 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="'. - $self->esc($match[3]).'"')).'>'. - // Link - $self->scan( - empty($expr[3])? - (empty($expr[1])? - $expr[4]: - $expr[1]): - $expr[3] - ).''): - // Image - (''.
-										$self->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( - 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 1ebce6b99..000000000 --- a/app/lib/matrix.php +++ /dev/null @@ -1,108 +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=array(); - 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)=array($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 - * @param $first int - **/ - function calendar($date='now',$first=0) { - $parts=getdate(strtotime($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=array(); - 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 8723c69f2..000000000 --- a/app/lib/session.php +++ /dev/null @@ -1,191 +0,0 @@ -. - -*/ - -//! Cache-based session handler -class Session { - - protected - //! Session ID - $sid; - - /** - * Open session - * @return TRUE - * @param $path string - * @param $name string - **/ - function open($path,$name) { - return TRUE; - } - - /** - * Close session - * @return TRUE - **/ - function close() { - return TRUE; - } - - /** - * Return session data in serialized format - * @return string|FALSE - * @param $id string - **/ - function read($id) { - if ($id!=$this->sid) - $this->sid=$id; - return Cache::instance()->exists($id.'.@',$data)?$data['data']:FALSE; - } - - /** - * Write session data - * @return TRUE - * @param $id string - * @param $data string - **/ - function write($id,$data) { - $fw=Base::instance(); - $sent=headers_sent(); - $headers=$fw->get('HEADERS'); - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); - $jar=$fw->get('JAR'); - if ($id!=$this->sid) - $this->sid=$id; - Cache::instance()->set($id.'.@', - array( - 'data'=>$data, - 'csrf'=>$sent?$this->csrf():$csrf, - 'ip'=>$fw->get('IP'), - 'agent'=>isset($headers['User-Agent'])? - $headers['User-Agent']:'', - 'stamp'=>time() - ), - $jar['expire']?($jar['expire']-time()):0 - ); - return TRUE; - } - - /** - * Destroy session - * @return TRUE - * @param $id string - **/ - function destroy($id) { - Cache::instance()->clear($id.'.@'); - setcookie(session_name(),'',strtotime('-1 year')); - unset($_COOKIE[session_name()]); - header_remove('Set-Cookie'); - return TRUE; - } - - /** - * Garbage collector - * @return TRUE - * @param $max int - **/ - function cleanup($max) { - Cache::instance()->reset('.@',$max); - return TRUE; - } - - /** - * Return anti-CSRF token - * @return string|FALSE - **/ - function csrf() { - return Cache::instance()-> - exists(($this->sid?:session_id()).'.@',$data)? - $data['csrf']:FALSE; - } - - /** - * Return IP address - * @return string|FALSE - **/ - function ip() { - return Cache::instance()-> - exists(($this->sid?:session_id()).'.@',$data)? - $data['ip']:FALSE; - } - - /** - * Return Unix timestamp - * @return string|FALSE - **/ - function stamp() { - return Cache::instance()-> - exists(($this->sid?:session_id()).'.@',$data)? - $data['stamp']:FALSE; - } - - /** - * Return HTTP user agent - * @return string|FALSE - **/ - function agent() { - return Cache::instance()-> - exists(($this->sid?:session_id()).'.@',$data)? - $data['agent']:FALSE; - } - - /** - * Instantiate class - * @param $onsuspect callback - **/ - function __construct($onsuspect=NULL) { - session_set_save_handler( - array($this,'open'), - array($this,'close'), - array($this,'read'), - array($this,'write'), - array($this,'destroy'), - array($this,'cleanup') - ); - register_shutdown_function('session_commit'); - @session_start(); - $fw=\Base::instance(); - $headers=$fw->get('HEADERS'); - if (($ip=$this->ip()) && $ip!=$fw->get('IP') || - ($agent=$this->agent()) && - (!isset($headers['User-Agent']) || - $agent!=$headers['User-Agent'])) { - if (isset($onsuspect)) - $fw->call($onsuspect,array($this)); - else { - session_destroy(); - $fw->error(403); - } - } - $csrf=$fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(mt_rand()); - $jar=$fw->get('JAR'); - if (Cache::instance()->exists(($this->sid=session_id()).'.@',$data)) { - $data['csrf']=$csrf; - Cache::instance()->set($this->sid.'.@', - $data, - $jar['expire']?($jar['expire']-time()):0 - ); - } - } - -} diff --git a/app/lib/smtp.php b/app/lib/smtp.php deleted file mode 100644 index f0351ae5e..000000000 --- a/app/lib/smtp.php +++ /dev/null @@ -1,304 +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, - //! 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 - **/ - protected function dialog($cmd=NULL,$log=TRUE) { - $socket=&$this->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; - } - if ($log) { - $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 (is_string($alias)) - $file=array($alias=>$file); - $this->attachments[]=array('filename'=>$file,'cid'=>$cid); - } - - /** - * Transmit message - * @return bool - * @param $message string - * @param $log bool - **/ - function send($message,$log=TRUE) { - 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 - $socket=&$this->socket; - $socket=@fsockopen($this->host,$this->port); - if (!$socket) - return FALSE; - stream_set_blocking($socket,TRUE); - // Get server's initial response - $this->dialog(NULL,FALSE); - // Announce presence - $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log); - if (strtolower($this->scheme)=='tls') { - $this->dialog('STARTTLS',$log); - stream_socket_enable_crypto( - $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); - $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log); - if (preg_match('/8BITMIME/',$reply)) - $headers['Content-Transfer-Encoding']='8bit'; - else { - $headers['Content-Transfer-Encoding']='quoted-printable'; - $message=quoted_printable_encode($message); - } - } - if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) { - // Authenticate - $this->dialog('AUTH LOGIN',$log); - $this->dialog(base64_encode($this->user),$log); - $this->dialog(base64_encode($this->pw),$log); - } - // Required headers - $reqd=array('From','To','Subject'); - foreach ($reqd as $id) - if (empty($headers[$id])) - user_error(sprintf(self::E_Header,$id),E_USER_ERROR); - $eol="\r\n"; - $str=''; - // Stringify headers - foreach ($headers as $key=>&$val) { - if (!in_array($key,$reqd)) { - $str.=$key.': '.$val.$eol; - } - if (in_array($key,array('From','To','Cc','Bcc')) && - !preg_match('/[<>]/',$val)) - $val='<'.$val.'>'; - unset($val); - } - // Start message dialog - $this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),$log); - foreach ($fw->split($headers['To']. - (isset($headers['Cc'])?(';'.$headers['Cc']):''). - (isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst) - $this->dialog('RCPT TO: '.strstr($dst,'<'),$log); - $this->dialog('DATA',$log); - if ($this->attachments) { - // Replace Content-Type - $hash=uniqid(NULL,TRUE); - $type=$headers['Content-Type']; - $headers['Content-Type']='multipart/mixed; '. - 'boundary="'.$hash.'"'; - // Send mail headers - $out=''; - 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.=$eol; - $out.=$message.$eol; - foreach ($this->attachments as $attachment) { - if (is_array($attachment['filename'])) { - list($alias,$file)=each($attachment); - $filename=$alias; - $attachment['filename']=$file; - } - else - $filename=basename($attachment); - $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="'.$filename.'"'.$eol; - $out.=$eol; - $out.=chunk_split( - base64_encode(file_get_contents($attachment))).$eol; - } - $out.=$eol; - $out.='--'.$hash.'--'.$eol; - $out.='.'; - $this->dialog($out,FALSE); - } - 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); - } - $this->dialog('QUIT',$log); - if ($socket) - fclose($socket); - return TRUE; - } - - /** - * Instantiate class - * @param $host string - * @param $port int - * @param $scheme string - * @param $user string - * @param $pw string - **/ - function __construct($host,$port,$scheme,$user,$pw) { - $this->headers=array( - 'MIME-Version'=>'1.0', - 'Content-Type'=>'text/plain; '. - 'charset='.Base::instance()->get('ENCODING') - ); - $this->host=$host; - if (strtolower($this->scheme=strtolower($scheme))=='ssl') - $this->host='ssl://'.$host; - $this->port=$port; - $this->user=$user; - $this->pw=$pw; - } - -} diff --git a/app/lib/template.php b/app/lib/template.php deleted file mode 100644 index 8c1c0e11e..000000000 --- a/app/lib/template.php +++ /dev/null @@ -1,357 +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=array(); - - /** - * 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)? - 'array('.implode(',', - array_map(function($pair) { - return '\''.$pair[1].'\'=>'. - (preg_match('/^\'.*\'$/',$pair[2]) || - preg_match('/\$/',$pair[2])? - $pair[2]: - \Base::instance()->stringify($pair[2])); - },$pairs)).')+get_defined_vars()': - 'get_defined_vars()'; - return - 'token($attrib['if']).') '):''). - ('echo $this->render('. - (preg_match('/^\{\{(.+?)\}\}$/',$attrib['href'])? - $this->token($attrib['href']): - Base::instance()->stringify($attrib['href'])).','. - '$this->mime,'.$hive.'); ?>'); - } - - /** - * 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']).'?:array()) 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=array($pos,$block); - elseif (isset($block['false'])) - $false=array($pos,$block); - if (isset($true,$false) && $true[0]>$false[0]) - // Reverse and blocks - list($node[$true[0]],$node[$false[0]])=array($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 - **/ - protected 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 callback - * @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(array($this,$func),$args); - user_error(sprintf(self::E_Method,$func),E_USER_ERROR); - } - - /** - * Parse string for template directives and tokens - * @return string|array - * @param $text string - **/ - function parse($text) { - // Build tree structure - for ($ptr=0,$len=strlen($text),$tree=array(),$node=&$tree, - $stack=array(),$depth=0,$tmp='';$ptr<$len;) - if (preg_match('/^<(\/?)(?:F3:)?'. - '('.$this->tags.')\b((?:\h+[\w-]+'. - '(?:\h*=\h*(?:"(?:.+?)"|\'(?:.+?)\'))?|'. - '\h*\{\{.+?\}\})*)\h*(\/?)>/is', - substr($text,$ptr),$match)) { - if (strlen($tmp)) - $node[]=$tmp; - // Element node - if ($match[1]) { - // Find matching start tag - $save=$depth; - $found=FALSE; - while ($depth>0) { - $depth--; - foreach ($stack[$depth] as $item) - if (is_array($item) && isset($item[$match[2]])) { - // Start tag found - $found=TRUE; - break 2; - } - } - if (!$found) - // Unbalanced tag - $depth=$save; - $node=&$stack[$depth]; - } - else { - // Start tag - $stack[$depth]=&$node; - $node=&$node[][$match[2]]; - if ($match[3]) { - // Process attributes - preg_match_all( - '/(?:\b([\w-]+)\h*'. - '(?:=\h*(?:"(.*?)"|\'(.*?)\'))?|'. - '(\{\{.+?\}\}))/s', - $match[3],$attr,PREG_SET_ORDER); - foreach ($attr as $kv) - if (isset($kv[4])) - $node['@attrib'][]=$kv[4]; - else - $node['@attrib'][$kv[1]]= - (isset($kv[2]) && $kv[2]!==''? - $kv[2]: - (isset($kv[3]) && $kv[3]!==''? - $kv[3]:NULL)); - } - if ($match[4]) - // Empty tag - $node=&$stack[$depth]; - else - $depth++; - } - $tmp=''; - $ptr+=strlen($match[0]); - } - else { - // Text node - $tmp.=substr($text,$ptr,1); - $ptr++; - } - if (strlen($tmp)) - // Append trailing text - $node[]=$tmp; - // Break references - unset($node); - unset($stack); - return $tree; - } - - /** - * Class constructor - * return object - **/ - function __construct() { - $ref=new ReflectionClass(__CLASS__); - $this->tags=''; - foreach ($ref->getmethods() as $method) - if (preg_match('/^_(?=[[:alpha:]])/',$method->name)) - $this->tags.=(strlen($this->tags)?'|':''). - substr($method->name,1); - } - -} diff --git a/app/lib/test.php b/app/lib/test.php deleted file mode 100644 index 05e6e6c5c..000000000 --- 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=array(), - //! 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=array('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 fbfe00053..000000000 --- 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=array( - ':('=>'\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()->get('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 ab3389ecb..000000000 --- a/app/lib/web.php +++ /dev/null @@ -1,853 +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=array( - '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'=>'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=array(); - 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 - **/ - function send($file,$mime=NULL,$kbps=0,$force=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='.var_export(basename($file),TRUE)); - header('Accept-Ranges: bytes'); - header('Content-Length: '.$size); - header('X-Powered-By: '.Base::instance()->get('PACKAGE')); - } - $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); - } - 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->get('UPLOADS'); - if (!is_dir($dir)) - mkdir($dir,Base::MODE,TRUE); - if ($fw->get('VERB')=='PUT') { - $tmp=$fw->get('TEMP'). - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $fw->hash(uniqid()); - if (!$fw->get('RAW')) - $fw->write($tmp,$fw->get('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->get('URI')); - $file=array( - '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,array($file))!==FALSE) && - rename($tmp,$file['name']); - } - $fetch=function($arr)use(&$fetch){ - if (!is_array($arr)) - return array($arr); - $data=array(); - foreach($arr as $k=>$sub) - $data=array_merge($data,$fetch($sub)); - return $data; - }; - $out=array(); - foreach ($_FILES as $name=>$item) { - $files=array(); - 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'] && - is_uploaded_file($file['tmp_name']) && - (!file_exists($file['name']) || $overwrite) && - (!$func || $fw->call($func,array($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); - curl_setopt($curl,CURLOPT_FOLLOWLOCATION, - $options['follow_location']); - curl_setopt($curl,CURLOPT_MAXREDIRS, - $options['max_redirects']); - 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']); - 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=array(); - 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_VERIFYPEER,FALSE); - ob_start(); - curl_exec($curl); - curl_close($curl); - $body=ob_get_clean(); - return array( - 'body'=>$body, - 'headers'=>$headers, - 'engine'=>'cURL', - 'cached'=>FALSE - ); - } - - /** - * HTTP request via PHP stream wrapper - * @return array - * @param $url string - * @param $options array - **/ - protected function _stream($url,$options) { - $eol="\r\n"; - $options['header']=implode($eol,$options['header']); - $body=@file_get_contents($url,FALSE, - stream_context_create(array('http'=>$options))); - $headers=isset($http_response_header)? - $http_response_header:array(); - $match=NULL; - foreach ($headers as $header) - if (preg_match('/Content-Encoding: (.+)/',$header,$match)) - break; - if ($match) - switch ($match[1]) { - case 'gzip': - $body=gzdecode($body); - break; - case 'deflate': - $body=gzuncompress($body); - break; - } - return array( - 'body'=>$body, - 'headers'=>$headers, - 'engine'=>'stream', - 'cached'=>FALSE - ); - } - - /** - * 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=array(); - $body=''; - $parts=parse_url($url); - $empty=empty($parts['port']); - if ($parts['scheme']=='https') { - $parts['host']='ssl://'.$parts['host']; - if ($empty) - $parts['port']=443; - } - elseif ($empty) - $parts['port']=80; - if (empty($parts['path'])) - $parts['path']='/'; - if (empty($parts['query'])) - $parts['query']=''; - $socket=@fsockopen($parts['host'],$parts['port']); - if (!$socket) - return FALSE; - stream_set_blocking($socket,TRUE); - stream_set_timeout($socket,$options['timeout']); - fputs($socket,$options['method'].' '.$parts['path']. - ($parts['query']?('?'.$parts['query']):'').' 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: (.+)/',$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_match('/Location: (.+?)'.preg_quote($eol).'/', - $html[0],$loc)) { - $options['max_redirects']--; - return $this->request($loc[1],$options); - } - return array( - 'body'=>$body, - 'headers'=>$headers, - 'engine'=>'socket', - 'cached'=>FALSE - ); - } - - /** - * 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=array( - '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(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=array($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->get('SCHEME').'://'. - $fw->get('HOST'). - ($url[0]!='/'?($fw->get('BASE').'/'):'').$url; - $parts=parse_url($url); - } - elseif (!preg_match('/https?/',$parts['scheme'])) - return FALSE; - if (!is_array($options)) - $options=array(); - if (empty($options['header'])) - $options['header']=array(); - elseif (is_string($options['header'])) - $options['header']=array($options['header']); - if (!$this->wrapper) - $this->engine(); - if ($this->wrapper!='stream') { - // PHP streams can't cope with redirects when Host header is set - foreach ($options['header'] as &$header) - if (preg_match('/^Host:/',$header)) { - $header='Host: '.$parts['host']; - unset($header); - break; - } - $this->subst($options['header'],'Host: '.$parts['host']); - } - $this->subst($options['header'], - array( - '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:/',$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+=array( - 'method'=>'GET', - 'header'=>$options['header'], - 'follow_location'=>TRUE, - 'max_redirects'=>20, - 'ignore_errors'=>FALSE - ); - $eol="\r\n"; - if ($fw->get('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=(.+?)'. - preg_quote($eol).'/',implode($eol,$result['headers']),$exp)) - $cache->set($hash,$result,$exp[1]); - } - return $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->get('UI').';./'; - foreach ($fw->split($path,FALSE) as $dir) - foreach ($files as $file) - if (is_file($save=$fw->fixslashes($dir.$file))) { - if ($fw->get('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*([\'"])(.+?)\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+=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],array('\'','"'))) { - $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+1get('CACHE')) - $cache->set($hash,$data); - $dst.=$data; - } - } - if (PHP_SAPI!='cli' && $header) - header('Content-Type: '.$mime.'; charset='.$fw->get('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=array(); - 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=array(''=>NULL)+$item->getnamespaces(TRUE); - $fields=array(); - 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 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(str_replace('\'','',$text), - array( - 'Ǎ'=>'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' - )+Base::instance()->get('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.=' '.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->get('TEMP'))) - mkdir($tmp,Base::MODE,TRUE); - file_put_contents($file=$tmp.'/'. - $fw->hash($fw->get('ROOT').$fw->get('BASE')).'.'. - $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 498f857d4..000000000 --- a/app/lib/web/geo.php +++ /dev/null @@ -1,108 +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=array( - 'offset'=>$ref-> - getOffset(new \DateTime('now',new \DateTimeZone('GMT')))/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->get('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']=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=array(); - 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 - **/ - function weather($latitude,$longitude) { - $fw=\Base::instance(); - $web=\Web::instance(); - $query=array( - 'lat'=>$latitude, - 'lon'=>$longitude - ); - $req=$web->request( - 'http://api.openweathermap.org/data/2.5/weather?'. - http_build_query($query)); - 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/staticmap.php b/app/lib/web/google/staticmap.php deleted file mode 100644 index 71acc5506..000000000 --- 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/openid.php b/app/lib/web/openid.php deleted file mode 100644 index 89173d0be..000000000 --- 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=array(); - - /** - * Determine OpenID provider - * @return string|FALSE - * @param $proxy string - **/ - protected function discover($proxy) { - // Normalize - if (!preg_match('/https?:\/\//i',$this->args['identity'])) - $this->args['identity']='http://'.$this->args['identity']; - $url=parse_url($this->args['identity']); - // Remove fragment; reconnect parts - $this->args['identity']=$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['identity'],array('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]; - 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=array(); - 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(),array $reqd=NULL) { - $fw=\Base::instance(); - $root=$fw->get('SCHEME').'://'.$fw->get('HOST'); - if (empty($this->args['trust_root'])) - $this->args['trust_root']=$root.$fw->get('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=array(); - 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=array(); - foreach ($this->args as $key=>$val) - $var['openid.'.$key]=$val; - $req=\Web::instance()->request( - $this->url, - array( - '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 9dd750a3b..000000000 --- a/app/lib/web/pingback.php +++ /dev/null @@ -1,177 +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 && $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->get('HOST')) { - $req=$web->request($source); - $doc=new \DOMDocument('1.0',$fw->get('ENCODING')); - $doc->stricterrorchecking=FALSE; - $doc->recover=TRUE; - if ($req && @$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, - array( - 'method'=>'POST', - 'header'=>'Content-Type: application/xml', - 'content'=>xmlrpc_encode_request( - 'pingback.ping', - array($source,$permalink), - array('encoding'=>$fw->get('ENCODING')) - ) - ) - ); - if ($req && $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->get('PACKAGE')); - header('Content-Type: application/xml; '. - 'charset='.$charset=$fw->get('ENCODING')); - } - if (!$path) - $path=$fw->get('BASE'); - $web=\Web::instance(); - $args=xmlrpc_decode_request($fw->get('BODY'),$method,$charset); - $options=array('encoding'=>$charset); - if ($method=='pingback.ping' && isset($args[0],$args[1])) { - list($source,$permalink)=$args; - $doc=new \DOMDocument('1.0',$fw->get('ENCODING')); - // Check local page if pingback-enabled - $parts=parse_url($permalink); - if ((empty($parts['scheme']) || - $parts['host']==$fw->get('HOST')) && - preg_match('/^'.preg_quote($path,'/').'/'. - ($fw->get('CASELESS')?'i':''),$parts['path']) && - $this->enabled($permalink)) { - // Check source - $parts=parse_url($source); - if ((empty($parts['scheme']) || - $parts['host']==$fw->get('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, - array($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/accesscontroller.php b/app/main/controller/accesscontroller.php deleted file mode 100644 index 50c1cc280..000000000 --- a/app/main/controller/accesscontroller.php +++ /dev/null @@ -1,62 +0,0 @@ -_checkLogIn(); - - if( !$loginCheck ){ - // no user found or LogIn timer expired - $this->logOut($f3); - } - - parent::beforeroute($f3); - } - - /** - * checks weather a user is currently logged in - * @return bool - */ - private function _checkLogIn(){ - - $loginCheck = false; - - if($this->f3->get('SESSION.user.time') > 0){ - // check logIn time - $logInTime = new \DateTime(); - $logInTime->setTimestamp($this->f3->get('SESSION.user.time')); - $now = new \DateTime(); - - $timeDiff = $now->diff($logInTime); - - $minutes = $timeDiff->days * 60 * 24 * 60; - $minutes += $timeDiff->h * 60; - $minutes += $timeDiff->i; - - if($minutes <= $this->f3->get('PATHFINDER.TIMER.LOGGED')){ - $loginCheck = true; - } - } - - return $loginCheck; - } - -} \ No newline at end of file diff --git a/app/main/controller/api/access.php b/app/main/controller/api/access.php deleted file mode 100644 index 6db026aac..000000000 --- a/app/main/controller/api/access.php +++ /dev/null @@ -1,80 +0,0 @@ -find( array( - "LOWER(name) LIKE :token AND " . - "active = 1 AND " . - "sharing = 1 ", - ':token' => '%' . $searchToken . '%' - )); - - if($accessList){ - foreach($accessList as $accessObject){ - $accessData[] = $accessObject->getData(); - } - } - } - - } - - echo json_encode($accessData); - } - - - - -} \ No newline at end of file diff --git a/app/main/controller/api/connection.php b/app/main/controller/api/connection.php deleted file mode 100644 index aefbca822..000000000 --- a/app/main/controller/api/connection.php +++ /dev/null @@ -1,110 +0,0 @@ - this function is called for update - * @param $f3 - */ - public function save($f3){ - $postData = (array)$f3->get('POST'); - $newConnectionData = []; - - if( - isset($postData['connectionData']) && - isset($postData['mapData']) - ){ - $mapData = (array)$postData['mapData']; - $connectionData = (array)$postData['connectionData']; - - $user = $this->_getUser(); - - if($user){ - // get map model and check map access - $map = Model\BasicModel::getNew('MapModel'); - $map->getById( (int)$mapData['id'] ); - - if( $map->hasAccess($user) ){ - $source = $map->getSystem( (int)$connectionData['source'] ); - $target = $map->getSystem( (int)$connectionData['target'] ); - - if( - !is_null($source) && - !is_null($target) - ){ - $connection = Model\BasicModel::getNew('ConnectionModel'); - $connection->getById( (int)$connectionData['id'] ); - - // search if systems are neighbors - $routeController = new Route(); - $route = $routeController->findRoute($connectionData['sourceName'], $connectionData['targetName'], 1); - - if($route['routePossible'] == true){ - // systems are next to each other - $connectionData['scope'] = 'stargate'; - $connectionData['type'] = ['stargate']; - }elseif($connectionData['scope'] == 'stargate'){ - // connection scope changed -> this can not be a stargate - $connectionData['scope'] = 'wh'; - $connectionData['type'] = ['wh_fresh']; - } - - $connectionData['mapId'] = $map; - - // "updated" should not be set by client e.g. after manual drag&drop - unset($connectionData['updated']); - - $connection->setData($connectionData); - - if( $connection->isValid() ){ - $connection->save(); - - $newConnectionData = $connection->getData(); - } - } - } - } - } - - echo json_encode($newConnectionData); - } - - public function delete($f3){ - $connectionIds = $f3->get('POST.connectionIds'); - - $user = $this->_getUser(); - $connection = Model\BasicModel::getNew('ConnectionModel'); - - foreach($connectionIds as $connectionId){ - - $connection->getById($connectionId); - $connection->delete($user); - - $connection->reset(); - } - - echo json_encode([]); - } - -} \ No newline at end of file diff --git a/app/main/controller/api/map.php b/app/main/controller/api/map.php deleted file mode 100644 index c7e47fd84..000000000 --- a/app/main/controller/api/map.php +++ /dev/null @@ -1,580 +0,0 @@ -expire($expireTimeHead); - - $initData = []; - - // static program data ------------------------------------------------ - $initData['timer'] = $f3->get('PATHFINDER.TIMER'); - - // get all available map types ---------------------------------------- - $mapType = Model\BasicModel::getNew('MapTypeModel'); - $rows = $mapType->find('active = 1', null, $expireTimeSQL); - - $mapTypeData = []; - foreach((array)$rows as $rowData){ - $data = [ - 'id' => $rowData->id, - 'label' => $rowData->label, - 'class' => $rowData->class, - 'classTab' => $rowData->classTab - ]; - $mapTypeData[$rowData->name] = $data; - - } - $initData['mapTypes'] = $mapTypeData; - - // get all available map scopes --------------------------------------- - $mapScope = Model\BasicModel::getNew('MapScopeModel'); - $rows = $mapScope->find('active = 1', null, $expireTimeSQL); - $mapScopeData = []; - foreach((array)$rows as $rowData){ - $data = [ - 'id' => $rowData->id, - 'label' => $rowData->label - ]; - $mapScopeData[$rowData->name] = $data; - } - $initData['mapScopes'] = $mapScopeData; - - // get all available system status ------------------------------------ - $systemStatus = Model\BasicModel::getNew('SystemStatusModel'); - $rows = $systemStatus->find('active = 1', null, $expireTimeSQL); - $systemScopeData = []; - foreach((array)$rows as $rowData){ - $data = [ - 'id' => $rowData->id, - 'label' => $rowData->label, - 'class' => $rowData->class - ]; - $systemScopeData[$rowData->name] = $data; - } - $initData['systemStatus'] = $systemScopeData; - - // get all available system types ------------------------------------- - $systemType = Model\BasicModel::getNew('SystemTypeModel'); - $rows = $systemType->find('active = 1', null, $expireTimeSQL); - $systemTypeData = []; - foreach((array)$rows as $rowData){ - $data = [ - 'id' => $rowData->id, - 'name' => $rowData->name - ]; - $systemTypeData[$rowData->name] = $data; - } - $initData['systemType'] = $systemTypeData; - - // get available connection scopes ------------------------------------ - $connectionScope = Model\BasicModel::getNew('ConnectionScopeModel'); - $rows = $connectionScope->find('active = 1', null, $expireTimeSQL); - $connectionScopeData = []; - foreach((array)$rows as $rowData){ - $data = [ - 'id' => $rowData->id, - 'label' => $rowData->label, - 'connectorDefinition' => $rowData->connectorDefinition - ]; - $connectionScopeData[$rowData->name] = $data; - } - $initData['connectionScopes'] = $connectionScopeData; - - // get available character status ------------------------------------- - $characterStatus = Model\BasicModel::getNew('CharacterStatusModel'); - $rows = $characterStatus->find('active = 1', null, $expireTimeSQL); - $characterStatusData = []; - foreach((array)$rows as $rowData){ - $data = [ - 'id' => $rowData->id, - 'name' => $rowData->name, - 'class' => $rowData->class - ]; - $characterStatusData[$rowData->name] = $data; - } - $initData['characterStatus'] = $characterStatusData; - - // get max number of shared entities per map -------------------------- - $maxSharedCount = [ - 'user' => $f3->get('PATHFINDER.MAX_SHARED_USER'), - 'corporation' => $f3->get('PATHFINDER.MAX_SHARED_CORPORATION'), - 'alliance' => $f3->get('PATHFINDER.MAX_SHARED_ALLIANCE'), - ]; - $initData['maxSharedCount'] = $maxSharedCount; - - echo json_encode($initData); - } - - /** - * save a new map or update an existing map - * @param $f3 - */ - public function save($f3){ - $formData = (array)$f3->get('POST.formData'); - - $return = (object) []; - $return->error = []; - - if( isset($formData['id']) ){ - - $user = $this->_getUser(0); - - if($user){ - $map = Model\BasicModel::getNew('MapModel'); - $map->getById( (int)$formData['id'] ); - - if( - $map->dry() || - $map->hasAccess($user) - ){ - // new map - $map->setData($formData); - $map = $map->save(); - - // save global map access. Depends on map "type" - if($map->isPrivate()){ - - // share map between users -> set access - if(isset($formData['mapUsers'])){ - // avoid abuse -> respect share limits - $accessUsers = array_slice( $formData['mapUsers'], 0, $f3->get('PATHFINDER.MAX_SHARED_USER') ); - - // clear map access. In case something has removed from access list - $map->clearAccess(); - - $tempUser = Model\BasicModel::getNew('UserModel'); - - foreach($accessUsers as $userId){ - $tempUser->getById( (int)$userId ); - - if( - !$tempUser->dry() && - $tempUser->sharing == 1 // check if map sharing is enabled - ){ - $map->setAccess($tempUser); - } - - $tempUser->reset(); - } - } - - // the current user itself should always have access - // just in case he removed himself :) - $map->setAccess($user); - }elseif($map->isCorporation()){ - $activeCharacter = $user->getActiveUserCharacter(); - - if($activeCharacter){ - $corporation = $activeCharacter->getCharacter()->getCorporation(); - - if($corporation){ - // the current user has to have a corporation when - // working on corporation maps! - - // share map between corporations -> set access - if(isset($formData['mapCorporations'])){ - // avoid abuse -> respect share limits - $accessCorporations = array_slice( $formData['mapCorporations'], 0, $f3->get('PATHFINDER.MAX_SHARED_CORPORATION') ); - - // clear map access. In case something has removed from access list - $map->clearAccess(); - - $tempCorporation = Model\BasicModel::getNew('CorporationModel'); - - foreach($accessCorporations as $corporationId){ - $tempCorporation->getById( (int)$corporationId ); - - if( - !$tempCorporation->dry() && - $tempCorporation->sharing == 1 // check if map sharing is enabled - ){ - $map->setAccess($tempCorporation); - } - - $tempCorporation->reset(); - } - } - - // the corporation of the current user should always have access - $map->setAccess($corporation); - } - } - }elseif($map->isAlliance()){ - $activeCharacter = $user->getActiveUserCharacter(); - - if($activeCharacter){ - $alliance = $activeCharacter->getCharacter()->getAlliance(); - - if($alliance){ - // the current user has to have a alliance when - // working on alliance maps! - - // share map between alliances -> set access - if(isset($formData['mapAlliances'])){ - // avoid abuse -> respect share limits - $accessAlliances = array_slice( $formData['mapAlliances'], 0, $f3->get('PATHFINDER.MAX_SHARED_ALLIANCE') ); - - // clear map access. In case something has removed from access list - $map->clearAccess(); - - $tempAlliance = Model\BasicModel::getNew('AllianceModel'); - - foreach($accessAlliances as $allianceId){ - $tempAlliance->getById( (int)$allianceId ); - - if( - !$tempAlliance->dry() && - $tempAlliance->sharing == 1 // check if map sharing is enabled - ){ - $map->setAccess($tempAlliance); - } - - $tempAlliance->reset(); - } - - } - - // the alliance of the current user should always have access - $map->setAccess($alliance); - } - } - } - // reload the same map model (refresh) - // this makes sure all data is up2date - $map->getById( $map->id, 0 ); - - - $return->mapData = $map->getData(); - - }else{ - // map access denied - $captchaError = (object) []; - $captchaError->type = 'error'; - $captchaError->message = 'Access denied'; - $return->error[] = $captchaError; - } - } - - }else{ - // map id field missing - $idError = (object) []; - $idError->type = 'error'; - $idError->message = 'Map id missing'; - $return->error[] = $idError; - } - - echo json_encode($return); - } - - /** - * delete a map and all dependencies - * @param $f3 - */ - public function delete($f3){ - $mapData = (array)$f3->get('POST.mapData'); - - $user = $this->_getUser(); - - if($user){ - $map = Model\BasicModel::getNew('MapModel'); - $map->getById($mapData['id']); - $map->delete($user); - } - - echo json_encode([]); - } - - /** - * update map data - * function is called continuously - * @param $f3 - */ - public function updateData($f3){ - - // cache time(s) per user should be equal or less than this function is called - // prevent request flooding - $responseTTL = $f3->get('PATHFINDER.TIMER.UPDATE_SERVER_MAP.DELAY') / 1000; - $mapData = (array)$f3->get('POST.mapData'); - - $user = $this->_getUser(); - $return = (object) []; - $return->error = []; - - if($user){ - // -> get active user object - $activeCharacter = $user->getActiveUserCharacter(); - - $cacheKey = 'user_map_data_' . $activeCharacter->id; - - // if there is any system/connection change data submitted -> clear cache - if(!empty($mapData)){ - $f3->clear($cacheKey); - } - - if($f3->exists($cacheKey) === false ){ - - // get current map data ======================================================== - $maps = $user->getMaps(); - - // loop all submitted map data that should be saved - // -> currently there will only be ONE map data change submitted -> single loop - foreach($mapData as $data){ - - $systems = []; - $connections = []; - - // check whether system data and/or connection data is send - // empty arrays are not included in ajax requests - if( isset($data['data']['systems']) ){ - $systems = (array)$data['data']['systems']; - } - - if( isset($data['data']['connections']) ){ - $connections = (array)$data['data']['connections']; - } - - // check if system data or connection data is send - if( - count($systems) > 0 || - count($connections) > 0 - ){ - - // map changes expected ============================================= - - - // loop current user maps and check for changes - foreach($maps as $map){ - - // update system data ----------------------------------------------- - foreach($systems as $i => $systemData){ - - // check if current system belongs to the current map - $map->filter('systems', array('id = ?', $systemData['id'] )); - $filteredMap = $map->find( - array('id = ?', $map->id ), - array('limit' => 1) - ); - - // this should never fail - if(is_object($filteredMap)){ - $filteredMap = $filteredMap->current(); - - // system belongs to the current map - if(is_object($filteredMap->systems)){ - // update - unset($systemData['updated']); - $system = $filteredMap->systems->current(); - $system->setData($systemData); - $system->updatedCharacterId = $activeCharacter->characterId; - $system->save(); - - // a system belongs to ONE map -> speed up for multiple maps - unset($systemData[$i]); - } - } - } - - // update connection data ------------------------------------------- - foreach($connections as $i => $connectionData){ - - // check if the current connection belongs to the current map - $map->filter('connections', array('id = ?', $connectionData['id'] )); - $filteredMap = $map->find( - array('id = ?', $map->id ), - array('limit' => 1) - ); - - // this should never fail - if(is_object($filteredMap)){ - $filteredMap = $filteredMap->current(); - - // connection belongs to the current map - if(is_object($filteredMap->connections)){ - // update - unset($connectionData['updated']); - $connection = $filteredMap->connections->current(); - $connection->setData($connectionData); - $connection->save($user); - - // a connection belongs to ONE map -> speed up for multiple maps - unset($connectionData[$i]); - } - } - } - } - } - } - - // format map Data for return - $return->mapData = self::getFormattedMapData($maps); - - $f3->set($cacheKey, $return, $responseTTL); - }else{ - // get from cache - $return = $f3->get($cacheKey); - } - - }else{ - // user logged of - $return->error[] = $this->getUserLoggedOffError(); - } - - echo json_encode( $return ); - } - - /** - * @param $mapModels - * @return Model\MapModel[] - */ - public static function getFormattedMapData($mapModels){ - - $mapData = []; - foreach($mapModels as $mapModel){ - - $allMapData = $mapModel->getData(); - - $mapData[] = [ - 'config' => $allMapData->mapData, - 'data' => [ - 'systems' => $allMapData->systems, - 'connections' => $allMapData->connections, - ] - ]; - } - - return $mapData; - } - - /** - * update map data api - * function is called continuously - * @param $f3 - */ - public function updateUserData($f3){ - - // cache time(s) should be equal or less than request trigger time - // prevent request flooding - $responseTTL = $f3->get('PATHFINDER.TIMER.UPDATE_SERVER_USER_DATA.DELAY') / 1000; - - // if the cache key will be set -> cache request - $cacheKey = null; - - $return = (object) []; - $return->error = []; - - if( !empty($f3->get('POST.mapIds')) ){ - $mapIds = (array)$f3->get('POST.mapIds'); - // check if data for specific system is requested - $systemData = (array)$f3->get('POST.systemData'); - - $user = $this->_getUser(); - - if($user){ - // update current location (IGB data) - $user->updateCharacterLog(60 * 5); - - // if data is requested extend the cache key in order to get new data - $requestSystemData = (object) []; - $requestSystemData->mapId = isset($systemData['mapId']) ? (int) $systemData['mapId'] : 0; - $requestSystemData->systemId = isset($systemData['systemData']['id']) ? (int) $systemData['systemData']['id'] : 0; - - // IMPORTANT for now -> just update a single map (save performance) - $mapIds = array_slice($mapIds, 0, 1); - - // the userMasData is cached per map (this must be changed if multiple maps - // will be allowed in future... - $tempId = (int)$mapIds[0]; - $cacheKey = 'user_data_' . $tempId . '_' . $requestSystemData->systemId; - - if( $f3->exists($cacheKey) === false ){ - foreach($mapIds as $mapId){ - $map = $user->getMap($mapId); - - if( !is_null($map) ){ - $return->mapUserData[] = $map->getUserData(); - - - // request signature data for a system if user has map access! - if( $map->id === $requestSystemData->mapId ){ - $system = $map->getSystem( $requestSystemData->systemId ); - - if( !is_null($system) ){ - // data for the current selected system - $return->system = $system->getData(); - $return->system->signatures = $system->getSignaturesData(); - } - } - } - } - - // cache response - $f3->set($cacheKey, $return, $responseTTL); - }else{ - // get from cache - // this should happen if a user has multiple program instances running - // with the same main char - $return = $f3->get($cacheKey); - } - - // get current user data -> this should not be cached because each user has different personal data - // even if they have multiple characters using the same map! - $return->userData = $user->getData(); - }else{ - // user logged of - $return->error[] = $this->getUserLoggedOffError(); - } - } - - echo json_encode( $return ); - } - -} - - - - - - - - - - - - - diff --git a/app/main/controller/api/route.php b/app/main/controller/api/route.php deleted file mode 100644 index 9ade172cb..000000000 --- a/app/main/controller/api/route.php +++ /dev/null @@ -1,404 +0,0 @@ - systemId matching - * @var array - */ - private $idArray = []; - - - - - function __construct() { - parent::__construct(); - - // set cache time for static jump data - $this->jumpDataCacheTime = 60 * 60 * 24; - - // set static system jump data - $this->setSystemJumpData(); - } - - /** - * set static system jump data for this instance - * the data is fixed and should not change - */ - private function setSystemJumpData(){ - $cacheKey = 'staticJumpData'; - - $cacheKeyNamedArray = $cacheKey . '.nameArray'; - $cacheKeyJumpArray = $cacheKey . '.jumpArray'; - $cacheKeyIdArray = $cacheKey . '.idArray'; - - if( - $this->f3->exists($cacheKeyNamedArray) && - $this->f3->exists($cacheKeyJumpArray) && - $this->f3->exists($cacheKeyIdArray) - ){ - // get cached values - $this->nameArray = $this->f3->get($cacheKeyNamedArray); - $this->jumpArray = $this->f3->get($cacheKeyJumpArray); - $this->idArray = $this->f3->get($cacheKeyIdArray); - }else{ - // nothing cached - - $query = "SELECT * FROM system_neighbour"; - - $rows = $this->f3->get('DB')->exec($query, null, $this->jumpDataCacheTime); - - - foreach($rows as $row){ - $regionId = $row['regionId']; - $constId = $row['constellationId']; - $systemName = strtoupper($row['systemName']); - $systemId = $row['systemId']; - $secStatus = $row['trueSec']; - - $this->nameArray[$systemId][0] = $systemName; - $this->nameArray[$systemId][1] = $regionId; - $this->nameArray[$systemId][2] = $constId; - $this->nameArray[$systemId][3] = $secStatus; - - $this->idArray[strtoupper($systemName)] = $systemId; - - $this->jumpArray[$systemName]= explode(":", strtoupper($row['jumpNodes'])); - array_push($this->jumpArray[$systemName],$systemId); - } - - $this->f3->set($cacheKeyNamedArray, $this->nameArray, $this->jumpDataCacheTime); - $this->f3->set($cacheKeyJumpArray, $this->jumpArray, $this->jumpDataCacheTime); - $this->f3->set($cacheKeyIdArray, $this->idArray, $this->jumpDataCacheTime); - } - } - - /** - * get system data by systemId and dataName - * @param $systemId - * @param $option - * @return null - */ - private function getSystemInfoBySystemId($systemId, $option){ - - $info = null; - - switch($option){ - case 'systemName': - $info = $this->nameArray[ $systemId ][0]; - break; - case 'regionId': - $info = $this->nameArray[ $systemId ][1]; - break; - case 'constellationId': - $info = $this->nameArray[ $systemId ][2]; - break; - case 'trueSec': - $info = $this->nameArray[ $systemId ][3]; - break; - } - - return $info; - } - - /** - * recursive search function within a undirected graph - * @param $G - * @param $A - * @param $B - * @param int $M - * @return array - */ - private function graph_find_path(&$G, $A, $B, $M = 50000){ - // $P will hold the result path at the end. - // Remains empty if no path was found. - $P = array(); - - // For each Node ID create a "visit information", - // initially set as 0 (meaning not yet visited) - // as soon as we visit a node we will tag it with the "source" - // so we can track the path when we reach the search target - - $V = array(); - - // We are going to keep a list of nodes that are "within reach", - // initially this list will only contain the start node, - // then gradually expand (almost like a flood fill) - $R = array(trim($A)); - - $A = trim($A); - $B = trim($B); - - while(count($R) > 0 && $M > 0){ - $M--; - - $X = trim(array_shift($R)); - - if( array_key_exists($X, $G) ){ - foreach($G[$X] as $Y){ - $Y = trim($Y); - // See if we got a solution - if($Y == $B){ - // We did? Construct a result path then - array_push($P, $B); - array_push($P, $X); - while($V[$X] != $A){ - array_push($P, trim($V[$X])); - $X = $V[$X]; - } - array_push($P, $A); - return array_reverse($P); - } - // First time we visit this node? - if(!array_key_exists($Y, $V)){ - // Store the path so we can track it back, - $V[$Y] = $X; - // and add it to the "within reach" list - array_push($R, $Y); - } - } - } - } - - return $P; - } - - /** - * This function is just for setting up the cache table 'system_neighbour' which is used - * for system jump calculation. Call this function manually if CCP adds Systems/Stargates - */ - private function setupSystemJumpTable(){ - - // switch DB - $this->setDB('CCP'); - - $query = "SELECT - map_sys.solarSystemID system_id, - map_sys.regionID region_id, - map_sys.constellationID constellation_id, - map_sys.solarSystemName system_name, - ROUND( map_sys.security, 2) system_security, - ( - SELECT - GROUP_CONCAT( NULLIF(map_sys_inner.solarSystemName, NULL) SEPARATOR ':') - FROM - mapsolarsystemjumps map_jump INNER JOIN - mapsolarsystems map_sys_inner ON - map_sys_inner.solarSystemID = map_jump.toSolarSystemID - WHERE - map_jump.fromSolarSystemID = map_sys.solarSystemID - ) system_neighbours - FROM - mapsolarsystems map_sys - HAVING - -- skip systems without neighbors (e.g. WHs) - system_neighbours IS NOT NULL - "; - - $rows = $this->f3->get('DB')->exec($query); - - if(count($rows) > 0){ - // switch DB back to pathfinder DB - $this->setDB('PF'); - - // clear cache table - $query = "TRUNCATE system_neighbour"; - $this->f3->get('DB')->exec($query); - - foreach($rows as $row){ - $this->f3->get('DB')->exec(" - INSERT INTO - system_neighbour( - regionId, - constellationId, - systemName, - systemId, - jumpNodes, - trueSec - ) - VALUES( - :regionId, - :constellationId, - :systemName, - :systemId, - :jumpNodes, - :trueSec - )", - [ - ':regionId' => $row['region_id'], - ':constellationId' => $row['constellation_id'], - ':systemName' => $row['system_name'], - ':systemId' => $row['system_id'], - ':jumpNodes' => $row['system_neighbours'], - ':trueSec' => $row['system_security'] - ]); - } - } - } - - /** - * find a route between two systems (system names) - * $searchDepth for recursive route search (5000 would be best but slow) - * -> in reality there are no routes > 100 jumps between systems - * @param $systemFrom - * @param $systemTo - * @param int $searchDepth - * @return array - */ - public function findRoute($systemFrom, $systemTo, $searchDepth = 5000){ - - $routeData = [ - 'routePossible' => false, - 'routeJumps' => 0, - 'route' => [] - ]; - - if( - !empty($systemFrom) && - !empty($systemTo) - ){ - $from = strtoupper( $systemFrom ); - $to = strtoupper( $systemTo ); - - // jump counter - $jumpNum = 0; - - if( isset($this->jumpArray[$from]) ){ - - - // check if the system we are looking for is a direct neighbour - foreach( $this->jumpArray[$from] as $n ) { - - if ($n == $to) { - $jumpNum = 2; - - $jumpNode = [ - 'system' => $n, - 'security' => $this->getSystemInfoBySystemId($this->idArray[$n], 'trueSec') - ]; - - $routeData['route'][] = $jumpNode; - break; - } - } - - // system is not a direct neighbour -> search recursive its neighbours - if ($jumpNum == 0) { - foreach( $this->graph_find_path( $this->jumpArray, $from, $to, $searchDepth ) as $n ) { - - if ($jumpNum > 0) { - - $jumpNode = [ - 'system' => $n, - 'security' => $this->getSystemInfoBySystemId($this->idArray[$n], 'trueSec') - ]; - - $routeData['route'][] = $jumpNode; - } - $jumpNum++; - } - } - - if ($jumpNum > 0) { - // route found - $routeData['routePossible'] = true; - - $jumpNode = [ - 'system' => $from, - 'security' => $this->getSystemInfoBySystemId($this->idArray[$from], 'trueSec') - ]; - - // insert "from" system on top - array_unshift($routeData['route'], $jumpNode); - } else { - // route not found - $routeData['routePossible'] = false; - } - } - - // route jumps - $routeData['routeJumps'] = $jumpNum - 1; - } - - return $routeData; - } - - /** - * search multiple route between two systems - * @param $f3 - */ - public function search($f3){ - $routesData = $data = (array)$f3->get('POST.routeData'); - - $return = (object) []; - $return->error = []; - $return->routesData = []; - - foreach($routesData as $routeData){ - $cacheKey = self::formatHiveKey($routeData['systemFrom']) . '_' . self::formatHiveKey($routeData['systemTo']); - - if($f3->exists($cacheKey)){ - // get data from cache - $return->routesData[] = $f3->get($cacheKey); - }else{ - // no cached route data found - $foundRoutData = $this->findRoute($routeData['systemFrom'], $routeData['systemTo']); - $f3->set($cacheKey, $foundRoutData, $this->jumpDataCacheTime); - - $return->routesData[] = $foundRoutData; - } - - } - - echo json_encode($return); - } - - - -} - - - - - - - - - - - - - diff --git a/app/main/controller/api/signature.php b/app/main/controller/api/signature.php deleted file mode 100644 index c69ef1b8a..000000000 --- a/app/main/controller/api/signature.php +++ /dev/null @@ -1,180 +0,0 @@ -get('POST.systemIds'); - - $user = $this->_getUser(); - - $system = Model\BasicModel::getNew('SystemModel'); - - foreach($systemIds as $systemId){ - $system->getById($systemId); - - if(!$system->dry()){ - - // check access - if($system->hasAccess($user)){ - $signatureData = $system->getSignaturesData(); - } - } - } - - echo json_encode($signatureData); - } - - /** - * save or update a full signature data set - * or save/update just single or multiple signature data - * @param $f3 - */ - public function save($f3){ - $requestData = $f3->get('POST'); - - $signatureData = null; - - $return = (object) []; - $return->error = []; - $return->signatures = []; - - if( isset($requestData['signatures']) ){ - // save multiple signatures - $signatureData = $requestData['signatures']; - }elseif( !empty($requestData) ){ - // single signature - $signatureData = [$requestData]; - } - - if( !is_null($signatureData) ){ - $user = $this->_getUser(); - - if($user){ - $activeCharacter = $user->getActiveUserCharacter(); - $system = Model\BasicModel::getNew('SystemModel'); - - // update/add all submitted signatures - foreach($signatureData as $data){ - $system->getById( (int)$data['systemId']); - - if(!$system->dry()){ - // update/save signature - - $signature = null; - if( isset($data['pk']) ){ - // try to get system by "primary key" - $signature = $system->getSignatureById($user, (int)$data['pk']); - }elseif( isset($data['name']) ){ - $signature = $system->getSignatureByName($user, $data['name']); - } - - if( is_null($signature) ){ - $signature = Model\BasicModel::getNew('SystemSignatureModel'); - } - - $signature->updatedCharacterId = $activeCharacter->getCharacter(); - - if($signature->dry()){ - // new signature - $signature->systemId = $system; - $signature->createdCharacterId = $activeCharacter->getCharacter(); - $signature->setData($data); - }else{ - // update signature - - if( - isset($data['name']) && - isset($data['value']) - ){ - // update single key => value pair - $newData = [ - $data['name'] => $data['value'] - ]; - }else{ - // update complete signature (signature reader dialog) - - // description should not be updated - unset( $data['description'] ); - - // wormhole typeID cant figured out/saved by the sig reader dialog - if($data['groupId'] == 5){ - unset( $data['typeId'] ); - } - - $newData = $data; - } - - $signature->setData($newData); - } - - - $signature->save(); - - // get a fresh signature object with the new data. This is a bad work around! - // but i could not figure out what the problem was when using the signature model, saved above :( - // -> some caching problems - $newSignature = Model\BasicModel::getNew('SystemSignatureModel'); - $newSignature->getById( $signature->id, 0); - - $return->signatures[] = $newSignature->getData(); - - $signature->reset(); - } - - $system->reset(); - } - } - } - - echo json_encode($return); - } - - /** - * delete signatures - * @param $f3 - */ - public function delete($f3){ - $signatureIds = $f3->get('POST.signatureIds'); - - $user = $this->_getUser(); - $signature = Model\BasicModel::getNew('SystemSignatureModel'); - - foreach($signatureIds as $signatureId){ - $signature->getById($signatureId); - - $signature->delete($user); - $signature->reset(); - } - - echo json_encode([]); - } - - -} \ No newline at end of file diff --git a/app/main/controller/api/system.php b/app/main/controller/api/system.php deleted file mode 100644 index d04033b32..000000000 --- a/app/main/controller/api/system.php +++ /dev/null @@ -1,393 +0,0 @@ -mainQuery; - $query .= ' ' . $this->whereQuery; - $query .= ' ' . $this->havingQuery; - $query .= ' ' . $this->orderByQuery; - $query .= ' ' . $this->limitQuery; - - return $query; - } - - /** - * get static system Data from CCPs Static DB export - * search column for IDs can be (solarSystemID, regionID, constellationID) - * @param array $columnIDs - * @return null - * @throws \Exception - */ - protected function _getSystemModelByIds($columnIDs = [], $column = 'solarSystemID'){ - - $systemModels = []; - - // switch DB - $this->setDB('CCP'); - - $this->whereQuery = "WHERE - map_sys." . $column . " IN (" . implode(',', $columnIDs) . ")"; - - $query = $this->_getQuery(); - - $rows = $this->f3->get('DB')->exec($query, null, 60 * 60 * 24); - - // format result - $mapper = new Mapper\CcpSystemsMapper($rows); - - $ccpSystemsData = $mapper->getData(); - - // switch DB - $this->setDB('PF'); - - foreach($ccpSystemsData as $ccpSystemData){ - $system = Model\BasicModel::getNew('SystemModel'); - $system->setData($ccpSystemData); - $systemModels[] = $system; - } - - return $systemModels; - } - - /** - * Get all static system Data from CCP DB (long cache timer) - * @return array - */ - public function getSystems(){ - - // switch DB - $this->setDB('CCP'); - - $query = $this->_getQuery(); - - $rows = $this->f3->get('DB')->exec($query, null, 60 * 60 * 24); - - // format result - $mapper = new Mapper\CcpSystemsMapper($rows); - - return $mapper->getData(); - } - - /** - * search systems by name - * @param $f3 - * @param $params - */ - public function search($f3, $params){ - - // switch DB - \DB\Database::instance(); - $this->setDB('CCP'); - - $searchToken = ''; - // check for search parameter - if( isset($params['arg1']) ){ - $searchToken = $params['arg1']; - } - - $this->whereQuery = "WHERE - map_sys.solarSystemName LIKE '%" . $searchToken . "%'"; - - $query = $this->_getQuery(); - - $rows = $f3->get('DB')->exec($query); - - // format result - $mapper = new Mapper\CcpSystemsMapper($rows); - - $data = $mapper->getData(); - - echo json_encode($data); - } - - /** - * save a new system to a a map - * @param $f3 - */ - public function save($f3){ - - $newSystemData = []; - - $postData = (array)$f3->get('POST'); - - // system to be saved - $systemModel = null; - - if( - isset($postData['systemData']) && - isset($postData['mapData']) - ){ - $user = $this->_getUser(); - - if($user){ - $systemData = (array)$postData['systemData']; - $mapData = (array)$postData['mapData']; - - $activeCharacter = $user->getActiveUserCharacter(); - - if( isset($systemData['id']) ){ - // update existing system - - $system = Model\BasicModel::getNew('SystemModel'); - $system->getById($systemData['id']); - - if( !$system->dry() ){ - if( $system->hasAccess($user) ){ - // system model found - $systemModel = $system; - } - } - }elseif( isset($mapData['id']) ){ - // save NEW system - - $map = Model\BasicModel::getNew('MapModel'); - $map->getById($mapData['id']); - - if( !$map->dry() ){ - if( $map->hasAccess($user) ){ - - $systemData['mapId'] = $map; - - // get static system data (CCP DB) - $systemModel = array_values( $this->_getSystemModelByIds([$systemData['systemId']]) )[0]; - - $systemModel->createdCharacterId = $activeCharacter->characterId; - - } - } - } - } - } - - - if( !is_null($systemModel) ){ - // set/update system - - $systemModel->setData($systemData); - $systemModel->updatedCharacterId = $activeCharacter->characterId; - $systemModel->save(); - - $newSystemData = $systemModel->getData(); - } - - echo json_encode($newSystemData); - } - - /** - * delete systems and all its connections - * @param $f3 - */ - public function delete($f3){ - $systemIds = $f3->get('POST.systemIds'); - - $user = $this->_getUser(); - - if($user){ - $system = Model\BasicModel::getNew('SystemModel'); - - foreach((array)$systemIds as $systemId){ - - $system->getById($systemId); - $system->delete($user); - - $system->reset(); - } - } - - echo json_encode([]); - } - - /** - * get system log data from CCP API import - * system Kills, Jumps,.... - * @param $f3 - */ - public function graphData($f3){ - $graphData = []; - $systemIds = $f3->get('POST.systemIds'); - - // number of log entries in each table per system (24 = 24h) - $logEntryCount = 24; - - // table names with system data - $logTables = [ - 'jumps' => 'SystemJumpModel', - 'shipKills' => 'SystemShipKillModel', - 'podKills' => 'SystemPodKillModel', - 'factionKills' => 'SystemFactionKillModel' - ]; - - foreach($systemIds as $systemId){ - - foreach($logTables as $label => $ModelClass){ - $systemLogModel = Model\BasicModel::getNew($ModelClass); - - // 10min cache (could be up to 1h cache time) - $systemLogModel->getByForeignKey('systemId', $systemId, array(), 60 * 10); - - if(!$systemLogModel->dry()){ - $counter = 0; - for( $i = $logEntryCount; $i >= 1; $i--){ - $column = 'value' . $i; - - // ship and pod kills should be merged into one table - if($label == 'podKills'){ - $graphData[$systemId]['shipKills'][$counter]['z'] = $systemLogModel->$column; - }else{ - $dataSet = [ - 'x' => ($i - 1) . 'h', - 'y' => $systemLogModel->$column - ]; - $graphData[$systemId][$label][] = $dataSet; - } - $counter++; - } - } - - } - } - - echo json_encode($graphData); - } - - /** - * get system data for all systems within a constellation - * @param $f3 - * @param $params - */ - public function constellationData($f3, $params){ - - $return = (object) []; - $return->error = []; - $return->systemData = []; - - $constellationId = 0; - - $user = $this->_getUser(); - - if($user){ - // check for search parameter - if( isset($params['arg1']) ){ - $constellationId = (int)$params['arg1']; - } - - $cacheKey = 'CACHE_CONSTELLATION_SYSTEMS_' . self::formatHiveKey($constellationId); - - if($f3->exists($cacheKey)){ - $return->systemData = $f3->get($cacheKey); - }else{ - if($constellationId > 0){ - $systemModels = $this->_getSystemModelByIds([$constellationId], 'constellationID'); - - foreach($systemModels as $systemModel){ - $return->systemData[] = $systemModel->getData(); - } - - $f3->set($cacheKey, $return->systemData, $f3->get('PATHFINDER.CACHE.CONSTELLATION_SYSTEMS') ); - } - } - } - - echo json_encode($return); - } - - -} - - - - - - - - - - - - - - - - - - - - - diff --git a/app/main/controller/api/user.php b/app/main/controller/api/user.php deleted file mode 100644 index 038fce462..000000000 --- a/app/main/controller/api/user.php +++ /dev/null @@ -1,393 +0,0 @@ -get('POST'); - - $return = (object) []; - - $user = null; - - if($data['loginData']){ - $loginData = $data['loginData']; - $user = $this->logUserIn( $loginData['userName'], $loginData['userPassword'] ); - } - - // set "vague" error - if(is_null($user)){ - $return->error = []; - $loginError = (object) []; - $loginError->type = 'login'; - $return->error[] = $loginError; - }else{ - // update/check api data - $user->updateApiData(); - - // route user to map app - $return->reroute = self::getEnvironmentData('URL') . $f3->alias('map'); - } - - echo json_encode($return); - } - - /** - * core function for user login - * @param $userName - * @param $password - * @return Model\UserModel|null - */ - private function logUserIn($userName, $password){ - - // try to verify user - $user = $this->_verifyUser($userName, $password); - - if( !is_null($user)){ - // user is verified -> ready for login - - // set Session login - $dateTime = new \DateTime(); - $this->f3->set('SESSION.user.time', $dateTime->getTimestamp()); - $this->f3->set('SESSION.user.name', $user->name); - $this->f3->set('SESSION.user.id', $user->id); - - - // save user login information - $user->touch('lastLogin'); - $user->save(); - - // save log - $logText = "id: %s, name: %s, ip: %s"; - self::getLogger( $this->f3->get('PATHFINDER.LOGFILES.LOGIN') )->write( - sprintf($logText, $user->id, $user->name, $this->f3->get('IP')) - ); - } - - return $user; - } - - /** - * get captcha image and store key to session - * @param $f3 - */ - public function getCaptcha($f3){ - - $img = new \Image(); - - $imgDump = $img->captcha( - 'fonts/oxygen-bold-webfont.ttf', - 14, - 6, - 'SESSION.captcha_code', - '', - '0x66C84F', - '0x313335' - )->dump(); - - echo $f3->base64( $imgDump, 'image/png'); - } - - /** - * delete the character log entry for the current active (main) character - * @param $f3 - */ - public function deleteLog($f3){ - - $user = $this->_getUser(); - if($user){ - $activeUserCharacter = $user->getActiveUserCharacter(); - - if($activeUserCharacter){ - $character = $activeUserCharacter->getCharacter(); - - if($characterLog = $character->getLog()){ - $characterLog->erase(); - $characterLog->save(); - - $character->clearCacheData(); - - // delete log cache key as well - $f3->clear('LOGGED.user.character.id_' . $characterLog->characterId->id . '.systemId'); - $f3->clear('LOGGED.user.character.id_' . $characterLog->characterId->id . '.shipId'); - - } - } - } - - - } - - /** - * log the current user out + clear character system log data - * @param $f3 - */ - public function logOut($f3){ - $this->deleteLog($f3); - - return parent::logOut($f3); - } - - /** - * save/update "map sharing" configurations for all map types - * the user has access to - * @param $f3 - */ - public function saveSharingConfig($f3){ - $data = $f3->get('POST'); - - $return = (object) []; - - $privateSharing = 0; - $corporationSharing = 0; - $allianceSharing = 0; - - $user = $this->_getUser(); - - if($user){ - - // form values - if(isset($data['formData'])){ - $formData = $data['formData']; - - if(isset($formData['privateSharing'])){ - $privateSharing = 1; - } - - if(isset($formData['corporationSharing'])){ - $corporationSharing = 1; - } - - if(isset($formData['allianceSharing'])){ - $allianceSharing = 1; - } - } - - $user->sharing = $privateSharing; - $user->save(); - - // update corp/ally --------------------------------------------------------------- - - $activeUserCharacter = $user->getActiveUserCharacter(); - - if(is_object($activeUserCharacter)){ - $corporation = $activeUserCharacter->getCharacter()->getCorporation(); - $alliance = $activeUserCharacter->getCharacter()->getAlliance(); - - if(is_object($corporation)){ - $corporation->sharing = $corporationSharing; - $corporation->save(); - } - - if(is_object($alliance)){ - $alliance->sharing = $allianceSharing; - $alliance->save(); - } - } - - $return->userData = $user->getData(); - } - - echo json_encode($return); - } - - /** - * save/update user data - * @param $f3 - */ - public function saveConfig($f3){ - $data = $f3->get('POST'); - - $return = (object) []; - $return->error = []; - - $captcha = $f3->get('SESSION.captcha_code'); - - // reset captcha -> forces user to enter new one - $f3->clear('SESSION.captcha_code'); - - $newUserData = null; - - // check user if if he is new - $loginAfterSave = false; - - if( isset($data['settingsData']) ){ - $settingsData = $data['settingsData']; - - try{ - $user = $this->_getUser(); - - // captcha is send -> check captcha - if( - isset($settingsData['captcha']) && - !empty($settingsData['captcha']) - ){ - - - if($settingsData['captcha'] === $captcha){ - // change/set sensitive user data requires captcha! - - if($user === false){ - // new user registration - $user = $mapType = Model\BasicModel::getNew('UserModel'); - $loginAfterSave = true; - - // set username - if( - isset($settingsData['name']) && - !empty($settingsData['name']) - ){ - $user->name = $settingsData['name']; - } - } - - // change/set email - if( - isset($settingsData['email']) && - isset($settingsData['email_confirm']) && - !empty($settingsData['email']) && - !empty($settingsData['email_confirm']) && - $settingsData['email'] == $settingsData['email_confirm'] - ){ - $user->email = $settingsData['email']; - } - - // change/set password - if( - isset($settingsData['password']) && - isset($settingsData['password_confirm']) && - !empty($settingsData['password']) && - !empty($settingsData['password_confirm']) && - $settingsData['password'] == $settingsData['password_confirm'] - ){ - $user->password = $settingsData['password']; - } - }else{ - // captcha was send but not valid -> return error - $captchaError = (object) []; - $captchaError->type = 'error'; - $captchaError->message = 'Captcha does not match'; - $return->error[] = $captchaError; - } - } - - // saving additional user info requires valid user object (no captcha required) - if($user){ - - // save API data - if( - isset($settingsData['keyId']) && - isset($settingsData['vCode']) && - is_array($settingsData['keyId']) && - is_array($settingsData['vCode']) - ){ - - // get all existing API models for this user - $apiModels = $user->getAPIs(); - - foreach($settingsData['keyId'] as $i => $keyId){ - $api = null; - - // search for existing API model - foreach($apiModels as $key => $apiModel){ - if($apiModel->keyId == $keyId){ - $api = $apiModel; - // make sure model is up2data -> cast() - $api->cast(); - unset($apiModels[$key]); - break; - } - } - - if(is_null($api)){ - // new API Key - $api = Model\BasicModel::getNew('UserApiModel'); - $api->userId = $user; - } - - $api->keyId = $keyId; - $api->vCode = $settingsData['vCode'][$i]; - $api->save(); - - $characterCount = $api->updateCharacters(); - - if($characterCount == 0){ - // no characters found -> return warning - $characterError = (object) []; - $characterError->type = 'warning'; - $characterError->message = 'API verification failed. No Characters found for KeyId ' . $api->keyId; - $return->error[] = $characterError; - } - } - - // delete API models that no longer exists - foreach($apiModels as $apiModel){ - $apiModel->delete(); - } - - } - - // set main character - if( isset($settingsData['mainCharacterId']) ){ - $user->setMainCharacterId((int)$settingsData['mainCharacterId']); - } - - // check if the user already has a main character - // if not -> save the next best character as main - $mainUserCharacter = $user->getMainUserCharacter(); - - // set main character if no main character exists - if(is_null($mainUserCharacter)){ - $user->setMainCharacterId(); - } - - // save/update user model - // this will fail if model validation fails! - $user->save(); - - // log user in (in case he is new - if($loginAfterSave){ - $this->logUserIn( $user->name, $settingsData['password'] ); - - // return reroute path - $return->reroute = self::getEnvironmentData('URL') . $this->f3->alias('map'); - } - - // get fresh updated user object - $user = $this->_getUser(0); - $newUserData = $user->getData(); - } - }catch(Exception\ValidationException $e){ - $validationError = (object) []; - $validationError->type = 'error'; - $validationError->field = $e->getField(); - $validationError->message = $e->getMessage(); - $return->error[] = $validationError; - }catch(Exception\RegistrationException $e){ - $registrationError = (object) []; - $registrationError->type = 'error'; - $registrationError->message = $e->getMessage(); - $return->error[] = $registrationError; - } - - // return new/updated user data - $return->userData = $newUserData; - - } - echo json_encode($return); - } -} \ No newline at end of file diff --git a/app/main/controller/appcontroller.php b/app/main/controller/appcontroller.php deleted file mode 100644 index 9d8461a28..000000000 --- a/app/main/controller/appcontroller.php +++ /dev/null @@ -1,35 +0,0 @@ -set('pageContent','templates/view/landingpage.html'); - - // body element class - $this->f3->set('bodyClass', 'pf-body pf-landing'); - - // landing page is always IGB trusted - $f3->set('trusted', 1); - - // JS main file - $f3->set('jsView', 'landingpage'); - - $this->setTemplate('templates/view/index.html'); - } - -} \ No newline at end of file diff --git a/app/main/controller/ccpapicontroller.php b/app/main/controller/ccpapicontroller.php deleted file mode 100644 index ed1f28e9f..000000000 --- a/app/main/controller/ccpapicontroller.php +++ /dev/null @@ -1,195 +0,0 @@ -f3->get('PATHFINDER.NAME'); - $userAgent .= ' - ' . $this->f3->get('PATHFINDER.VERSION'); - $userAgent .= ' | ' . $this->f3->get('PATHFINDER.CONTACT'); - $userAgent .= ' (' . $_SERVER['SERVER_NAME'] . ')'; - - return $userAgent; - } - - /** - * get HTTP request options for API (curl) request - * @return array - */ - protected function getRequestOptions(){ - $requestOptions = [ - 'timeout' => 8, - 'method' => 'POST', - 'user_agent' => $this->getUserAgent() - ]; - - return $requestOptions; - } - - /** - * request character information from CCP API - * @param $keyID - * @param $vCode - * @return bool|\SimpleXMLElement - */ - public function requestCharacters($keyID, $vCode){ - - $apiPath = $this->f3->get('PATHFINDER.API.CCP_XML') . '/account/APIKeyInfo.xml.aspx'; - - $xml = false; - - // build request URL - $options = $this->getRequestOptions(); - $options['content'] = http_build_query( [ - 'keyID' => $keyID, - 'vCode' => $vCode - ]); - - $apiResponse = \Web::instance()->request($apiPath, $options ); - - if($apiResponse['body']){ - $xml = simplexml_load_string($apiResponse['body']); - } - - return $xml; - } - - /** - * update all character information for a given apiModel - * @param $userApiModel - * @return int - * @throws \Exception - */ - public function updateCharacters($userApiModel){ - - $xml = $this->requestCharacters($userApiModel->keyId, $userApiModel->vCode); - - $characterCount = 0; - - // important -> user API model must be up2date - // if not -> matched userCharacter cant be found - $userApiModel->getById($userApiModel->id, 0); - - if($xml){ - // request successful - $rowApiData = $xml->result->key->rowset; - - if($rowApiData->children()){ - $characterModel = Model\BasicModel::getNew('CharacterModel'); - $corporationModel = Model\BasicModel::getNew('CorporationModel'); - $allianceModel = Model\BasicModel::getNew('AllianceModel'); - - foreach($rowApiData->children() as $characterApiData){ - // map attributes to array - $attributeData = current( $characterApiData->attributes() ); - - $newCharacter = true; - - $characterId = (int)$attributeData['characterID']; - $characterModel->getById($characterId); - - // check if corporation already exists - if($attributeData['corporationID'] > 0){ - $corporationModel->getById($attributeData['corporationID']); - if( $corporationModel->dry() ){ - $corporationModel->id = $attributeData['corporationID']; - $corporationModel->name = $attributeData['corporationName']; - $corporationModel->save(); - } - $corporationModelTemp = $corporationModel; - } - - // check if alliance already exists - if($attributeData['allianceID'] > 0){ - $allianceModel->getById($attributeData['allianceID']); - if( $allianceModel->dry() ){ - $allianceModel->id = $attributeData['allianceID']; - $allianceModel->name = $attributeData['allianceName']; - $allianceModel->save(); - } - $allianceModelTemp = $allianceModel; - } - - if($userApiModel->userCharacters){ - $userApiModel->userCharacters->rewind(); - while($userApiModel->userCharacters->valid()){ - $tempCharacterModel = $userApiModel->userCharacters->current()->getCharacter(); - - // character already exists -> update - if($tempCharacterModel->id == $characterId){ - $characterModel = $tempCharacterModel; - - // unset userCharacter -> all leftover models are no longer part of this API - // --> delete leftover models at the end - $userApiModel->userCharacters->offsetUnset($userApiModel->userCharacters->key()); - - $newCharacter = false; - break; - }else{ - $userApiModel->userCharacters->next(); - } - } - - $userApiModel->userCharacters->rewind(); - - } - - $characterModel->id = $characterId; - $characterModel->name = $attributeData['characterName']; - $characterModel->corporationId = $corporationModelTemp; - $characterModel->allianceId = $allianceModelTemp; - $characterModel->factionId = $attributeData['factionID']; - $characterModel->factionName = $attributeData['factionName']; - $characterModel->save(); - - if($newCharacter){ - // new character for this API - $userCharactersModel = Model\BasicModel::getNew('UserCharacterModel', 0); - $userCharactersModel->userId = $userApiModel->userId; - $userCharactersModel->apiId = $userApiModel; - $userCharactersModel->characterId = $characterModel; - $userCharactersModel->save(); - } - - $corporationModel->reset(); - $allianceModel->reset(); - $characterModel->reset(); - - $characterCount++; - } - } - - // delete leftover userCharacters from this API - if(count($userApiModel->userCharacters) > 0){ - while($userApiModel->userCharacters->valid()){ - $userApiModel->userCharacters->current()->erase(); - $userApiModel->userCharacters->next(); - } - } - - } - - return $characterCount; - } - -} \ No newline at end of file diff --git a/app/main/controller/controller.php b/app/main/controller/controller.php deleted file mode 100644 index c435ac8cc..000000000 --- a/app/main/controller/controller.php +++ /dev/null @@ -1,259 +0,0 @@ -f3 = \Base::instance(); - - // initiate DB connection - DB\Database::instance('PF'); - } - - /** - * @param mixed $template - */ - public function setTemplate($template){ - $this->template = $template; - } - - /** - * @return mixed - */ - public function getTemplate(){ - return $this->template; - } - - - /** - * event handler for all "views" - * some global template variables are set in here - * @param $f3 - */ - function beforeroute($f3) { - - // check if user is in game - $f3->set('isIngame', self::isIGB() ); - - // js path (build/minified or raw uncompressed files) - $f3->set('pathJs', self::getEnvironmentData('PATH_JS') ); - } - - /** - * event handler - */ - function afterroute() { - if($this->template){ - echo \Template::instance()->render( $this->template ); - - } - } - - /** - * set change the DB connection - * @param string $database - */ - protected function setDB($database = 'PF'){ - DB\Database::instance()->setDB($database); - } - - /** - * get current user model - * @param int $ttl - * @return bool|null - * @throws \Exception - */ - protected function _getUser($ttl = 5){ - - $user = false; - $userId = $this->f3->get('SESSION.user.id'); - - if($userId > 0){ - $userModel = Model\BasicModel::getNew('UserModel'); - $userModel->getById($userId, $ttl); - - if( !$userModel->dry() ){ - $user = $userModel; - } - } - - return $user; - } - - /** - * check weather the page is IGB trusted or not - * @return mixed - */ - static function isIGBTrusted(){ - - $igbHeaderData = self::getIGBHeaderData(); - - return $igbHeaderData->trusted; - } - - /** - * extract all eve IGB specific header data - * @return object - */ - static function getIGBHeaderData(){ - $data = (object) []; - $data->trusted = false; - $data->values = []; - $headerData = apache_request_headers(); - - foreach($headerData as $key => $value){ - if (strpos($key, 'EVE_') === 0) { - $key = str_replace('EVE_', '', $key); - $key = strtolower($key); - - if ( - $key === 'trusted' && - $value === 'Yes' - ) { - $data->trusted = true; - } - - $data->values[$key] = $value; - } - } - - return $data; - } - - /** - * check if the current request was send from inGame - * @return bool - */ - static function isIGB(){ - $isIGB = false; - - $igbHeaderData = self::getIGBHeaderData(); - - if(count($igbHeaderData->values) > 0){ - $isIGB = true; - } - - return $isIGB; - } - - /** - * verifies weather a given username and password is valid - * @param $userName - * @param $password - * @return Model\UserModel|null - */ - protected function _verifyUser($userName, $password) { - - $validUser = null; - - $user = Model\BasicModel::getNew('UserModel', 0); - - $user->getByName($userName); - - // check userName is valid - if( !$user->dry() ){ - // check if password is valid - $isValid = $user->verify($password); - - if($isValid === true){ - $validUser = $user; - } - } - - return $validUser; - } - - /** - * log the current user out - * @param $f3 - */ - public function logOut($f3){ - - // destroy session - $f3->clear('SESSION'); - - if( !$f3->get('AJAX') ){ - // redirect to landing page - $f3->reroute('@landing'); - }else{ - $return = (object) []; - $return->reroute = self::getEnvironmentData('URL') . $f3->alias('landing'); - $return->error[] = $this->getUserLoggedOffError(); - - echo json_encode($return); - die(); - } - } - - /** - * get error object is a user is not found/logged of - * @return object - */ - protected function getUserLoggedOffError(){ - $userError = (object) []; - $userError->type = 'error'; - $userError->message = 'User not found'; - - return $userError; - } - - /** - * get the current registration status - * 0=registration stop |1=new registration allowed - * @return int - */ - static function getRegistrationStatus(){ - return (int)\Base::instance()->get('PATHFINDER.REGISTRATION.STATUS'); - } - - /** - * get a log controller e.g. "debug" - * @param $loggerType - * @return mixed - */ - static function getLogger($loggerType){ - return LogController::getLogger($loggerType); - } - - /** - * removes illegal characters from a Hive-key that are not allowed - * @param $key - * @return mixed - */ - static function formatHiveKey($key){ - $illegalCharacters = ['-']; - return str_replace($illegalCharacters, '', $key); - } - - /** - * get environment specific configuration data - * @param $key - * @return mixed|null - */ - static function getEnvironmentData($key){ - $f3 = \Base::instance(); - $environment = $f3->get('PATHFINDER.ENVIRONMENT.SERVER'); - $environmentKey = 'PATHFINDER.ENVIRONMENT[' . $environment . '][' . $key . ']'; - $data = null; - - if( $f3->exists($environmentKey) ){ - $data = $f3->get($environmentKey); - } - - return $data; - } - -} \ No newline at end of file diff --git a/app/main/controller/logcontroller.php b/app/main/controller/logcontroller.php deleted file mode 100644 index 595890332..000000000 --- a/app/main/controller/logcontroller.php +++ /dev/null @@ -1,39 +0,0 @@ -exists($hiveKey) ){ - // create new logger instance - - $logFile = $logFileName . '.log'; - - $f3->set($hiveKey, new \Log($logFile)); - } - - - return $f3->get($hiveKey); - } - - -} \ No newline at end of file diff --git a/app/main/controller/mapcontroller.php b/app/main/controller/mapcontroller.php deleted file mode 100644 index cc69a4c56..000000000 --- a/app/main/controller/mapcontroller.php +++ /dev/null @@ -1,68 +0,0 @@ -set('pageContent', false); - - // body element class - $this->f3->set('bodyClass', 'pf-body'); - - // set trust attribute to template - $this->f3->set('trusted', (int)self::isIGBTrusted()); - - // JS main file - $this->f3->set('jsView', 'mappage'); - - $this->setTemplate('templates/view/index.html'); - } - - /** - * function is called on each error - * @param $f3 - */ - public function showError($f3){ - - // set HTTP status - if(!empty($f3->get('ERROR.code'))){ - $f3->status($f3->get('ERROR.code')); - } - - if($f3->get('AJAX')){ - header('Content-type: application/json'); - - // error on ajax call - $errorData = [ - 'status' => $f3->get('ERROR.status'), - 'code' => $f3->get('ERROR.code'), - 'text' => $f3->get('ERROR.text') - ]; - - // append stack trace for greater debug level - if( $f3->get('DEBUG') === 3){ - $errorData['trace'] = $f3->get('ERROR.trace'); - } - - echo json_encode($errorData); - }else{ - echo $f3->get('ERROR.text'); - } - - die(); - } - -} \ No newline at end of file diff --git a/app/main/cron/ccpsystemsupdate.php b/app/main/cron/ccpsystemsupdate.php deleted file mode 100644 index 4dea721b0..000000000 --- a/app/main/cron/ccpsystemsupdate.php +++ /dev/null @@ -1,220 +0,0 @@ - 5, - 'follow_location' => false // otherwise CURLOPT_FOLLOWLOCATION will fail - ]; - - /** - * table names for all system log tables - * @var array - */ - protected $logTables = [ - 'jumps' => 'system_jumps', - 'shipKills' => 'system_kills_ships', - 'podKills' => 'system_kills_pods', - 'factionKills' => 'system_kills_factions' - ]; - - /** - * check all system log tables for the correct number of system entries that will be locked - * @return array - */ - private function prepareSystemLogTables(){ - - $f3 = \Base::instance(); - - // get information for all systems from CCP DB - $systemController = new Controller\Api\System(); - $systemsData = $systemController->getSystems(); - - // switch DB back to pathfinder - DB\Database::instance()->setDB('PF'); - - // insert systems into each log table if not exist - $f3->get('DB')->begin(); - foreach($this->logTables as $tableName){ - - // insert systems into jump log table - $sqlInsertSystem = "INSERT IGNORE INTO " . $tableName . " (systemId) - VALUES(:systemId)"; - - foreach($systemsData as $systemData){ - // skip WH systems -> no jump data available - if($systemData['type']['name'] == 'k-space'){ - $f3->get('DB')->exec($sqlInsertSystem, array( - ':systemId' => $systemData['systemId'] - ), 0, false); - } - } - - } - $f3->get('DB')->commit(); - - return $systemsData; - } - - - /** - * imports all relevant map stats from CCPs API - * >> php index.php "/cron/importSystemData" - * @param $f3 - */ - function importSystemData($f3){ - - $time_start = microtime(true); - // prepare system jump log table - $systemsData = $this->prepareSystemLogTables(); - $time_end = microtime(true); - $execTimePrepareSystemLogTables = $time_end - $time_start; - - - // get current jump Data ------------------------------------------------------- - $time_start = microtime(true); - $apiPath = $f3->get('PATHFINDER.API.CCP_XML') . '/map/Jumps.xml.aspx'; - - $apiResponse = \Web::instance()->request($apiPath, $this->apiRequestOptions ); - - $jumpData = []; - $updateJumps = false; - if($apiResponse['body']){ - $xml = simplexml_load_string($apiResponse['body']); - $rowApiData = $xml->result->rowset; - - foreach($rowApiData->children() as $systemApiData){ - $attributeApiData = $systemApiData->attributes(); - $systemId = $attributeApiData->solarSystemID->__toString(); - $shipJumps =$attributeApiData->shipJumps->__toString(); - - $jumpData[$systemId] = $shipJumps; - } - - $updateJumps = true; - } - $time_end = microtime(true); - $execTimeGetJumpData = $time_end - $time_start; - - // get current kill Data ------------------------------------------------------- - $time_start = microtime(true); - $apiPath = $f3->get('PATHFINDER.API.CCP_XML') . '/map/Kills.xml.aspx'; - - $apiResponse = \Web::instance()->request($apiPath, $this->apiRequestOptions ); - $killData = []; - $updateKills = false; - if($apiResponse['body']){ - $xml = simplexml_load_string($apiResponse['body']); - $rowApiData = $xml->result->rowset; - foreach($rowApiData->children() as $systemApiData){ - $attributeApiData = $systemApiData->attributes(); - $systemId = $attributeApiData->solarSystemID->__toString(); - $shipKills =$attributeApiData->shipKills->__toString(); - $podKills =$attributeApiData->podKills->__toString(); - $factionKills =$attributeApiData->factionKills->__toString(); - - $killData[$systemId] = [ - 'shipKills' => $shipKills, - 'podKills' => $podKills, - 'factionKills' => $factionKills, - ]; - } - - $updateKills = true; - - } - $time_end = microtime(true); - $execTimeGetKillData = $time_end - $time_start; - - // update system log tables ----------------------------------------------------- - $time_start = microtime(true); - // make sure last update is (at least) 1h ago - $f3->get('DB')->begin(); - - foreach($this->logTables as $key => $tableName){ - $sql = "UPDATE - " . $tableName . " - SET - value24 = value23, - value23 = value22, - value22 = value21, - value21 = value20, - value20 = value19, - value19 = value18, - value18 = value17, - value17 = value16, - value16 = value15, - value15 = value14, - value14 = value13, - value13 = value12, - value12 = value11, - value11 = value10, - value10 = value9, - value9 = value8, - value8 = value7, - value7 = value6, - value6 = value5, - value5 = value4, - value4 = value3, - value3 = value2, - value2 = value1, - value1 = :value - WHERE - systemId = :systemId - "; - - foreach($systemsData as $systemData){ - - if( - $key == 'jumps' && - $updateJumps - ){ - // update jump data (if available) - $currentJumps = 0; - if(array_key_exists($systemData['systemId'], $jumpData)){ - $currentJumps = $jumpData[$systemData['systemId']]; - } - - $f3->get('DB')->exec($sql, array( - ':systemId' => $systemData['systemId'], - ':value' => $currentJumps - ), 0, false); - }else if($updateKills){ - - // update kill data (if available) - $currentKills = 0; - if(array_key_exists($systemData['systemId'], $killData)){ - $currentKillData = $killData[$systemData['systemId']]; - - $currentKills = $currentKillData[$key]; - } - - $f3->get('DB')->exec($sql, array( - ':systemId' => $systemData['systemId'], - ':value' => $currentKills - ), 0, false); - } - } - } - $f3->get('DB')->commit(); - - $time_end = microtime(true); - $execTimeUpdateTables = $time_end - $time_start; - - // Log ------------------------ - $log = Controller\LogController::getLogger('cron_' . __FUNCTION__); - $log->write( sprintf(self::LOG_TEXT, __FUNCTION__, $execTimePrepareSystemLogTables, $execTimeGetJumpData, $execTimeGetKillData, $execTimeUpdateTables) ); - } -} \ No newline at end of file diff --git a/app/main/cron/characterupdate.php b/app/main/cron/characterupdate.php deleted file mode 100644 index 243ac1ef6..000000000 --- a/app/main/cron/characterupdate.php +++ /dev/null @@ -1,28 +0,0 @@ -> php index.php "/cron/deleteLogData" - * @param $f3 - */ - function deleteLogData($f3){ - - DB\Database::instance()->setDB('PF'); - - $sqlDeleteCharacterLogs = "TRUNCATE TABLE character_log"; - $f3->get('DB')->exec($sqlDeleteCharacterLogs); - } - -} \ No newline at end of file diff --git a/app/main/cron/mapupdate.php b/app/main/cron/mapupdate.php deleted file mode 100644 index 4e0f27c90..000000000 --- a/app/main/cron/mapupdate.php +++ /dev/null @@ -1,70 +0,0 @@ -> php index.php "/cron/deactivateMapData" - * @param $f3 - */ - function deactivateMapData($f3){ - - DB\Database::instance()->setDB('PF'); - - $sqlDeactivateExpiredMaps = "UPDATE map SET - active = 0 - WHERE - map.active = 1 AND - map.typeId = 2 AND - TIMESTAMPDIFF(DAY, map.created, NOW() ) > :lifetime"; - - $privateMapLifetime = (int)$f3->get('PATHFINDER.MAP.PRIVATE.LIFETIME'); - - $f3->get('DB')->exec($sqlDeactivateExpiredMaps, ['lifetime' => $privateMapLifetime]); - $deactivatedMapsCount = $f3->get('DB')->count(); - - // Log ------------------------ - $log = Controller\LogController::getLogger('cron_' . __FUNCTION__); - $log->write( sprintf(self::LOG_TEXT_MAPS, __FUNCTION__, $deactivatedMapsCount) ); - } - - /** - * delete all deactivated maps - * >> php index.php "/cron/deleteMapData" - * @param $f3 - */ - function deleteMapData($f3){ - - DB\Database::instance()->setDB('PF'); - - $sqlDeleteDisabledMaps = "DELETE FROM - map - WHERE - map.active = 0 AND - TIMESTAMPDIFF(DAY, map.updated, NOW() ) > :deletion_time"; - - $f3->get('DB')->exec($sqlDeleteDisabledMaps, ['deletion_time' => self::DAYS_UNTIL_MAP_DELETION]); - - $deletedMapsCount = $f3->get('DB')->count(); - - // Log ------------------------ - $log = Controller\LogController::getLogger('cron_' . __FUNCTION__); - $log->write( sprintf(self::LOG_TEXT_MAPS, __FUNCTION__, $deletedMapsCount) ); - } - -} \ No newline at end of file diff --git a/app/main/data/mapper/ccpsystemsmapper.php b/app/main/data/mapper/ccpsystemsmapper.php deleted file mode 100644 index 898f7f72e..000000000 --- a/app/main/data/mapper/ccpsystemsmapper.php +++ /dev/null @@ -1,184 +0,0 @@ - 'systemId', - 'system_name' => 'name', - 'system_security' => 'trueSec', - 'connstallation_id' => array('constellation' => 'id'), - 'constallation_name' => array('constellation' => 'name'), - 'region_id' => array('region' => 'id'), - 'region_name' => array('region' => 'name') - ); - - function __construct($data){ - - parent::__construct($data, \RecursiveIteratorIterator::SELF_FIRST); - } - - /** - * get formatted data - * @return array - */ - public function getData(){ - - // format functions - self::$map['effect'] = function($iterator){ - - $effect = $iterator['effect']; - - switch($iterator['effect']){ - case 'magnetar': - $effect = 'magnetar'; - break; - case 'red giant': - $effect = 'redGiant'; - break; - case 'pulsar': - $effect = 'pulsar'; - break; - case 'wolf-rayet star': - $effect = 'wolfRayet'; - break; - case 'cataclysmic variable': - $effect = 'cataclysmic'; - break; - case 'black hole': - $effect = 'blackHole'; - break; - } - - return $effect; - }; - - // format functions - self::$map['security'] = function($iterator){ - - if( - $iterator['security'] == 7 || - $iterator['security'] == 8 || - $iterator['security'] == 9 - ){ - if($iterator['trueSec'] <= 0){ - $security = '0.0'; - }elseif($iterator['trueSec'] < 0.5){ - $security = 'L'; - }else{ - $security = 'H'; - } - }else{ - $security = 'C' . $iterator['security']; - } - - return $security; - }; - - // format functions - self::$map['type'] = function($iterator){ - - // TODO refactor - $type = 'w-space'; - $typeId = 1; - if( - $iterator['security'] == 7 || - $iterator['security'] == 8 || - $iterator['security'] == 9 - ){ - $type = 'k-space'; - $typeId = 2; - - } - - return [ - 'id' => $typeId, - 'name' => $type - ]; - }; - - iterator_apply($this, 'self::recursiveIterator', array($this)); - - - return iterator_to_array($this, false); - } - - /** - * recursive iterator function called on every node - * @param $iterator - * @return mixed - */ - static function recursiveIterator($iterator){ - - while ( $iterator -> valid() ) { - if ( $iterator->hasChildren() ) { - $iterator->offsetSet($iterator->key(), self::recursiveIterator( $iterator->getChildren() )->getArrayCopy() ); - }else { - - while( $iterator -> valid() ){ - - // check for mapping key - if(array_key_exists($iterator->key(), self::$map)){ - - if(is_array(self::$map[$iterator->key()])){ - // a -> array mapping - - $parentKey = array_keys( self::$map[$iterator->key()] )[0]; - $entryKey = array_values( self::$map[$iterator->key()] )[0]; - - // check if key already exists - if($iterator->offsetExists($parentKey)){ - $currentValue = $iterator->offsetGet($parentKey); - // add new array entry - $currentValue[$entryKey] = $iterator->current(); - - $iterator->offsetSet( $parentKey, $currentValue ); - }else{ - $iterator->offsetSet( $parentKey, array($entryKey => $iterator->current() ) ); - } - - $removeOldEntry = true; - }elseif(is_object(self::$map[$iterator->key()])){ - // a -> a (format by function) - - $formatFunction = self::$map[$iterator->key()]; - - $iterator->offsetSet( $iterator->key(), call_user_func($formatFunction, $iterator) ); - - // just value change no key change - $removeOldEntry = false; - $iterator -> next(); - }else{ - // a -> b mapping - $iterator->offsetSet( self::$map[$iterator->key()], $iterator->current() ); - - $removeOldEntry = true; - } - - // remove "old" entry - if($removeOldEntry){ - $iterator->offsetUnset($iterator->key()); - } - - }else{ - // continue with next entry - $iterator -> next(); - } - - } - } - - $iterator -> next(); - } - - return $iterator; - } - -} \ No newline at end of file diff --git a/app/main/db/database.php b/app/main/db/database.php deleted file mode 100644 index c8fb77563..000000000 --- a/app/main/db/database.php +++ /dev/null @@ -1,91 +0,0 @@ -setDB($database); - $this->setDB($database); - } - - /** - * set database - * @param string $database - */ - public function setDB($database = 'PF'){ - - $f3 = \Base::instance(); - - if($database === 'CCP'){ - // CCP DB - $dns = Controller\Controller::getEnvironmentData('DB_CCP_DNS'); - $name = Controller\Controller::getEnvironmentData('DB_CCP_NAME'); - $user = Controller\Controller::getEnvironmentData('DB_CCP_USER'); - $password = Controller\Controller::getEnvironmentData('DB_CCP_PASS'); - }else{ - // Pathfinder DB - $dns = Controller\Controller::getEnvironmentData('DB_DNS'); - $name = Controller\Controller::getEnvironmentData('DB_NAME'); - $user = Controller\Controller::getEnvironmentData('DB_USER'); - $password = Controller\Controller::getEnvironmentData('DB_PASS'); - } - - // check for DB switch. If current DB equal new DB -> no switch needed - if( - !$f3->exists('DB') || - $name !== $f3->get('DB')->name() - ){ - - $db = $this->connect($dns, $name, $user, $password); - - $f3->set('DB', $db); - - // set DB timezone to UTC +00:00 (eve server time) - $f3->get('DB')->exec('SET @@session.time_zone = "+00:00";'); - - // disable innoDB schema (relevant vor MySql 5.5) - // not necessary for MySql > v.5.6 - //$f3->get('DB')->exec('SET GLOBAL innodb_stats_on_metadata = OFF;'); - } - - } - - /** - * connect to a database - * @param $dns - * @param $name - * @param $user - * @param $password - * @return SQL - */ - protected function connect($dns, $name, $user, $password){ - - try { - $db = new SQL( - $dns . $name, - $user, - $password, - [ - \PDO::MYSQL_ATTR_COMPRESS => TRUE - ] - ); - }catch(\PDOException $e){ - // DB connection error - LogController::getLogger('error')->write($e->getMessage()); - } - - return $db; - } - -} \ No newline at end of file diff --git a/app/main/exception/baseexception.php b/app/main/exception/baseexception.php deleted file mode 100644 index 5d590dc6e..000000000 --- a/app/main/exception/baseexception.php +++ /dev/null @@ -1,21 +0,0 @@ -field; - } - - /** - * @param mixed $field - */ - public function setField($field){ - $this->field = $field; - } - - - public function __construct($message, $field = 0){ - - parent::__construct($message, self::VALIDATION_FAILED); - - $this->setField($field); - } -} \ No newline at end of file diff --git a/app/main/model/alliancemapmodel.php b/app/main/model/alliancemapmodel.php deleted file mode 100644 index 1eaaf49af..000000000 --- a/app/main/model/alliancemapmodel.php +++ /dev/null @@ -1,35 +0,0 @@ - [ - 'belongs-to-one' => 'Model\AllianceModel' - ], - 'mapId' => [ - 'belongs-to-one' => 'Model\MapModel' - ] - ]; - - /** - * see parent - */ - public function clearCacheData(){ - parent::clearCacheData(); - - // clear map cache as well - $this->mapId->clearCacheData(); - } -} \ No newline at end of file diff --git a/app/main/model/alliancemodel.php b/app/main/model/alliancemodel.php deleted file mode 100644 index 85c1e5951..000000000 --- a/app/main/model/alliancemodel.php +++ /dev/null @@ -1,83 +0,0 @@ - [ - 'has-many' => ['Model\CharacterModel', 'allianceId'] - ], - 'mapAlliances' => [ - 'has-many' => ['Model\AllianceMapModel', 'allianceId'] - ] - ]; - - /** - * get all alliance data - * @return array - */ - public function getData(){ - $allianceData = (object) []; - - $allianceData->id = $this->id; - $allianceData->name = $this->name; - $allianceData->sharing = $this->sharing; - - return $allianceData; - } - - /** - * get all maps for this alliance - * @return array|mixed - */ - public function getMaps(){ - $maps = []; - - $f3 = self::getF3(); - - $this->filter('mapAlliances', - ['active = ?', 1], - [ - 'limit' => $f3->get('PATHFINDER.MAX_MAPS_ALLIANCE'), - 'order' => 'created' - ] - ); - - if($this->mapAlliances){ - foreach($this->mapAlliances as $mapAlliance){ - if($mapAlliance->mapId->isActive()){ - $maps[] = $mapAlliance->mapId; - } - } - } - - return $maps; - } - - /** - * get all characters in this alliance - * @return array - */ - public function getCharacters(){ - $characters = []; - - $this->filter('allianceCharacters', ['active = ?', 1]); - - if($this->allianceCharacters){ - foreach($this->allianceCharacters as $character){ - $characters[] = $character; - } - } - - return $characters; - } -} \ No newline at end of file diff --git a/app/main/model/basicmodel.php b/app/main/model/basicmodel.php deleted file mode 100644 index de498e1e7..000000000 --- a/app/main/model/basicmodel.php +++ /dev/null @@ -1,429 +0,0 @@ - leave this at a higher value - * @var int - */ - //protected $ttl = 86400; - - /** - * caching for relational data - * @var int - */ - protected $rel_ttl = 0; - - /** - * field validation array - * @var array - */ - protected $validate = []; - - /** - * getData() cache key prefix - * -> do not change, otherwise cached data is lost - * @var string - */ - private $dataCacheKeyPrefix = 'DATACACHE'; - - - public function __construct($db = NULL, $table = NULL, $fluid = NULL, $ttl = 0){ - - // add static fields to this mapper - $this->addStaticFieldConfig(); - - parent::__construct($db, $table, $fluid, $ttl); - - // events ----------------------------------------- - $this->afterinsert(function($self){ - $self->clearCacheData(); - }); - - // model updated - $this->afterupdate( function($self){ - $self->clearCacheData(); - }); - - // model updated - $this->beforeinsert( function($self){ - $self->beforeInsertEvent($self); - }); - } - - - /** - * @param string $key - * @param mixed $val - * @return mixed|void - * @throws Exception\ValidationException - */ - public function set($key, $val){ - - if($key == 'active'){ - // prevent abuse - return; - } - - if($key != 'updated'){ - if( $this->exists($key) ){ - $currentVal = $this->get($key); - - // if current value is not a relational object - // and value has changed -> update table col - if( - !is_object($currentVal) && - $currentVal != $val - ){ - $this->touch('updated'); - } - } - } - - // trim all values - if(is_string($val)){ - $val = trim($val); - } - - $valid = $this->validateField($key, $val); - - if(!$valid){ - $this->throwValidationError($key); - }else{ - return parent::set($key, $val); - } - } - - - /** - * extent the fieldConf Array with static fields for each table - */ - private function addStaticFieldConfig(){ - - if(is_array($this->fieldConf)){ - - $staticFieldConfig = [ - 'created' => [ - 'type' => Schema::DT_TIMESTAMP - ], - 'updated' => [ - 'type' => Schema::DT_TIMESTAMP - ] - ]; - - $this->fieldConf = array_merge($this->fieldConf, $staticFieldConfig); - } - } - - /** - * validates a table column based on validation settings - * @param $col - * @param $val - * @return bool - */ - private function validateField($col, $val){ - $valid = true; - - if(array_key_exists($col, $this->validate)){ - - $fieldValidationOptions = $this->validate[$col]; - - foreach($fieldValidationOptions as $validateKey => $validateOption ){ - if(is_array($fieldValidationOptions[$validateKey])){ - $fieldSubValidationOptions = $fieldValidationOptions[$validateKey]; - - foreach($fieldSubValidationOptions as $validateSubKey => $validateSubOption ){ - switch($validateKey){ - case 'length': - switch($validateSubKey){ - case 'min'; - if(strlen($val) < $validateSubOption){ - $valid = false; - } - break; - case 'max'; - - if(strlen($val) > $validateSubOption){ - $valid = false; - } - break; - } - break; - } - } - - }else{ - switch($validateKey){ - case 'regex': - $valid = (bool)preg_match($fieldValidationOptions[$validateKey], $val); - break; - } - } - - // a validation rule failed - if(!$valid){ - break; - } - } - } - - return $valid; - } - - /** - * get the cache key for this model - * ->do not set a key if the model is not saved! - * @param string $dataCacheTableKeyPrefix - * @return null|string - */ - protected function getCacheKey($dataCacheTableKeyPrefix = ''){ - $cacheKey = null; - - // set a model unique cache key if the model is saved - if( $this->_id > 0){ - // check if there is a given key prefix - // -> if not, use the standard key. - // this is useful for caching multiple data sets according to one row entry - - $cacheKey = $this->dataCacheKeyPrefix; - $cacheKey .= '.' . strtoupper($this->table); - - if($dataCacheTableKeyPrefix){ - $cacheKey .= '.' . $dataCacheTableKeyPrefix . '_'; - }else{ - $cacheKey .= '.ID_'; - } - $cacheKey .= (string) $this->_id; - } - - return $cacheKey; - } - - /** - * Throws a validation error for a giben column - * @param $col - * @throws \Exception\ValidationException - */ - protected function throwValidationError($col){ - throw new Exception\ValidationException('Validation failed: "' . $col . '".', $col); - - } - - /** - * set "updated" field to current timestamp - * this is useful to mark a row as "changed" - */ - protected function setUpdated(){ - if($this->_id > 0){ - $f3 = self::getF3(); - $f3->get('DB')->exec( - ["UPDATE " . $this->table . " SET updated=NOW() WHERE id=:id"], - [ - [':id' => $this->_id] - ] - ); - } - } - - /** - * get single dataSet by id - * @param $id - * @param int $ttl - * @return \DB\Cortex - */ - public function getById($id, $ttl = 3) { - - return $this->getByForeignKey('id', (int)$id, ['limit' => 1], $ttl); - } - - /** - * checks weather this model is active or not - * each model should have an "active" column - * @return bool - */ - public function isActive(){ - $isActive = false; - - if($this->active === 1){ - $isActive = true; - } - - return $isActive; - } - - /** - * set active state for a model - * @param $value - */ - public function setActive($value){ - $this->set('active', (int)$value); - } - - /** - * get dataSet by foreign column (single result) - * @param $key - * @param $id - * @param array $options - * @param int $ttl - * @return \DB\Cortex - */ - public function getByForeignKey($key, $id, $options = [], $ttl = 60){ - - $querySet = []; - $query = []; - if($this->exists($key)){ - $query[] = $key . " = :" . $key; - $querySet[':' . $key] = $id; - } - - // check active column - if($this->exists('active')){ - $query[] = "active = :active"; - $querySet[':active'] = 1; - } - - array_unshift($querySet, implode(' AND ', $query)); - - return $this->load( $querySet, $options, $ttl ); - } - - /** - * Event "Hook" function - * can be overwritten - * return false will stop any further action - */ - public function beforeInsertEvent(){ - return true; - } - - /** - * function should be overwritten in child classes with access restriction - * @param $accessObject - * @return bool - */ - public function hasAccess($accessObject){ - return true; - } - - /** - * function should be overwritten in parent classes - * @return bool - */ - public function isValid(){ - return true; - } - - /** - * get cached data from this model - * @param string $dataCacheKeyPrefix - optional key prefix - * @return mixed|null - */ - protected function getCacheData($dataCacheKeyPrefix = ''){ - - $cacheKey = $this->getCacheKey($dataCacheKeyPrefix); - $cacheData = null; - - if( !is_null($cacheKey) ){ - $f3 = self::getF3(); - - if( $f3->exists($cacheKey) ){ - $cacheData = $f3->get( $cacheKey ); - } - } - - return $cacheData; - } - - /** - * update/set the getData() cache for this object - * @param $cacheData - * @param string $dataCacheKeyPrefix - * @param int $data_ttl - */ - public function updateCacheData($cacheData, $dataCacheKeyPrefix = '', $data_ttl = 300){ - - // check if data should be cached - // and cacheData is not empty - if( - $data_ttl > 0 && - !empty( (array)$cacheData ) - ){ - $cacheKey = $this->getCacheKey($dataCacheKeyPrefix); - - if( !is_null($cacheKey) ){ - self::getF3()->set($cacheKey, $cacheData, $data_ttl); - } - } - } - - /** - * unset the getData() cache for this object - */ - public function clearCacheData(){ - $cacheKey = $this->getCacheKey(); - - if( !is_null($cacheKey) ){ - $f3 = self::getF3(); - - if( $f3->exists($cacheKey) ){ - $f3->clear($cacheKey); - } - - } - - } - - /** - * factory for all Models - * @param $model - * @param int $ttl - * @return null - * @throws \Exception - */ - public static function getNew($model, $ttl = 86400){ - $class = null; - - $model = '\\' . __NAMESPACE__ . '\\' . $model; - if(class_exists($model)){ - $class = new $model( self::getF3()->get('DB'), null, null, $ttl ); - }else{ - throw new \Exception('No model class found'); - } - - return $class; - } - - /** - * get the framework instance (singleton) - * @return static - */ - public static function getF3(){ - return \Base::instance(); - } - - /** - * debug log function - * @param $text - */ - public static function log($text){ - Controller\LogController::getLogger('debug')->write($text); - - } - -} \ No newline at end of file diff --git a/app/main/model/characterlogmodel.php b/app/main/model/characterlogmodel.php deleted file mode 100644 index 8424f6767..000000000 --- a/app/main/model/characterlogmodel.php +++ /dev/null @@ -1,42 +0,0 @@ - [ - 'belongs-to-one' => 'Model\CharacterModel' - ] - ]; - - /** - * get all character log data - * @return object - */ - public function getData(){ - - $logData = (object) []; - $logData->system = (object) []; - $logData->system->id = $this->systemId; - $logData->system->name = $this->systemName; - - $logData->ship = (object) []; - $logData->ship->id = $this->shipId; - $logData->ship->name = $this->shipName; - $logData->ship->typeName = $this->shipTypeName; - - return $logData; - } - - -} \ No newline at end of file diff --git a/app/main/model/charactermodel.php b/app/main/model/charactermodel.php deleted file mode 100644 index 016b1214c..000000000 --- a/app/main/model/charactermodel.php +++ /dev/null @@ -1,145 +0,0 @@ - [ - 'belongs-to-one' => 'Model\CorporationModel' - ], - 'allianceId' => [ - 'belongs-to-one' => 'Model\AllianceModel' - ], - 'characterLog' => [ - 'has-one' => ['Model\CharacterLogModel', 'characterId'] - ] - ]; - - /** - * get character data - * @param bool|false $addCharacterLogData - * @return object - */ - public function getData($addCharacterLogData = false){ - - // check if there is cached data - // temporary disabled (performance test) - $characterData = null; //$this->getCacheData(); - - if(is_null($characterData)){ - // no cached character data found - - $characterData = (object) []; - - $characterData->id = $this->id; - $characterData->name = $this->name; - - if($addCharacterLogData){ - if($logModel = $this->getLog()){ - $characterData->log = $logModel->getData(); - } - } - - // check for corporation - if($corporation = $this->getCorporation()){ - $characterData->corporation = $corporation->getData(); - } - - // check for alliance - if($alliance = $this->getAlliance()){ - $characterData->alliance = $alliance->getData(); - } - - // max caching time for a system - // the cached date has to be cleared manually on any change - // this includes system, connection,... changes (all dependencies) - $this->updateCacheData($characterData, '', 300); - } - - return $characterData; - } - - /** - * check whether this character has a corporation - * @return bool - */ - public function hasCorporation(){ - $hasCorporation = false; - - if($this->corporationId){ - $hasCorporation = true; - } - - return $hasCorporation; - } - - /** - * check whether this character has an alliance - * @return bool - */ - public function hasAlliance(){ - $hasAlliance = false; - - if($this->allianceId){ - $hasAlliance = true; - } - - return $hasAlliance; - } - - /** - * get the corporation for this user - * @return mixed|null - */ - public function getCorporation(){ - $corporation = null; - - if($this->hasCorporation()){ - $corporation = $this->corporationId; - } - - return $corporation; - } - - /** - * get the alliance of this user - * @return mixed|null - */ - public function getAlliance(){ - $alliance = null; - - if($this->hasAlliance()){ - $alliance = $this->allianceId; - } - - return $alliance; - } - - /** - * get the character log entry for this character - * @return bool|null - */ - public function getLog(){ - - $characterLog = false; - if( - is_object($this->characterLog) && - !$this->characterLog->dry() - ){ - $characterLog = $this->characterLog; - } - - return $characterLog; - } - -} \ No newline at end of file diff --git a/app/main/model/characterstatusmodel.php b/app/main/model/characterstatusmodel.php deleted file mode 100644 index 16baf9767..000000000 --- a/app/main/model/characterstatusmodel.php +++ /dev/null @@ -1,15 +0,0 @@ - [ - 'belongs-to-one' => 'Model\MapModel' - ], - 'source' => [ - 'belongs-to-one' => 'Model\SystemModel' - ], - 'target' => [ - 'belongs-to-one' => 'Model\SystemModel' - ], - 'type' => [ - 'type' => self::DT_JSON - ] - ]; - - /** - * set an array with all data for a system - * @param $systemData - */ - public function setData($systemData){ - - foreach((array)$systemData as $key => $value){ - - if( !is_array($value) ){ - if( $this->exists($key) ){ - $this->$key = $value; - } - }elseif($key == 'type'){ - // json field - $this->$key = $value; - } - } - } - - /** - * get connection data as array - * @return array - */ - public function getData(){ - - $connectionData = [ - 'id' => $this->id, - 'source' => $this->source->id, - 'target' => $this->target->id, - 'scope' => $this->scope, - 'type' => $this->type, - 'updated' => strtotime($this->updated) - ]; - - return $connectionData; - } - - /** - * check object for model access - * @param $accessObject - * @return bool - */ - public function hasAccess($accessObject){ - return $this->mapId->hasAccess($accessObject); - } - - /** - * check weather this model is valid or not - * @return bool - */ - public function isValid(){ - $isValid = true; - - // check if source/target belong to same map - if( $this->source->mapId->id !== $this->target->mapId->id ){ - $isValid = false; - } - - return $isValid; - } - - /** - * delete a connection - * @param $accessObject - */ - public function delete($accessObject){ - - if(!$this->dry()){ - // check if editor has access - if($this->hasAccess($accessObject)){ - $this->erase(); - } - } - } - - /** - * see parent - */ - public function clearCacheData(){ - parent::clearCacheData(); - - // clear map cache as well - $this->mapId->clearCacheData(); - } - -} \ No newline at end of file diff --git a/app/main/model/connectionscopemodel.php b/app/main/model/connectionscopemodel.php deleted file mode 100644 index 303ee1035..000000000 --- a/app/main/model/connectionscopemodel.php +++ /dev/null @@ -1,16 +0,0 @@ - [ - 'belongs-to-one' => 'Model\CorporationModel' - ], - 'mapId' => [ - 'belongs-to-one' => 'Model\MapModel' - ] - ]; - - /** - * see parent - */ - public function clearCacheData(){ - parent::clearCacheData(); - - // clear map cache as well - $this->mapId->clearCacheData(); - } - -} \ No newline at end of file diff --git a/app/main/model/corporationmodel.php b/app/main/model/corporationmodel.php deleted file mode 100644 index b2926c5e4..000000000 --- a/app/main/model/corporationmodel.php +++ /dev/null @@ -1,84 +0,0 @@ - [ - 'has-many' => ['Model\CharacterModel', 'allianceId'] - ], - 'mapCorporations' => [ - 'has-many' => ['Model\CorporationMapModel', 'corporationId'] - ] - ]; - - /** - * get all cooperation data - * @return array - */ - public function getData(){ - $cooperationData = (object) []; - - $cooperationData->id = $this->id; - $cooperationData->name = $this->name; - $cooperationData->sharing = $this->sharing; - - - return $cooperationData; - } - - /** - * get all maps for this corporation - * @return array|mixed - */ - public function getMaps(){ - $maps = []; - - $f3 = self::getF3(); - - $this->filter('mapCorporations', - ['active = ?', 1], - [ - 'limit' => $f3->get('PATHFINDER.MAX_MAPS_CORPORATION'), - 'order' => 'created' - ] - ); - - if($this->mapCorporations){ - foreach($this->mapCorporations as $mapCorporation){ - if($mapCorporation->mapId->isActive()){ - $maps[] = $mapCorporation->mapId; - } - } - } - - return $maps; - } - - /** - * get all characters in this corporation - * @return array - */ - public function getCharacters(){ - $characters = []; - - $this->filter('corporationCharacters', ['active = ?', 1]); - - if($this->corporationCharacters){ - foreach($this->corporationCharacters as $character){ - $characters[] = $character; - } - } - - return $characters; - } -} \ No newline at end of file diff --git a/app/main/model/mapmodel.php b/app/main/model/mapmodel.php deleted file mode 100644 index d8c55078a..000000000 --- a/app/main/model/mapmodel.php +++ /dev/null @@ -1,610 +0,0 @@ - [ - 'belongs-to-one' => 'Model\MapScopeModel' - ], - 'typeId' => [ - 'belongs-to-one' => 'Model\MapTypeModel' - ], - 'systems' => [ - 'has-many' => ['Model\SystemModel', 'mapId'] - ], - 'connections' => [ - 'has-many' => ['Model\ConnectionModel', 'mapId'] - ], - 'mapUsers' => [ - 'has-many' => ['Model\UserMapModel', 'mapId'] - ], - 'mapCorporations' => [ - 'has-many' => ['Model\CorporationMapModel', 'mapId'] - ], - 'mapAlliances' => ['has-many' => [ - 'Model\AllianceMapModel', 'mapId'] - ] - ]; - - protected $validate = [ - 'name' => [ - 'length' => [ - 'min' => 3 - ] - ], - 'icon' => [ - 'length' => [ - 'min' => 3 - ] - ], - 'scopeId' => [ - 'regex' => '/^[1-9]+$/' - ], - 'typeId' => [ - 'regex' => '/^[1-9]+$/' - ] - ]; - - /** - * set map data by an associative array - * @param $data - */ - public function setData($data){ - - foreach((array)$data as $key => $value){ - - if(!is_array($value)){ - if($this->exists($key)){ - $this->$key = $value; - } - } - } - } - - - /** - * get map data - * -> this includes system and connection data as well! - * @return array - */ - public function getData(){ - - // check if there is cached data - $mapDataAll = $this->getCacheData(); - - if(is_null($mapDataAll)){ - // no cached map data found - - $mapData = (object) []; - $mapData->id = $this->id; - $mapData->name = $this->name; - $mapData->icon = $this->icon; - $mapData->created = strtotime($this->created); - $mapData->updated = strtotime($this->updated); - - // map scope - $mapData->scope = (object) []; - $mapData->scope->id = $this->scopeId->id; - $mapData->scope->name = $this->scopeId->name; - $mapData->scope->label = $this->scopeId->label; - - // map type - $mapData->type = (object) []; - $mapData->type->id = $this->typeId->id; - $mapData->type->name = $this->typeId->name; - $mapData->type->classTab = $this->typeId->classTab; - - // map access - $mapData->access = (object) []; - $mapData->access->user = []; - $mapData->access->corporation = []; - $mapData->access->alliance = []; - - // get access object data ------------------------------------- - if($this->isPrivate()){ - $users = $this->getUsers(); - $userData = []; - foreach($users as $user){ - $userData[] = $user->getSimpleData(); - } - $mapData->access->user = $userData; - } elseif($this->isCorporation()){ - $corporations = $this->getCorporations(); - $corporationData = []; - - foreach($corporations as $corporation){ - $corporationData[] = $corporation->getData(); - } - $mapData->access->corporation = $corporationData; - } elseif($this->isAlliance()){ - $alliances = $this->getAlliances(); - $allianceData = []; - - foreach($alliances as $alliance){ - $allianceData[] = $alliance->getData(); - } - $mapData->access->alliance = $allianceData; - } - - // merge all data --------------------------------------------- - $mapDataAll = (object) []; - $mapDataAll->mapData = $mapData; - - // map system data -------------------------------------------- - $mapDataAll->systems = $this->getSystemData(); - - // map connection data ---------------------------------------- - $mapDataAll->connections = $this->getConnectionData(); - - // max caching time for a map - // the cached date has to be cleared manually on any change - // this includes system, connection,... changes (all dependencies) - $this->updateCacheData($mapDataAll, '', 300); - } - - return $mapDataAll; - } - - /** - * search for a system by id - * @param $systemId - * @return null - */ - public function getSystem($systemId){ - $systems = $this->getSystems(); - $searchSystem = null; - foreach($systems as $system){ - if($system->id == $systemId){ - $searchSystem = $system; - break; - } - } - - return $searchSystem; - } - - /** - * get all system models in this map - * @return array|mixed - */ - public function getSystems(){ - // orderBy x-Coordinate for cleaner frontend animation (left to right) - $this->filter('systems', ['active = ?', 1], ['order' => 'posX']); - - $systems = []; - if($this->systems){ - $systems = $this->systems; - } - - return $systems; - } - - /** - * get all system data for all systems in this map - * @return array - */ - public function getSystemData(){ - - $systems = $this->getSystems(); - - $systemData = []; - foreach($systems as $system){ - $systemData[] = $system->getData(); - } - - return $systemData; - } - - /** - * get all connections in this map - * @return array|mixed - */ - public function getConnections(){ - $this->filter('connections', ['active = ?', 1]); - - $connections = []; - if($this->connections){ - $connections = $this->connections; - } - - return $connections; - } - - /** - * get all connection data in this map - * @return array - */ - public function getConnectionData(){ - $connections = $this->getConnections(); - - $connectionData = []; - foreach($connections as $connection){ - $connectionData[] = $connection->getData(); - } - - return $connectionData; - } - - /** - * set map access for an object (user, corporation or alliance) - * @param $obj - */ - public function setAccess($obj){ - - $newAccessGranted = false; - - if($obj instanceof UserModel){ - // private map - - // check whether the user has already map access - $this->has('mapUsers', ['active = 1 AND userId = :userId', ':userId' => $obj->id]); - $result = $this->findone(['id = :id', ':id' => $this->id]); - - if($result === false){ - // grant access for the user - $userMap = self::getNew('UserMapModel'); - $userMap->userId = $obj; - $userMap->mapId = $this; - $userMap->save(); - - $newAccessGranted = true; - } - } elseif($obj instanceof CorporationModel){ - - // check whether the corporation already has map access - $this->has('mapCorporations', ['active = 1 AND corporationId = :corporationId', ':corporationId' => $obj->id]); - $result = $this->findone(['id = :id', ':id' => $this->id]); - - if($result === false){ - // grant access for this corporation - $corporationMap = self::getNew('CorporationMapModel'); - $corporationMap->corporationId = $obj; - $corporationMap->mapId = $this; - $corporationMap->save(); - - $newAccessGranted = true; - } - } elseif($obj instanceof AllianceModel){ - - // check whether the corporation already has map access - $this->has('mapAlliances', ['active = 1 AND allianceId = :allianceId', ':allianceId' => $obj->id]); - $result = $this->findone(['id = :id', ':id' => $this->id]); - - if($result === false){ - $allianceMap = self::getNew('AllianceMapModel'); - $allianceMap->allianceId = $obj; - $allianceMap->mapId = $this; - $allianceMap->save(); - - $newAccessGranted = true; - } - } - - if($newAccessGranted){ - // mark this map as updated - $this->setUpdated(); - } - - } - - /** - * clear access for a given type of objects - * @param $clearKeys - */ - public function clearAccess($clearKeys = ['user', 'corporation', 'alliance']){ - - foreach($clearKeys as $key){ - switch($key){ - case 'user': - foreach((array)$this->mapUsers as $obj){ - $obj->erase(); - }; - break; - case 'corporation': - foreach((array)$this->mapCorporations as $obj){ - $obj->erase(); - }; - break; - case 'alliance': - foreach((array)$this->mapAlliances as $obj){ - $obj->erase(); - }; - break; - } - } - } - - /** - * checks weather a user has access to this map or not - * @param $user - * @return bool - */ - public function hasAccess($user){ - $hasAccess = false; - - if( - !$this->dry() && - $user instanceof UserModel - ){ - - // get all maps the user has access to - // this includes corporation and alliance maps - $maps = $user->getMaps(); - - foreach($maps as $map){ - if($map->id === $this->id){ - $hasAccess = true; - break; - } - } - } - - return $hasAccess; - } - - /** - * get all user models that have access to this map - * note: This function is just for "private" maps - * @return array - */ - public function getUsers(){ - $users = []; - - if($this->isPrivate()){ - $this->filter('mapUsers', ['active = ?', 1]); - - if($this->mapUsers){ - foreach($this->mapUsers as $mapUser){ - $users[] = $mapUser->userId; - } - } - } - - return $users; - } - - /** - * get all character models that are currently online "viewing" this map - * @return array - */ - private function getCharacters(){ - $characters = []; - - if($this->isPrivate()){ - $users = $this->getUsers(); - - foreach($users as $user){ - // get all active character logs for a user - $tempActiveUserCharacters = $user->getActiveUserCharacters(); - - foreach($tempActiveUserCharacters as $tempActiveUserCharacter){ - $characters[] = $tempActiveUserCharacter; - } - } - }elseif($this->isCorporation()){ - $corporations = $this->getCorporations(); - - foreach($corporations as $corporation){ - $characters = array_merge($characters, $corporation->getCharacters()); - } - }elseif($this->isAlliance()){ - $alliances = $this->getAlliances(); - - foreach($alliances as $alliance){ - $characters = array_merge($characters, $alliance->getCharacters()); - } - } - - return $characters; - } - - /** - * get data for all characters that are currently online "viewing" this map - * -> the result of this function is cached! - * @return array - */ - private function getCharactersData(){ - - // check if there is cached data - $charactersData = $this->getCacheData('CHARACTERS'); - - if(is_null($charactersData)){ - $charactersData = []; - - $characters = $this->getCharacters(); - - foreach($characters as $character){ - $charactersData[] = $character->getData(true); - } - - $this->updateCacheData($charactersData, 'CHARACTERS', 10); - } - - return $charactersData; - } - - /** - * get all corporations that have access to this map - * @return array - */ - public function getCorporations(){ - $corporations = []; - - if($this->isCorporation()){ - $this->filter('mapCorporations', ['active = ?', 1]); - - if($this->mapCorporations){ - foreach($this->mapCorporations as $mapCorporation){ - $corporations[] = $mapCorporation->corporationId; - } - } - } - - return $corporations; - } - - /** - * get all alliances that have access to this map - * @return array - */ - public function getAlliances(){ - $alliances = []; - - if($this->isAlliance()){ - $this->filter('mapAlliances', ['active = ?', 1]); - - if($this->mapAlliances){ - foreach($this->mapAlliances as $mapAlliance){ - $alliances[] = $mapAlliance->allianceId; - } - } - } - - return $alliances; - } - - - /** - * delete this map and all dependencies - * @param $accessObject - */ - public function delete($accessObject){ - - if(!$this->dry()){ - // check if editor has access - if($this->hasAccess($accessObject)){ - // all map related tables will be deleted on cascade - - // delete map - $this->erase(); - } - } - } - - /** - * checks weather this map is private map - * @return bool - */ - public function isPrivate(){ - $isPrivate = false; - - if($this->typeId->id == 2){ - $isPrivate = true; - } - - return $isPrivate; - } - - /** - * checks weather this map is corporation map - * @return bool - */ - public function isCorporation(){ - $isCorporation = false; - - if($this->typeId->id == 3){ - $isCorporation = true; - } - - return $isCorporation; - } - - /** - * checks weather this map is alliance map - * @return bool - */ - public function isAlliance(){ - $isAlliance = false; - - if($this->typeId->id == 4){ - $isAlliance = true; - } - - return $isAlliance; - } - - /** - * get all active characters (with active log) - * grouped by systems - * @return object - */ - public function getUserData(){ - - // get systems for this map - // the getData() function is cached. So this is more suitable than getSystems(); - $mapDataAll = $this->getData(); - - // get data of characters which have with map access - $activeUserCharactersData = $this->getCharactersData(); - - $mapUserData = (object)[]; - $mapUserData->config = (object)[]; - $mapUserData->config->id = $this->id; - $mapUserData->data = (object)[]; - $mapUserData->data->systems = []; - foreach($mapDataAll->systems as $systemData){ - $systemUserData = (object)[]; - $systemUserData->id = $systemData->systemId; - $systemUserData->user = []; - - // check if a system has active characters - foreach($activeUserCharactersData as $key => $activeUserCharacterData){ - - if(isset($activeUserCharacterData->log)){ - // user as log data - if($activeUserCharacterData->log->system->id == $systemData->systemId){ - $systemUserData->user[] = $activeUserCharacterData; - - // remove user from array -> speed up looping over characters. - // each userCharacter can only be active in a SINGLE system - unset($activeUserCharactersData[$key]); - } - }else{ - // user has NO log data. If its an corp/ally map not each member is active - // user is not relevant for this function! - unset($activeUserCharactersData[$key]); - } - } - - // add system if active users were found - if(count($systemUserData->user) > 0){ - $mapUserData->data->systems[] = $systemUserData; - } - } - - return $mapUserData; - } - - /** - * save a map - * @return mixed - */ - public function save(){ - - $mapModel = parent::save(); - - // check if map type has changed and clear access objects - if( !$mapModel->dry() ){ - if( $mapModel->isPrivate() ){ - $mapModel->clearAccess(['corporation', 'alliance']); - }elseif( $mapModel->isCorporation() ){ - $mapModel->clearAccess(['user', 'alliance']); - }elseif( $mapModel->isAlliance() ){ - $mapModel->clearAccess(['user', 'corporation']); - } - } - - return $mapModel; - } - -} diff --git a/app/main/model/mapscopemodel.php b/app/main/model/mapscopemodel.php deleted file mode 100644 index 8ff847231..000000000 --- a/app/main/model/mapscopemodel.php +++ /dev/null @@ -1,16 +0,0 @@ - [ - 'belongs-to-one' => 'Model\MapModel' - ], - 'typeId' => [ - 'belongs-to-one' => 'Model\SystemTypeModel' - ], - 'statusId' => [ - 'belongs-to-one' => 'Model\SystemStatusModel' - ], - 'createdCharacterId' => [ - 'belongs-to-one' => 'Model\CharacterModel' - ], - 'updatedCharacterId' => [ - 'belongs-to-one' => 'Model\CharacterModel' - ], - 'signatures' => [ - 'has-many' => ['Model\SystemSignatureModel', 'systemId'] - ], - ]; - - /** - * set an array with all data for a system - * @param $systemData - */ - public function setData($systemData){ - - foreach((array)$systemData as $key => $value){ - - if(!is_array($value)){ - if($this->exists($key)){ - $this->$key = $value; - } - }else{ - // special array data - if($key == 'constellation'){ - $this->constellationId = $value['id']; - $this->constellation = $value['name']; - }elseif($key == 'region'){ - $this->regionId = $value['id']; - $this->region = $value['name']; - }elseif($key == 'type'){ - $this->typeId = $value['id']; - }elseif($key == 'status'){ - $this->statusId = $value['id']; - }elseif($key == 'position'){ - $this->posX = $value['x']; - $this->posY = $value['y']; - } - } - } - } - - /** - * get map data as object - * @return object - */ - public function getData(){ - - // check if there is cached data - $systemData = $this->getCacheData(); - - if(is_null($systemData)){ - // no cached system data found - - $systemData = (object) []; - $systemData->id = $this->id; - $systemData->mapId = is_object($this->mapId) ? $this->mapId->id : 0; - $systemData->systemId = $this->systemId; - $systemData->name = $this->name; - $systemData->alias = $this->alias; - $systemData->effect = $this->effect; - $systemData->security = $this->security; - $systemData->trueSec = $this->trueSec; - - $systemData->region = (object) []; - $systemData->region->id = $this->regionId; - $systemData->region->name = $this->region; - - $systemData->constellation = (object) []; - $systemData->constellation->id = $this->constellationId; - $systemData->constellation->name = $this->constellation; - - $systemData->type = (object) []; - $systemData->type->id = $this->typeId->id; - $systemData->type->name = $this->typeId->name; - - $systemData->status = (object) []; - $systemData->status->id = is_object($this->statusId) ? $this->statusId->id : 0; - $systemData->status->name = is_object($this->statusId) ? $this->statusId->name : ''; - - $systemData->locked = $this->locked; - $systemData->rally = $this->rally; - $systemData->description = $this->description; - - $systemData->statics = $this->getStaticWormholeData(); - - $systemData->position = (object) []; - $systemData->position->x = $this->posX; - $systemData->position->y = $this->posY; - - if($this->createdCharacterId){ - $systemData->created = (object) []; - $systemData->created->character = $this->createdCharacterId->getData(); - $systemData->created->created = strtotime($this->created); - } - - if($this->updatedCharacterId){ - $systemData->updated = (object) []; - $systemData->updated->character = $this->updatedCharacterId->getData(); - $systemData->updated->updated = strtotime($this->updated); - } - - // max caching time for a system - // the cached date has to be cleared manually on any change - // this includes system, connection,... changes (all dependencies) - $this->updateCacheData($systemData, '', 300); - } - - return $systemData; - } - - /** - * setter validation for x coordinate - * @param $posX - * @return int|number - */ - public function set_posX($posX){ - $posX = abs($posX); - if($posX > self::MAX_POS_X){ - $posX = self::MAX_POS_X; - } - - return $posX; - } - - /** - * setter validation for y coordinate - * @param $posY - * @return int|number - */ - public function set_posY($posY){ - $posY = abs($posY); - if($posY > self::MAX_POS_Y){ - $posY = self::MAX_POS_Y; - } - - return $posY; - } - - /** - * check object for model access - * @param $accessObject - * @return bool - */ - public function hasAccess($accessObject){ - return $this->mapId->hasAccess($accessObject); - } - - /** - * delete a system from a map - * hint: signatures and connections will be deleted on cascade - * @param $accessObject - */ - public function delete($accessObject){ - - if(! $this->dry()){ - // check if user has access - if($this->hasAccess($accessObject)){ - $this->erase(); - } - } - } - - /** - * get all signatures of this system - * @return array - */ - public function getSignatures(){ - $this->filter('signatures', ['active = ?', 1], ['order' => 'name']); - - $signatures = []; - if($this->signatures){ - $signatures = $this->signatures; - } - - return $signatures; - } - - /** - * get all data for all Signatures in this system - * @return array - */ - public function getSignaturesData(){ - $signatures = $this->getSignatures(); - - $signaturesData = []; - foreach($signatures as $signature){ - $signaturesData[] = $signature->getData(); - } - - return $signaturesData; - } - - /** - * get Signature by id and check for access - * @param $accessObject - * @param $id - * @return bool|null - */ - public function getSignatureById($accessObject, $id){ - $signature = null; - - if($this->hasAccess($accessObject)){ - $this->filter('signatures', ['active = ? AND id = ?', 1, $id]); - if($this->signatures){ - $signature = reset( $this->signatures ); - } - } - - return $signature; - } - - /** - * get a signature by its "unique" 3-digit name - * @param $accessObject - * @param $name - * @return mixed|null - */ - public function getSignatureByName($accessObject, $name){ - $signature = null; - - if($this->hasAccess($accessObject)){ - $this->filter('signatures', ['active = ? AND name = ?', 1, $name]); - if($this->signatures){ - $signature = reset( $this->signatures ); - } - } - - return $signature; - } - - /** - * checks weather this system is a wormhole - * @return bool - */ - protected function isWormhole(){ - $isWormhole = false; - - if($this->typeId->id == 1){ - $isWormhole = true; - } - - return $isWormhole; - } - - /** - * get static WH data for this system - * -> any WH system has at least one static WH - * @return array - * @throws \Exception - */ - protected function getStaticWormholeData(){ - $wormholeData = []; - - // check if this system is a wormhole - if($this->isWormhole()){ - $systemStaticModel = self::getNew('SystemStaticModel'); - $systemStatics = $systemStaticModel->find([ - 'constellationId = :constellationId', - ':constellationId' => $this->constellationId - ]); - - if( is_object($systemStatics) ){ - foreach($systemStatics as $systemStatic){ - $wormholeData[] = $systemStatic->getData(); - } - } - } - - return $wormholeData; - } - - /** - * see parent - */ - public function clearCacheData(){ - parent::clearCacheData(); - - // clear map cache as well - $this->mapId->clearCacheData(); - } - -} \ No newline at end of file diff --git a/app/main/model/systempodkillmodel.php b/app/main/model/systempodkillmodel.php deleted file mode 100644 index be7e2b94b..000000000 --- a/app/main/model/systempodkillmodel.php +++ /dev/null @@ -1,15 +0,0 @@ - [ - 'belongs-to-one' => 'Model\SystemModel' - ], - 'createdCharacterId' => [ - 'belongs-to-one' => 'Model\CharacterModel' - ], - 'updatedCharacterId' => [ - 'belongs-to-one' => 'Model\CharacterModel' - ] - ]; - - protected $validate = [ - 'name' => [ - 'length' => [ - 'min' => 3 - ] - ] - ]; - - /** - * set an array with all data for a system - * @param $signatureData - */ - public function setData($signatureData){ - - foreach((array)$signatureData as $key => $value){ - - if(!is_array($value)){ - if($this->exists($key)){ - $this->$key = $value; - } - } - } - } - - /** - * get signature data as array - * @return array - */ - public function getData(){ - - $signatureData = [ - 'id' => $this->id, - 'groupId' => $this->groupId, - 'typeId' => $this->typeId, - 'name' => $this->name, - 'description' => $this->description, - 'created' => [ - 'character' => $this->createdCharacterId->getData(), - 'created' => strtotime($this->created) - ], - 'updated' => [ - 'character' => $this->updatedCharacterId->getData(), - 'updated' => strtotime($this->updated) - ] - - ]; - - return $signatureData; - } - - /** - * check object for model access - * @param $accessObject - * @return bool - */ - public function hasAccess($accessObject){ - return $this->systemId->hasAccess($accessObject); - } - - public function delete($accessObject){ - - if(!$this->dry()){ - // check if editor has access - if($this->hasAccess($accessObject)){ - $this->erase(); - } - } - } -} \ No newline at end of file diff --git a/app/main/model/systemstaticmodel.php b/app/main/model/systemstaticmodel.php deleted file mode 100644 index 395ba250b..000000000 --- a/app/main/model/systemstaticmodel.php +++ /dev/null @@ -1,28 +0,0 @@ -security = $this->security; - $systemStaticData->name = $this->name; - - return $systemStaticData; - } -} diff --git a/app/main/model/systemstatusmodel.php b/app/main/model/systemstatusmodel.php deleted file mode 100644 index dfebf3b79..000000000 --- a/app/main/model/systemstatusmodel.php +++ /dev/null @@ -1,15 +0,0 @@ - [ - 'belongs-to-one' => 'Model\UserModel' - ], - 'userCharacters' => [ - 'has-many' => ['Model\UserCharacterModel', 'apiId'] - ] - ]; - - /** - * get all data for this api - * @return object - */ - public function getData(){ - $apiData = (object) []; - $apiData->keyId = $this->keyId; - $apiData->vCode = $this->vCode; - - return $apiData; - } - - /** - * @return int - */ - public function updateCharacters(){ - $apiController = new Controller\CcpApiController(); - - return $apiController->updateCharacters($this); - } - - /** - * get all characters for this API - * @return array|mixed - */ - public function getUserCharacters(){ - $this->filter('userCharacters', ['active = ?', 1]); - - $userCharacters = []; - if($this->userCharacters){ - $userCharacters = $this->userCharacters; - } - - return $userCharacters; - } - - /** - * search for a user character model by a characterId - * @param $characterId - * @return null - */ - public function getUserCharacterById($characterId){ - $userCharacters = $this->getUserCharacters(); - $returnUserCharacter = null; - - foreach($userCharacters as $userCharacter){ - if($userCharacter->characterId->id == $characterId){ - $returnUserCharacter = $userCharacter; - break; - } - } - - return $returnUserCharacter; - } - - /** - * check if this api model has a main character - * @return bool - */ - public function hasMainCharacter(){ - $hasMain = false; - - $characters = $this->getCharacters(); - foreach($characters as $character){ - if($character->isMain()){ - $hasMain = true; - break; - } - } - - return $hasMain; - } - - /** - * get the user object for this model - * @return mixed - */ - public function getUser(){ - return $this->userId; - } - - /** - * delete this api model - */ - public function delete(){ - - // check if this api model had a main character - $user = $this->userId; - $setNewMain = false; - if($this->hasMainCharacter()){ - $setNewMain = true; - } - $this->erase(); - - if($setNewMain){ - $user->setMainCharacterId(); - } - - } - -} \ No newline at end of file diff --git a/app/main/model/usercharactermodel.php b/app/main/model/usercharactermodel.php deleted file mode 100644 index 3c8effa4e..000000000 --- a/app/main/model/usercharactermodel.php +++ /dev/null @@ -1,101 +0,0 @@ - [ - 'belongs-to-one' => 'Model\UserModel' - ], - 'apiId' => [ - 'belongs-to-one' => 'Model\UserApiModel' - ], - 'characterId' => [ - 'belongs-to-one' => 'Model\CharacterModel' - ] - ]; - - /** - * set an array with all data for a character - * @param $characterData - */ - public function setData($characterData){ - - foreach((array)$characterData as $key => $value){ - - if(!is_array($value)){ - if($this->exists($key)){ - $this->$key = $value; - } - } - } - } - - /** - * get all character data - * @param $addCharacterLogData - * @return array - */ - public function getData($addCharacterLogData = false){ - - // get characterModel - $characterModel = $this->getCharacter(); - - // get static character data - $characterData = $characterModel->getData($addCharacterLogData); - - // add user specific character data - $characterData->isMain = $this->isMain; - - // check for corporation - if( is_object( $characterModel->corporationId ) ){ - $characterData->corporation = $characterModel->corporationId->getData(); - } - - // check for alliance - if( is_object( $characterModel->allianceId ) ){ - $characterData->alliance = $characterModel->allianceId->getData(); - } - - return $characterData; - } - - /** - * check if this character is Main character or not - * @return bool - */ - public function isMain(){ - $isMain = false; - if($this->isMain == 1){ - $isMain = true; - } - - return $isMain; - } - - /** - * set this character as main character - */ - public function setMain($value = 0){ - $this->isMain = $value; - } - - /** - * get the character model of this character - * @return mixed - */ - public function getCharacter(){ - return $this->characterId; - } - -} \ No newline at end of file diff --git a/app/main/model/usermapmodel.php b/app/main/model/usermapmodel.php deleted file mode 100644 index 9b21a72e2..000000000 --- a/app/main/model/usermapmodel.php +++ /dev/null @@ -1,35 +0,0 @@ - [ - 'belongs-to-one' => 'Model\UserModel' - ], - 'mapId' => [ - 'belongs-to-one' => 'Model\MapModel' - ] - ]; - - /** - * see parent - */ - public function clearCacheData(){ - parent::clearCacheData(); - - // clear map cache as well - $this->mapId->clearCacheData(); - } - -} \ No newline at end of file diff --git a/app/main/model/usermodel.php b/app/main/model/usermodel.php deleted file mode 100644 index de611f991..000000000 --- a/app/main/model/usermodel.php +++ /dev/null @@ -1,487 +0,0 @@ - array( - 'type' => Schema::DT_TIMESTAMP - ), - 'apis' => [ - 'has-many' => ['Model\UserApiModel', 'userId'] - ], - 'userCharacters' => [ - 'has-many' => ['Model\UserCharacterModel', 'userId'] - ], - 'userMaps' => [ - 'has-many' => ['Model\UserMapModel', 'userId'] - ] - ]; - - protected $validate = [ - 'name' => [ - 'length' => [ - 'min' => 5, - 'max' => 20 - ] - ], - 'email' => [ - 'length' => [ - 'min' => 5 - ] - ], - 'password' => [ - 'length' => [ - 'min' => 6 - ] - ] - ]; - - /** - * get all data for this user - * ! caution ! this function returns sensitive data! - * -> user getSimpleData() for faster performance and public user data - * @return object - */ - public function getData(){ - - // get public user data for this user - $userData = $this->getSimpleData(); - - // add sensitive user data - $userData->email = $this->email; - - // user sharing info - $userData->sharing = $this->sharing; - - // api data - $APIs = $this->getAPIs(); - foreach($APIs as $api){ - $userData->api[] = $api->getData(); - } - - // all chars - $userData->characters = []; - $userCharacters = $this->getUserCharacters(); - foreach($userCharacters as $userCharacter){ - $userData->characters[] = $userCharacter->getData(); - } - - // set active character with log data - $activeUserCharacter = $this->getActiveUserCharacter(); - if($activeUserCharacter){ - $userData->character = $activeUserCharacter->getData(); - } - - return $userData; - } - - /** - * get public user data - * - check out getData() for all user data - * @return object - */ - public function getSimpleData(){ - $userData = (object) []; - $userData->id = $this->id; - $userData->name = $this->name; - - return $userData; - } - - /** - * validate and set a email address for this user - * @param $email - * @return mixed - */ - public function set_email($email){ - if (\Audit::instance()->email($email) == false) { - // no valid email address - $this->throwValidationError('email'); - } - return $email; - } - - /** - * set a password hash for this user - * @param $password - * @return FALSE|string - */ - public function set_password($password){ - if(strlen($password) < 6){ - $this->throwValidationError('password'); - } - - $salt = uniqid('', true); - return \Bcrypt::instance()->hash($password, $salt); - } - - /** - * check if new user registration is allowed - * @return bool - * @throws Exception\RegistrationException - */ - public function beforeInsertEvent(){ - $registrationStatus = Controller\Controller::getRegistrationStatus(); - - switch($registrationStatus){ - case 0: - $f3 = self::getF3(); - throw new Exception\RegistrationException($f3->get('PATHFINDER.REGISTRATION.MSG_DISABLED')); - return false; - break; - case 1: - return true; - break; - default: - return false; - } - } - - /** - * search for user by unique username - * @param $name - * @return array|FALSE - */ - public function getByName($name){ - return $this->getByForeignKey('name', $name); - } - - /** - * verify a user by his password - * @param $password - * @return bool - */ - public function verify($password){ - $valid = false; - - if(! $this->dry()){ - $valid = (bool) \Bcrypt::instance()->verify($password, $this->password); - } - - return $valid; - } - - /** - * get all accessible map models for this user - * @return array - */ - public function getMaps(){ - - $f3 = self::getF3(); - - $this->filter( - 'userMaps', - ['active = ?', 1], - [ - 'limit' => $f3->get('PATHFINDER.MAX_MAPS_PRIVATE'), - 'order' => 'created' - ] - ); - - $maps = []; - if($this->userMaps){ - foreach($this->userMaps as $userMap){ - if($userMap->mapId->isActive()){ - $maps[] = $userMap->mapId; - } - } - } - - $activeUserCharacter = $this->getActiveUserCharacter(); - - if($activeUserCharacter){ - $character = $activeUserCharacter->getCharacter(); - $corporation = $character->getCorporation(); - $alliance = $character->getAlliance(); - - if($alliance){ - $allianceMaps = $alliance->getMaps(); - $maps = array_merge($maps, $allianceMaps); - } - - if($corporation){ - $corporationMaps = $corporation->getMaps(); - $maps = array_merge($maps, $corporationMaps); - - } - } - - return $maps; - } - - /** - * get mapModel by id and check if user has access - * @param $mapId - * @return null - * @throws \Exception - */ - public function getMap($mapId){ - $map = self::getNew('MapModel'); - $map->getById( (int)$mapId ); - - $returnMap = null; - if($map->hasAccess($this)){ - $returnMap = $map; - } - - return $returnMap; - } - - - /** - * get all API models for this user - * @return array|mixed - */ - public function getAPIs(){ - $this->filter('apis', ['active = ?', 1]); - - $apis = []; - if($this->apis){ - $apis = $this->apis; - } - - return $apis; - } - - /** - * set main character ID for this user. - * If id does not match with his API chars -> select "random" main character - * @param int $characterId - */ - public function setMainCharacterId($characterId = 0){ - - if(is_int($characterId)){ - $userCharacters = $this->getUserCharacters(); - - if(count($userCharacters) > 0){ - $mainSet = false; - foreach($userCharacters as $userCharacter){ - if($characterId == $userCharacter->getCharacter()->id){ - $mainSet = true; - $userCharacter->setMain(1); - }else{ - $userCharacter->setMain(0); - } - $userCharacter->save(); - } - - // set random main character - if(! $mainSet ){ - $userCharacters[0]->setMain(1); - $userCharacters[0]->save(); - } - } - } - } - - /** - * get all userCharacters models for a user - * characters will be checked/updated on login by CCP API call - * @return array|mixed - */ - public function getUserCharacters(){ - - $this->filter('apis', ['active = ?', 1]); - - $userCharacters = []; - - if($this->apis){ - $this->apis->rewind(); - while($this->apis->valid()){ - - $this->apis->current()->filter('userCharacters', ['active = ?', 1]); - if($this->apis->current()->userCharacters){ - $this->apis->current()->userCharacters->rewind(); - while($this->apis->current()->userCharacters->valid()){ - $userCharacters[] = $this->apis->current()->userCharacters->current(); - $this->apis->current()->userCharacters->next(); - } - } - - $this->apis->next(); - } - } - - return $userCharacters; - } - - /** - * Get the main user character for this user - * @return null - */ - public function getMainUserCharacter(){ - $mainUserCharacter = null; - $userCharacters = $this->getUserCharacters(); - - foreach($userCharacters as $userCharacter){ - if($userCharacter->isMain()){ - $mainUserCharacter = $userCharacter; - break; - } - } - - return $mainUserCharacter; - } - - /** - * get the active user character for this user - * either there is an active Character (IGB) or the character labeled as "main" - * @return null - */ - public function getActiveUserCharacter(){ - $activeUserCharacter = null; - - $apiController = Controller\CcpApiController::getIGBHeaderData(); - - // check if IGB Data is available - if( !empty($apiController->values) ){ - // search for the active character by IGB Header Data - - $this->filter('userCharacters', - [ - 'active = :active AND characterId = :characterId', - ':active' => 1, - ':characterId' => intval($apiController->values['charid']) - ], - ['limit' => 1] - ); - - if($this->userCharacters){ - // check if userCharacter has active log - $userCharacter = current($this->userCharacters); - - if( $userCharacter->getCharacter()->getLog() ){ - $activeUserCharacter = $userCharacter; - } - } - } - - // if no active character is found - // e.g. not online in IGB - // -> get main Character - if(is_null($activeUserCharacter)){ - $activeUserCharacter = $this->getMainUserCharacter(); - } - - return $activeUserCharacter; - } - - /** - * get all active user characters (with log entry) - * hint: a user can have multiple active characters - * @return array - */ - public function getActiveUserCharacters(){ - $userCharacters = $this->getUserCharacters(); - - $activeUserCharacters = []; - foreach($userCharacters as $userCharacter){ - $characterLog = $userCharacter->getCharacter()->getLog(); - - if($characterLog){ - $activeUserCharacters[] = $userCharacter; - } - } - - return $activeUserCharacters; - } - - /** - * update/check API information. - * request API information from CCP - */ - public function updateApiData(){ - $this->filter('apis', ['active = ?', 1]); - - if($this->apis){ - $this->apis->rewind(); - while($this->apis->valid()){ - $this->apis->current()->updateCharacters(); - $this->apis->next(); - } - } - } - - /** - * updated the character log entry for a user character by IGB Header data - * @param int $ttl cache time in seconds - * @throws \Exception - */ - public function updateCharacterLog($ttl = 0){ - $apiController = Controller\CcpApiController::getIGBHeaderData(); - - // check if IGB Data is available - if( !empty($apiController->values) ){ - $f3 = self::getF3(); - - // check if system has changed since the last call - // current location is stored in session to avoid unnecessary DB calls - $sessionCharacterKey = 'LOGGED.user.character.id_' . $apiController->values['charid']; - - if( - !$f3->exists($sessionCharacterKey) || - $f3->get($sessionCharacterKey . '.systemId') != $apiController->values['solarsystemid'] || - $f3->get($sessionCharacterKey . '.shipId') != $apiController->values['shiptypeid'] - ){ - - $cacheData = [ - 'systemId' => $apiController->values['solarsystemid'], - 'shipId' => $apiController->values['shiptypeid'] - ]; - - // character has changed system, or character just logged on - $character = self::getNew('CharacterModel'); - $character->getById( (int)$apiController->values['charid'] ); - - if( $character->dry() ){ - // this can happen if a valid user plays the game with a not registered character - // whose API is not registered -> save new character or update character data - - $character->id = (int) $apiController->values['charid']; - $character->name = $apiController->values['charname']; - $character->corporationId = array_key_exists('corpid', $apiController->values) ? $apiController->values['corpid'] : null; - $character->allianceId = array_key_exists('allianceid', $apiController->values) ? $apiController->values['allianceid'] : null; - $character->save(); - } - - // check if this character has an active log - if( !$characterLog = $character->getLog() ){ - $characterLog = self::getNew('CharacterLogModel'); - } - - // set character log values - $characterLog->characterId = $character; - $characterLog->systemId = $apiController->values['solarsystemid']; - $characterLog->systemName = $apiController->values['solarsystemname']; - $characterLog->shipId = $apiController->values['shiptypeid']; - $characterLog->shipName = $apiController->values['shipname']; - $characterLog->shipTypeName = $apiController->values['shiptypename']; - - $characterLog->save(); - - // clear cache for the characterModel as well - $character->clearCacheData(); - - // cache character log information - $f3->set($sessionCharacterKey, $cacheData, $ttl); - } - - } - } - - -} \ No newline at end of file diff --git a/app/pathfinder.ini b/app/pathfinder.ini index 94ae8fcf3..63b5868d5 100644 --- a/app/pathfinder.ini +++ b/app/pathfinder.ini @@ -1,142 +1,397 @@ +; Pathfinder Config + [PATHFINDER] -NAME = "PATHFINDER" -; installed version (used for CSS/JS cache busting) -VERSION = "v0.0.4" -; contact information (DO NOT CHANGE) -CONTACT = "https://github.com/exodus4d" -; source code (DO NOT CHANGE) -REPO = "https://github.com/exodus4d/pathfinder" - -; Max number of maps an entity can create -MAX_MAPS_PRIVATE = 3 -MAX_MAPS_CORPORATION = 3 -MAX_MAPS_ALLIANCE = 3 - -; Max number of shared entities per map -MAX_SHARED_USER = 10 -MAX_SHARED_CORPORATION = 3 -MAX_SHARED_ALLIANCE = 2 - -[PATHFINDER.ENVIRONMENT] -; project environment ("DEVELOP", "PRODUCTION"). -; This affects: DB connection, JS build path -SERVER = "DEVELOP" - -[PATHFINDER.ENVIRONMENT.DEVELOP] -BASE = /exodus4d/pathfinder - -; deployment URL (what you type in the browser -URL = http://localhost/exodus4d/pathfinder - -; Verbosity level of the stack trace -DEBUG = 3 - -; js path -> use raw files -PATH_JS = "js" - -; main db -DB_DNS = mysql:host=localhost;port=3306;dbname= -DB_NAME = pathfinder -DB_USER = root -DB_PASS = - -; EVE-Online CCP Database export -DB_CCP_DNS = mysql:host=localhost;port=3306;dbname= -DB_CCP_NAME = eve_test -DB_CCP_USER = root -DB_CCP_PASS = - -[PATHFINDER.ENVIRONMENT.PRODUCTION] -BASE = /www/htdocs/w0128162/www.pathfinder.exodus4d.de - -; deployment URL (what you type in the browser -URL = https://www.pathfinder.exodus4d.de - -; Verbosity level of the stack trace -DEBUG = 0 - -; js path -> use build files -PATH_JS = "build_js" - -; main db -DB_DNS = mysql:host=localhost;port=3306;dbname= -DB_NAME = d01d8636 -DB_USER = d01d8636 -DB_PASS = bQ9VAd6fE86sVs4s - -; EVE-Online CCP Database export -DB_CCP_DNS = mysql:host=localhost;port=3306;dbname= -DB_CCP_NAME = d01f20be -DB_CCP_USER = d01f20be -DB_CCP_PASS = 2gkBWs87zDcApH4A - -; ====================================================================================================== +; Name of installation +; This can be changed to any name +; This name is used in e.g. emails, user interface +; Syntax: String +; Default: Pathfinder +NAME = Pathfinder + +; Pathfinder version +; Version number should not be changed manually. +; Version is used for CSS/JS cache busting and is part of the URL for static resources: +; e.g. public/js/vX.X.X/app.js +; Syntax: String (current version) +; Default: v2.0.0 +VERSION = v2.0.0 + +; Contact information [optional] +; Shown on 'licence', 'contact' page. +; Syntax: String +; Default: https://github.com/exodus4d +CONTACT = https://github.com/exodus4d + +; Public contact email [optional] +; Syntax: String +; Default: +EMAIL = + +; Repository URL [optional] +; Used for 'licence', 'contact' page. +; Syntax: String +; Default: https://github.com/exodus4d/pathfinder +REPO = https://github.com/exodus4d/pathfinder + +; Show warning on 'login' page if /setup route is active +; DO NOT disable this warning unless /setup route is protected or commented in routes.ini +; Syntax: 0 | 1 +; Default: 1 +SHOW_SETUP_WARNING = 1 + +; Show full login page +; If disabled, some section don´t appear: +; 'Slideshow', 'Features', 'Admin', 'Install', 'About' +; Syntax: 0 | 1 +; Default: 1 +SHOW_COMPLETE_LOGIN_PAGE = 1 + +; REGISTRATION ==================================================================================== [PATHFINDER.REGISTRATION] -; registration status (0=disabled, 1=enabled) -STATUS = 1 -; disabled message -MSG_DISABLED = "User registration is currently not allowed" - -; ====================================================================================================== -; Lifetime for map types +; Registration status (for new users) +; If disabled, users can no longer register a new account on this installation. +; Syntax: 0 | 1 +; Default: 1 +STATUS = 1 + +[PATHFINDER.LOGIN] +; Expire time for login cookies +; Login Cookie information send by clients is re-validated by the server. +; The expire time for each cookie is stored in DB. Expired Cookies become invalid. +; Syntax: Integer (days) +; Default: 30 +COOKIE_EXPIRE = 30 + +; Show 'scheduled maintenance' warning +; If enabled, active users will see a notification panel. +; This can be used to inform users about upcoming maintenance shutdown. +; This flag can be enabled "on the fly" (no page reload required to see the notice). +; Syntax: 0 | 1 +; Default: 0 +MODE_MAINTENANCE = 0 + +; Login restrictions (white lists) +; Login/registration can be restricted to specific groups. +; Use comma separated strings for CCP Ids (e.g. 1000166,1000080). +; If no groups are specified, all characters are allowed. +; Syntax: String (comma separated) +; Default: +CHARACTER = +CORPORATION = +ALLIANCE = + +[PATHFINDER.CHARACTER] +; Auto location select for characters +; If enabled, characters can activate the "auto location select" checkbox in their account settings. +; If checkbox active, solar systems get auto selected on map based on their current system. +; Hint: This can increase server load because of more client requests. +; Syntax: 0 | 1 +; Default: 1 +AUTO_LOCATION_SELECT = 1 + +; Slack API integration =========================================================================== +[PATHFINDER.SLACK] +; Slack API status +; This is a global toggle for all Slack related features. +; Check PATHFINDER.MAP section for individual control. +; Syntax: 0 | 1 +; Default: 1 +STATUS = 1 + +; Discord API integration ========================================================================= +[PATHFINDER.DISCORD] +; Discord API status +; This is a global toggle for all Discord related features. +; Check PATHFINDER.MAP section for individual control. +; Syntax: 0 | 1 +; Default: 1 +STATUS = 1 + +; View ============================================================================================ +[PATHFINDER.VIEW] +; Page templates +; Hint: You should not change this. +INDEX = templates/view/index.html +SETUP = templates/view/setup.html +LOGIN = templates/view/login.html +ADMIN = templates/view/admin.html + +; HTTP status pages =============================================================================== +[PATHFINDER.STATUS] +; Error page templates +; Hint: You should not change this. +4XX = templates/status/4xx.html +5XX = templates/status/5xx.html + +; MAP ============================================================================================= +; Map settings for 'private', 'corporation' and 'alliance' maps: +; LIFETIME (days) +; - Map will be deleted after 'X' days, by cronjob +; MAX_COUNT +; - Users can create/view up to 'X' maps of a type +; MAX_SHARED +; - Max number of shared entities per map +; MAX_SYSTEMS +; - Max number of active systems per map +; LOG_ACTIVITY_ENABLED (Syntax: 0 | 1) +; - Whether user activity statistics can be enabled for a map type +; - E.g. create/update/delete of systems/connections/signatures/... +; LOG_HISTORY_ENABLED (Syntax: 0 | 1) +; - Whether map change history should be logged to separate *.log files +; - see: [PATHFINDER.HISTORY] config section below +; SEND_HISTORY_SLACK_ENABLED (Syntax: 0 | 1) +; - Send map updates to a Slack channel per map +; SEND_RALLY_SLACK_ENABLED (Syntax: 0 | 1) +; - Send rally point pokes to a Slack channel per map +; SEND_HISTORY_DISCORD_ENABLED (Syntax: 0 | 1) +; - Send map updates to a Discord channel per map +; SEND_RALLY_DISCORD_ENABLED (Syntax: 0 | 1) +; - Send rally point pokes to a Discord channel per map +; SEND_RALLY_Mail_ENABLED (Syntax: 0 | 1) +; - Send rally point pokes by mail +; - see: [PATHFINDER.NOTIFICATION] section below [PATHFINDER.MAP.PRIVATE] -LIFETIME = 2 +LIFETIME = 60 +MAX_COUNT = 3 +MAX_SHARED = 10 +MAX_SYSTEMS = 50 +LOG_ACTIVITY_ENABLED = 1 +LOG_HISTORY_ENABLED = 1 +SEND_HISTORY_SLACK_ENABLED = 0 +SEND_RALLY_SLACK_ENABLED = 1 +SEND_HISTORY_DISCORD_ENABLED = 0 +SEND_RALLY_DISCORD_ENABLED = 1 +SEND_RALLY_Mail_ENABLED = 0 [PATHFINDER.MAP.CORPORATION] -LIFETIME = 99999 +LIFETIME = 99999 +MAX_COUNT = 5 +MAX_SHARED = 4 +MAX_SYSTEMS = 100 +LOG_ACTIVITY_ENABLED = 1 +LOG_HISTORY_ENABLED = 1 +SEND_HISTORY_SLACK_ENABLED = 1 +SEND_RALLY_SLACK_ENABLED = 1 +SEND_HISTORY_DISCORD_ENABLED = 1 +SEND_RALLY_DISCORD_ENABLED = 1 +SEND_RALLY_Mail_ENABLED = 0 [PATHFINDER.MAP.ALLIANCE] -LIFETIME = 99999 -; ====================================================================================================== -[PATHFINDER.CACHE] - -; cache character log informations in seconds. This is ignored if ship/system switch was detected -CHARACTER_LOG = 600 - -; cache time for all system data within a constellation (this will never change) 30d -CONSTELLATION_SYSTEMS = 2592000 - -; ====================================================================================================== +LIFETIME = 99999 +MAX_COUNT = 4 +MAX_SHARED = 2 +MAX_SYSTEMS = 100 +LOG_ACTIVITY_ENABLED = 0 +LOG_HISTORY_ENABLED = 1 +SEND_HISTORY_SLACK_ENABLED = 1 +SEND_RALLY_SLACK_ENABLED = 1 +SEND_HISTORY_DISCORD_ENABLED = 1 +SEND_RALLY_DISCORD_ENABLED = 1 +SEND_RALLY_Mail_ENABLED = 0 + +; Route search ==================================================================================== +[PATHFINDER.ROUTE] +; Search depth for system route search +; Recursive search depth for search algorithm. +; This is only used in case ESIs /route/ API responds with errors and the custom search algorithm is used. +; Hint: Higher values can lead to high CPU load. If to low, routes might not be found even if exist. +; Syntax: Integer +; Default: 9000 +SEARCH_DEPTH = 9000 + +; Initial count of routes that will be checked when a system becomes active +; Syntax: Integer +; Default: 4 +SEARCH_DEFAULT_COUNT = 4 + +; Max count of routes that can be selected in 'route settings' dialog +; Syntax: Integer +; Default: 6 +MAX_DEFAULT_COUNT = 6 + +; Max count of routes that will be checked (MAX_DEFAULT_COUNT + custom routes) +; Syntax: Integer +; Default: 8 +LIMIT = 8 + +; Email notifications ============================================================================= +[PATHFINDER.NOTIFICATION] +; Email address for rally point pokes +; Requires SMTP configuration (see environment.ini). +; Hint: This only makes sens if the installation is restricted to allied groups only. +; This email address is used for all maps on this installation. +; Syntax: String +; Default: +RALLY_SET = + +; TIMER =========================================================================================== +; Timer values should NOT be changed unless you know what they affect! +; ================================================================================================= [PATHFINDER.TIMER] -; login time (minutes) -LOGGED = 120 - -; double click timer (ms) -DBL_CLICK = 250 +; Login time for characters. Users get logged out after X minutes +; Hint: Set to 0 disables login time and characters stay logged in until Cookie data expires +; Syntax: Integer (minutes) +; Default: 480 +LOGGED = 480 + +; Double click timer +; Syntax: Integer (milliseconds) +; Default: 250 +DBL_CLICK = 250 + +; Time for status change visibility in header +; Syntax: Integer (milliseconds) +; Default: 5000 +PROGRAM_STATUS_VISIBLE = 5000 -; time for status change visibility in header (ms) -PROGRAM_STATUS_VISIBLE = 5000 - -; get all client map data (ms) -[PATHFINDER.TIMER.GET_CLIENT_MAP_DATA] -EXECUTION_LIMIT = 50 - -; main map update ping (ajax) (ms) [PATHFINDER.TIMER.UPDATE_SERVER_MAP] -DELAY = 5000 -EXECUTION_LIMIT = 200 +; Map data update interval (ajax long polling) +; This is not used for 'WebSocket' configured installations. +; Syntax: Integer (milliseconds) +; Default: 5000 +DELAY = 5000 + +; Execution limit for map data update request (ajax long polling) +; Requests that exceed the limit are logged as 'warning'. +; Syntax: Integer (milliseconds) +; Default: 200 +EXECUTION_LIMIT = 500 -; update client map data (ms) [PATHFINDER.TIMER.UPDATE_CLIENT_MAP] -EXECUTION_LIMIT = 50 +; Execution limit for client side (javascript) map data updates +; Map data updates that exceed the limit are logged as 'warning'. +; Syntax: Integer (milliseconds) +; Default: 50 +EXECUTION_LIMIT = 100 -; map user update ping (ajax) (ms) [PATHFINDER.TIMER.UPDATE_SERVER_USER_DATA] -DELAY = 5000 -EXECUTION_LIMIT = 200 - -; update client user data (ms) +; User data update interval (ajax long polling) +; This is not used for 'WebSocket' configured installations. +; Syntax: Integer (milliseconds) +; Default: 5000 +DELAY = 5000 + +; Execution limit for user data update request (ajax long polling) +; Requests that exceed the limit are logged as 'warning'. +; Syntax: Integer (milliseconds) +; Default: 500 +EXECUTION_LIMIT = 1000 + +; update client user data (milliseconds) [PATHFINDER.TIMER.UPDATE_CLIENT_USER_DATA] -EXECUTION_LIMIT = 50 +; Execution limit for client side (javascript) user data updates +; User data updates that exceed the limit are logged as 'warning'. +; Syntax: Integer (milliseconds) +; Default: 50 +EXECUTION_LIMIT = 100 -; ====================================================================================================== +; CACHE =========================================================================================== +[PATHFINDER.CACHE] +; Checks "character log" data by cronjob after x seconds +; If character is ingame offline -> delete "character log" +; Syntax: Integer (seconds) +; Default: 180 +CHARACTER_LOG_INACTIVE = 180 + +; Max expire time for cache files +; Files will be deleted by cronjob afterwards. +; This setting only affects 'file cache'. Redis installations are not affected by this. +; Syntax: Integer (seconds) +; Default: 864000 (10d) +EXPIRE_MAX = 864000 + +; Expire time for EOL (end of life) connections +; EOL connections get auto deleted by cronjob afterwards. +; Syntax: Integer (seconds) +; Default: 15300 (4h + 15min) +EXPIRE_CONNECTIONS_EOL = 15300 + +; Expire time for WH connections +; WH connections get auto deleted by cronjob afterwards. +; This can be overwritten for each map in the UI. +; Syntax: Integer (seconds) +; Default: 172800 (2d) +EXPIRE_CONNECTIONS_WH = 172800 + +; Expire time for signatures (inactive systems) +; Signatures get auto deleted by cronjob afterwards. +; This can be overwritten for each map in the UI. +; Syntax: Integer (seconds) +; Default: 259200 (3d) +EXPIRE_SIGNATURES = 259200 + +; LOGGING ========================================================================================= +; Log file configurations +; Log files are location in [PATHFINDER]/logs/ dir (see: config.ini) +; Syntax: String [PATHFINDER.LOGFILES] -; just for manuel debug during development -DEBUG = "debug" - -; user login information -LOGIN = "login" - +; Error log +ERROR = error +; SSO error log +SSO = sso +; Login info +CHARACTER_LOGIN = character_login +; Character access +CHARACTER_ACCESS = character_access +; Session warnings (mysql sessions only) +SESSION_SUSPECT = session_suspect +; Account deleted +DELETE_ACCOUNT = account_delete +; Admin action (e.g. kick, ban) +ADMIN = admin +; TCP socket errors +SOCKET_ERROR = socket_error +; debug log for development +DEBUG = debug + +[PATHFINDER.HISTORY] +; cache time for parsed history log file data +; Syntax: Integer (seconds) +; Default: 5 +CACHE = 5 + +; File folder for 'history' logs (e.g. map history) +; Syntax: String +; Default: history/ +LOG = history/ + +; Max file size for 'history' logs before getting truncated by cronjob +; Syntax: Integer (MB) +; Default: 2 +LOG_SIZE_THRESHOLD = 2 + +; log entries (lines) after file getting truncated by cronjob +; Syntax: Integer +; Default: 1000 +LOG_LINES = 1000 + +; ADMIN =========================================================================================== +; "SUPER" admins and additional "CORPORATION" admins can be added here +;[PATHFINDER.ROLES] +;CHARACTER.0.ID = 123456789 +;CHARACTER.0.ROLE = SUPER +;CHARACTER.1.ID = 1122334455 +;CHARACTER.1.ROLE = CORPORATION + +; API ============================================================================================= [PATHFINDER.API] -; Path for CCPs XML APIv2 -CCP_XML = "https://api.eveonline.com" \ No newline at end of file +CCP_IMAGE_SERVER = https://images.evetech.net +Z_KILLBOARD = https://zkillboard.com/api +EVEEYE = https://eveeye.com +DOTLAN = http://evemaps.dotlan.net +ANOIK = http://anoik.is +EVE_SCOUT = https://www.eve-scout.com/api +; GitHub Developer API +GIT_HUB = https://api.github.com + +; EXPERIMENTAL [BETA] ============================================================================= +; Use these settings with caution! +; They are currently under testing and might be removed in further releases. +[PATHFINDER.EXPERIMENTS] +; Try to use persistent database connections +; PDO connections get initialized with ATTR_PERSISTENT => true . +; http://php.net/manual/en/pdo.connections.php#example-1030 +; Hint: Set 'wait_timeout' to a high value in your my.conf to keep them open +; Syntax: 0 | 1 +; Default: 0 +PERSISTENT_DB_CONNECTIONS = 1 \ No newline at end of file diff --git a/app/plugin.ini b/app/plugin.ini new file mode 100644 index 000000000..694c48ae7 --- /dev/null +++ b/app/plugin.ini @@ -0,0 +1,6 @@ +[PLUGIN] +MODULES_ENABLED = 1 + +[PLUGIN.MODULES] +DEMO = ./app/ui/module/demo +EMPTY = ./app/ui/module/empty diff --git a/app/requirements.ini b/app/requirements.ini new file mode 100644 index 000000000..7df2980ed --- /dev/null +++ b/app/requirements.ini @@ -0,0 +1,88 @@ +; Requirements Config (Do not change!) +[REQUIREMENTS] + +[REQUIREMENTS.SERVER] +; Apache +APACHE.VERSION = 2.5 + +; Nginx +NGINX.VERSION = 1.9 + +[REQUIREMENTS.PHP] +VERSION = 7.2 + +; 64-bit version of PHP (4 = 32-bit, 8 = 64-bit) +PHP_INT_SIZE = 8 + +; "Perl-Compatible Regular Expressions" +; usually shipped with PHP package, +; but needs to be additionally updated on CentOS or Red Hat systems +PCRE_VERSION = 8.02 + +; Redis extension (optional), required if you want to use Redis as caching Engine (recommended) +REDIS = 3.0.0 + +; Event extension (optional) for WebSocket configuration. Better performance +; https://pecl.php.net/package/event +EVENT = 2.3.0 + +; exec() function required for run Shell scripts from PHP +EXEC = 1 + +; max execution time for requests (seconds) +MAX_EXECUTION_TIME = 10 + +; max memory limit +; some requests e.g. build indexes on /setup page require more RAM +MEMORY_LIMIT = 128M + +; max variable size for $_GET, $_POST and $_COOKIE +; this is required for importing larger maps +; http://php.net/manual/en/info.configuration.php +; PHP default = 1000 +MAX_INPUT_VARS = 3000 + +; Formatted HTML StackTraces +HTML_ERRORS = 0 + +[REQUIREMENTS.MYSQL] +; min MySQL Version +; newer "deviation" of MySQL like "MariaDB" > 10.1 are recommended +VERSION = 5.7 +; DB timeout (seconds) +PDO_TIMEOUT = 2 + +[REQUIREMENTS.MYSQL.VARS] +; MySql variables. Values are auto. set as 'SESSION' vars +; https://dev.mysql.com/doc/refman/5.5/en/show-variables.html +DEFAULT_STORAGE_ENGINE = InnoDB +CHARACTER_SET_SERVER = utf8mb4 +CHARACTER_SET_DATABASE = utf8mb4 +CHARACTER_SET_CLIENT = utf8mb4 +CHARACTER_SET_RESULTS = utf8mb4 +CHARACTER_SET_CONNECTION = utf8mb4 +COLLATION_DATABASE = utf8mb4_unicode_ci +COLLATION_CONNECTION = utf8mb4_unicode_ci +FOREIGN_KEY_CHECKS = ON +INNODB_FILE_PER_TABLE = ON +WAIT_TIMEOUT = 28800 +INTERACTIVE_TIMEOUT = {{ @REQUIREMENTS.MYSQL.VARS.WAIT_TIMEOUT }} + +[REQUIREMENTS.REDIS] +VERSION = 3.0 +; max memory limit (Bytes) "binary" (default: 64M) +MAX_MEMORY = 67108864 +; how Redis behaves if "maxmemory" limit reached +; https://redis.io/topics/lru-cache +MAXMEMORY_POLICY = allkeys-lru + +[REQUIREMENTS.PATH] +NODE = 12.16.0 +NPM = 6.13.4 + +[REQUIREMENTS.CRON] +CLI = 1 +LOG = 1 + +[REQUIREMENTS.DATA] +NEIGHBOURS = 5201 diff --git a/app/routes.ini b/app/routes.ini index ebf46f3f0..cfe286870 100644 --- a/app/routes.ini +++ b/app/routes.ini @@ -1,10 +1,27 @@ +; Route config + [routes] +; DB setup setup +; IMPORTANT: remove/comment this line after setup/update is finished! +GET @setup: /setup [sync] = {{ @NAMESPACE }}\Controller\Setup->init +; login (index) page +GET @login: / [sync] = {{ @NAMESPACE }}\Controller\AppController->init +; CCP SSO redirect +GET @sso: /sso/@action [sync] = {{ @NAMESPACE }}\Controller\Ccp\Sso->@action +; map page +GET @map: /map* [sync] = {{ @NAMESPACE }}\Controller\MapController->init +; admin panel +GET @admin: /admin* [sync] = {{ @NAMESPACE }}\Controller\Admin->dispatch + +; AJAX API wildcard endpoints (not cached, throttled) +GET|POST /api/@controller/@action [ajax] = {{ @NAMESPACE }}\Controller\Api\@controller->@action, 0, 512 +GET|POST /api/@controller/@action/@arg1 [ajax] = {{ @NAMESPACE }}\Controller\Api\@controller->@action, 0, 512 +GET|POST /api/@controller/@action/@arg1/@arg2 [ajax] = {{ @NAMESPACE }}\Controller\Api\@controller->@action, 0, 512 -; static routes (main views) default cache: 86400 -GET|POST @landing: /= Controller\AppController->showLandingpage, 0 -GET|POST @map: /map= Controller\MapController->showMap, 0 +; onUnload route or final map sync (@see https://developer.mozilla.org/docs/Web/API/Navigator/sendBeacon) +POST /api/Map/updateUnloadData = {{ @NAMESPACE }}\Controller\Api\Map->updateUnloadData, 0, 512 -; ajax wildcard APIs (throttled) -GET|POST /api/@controller/@action [ajax] = Controller\Api\@controller->@action, 0, 512 -GET|POST /api/@controller/@action/@arg1 [ajax] = Controller\Api\@controller->@action, 0, 512 -GET|POST /api/@controller/@action/@arg1/@arg2 [ajax] = Controller\Api\@controller->@action, 0, 512 +[maps] +; REST API wildcard endpoints (not cached, throttled) +/api/rest/@controller* [ajax] = {{ @NAMESPACE }}\Controller\Api\Rest\@controller, 0, 512 +/api/rest/@controller/@id [ajax] = {{ @NAMESPACE }}\Controller\Api\Rest\@controller, 0, 512 \ No newline at end of file diff --git a/build_js/app.js b/build_js/app.js deleted file mode 100644 index 89bf73373..000000000 --- a/build_js/app.js +++ /dev/null @@ -1,2 +0,0 @@ -var mainScriptPath=document.body.getAttribute("data-script"),jsBaseUrl=document.body.getAttribute("data-js-path");requirejs.config({baseUrl:"js",paths:{layout:"layout",config:"app/config",dialog:"app/ui/dialog",landingpage:"./app/landingpage",mappage:"./app/mappage",jquery:"lib/jquery-1.11.3.min",bootstrap:"lib/bootstrap.min",text:"lib/requirejs/text",mustache:"lib/mustache.min",velocity:"lib/velocity.min",velocityUI:"lib/velocity.ui.min",templates:"../public/templates",img:"../public/img",slidebars:"lib/slidebars",jsPlumb:"lib/dom.jsPlumb-1.7.6-min",customScrollbar:"lib/jquery.mCustomScrollbar.concat.min",datatables:"lib/datatables/jquery.dataTables.min",datatablesResponsive:"lib/datatables/extensions/responsive/dataTables.responsive",datatablesTableTools:"lib/datatables/extensions/tabletools/js/dataTables.tableTools",xEditable:"lib/bootstrap-editable.min",morris:"lib/morris.min",raphael:"lib/raphael-min",bootbox:"lib/bootbox.min",easyPieChart:"lib/jquery.easypiechart.min",dragToSelect:"lib/jquery.dragToSelect",hoverIntent:"lib/jquery.hoverIntent.minified",fullScreen:"lib/jquery.fullscreen.min",select2:"lib/select2.min",validator:"lib/validator.min",lazylinepainter:"lib/jquery.lazylinepainter-1.5.1.min",blueImpGallery:"lib/blueimp-gallery",blueImpGalleryHelper:"lib/blueimp-helper",blueImpGalleryBootstrap:"lib/bootstrap-image-gallery",bootstrapConfirmation:"lib/bootstrap-confirmation",bootstrapToggle:"lib/bootstrap2-toggle.min",easePack:"lib/EasePack.min",tweenLite:"lib/TweenLite.min",pnotify:"lib/pnotify/pnotify.core","pnotify.buttons":"lib/pnotify/pnotify.buttons","pnotify.confirm":"lib/pnotify/pnotify.confirm","pnotify.nonblock":"lib/pnotify/pnotify.nonblock","pnotify.desktop":"lib/pnotify/pnotify.desktop","pnotify.history":"lib/pnotify/pnotify.history","pnotify.callbacks":"lib/pnotify/pnotify.callbacks","pnotify.reference":"lib/pnotify/pnotify.reference"},shim:{bootstrap:{deps:["jquery"]},velocity:{deps:["jquery"]},velocityUI:{deps:["velocity"]},slidebars:{deps:["jquery"]},customScrollbar:{deps:["jquery"]},datatables:{deps:["jquery"]},datatablesBootstrap:{deps:["datatables"]},datatablesResponsive:{deps:["datatables"]},datatablesTableTools:{deps:["datatables"]},xEditable:{deps:["bootstrap"]},bootbox:{deps:["jquery","bootstrap"],exports:"bootbox"},morris:{deps:["jquery","raphael"],exports:"Morris"},pnotify:{deps:["jquery"]},easyPieChart:{deps:["jquery"]},dragToSelect:{deps:["jquery"]},hoverIntent:{deps:["jquery"]},fullScreen:{deps:["jquery"]},select2:{deps:["jquery"],exports:"Select2"},validator:{deps:["jquery","bootstrap"]},lazylinepainter:{deps:["jquery","bootstrap"]},blueImpGallery:{deps:["jquery"]},bootstrapConfirmation:{deps:["bootstrap"]},bootstrapToggle:{deps:["jquery"]}}});require.config({baseUrl:jsBaseUrl});requirejs([mainScriptPath]); -//# sourceMappingURL=app.js.map \ No newline at end of file diff --git a/build_js/app.js.map b/build_js/app.js.map deleted file mode 100644 index 84c46ea04..000000000 --- a/build_js/app.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"app.js.map","sources":["app.js.src.js"],"names":["mainScriptPath","document","body","getAttribute","jsBaseUrl","requirejs","config","baseUrl","paths","layout","dialog","landingpage","mappage","jquery","bootstrap","text","mustache","velocity","velocityUI","templates","img","slidebars","jsPlumb","customScrollbar","datatables","datatablesResponsive","datatablesTableTools","xEditable","morris","raphael","bootbox","easyPieChart","dragToSelect","hoverIntent","fullScreen","select2","validator","lazylinepainter","blueImpGallery","blueImpGalleryHelper","blueImpGalleryBootstrap","bootstrapConfirmation","bootstrapToggle","easePack","tweenLite","pnotify","pnotify.buttons","pnotify.confirm","pnotify.nonblock","pnotify.desktop","pnotify.history","pnotify.callbacks","pnotify.reference","shim","deps","datatablesBootstrap","exports","require"],"mappings":"AACA,GAAIA,gBAAiBC,SAASC,KAAKC,aAAa,eAI5CC,UAAYH,SAASC,KAAKC,aAAa,eAG3CE,WAAUC,QACNC,QAAS,KAETC,OACIC,OAAQ,SACRH,OAAQ,aACRI,OAAQ,gBAGRC,YAAa,oBACbC,QAAS,gBAETC,OAAQ,wBACRC,UAAW,oBACXC,KAAM,qBACNC,SAAU,mBACVC,SAAU,mBACVC,WAAY,sBACZC,UAAW,sBACXC,IAAK,gBACLC,UAAW,gBACXC,QAAS,4BACTC,gBAAiB,yCACjBC,WAAY,uCAEZC,qBAAsB,6DAEtBC,qBAAsB,gEACtBC,UAAW,6BACXC,OAAQ,iBACRC,QAAS,kBACTC,QAAS,kBACTC,aAAc,8BACdC,aAAc,0BACdC,YAAa,kCACbC,WAAY,4BACZC,QAAS,kBACTC,UAAW,oBACXC,gBAAiB,uCACjBC,eAAgB,sBAChBC,qBAAsB,qBACtBC,wBAAyB,8BACzBC,sBAAuB,6BACvBC,gBAAiB,4BAGjBC,SAAU,mBACVC,UAAW,oBAGXC,QAAS,2BACTC,kBAAmB,8BACnBC,kBAAmB,8BACnBC,mBAAoB,+BACpBC,kBAAmB,8BACnBC,kBAAmB,8BACnBC,oBAAqB,gCACrBC,oBAAqB,iCAGzBC,MACIvC,WACIwC,MAAO,WAEXrC,UACIqC,MAAO,WAEXpC,YACIoC,MAAO,aAEXjC,WACIiC,MAAO,WAEX/B,iBACI+B,MAAO,WAEX9B,YACI8B,MAAO,WAEXC,qBACID,MAAO,eAEX7B,sBACI6B,MAAO,eAEX5B,sBACI4B,MAAO,eAEX3B,WACI2B,MAAO,cAEXxB,SACIwB,MAAO,SAAU,aACjBE,QAAS,WAEb5B,QACI0B,MAAO,SAAU,WACjBE,QAAS,UAEbX,SACIS,MAAQ,WAEZvB,cACIuB,MAAQ,WAEZtB,cACIsB,MAAQ,WAEZrB,aACIqB,MAAQ,WAEZpB,YACIoB,MAAQ,WAEZnB,SACImB,MAAQ,UACRE,QAAS,WAEbpB,WACIkB,MAAQ,SAAU,cAEtBjB,iBACIiB,MAAQ,SAAU,cAEtBhB,gBACIgB,MAAQ,WAEZb,uBACIa,MAAQ,cAEZZ,iBACIY,MAAQ,aAQpBG,SAAQnD,QACJC,QAASH,WAIbC,YAAYL"} \ No newline at end of file diff --git a/build_js/app.js.src.js b/build_js/app.js.src.js deleted file mode 100644 index 3f5affd34..000000000 --- a/build_js/app.js.src.js +++ /dev/null @@ -1,153 +0,0 @@ -// main script path -var mainScriptPath = document.body.getAttribute('data-script'); - -// js baseURL. Depends on the environment. -// e.g. use raw files (develop) or build files (production) -var jsBaseUrl = document.body.getAttribute('data-js-path'); - -// requireJs configuration -requirejs.config({ - baseUrl: 'js', // path for baseUrl - dynamically set !below! ("build_js" | "js") - - paths: { - layout: 'layout', - config: 'app/config', // path for "configuration" files dir - dialog: 'app/ui/dialog', // path for "dialog" files dir - - // main views - landingpage: './app/landingpage', // initial start "landing page" view - mappage: './app/mappage', // initial start "map page" view - - jquery: 'lib/jquery-1.11.3.min', // v1.11.3 jQuery - bootstrap: 'lib/bootstrap.min', // v3.3.0 Bootstrap js code - http://getbootstrap.com/javascript/ - text: 'lib/requirejs/text', // v2.0.12 A RequireJS/AMD loader plugin for loading text resources. - mustache: 'lib/mustache.min', // v1.0.0 Javascript template engine - http://mustache.github.io/ - velocity: 'lib/velocity.min', // v1.2.2 animation engine - http://julian.com/research/velocity/ - velocityUI: 'lib/velocity.ui.min', // v5.0.4 plugin for velocity - http://julian.com/research/velocity/#uiPack - templates: '../public/templates', // template dir - img: '../public/img', // images dir - slidebars: 'lib/slidebars', // v0.10 Slidebars - side menu plugin http://plugins.adchsm.me/slidebars/ - jsPlumb: 'lib/dom.jsPlumb-1.7.6-min', // v1.7.6 jsPlumb (Vanilla)- main map draw plugin https://jsplumbtoolkit.com/ - customScrollbar: 'lib/jquery.mCustomScrollbar.concat.min', // v3.0.9 Custom scroll bars - http://manos.malihu.gr/ - datatables: 'lib/datatables/jquery.dataTables.min', // v1.10.7 DataTables - https://datatables.net/ - //datatablesBootstrap: 'lib/datatables/dataTables.bootstrap', // DataTables - not used (bootstrap style) - datatablesResponsive: 'lib/datatables/extensions/responsive/dataTables.responsive', // v1.0.6 TableTools (PlugIn) - https://datatables.net/extensions/responsive/ - - datatablesTableTools: 'lib/datatables/extensions/tabletools/js/dataTables.tableTools', // v2.2.3 TableTools (PlugIn) - https://datatables.net/extensions/tabletools/ - xEditable: 'lib/bootstrap-editable.min', // v1.5.1 X-editable - in placed editing - morris: 'lib/morris.min', // v0.5.1 Morris.js - graphs and charts - raphael: 'lib/raphael-min', // v2.1.2 Raphaël - required for morris (dependency) - bootbox: 'lib/bootbox.min', // v4.3.0 Bootbox.js - custom dialogs - easyPieChart: 'lib/jquery.easypiechart.min', // v2.1.6 Easy Pie Chart - HTML 5 pie charts - http://rendro.github.io/easy-pie-chart/ - dragToSelect: 'lib/jquery.dragToSelect', // v1.1 Drag to Select - http://andreaslagerkvist.com/jquery/drag-to-select/ - hoverIntent: 'lib/jquery.hoverIntent.minified', // v1.8.0 Hover intention - http://cherne.net/brian/resources/jquery.hoverIntent.html - fullScreen: 'lib/jquery.fullscreen.min', // v0.5.0 Full screen mode - https://github.com/private-face/jquery.fullscreen - select2: 'lib/select2.min', // v4.0.0 Drop Down customization - https://select2.github.io/ - validator: 'lib/validator.min', // v0.7.2 Validator for Bootstrap 3 - https://github.com/1000hz/bootstrap-validator - lazylinepainter: 'lib/jquery.lazylinepainter-1.5.1.min', // v1.5.1 SVG line animation plugin - http://lazylinepainter.info/ - blueImpGallery: 'lib/blueimp-gallery', // v2.15.2 Image Gallery - https://blueimp.github.io/Bootstrap-Image-Gallery/ - blueImpGalleryHelper: 'lib/blueimp-helper', // helper function for Blue Imp Gallery - blueImpGalleryBootstrap: 'lib/bootstrap-image-gallery', // v3.1.1 Bootstrap extension for Blue Imp Gallery - https://blueimp.github.io/Bootstrap-Image-Gallery/ - bootstrapConfirmation: 'lib/bootstrap-confirmation', // v1.0.1 Bootstrap extension for inline confirm dialog - https://github.com/tavicu/bs-confirmation - bootstrapToggle: 'lib/bootstrap2-toggle.min', // v2.2.0 Bootstrap Toggle (Checkbox) - http://www.bootstraptoggle.com/ - - // header animation - easePack: 'lib/EasePack.min', - tweenLite: 'lib/TweenLite.min', - - // notification plugin - pnotify: 'lib/pnotify/pnotify.core', // v2.0.1 PNotify - notification core file - 'pnotify.buttons': 'lib/pnotify/pnotify.buttons', // PNotify - buttons notification extension - 'pnotify.confirm': 'lib/pnotify/pnotify.confirm', // PNotify - confirmation notification extension - 'pnotify.nonblock': 'lib/pnotify/pnotify.nonblock', // PNotify - notification non-block extension (hover effect) - 'pnotify.desktop': 'lib/pnotify/pnotify.desktop', // PNotify - desktop push notification extension - 'pnotify.history': 'lib/pnotify/pnotify.history', // PNotify - history push notification history extension - 'pnotify.callbacks': 'lib/pnotify/pnotify.callbacks', // PNotify - callbacks push notification extension - 'pnotify.reference': 'lib/pnotify/pnotify.reference' // PNotify - reference push notification extension - - }, - shim: { - bootstrap: { - deps: ['jquery'] - }, - velocity: { - deps: ['jquery'] - }, - velocityUI: { - deps: ['velocity'] - }, - slidebars: { - deps: ['jquery'] - }, - customScrollbar: { - deps: ['jquery'] - }, - datatables: { - deps: ['jquery'] - }, - datatablesBootstrap: { - deps: ['datatables'] - }, - datatablesResponsive: { - deps: ['datatables'] - }, - datatablesTableTools: { - deps: ['datatables'] - }, - xEditable: { - deps: ['bootstrap'] - }, - bootbox: { - deps: ['jquery', 'bootstrap'], - exports: 'bootbox' - }, - morris: { - deps: ['jquery', 'raphael'], - exports: 'Morris' - }, - pnotify: { - deps : ['jquery'] - }, - easyPieChart: { - deps : ['jquery'] - }, - dragToSelect: { - deps : ['jquery'] - }, - hoverIntent: { - deps : ['jquery'] - }, - fullScreen: { - deps : ['jquery'] - }, - select2: { - deps : ['jquery'], - exports: 'Select2' - }, - validator: { - deps : ['jquery', 'bootstrap'] - }, - lazylinepainter: { - deps : ['jquery', 'bootstrap'] - }, - blueImpGallery: { - deps : ['jquery'] - }, - bootstrapConfirmation: { - deps : ['bootstrap'] - }, - bootstrapToggle: { - deps : ['jquery'] - } - } -}); - -// switch baseUrl to js "build_js" in production environment -// this has no effect for js build process! -// check build.js for build configuration -require.config({ - baseUrl: jsBaseUrl -}); - -// load the main app module -> initial app start -requirejs( [mainScriptPath] ); diff --git a/build_js/app/landingpage.js b/build_js/app/landingpage.js deleted file mode 100644 index 9a51211f6..000000000 --- a/build_js/app/landingpage.js +++ /dev/null @@ -1,15 +0,0 @@ -!function(t,e){"object"==typeof module&&"object"==typeof module.exports?module.exports=t.document?e(t,!0):function(t){if(!t.document)throw new Error("jQuery requires a window with a document");return e(t)}:e(t)}("undefined"!=typeof window?window:this,function(t,e){function i(t){var e="length"in t&&t.length,i=at.type(t);return"function"===i||at.isWindow(t)?!1:1===t.nodeType&&e?!0:"array"===i||0===e||"number"==typeof e&&e>0&&e-1 in t}function n(t,e,i){if(at.isFunction(e))return at.grep(t,function(t,n){return!!e.call(t,n,t)!==i});if(e.nodeType)return at.grep(t,function(t){return t===e!==i});if("string"==typeof e){if(ht.test(e))return at.filter(e,t,i);e=at.filter(e,t)}return at.grep(t,function(t){return at.inArray(t,e)>=0!==i})}function a(t,e){do t=t[e];while(t&&1!==t.nodeType);return t}function o(t){var e=wt[t]={};return at.each(t.match(bt)||[],function(t,i){e[i]=!0}),e}function s(){ft.addEventListener?(ft.removeEventListener("DOMContentLoaded",r,!1),t.removeEventListener("load",r,!1)):(ft.detachEvent("onreadystatechange",r),t.detachEvent("onload",r))}function r(){(ft.addEventListener||"load"===event.type||"complete"===ft.readyState)&&(s(),at.ready())}function l(t,e,i){if(void 0===i&&1===t.nodeType){var n="data-"+e.replace(_t,"-$1").toLowerCase();if(i=t.getAttribute(n),"string"==typeof i){try{i="true"===i?!0:"false"===i?!1:"null"===i?null:+i+""===i?+i:Tt.test(i)?at.parseJSON(i):i}catch(a){}at.data(t,e,i)}else i=void 0}return i}function c(t){var e;for(e in t)if(("data"!==e||!at.isEmptyObject(t[e]))&&"toJSON"!==e)return!1;return!0}function u(t,e,i,n){if(at.acceptData(t)){var a,o,s=at.expando,r=t.nodeType,l=r?at.cache:t,c=r?t[s]:t[s]&&s;if(c&&l[c]&&(n||l[c].data)||void 0!==i||"string"!=typeof e)return c||(c=r?t[s]=X.pop()||at.guid++:s),l[c]||(l[c]=r?{}:{toJSON:at.noop}),("object"==typeof e||"function"==typeof e)&&(n?l[c]=at.extend(l[c],e):l[c].data=at.extend(l[c].data,e)),o=l[c],n||(o.data||(o.data={}),o=o.data),void 0!==i&&(o[at.camelCase(e)]=i),"string"==typeof e?(a=o[e],null==a&&(a=o[at.camelCase(e)])):a=o,a}}function d(t,e,i){if(at.acceptData(t)){var n,a,o=t.nodeType,s=o?at.cache:t,r=o?t[at.expando]:at.expando;if(s[r]){if(e&&(n=i?s[r]:s[r].data)){at.isArray(e)?e=e.concat(at.map(e,at.camelCase)):e in n?e=[e]:(e=at.camelCase(e),e=e in n?[e]:e.split(" ")),a=e.length;for(;a--;)delete n[e[a]];if(i?!c(n):!at.isEmptyObject(n))return}(i||(delete s[r].data,c(s[r])))&&(o?at.cleanData([t],!0):it.deleteExpando||s!=s.window?delete s[r]:s[r]=null)}}}function h(){return!0}function p(){return!1}function f(){try{return ft.activeElement}catch(t){}}function m(t){var e=Lt.split("|"),i=t.createDocumentFragment();if(i.createElement)for(;e.length;)i.createElement(e.pop());return i}function g(t,e){var i,n,a=0,o=typeof t.getElementsByTagName!==St?t.getElementsByTagName(e||"*"):typeof t.querySelectorAll!==St?t.querySelectorAll(e||"*"):void 0;if(!o)for(o=[],i=t.childNodes||t;null!=(n=i[a]);a++)!e||at.nodeName(n,e)?o.push(n):at.merge(o,g(n,e));return void 0===e||e&&at.nodeName(t,e)?at.merge([t],o):o}function v(t){Pt.test(t.type)&&(t.defaultChecked=t.checked)}function y(t,e){return at.nodeName(t,"table")&&at.nodeName(11!==e.nodeType?e:e.firstChild,"tr")?t.getElementsByTagName("tbody")[0]||t.appendChild(t.ownerDocument.createElement("tbody")):t}function b(t){return t.type=(null!==at.find.attr(t,"type"))+"/"+t.type,t}function w(t){var e=Yt.exec(t.type);return e?t.type=e[1]:t.removeAttribute("type"),t}function C(t,e){for(var i,n=0;null!=(i=t[n]);n++)at._data(i,"globalEval",!e||at._data(e[n],"globalEval"))}function x(t,e){if(1===e.nodeType&&at.hasData(t)){var i,n,a,o=at._data(t),s=at._data(e,o),r=o.events;if(r){delete s.handle,s.events={};for(i in r)for(n=0,a=r[i].length;a>n;n++)at.event.add(e,i,r[i][n])}s.data&&(s.data=at.extend({},s.data))}}function S(t,e){var i,n,a;if(1===e.nodeType){if(i=e.nodeName.toLowerCase(),!it.noCloneEvent&&e[at.expando]){a=at._data(e);for(n in a.events)at.removeEvent(e,n,a.handle);e.removeAttribute(at.expando)}"script"===i&&e.text!==t.text?(b(e).text=t.text,w(e)):"object"===i?(e.parentNode&&(e.outerHTML=t.outerHTML),it.html5Clone&&t.innerHTML&&!at.trim(e.innerHTML)&&(e.innerHTML=t.innerHTML)):"input"===i&&Pt.test(t.type)?(e.defaultChecked=e.checked=t.checked,e.value!==t.value&&(e.value=t.value)):"option"===i?e.defaultSelected=e.selected=t.defaultSelected:("input"===i||"textarea"===i)&&(e.defaultValue=t.defaultValue)}}function T(e,i){var n,a=at(i.createElement(e)).appendTo(i.body),o=t.getDefaultComputedStyle&&(n=t.getDefaultComputedStyle(a[0]))?n.display:at.css(a[0],"display");return a.detach(),o}function _(t){var e=ft,i=Jt[t];return i||(i=T(t,e),"none"!==i&&i||(Zt=(Zt||at("